diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index c9c7c2cbb2..4dd14f281a 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -25,7 +25,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: platforms: linux/arm64,linux/amd64 push: true @@ -61,7 +61,7 @@ jobs: print(f"tag_name={ref_tag}", file=f) - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: platforms: linux/arm64,linux/amd64 push: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10c390c721..fd6b7ffc3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: python-check-blanket-noqa name: Precision flake ignores - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.9 + rev: v0.5.1 hooks: - id: ruff name: ruff lint @@ -41,7 +41,7 @@ repos: flake8-simplify==0.14.1, ] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.10.1 hooks: - id: mypy additional_dependencies: diff --git a/README.md b/README.md index cc394142d2..bfc4ff38e0 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as demonstrated in the videos of [3Blue1Brown](https://www.3blue1brown.com/). -> NOTE: This repository is maintained by the Manim Community and is not associated with Grant Sanderson or 3Blue1Brown in any way (although we are definitely indebted to him for providing his work to the world). If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)). This fork is updated more frequently than his, and it's recommended to use this fork if you'd like to use Manim for your own projects. +> [!NOTE] +> This repository is maintained by the Manim Community and is not associated with Grant Sanderson or 3Blue1Brown in any way (although we are definitely indebted to him for providing his work to the world). If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)). This fork is updated more frequently than his, and it's recommended to use this fork if you'd like to use Manim for your own projects. ## Table of Contents: @@ -35,7 +36,8 @@ Manim is an animation engine for explanatory math videos. It's used to create pr ## Installation -> **WARNING:** These instructions are for the community version _only_. Trying to use these instructions to install [3b1b/manim](https://github.com/3b1b/manim) or instructions there to install this version will cause problems. Read [this](https://docs.manim.community/en/stable/faq/installation.html#why-are-there-different-versions-of-manim) and decide which version you wish to install, then only follow the instructions for your desired version. +> [!WARNING] +> These instructions are for the community version _only_. Trying to use these instructions to install [3b1b/manim](https://github.com/3b1b/manim) or instructions there to install this version will cause problems. Read [this](https://docs.manim.community/en/stable/faq/installation.html#why-are-there-different-versions-of-manim) and decide which version you wish to install, then only follow the instructions for your desired version. Manim requires a few dependencies that must be installed prior to using it. If you want to try it out first before installing it locally, you can do so @@ -71,7 +73,7 @@ In order to view the output of this scene, save the code in a file called `examp manim -p -ql example.py SquareToCircle ``` -You should see your native video player program pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this +You should see a window pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this [GitHub repository](example_scenes). You can also visit the [official gallery](https://docs.manim.community/en/stable/examples.html) for more advanced examples. Manim also ships with a `%%manim` IPython magic which allows to use it conveniently in JupyterLab (as well as classic Jupyter) notebooks. See the @@ -84,13 +86,15 @@ The general usage of Manim is as follows: ![manim-illustration](https://raw.githubusercontent.com/ManimCommunity/manim/main/docs/source/_static/command.png) -The `-p` flag in the command above is for previewing, meaning the video file will automatically open when it is done rendering. The `-ql` flag is for a faster rendering at a lower quality. +The `-p` flag in the command above is for previewing, meaning a window will show up to render it in real time. +The `-ql` flag is for a faster rendering at a lower quality. Some other useful flags include: - `-s` to skip to the end and just show the final frame. - `-n ` to skip ahead to the `n`'th animation of a scene. - `-f` show the file in the file browser. +- `-w` to actually write the result into a video file. For a thorough list of command line arguments, visit the [documentation](https://docs.manim.community/en/stable/guides/configuration.html). diff --git a/conftest.py b/conftest.py index 683bd2bc05..f52f241134 100644 --- a/conftest.py +++ b/conftest.py @@ -5,7 +5,6 @@ from __future__ import annotations -import cairo import moderngl # If it is running Doctest the current directory @@ -34,7 +33,6 @@ def pytest_report_header(config): info = ctx.info ctx.release() return ( - f"\nCairo Version: {cairo.cairo_version()}", "\nOpenGL information", "------------------", f"vendor: {info['GL_VENDOR'].strip()}", diff --git a/docker/Dockerfile b/docker/Dockerfile index b41647e4e8..34d21fa0a0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,9 +22,9 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tl tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \ /tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \ && tlmgr install \ - amsmath babel-english cbfonts-fd cm-super ctex doublestroke dvisvgm everysel \ + amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \ fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \ - mathastext microtype ms physics preview ragged2e relsize rsfs \ + mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \ setspace standalone tipa wasy wasysym xcolor xetex xkeyval # clone and build manim diff --git a/docs/i18n/sv/LC_MESSAGES/tutorials/building_blocks.po b/docs/i18n/sv/LC_MESSAGES/tutorials/building_blocks.po index 02ec3cc55d..373f2f7d69 100644 --- a/docs/i18n/sv/LC_MESSAGES/tutorials/building_blocks.po +++ b/docs/i18n/sv/LC_MESSAGES/tutorials/building_blocks.po @@ -163,8 +163,8 @@ msgid "Creating a custom animation" msgstr "Skapa en egen animation" #: ../../source/tutorials/building_blocks.rst:299 -msgid "Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another. If you find yourself in that situation, then you can define your own custom animation. You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate_mobject`. The :meth:`~.Animation.interpolate_mobject` method receives alpha as a parameter that starts at 0 and changes throughout the animation. So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate_mobject method. Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions." -msgstr "Även om Manim har många inbyggda animationer, kommer det vara stunder när du behöver animera smidigt från ett tillstånd av ett :class:`~.Mobject` till ett annat. Om du befinner dig i den situationen kan du definiera en egen animation. Du börjar med att utöka :class:`~.Animation`-klassen och åsidosätter dess :meth:`~.Animation.interpolate_mobject`. :meth:`~.Animation.interpolate_mobject` -metoden får alfa som en parameter som startar vid 0 och ändras under hela animationen. Så du behöver bara manipulera self.mobject inuti Animation enligt alfa värdet i dess interpolate_mobject-metod. Då får du alla fördelar med :class:`~.Animation` såsom att spela det under olika körtider eller använda olika hastighetsfunktioner." +msgid "Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another. If you find yourself in that situation, then you can define your own custom animation. You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate`. The :meth:`~.Animation.interpolate` method receives alpha as a parameter that starts at 0 and changes throughout the animation. So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate method. Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions." +msgstr "Även om Manim har många inbyggda animationer, kommer det vara stunder när du behöver animera smidigt från ett tillstånd av ett :class:`~.Mobject` till ett annat. Om du befinner dig i den situationen kan du definiera en egen animation. Du börjar med att utöka :class:`~.Animation`-klassen och åsidosätter dess :meth:`~.Animation.interpolate`. :meth:`~.Animation.interpolate` -metoden får alfa som en parameter som startar vid 0 och ändras under hela animationen. Så du behöver bara manipulera self.mobject inuti Animation enligt alfa värdet i dess interpolate-metod. Då får du alla fördelar med :class:`~.Animation` såsom att spela det under olika körtider eller använda olika hastighetsfunktioner." #: ../../source/tutorials/building_blocks.rst:306 msgid "Let's say you start with a number and want to create a :class:`~.Transform` animation that transforms it to a target number. You can do it using :class:`~.FadeTransform`, which will fade out the starting number and fade in the target number. But when we think about transforming a number from one to another, an intuitive way of doing it is by incrementing or decrementing it smoothly. Manim has a feature that allows you to customize this behavior by defining your own custom animation." @@ -175,12 +175,12 @@ msgid "You can start by creating your own ``Count`` class that extends :class:`~ msgstr "Du kan börja med att skapa din egen ``Count`` klass som utökar klassen :class:`~.Animation`. Klassen kan ha en konstruktor med tre argument, ett :class:`~.DecimalNumber` Mobject, start and end. Konstruktorn kommer att skicka :class:`~.DecimalNumber` Mobject till superkonstruktorn (i detta fall konstruktorn i :class:`~.Animation`) och kommer att ställa in start och slut." #: ../../source/tutorials/building_blocks.rst:315 -msgid "The only thing that you need to do is to define how you want it to look at every step of the animation. Manim provides you with the alpha value in the :meth:`~.Animation.interpolate_mobject` method based on frame rate of video, rate function, and run time of animation played. The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation. For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation." -msgstr "Det enda du behöver göra är att definiera hur du vill att det ska se ut för varje steg i animationen. Manim ger dig alfavärdet i :meth:`~. nimation.interpolate_mobject' metoden baserat på bildhastigheten av videon, hastighetsfunktionen och körtiden för den animation som spelas. Alfaparametern har ett värde mellan 0 och 1 som representerar steget i den aktuella animationen. Till exempel betyder 0 början av animationen, 0,5 betyder halvvägs genom animationen och 1 betyder slutet av animationen." +msgid "The only thing that you need to do is to define how you want it to look at every step of the animation. Manim provides you with the alpha value in the :meth:`~.Animation.interpolate` method based on frame rate of video, rate function, and run time of animation played. The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation. For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation." +msgstr "Det enda du behöver göra är att definiera hur du vill att det ska se ut för varje steg i animationen. Manim ger dig alfavärdet i :meth:`~. nimation.interpolate' metoden baserat på bildhastigheten av videon, hastighetsfunktionen och körtiden för den animation som spelas. Alfaparametern har ett värde mellan 0 och 1 som representerar steget i den aktuella animationen. Till exempel betyder 0 början av animationen, 0,5 betyder halvvägs genom animationen och 1 betyder slutet av animationen." #: ../../source/tutorials/building_blocks.rst:320 -msgid "In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate_mobject` method of the ``Count`` animation. Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation." -msgstr "I fallet med ``Count`` animationen, behöver du bara hitta ett sätt att bestämma talet som ska visas för det givna alfavärdet och sedan ange det värdet i :meth:`~.Animation.interpolate_mobject` -metoden för ``Count``-animationen. Antag att du börjar med 50 och inkrementerar värdet tills :class:`~.DecimalNumber` når 100 i slutet av animationen." +msgid "In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate` method of the ``Count`` animation. Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation." +msgstr "I fallet med ``Count`` animationen, behöver du bara hitta ett sätt att bestämma talet som ska visas för det givna alfavärdet och sedan ange det värdet i :meth:`~.Animation.interpolate` -metoden för ``Count``-animationen. Antag att du börjar med 50 och inkrementerar värdet tills :class:`~.DecimalNumber` når 100 i slutet av animationen." #: ../../source/tutorials/building_blocks.rst:323 msgid "If alpha is 0, you want the value to be 50." diff --git a/docs/source/contributing/docs.rst b/docs/source/contributing/docs.rst index a8bb61c535..aaac806a83 100644 --- a/docs/source/contributing/docs.rst +++ b/docs/source/contributing/docs.rst @@ -81,3 +81,4 @@ Index docs/examples docs/references docs/typings + docs/types diff --git a/docs/source/contributing/docs/types.rst b/docs/source/contributing/docs/types.rst new file mode 100644 index 0000000000..fb6f06732b --- /dev/null +++ b/docs/source/contributing/docs/types.rst @@ -0,0 +1,134 @@ +=================== +Choosing Type Hints +=================== +In order to provide the best user experience, +it's important that type hints are chosen correctly. +With the large variety of types provided by Manim, choosing +which one to use can be difficult. This guide aims to +aid you in the process of choosing the right type for the scenario. + + +The first step is figuring out which category your type hint fits into. + +Coordinates +----------- +Coordinates encompass two main categories: points, and vectors. + + +Points +~~~~~~ +The purpose of points is pretty straightforward: they represent a point +in space. For example: + +.. code-block:: python + + def status2D(coord: Point2D) -> None: + x, y = coord + print(f"Point at {x=},{y=}") + + + def status3D(coord: Point3D) -> None: + x, y, z = coord + print(f"Point at {x=},{y=},{z=}") + + + def get_statuses(coords: Point2D_Array | Point3D_Array) -> None: + for coord in coords: + if len(coord) == 2: + # it's a Point2D + status2D(coord) + else: + # it's a point3D + status3D(coord) + +It's important to realize that the status functions accepted both +tuples/lists of the correct length, and ``NDArray``'s of the correct shape. +If they only accepted ``NDArray``'s, we would use their ``Internal`` counterparts: +:class:`~.typing.InternalPoint2D`, :class:`~.typing.InternalPoint3D`, :class:`~.typing.InternalPoint2D_Array` and :class:`~.typing.InternalPoint3D_Array`. + +In general, the type aliases prefixed with ``Internal`` should never be used on +user-facing classes and functions, but should be reserved for internal behavior. + +Vectors +~~~~~~~ +Vectors share many similarities to points. However, they have a different +connotation. Vectors should be used to represent direction. For example, +consider this slightly contrived function: + +.. code-block:: python + + def shift_mobject(mob: Mobject, direction: Vector3D, scale_factor: float = 1) -> mob: + return mob.shift(direction * scale_factor) + +Here we see an important example of the difference. ``direction`` can not, and +should not, be typed as a :class:`~.typing.Point3D` because the function does not accept tuples/lists, +like ``direction=(0, 1, 0)``. You could type it as :class:`~.typing.InternalPoint3D` and +the type checker and linter would be happy; however, this makes the code harder +to understand. + +As a general rule, if a parameter is called ``direction`` or ``axis``, +it should be type hinted as some form of :class:`~.VectorND`. + +.. warning:: + + This is not always true. For example, as of Manim 0.18.0, the direction + parameter of the :class:`.Vector` Mobject should be ``Point2D | Point3D``, + as it can also accept ``tuple[float, float]`` and ``tuple[float, float, float]``. + +Colors +------ +The interface Manim provides for working with colors is :class:`.ManimColor`. +The main color types Manim supports are RGB, RGBA, and HSV. You will want +to add type hints to a function depending on which type it uses. If any color will work, +you will need something like: + +.. code-block:: python + + if TYPE_CHECKING: + from manim.utils.color import ParsableManimColor + + # type hint stuff with ParsableManimColor + + + +Béziers +------- +Manim internally represents a :class:`.Mobject` by a collection of points. In the case of :class:`.VMobject`, +the most commonly used subclass of :class:`.Mobject`, these points represent Bézier curves, +which are a way of representing a curve using a sequence of points. + +.. note:: + + To learn more about Béziers, take a look at https://pomax.github.io/bezierinfo/ + + +Manim supports two different renderers, which each have different representations of +Béziers: Cairo uses cubic Bézier curves, while OpenGL uses quadratic Bézier curves. + +Type hints like :class:`~.typing.BezierPoints` represent a single bezier curve, and :class:`~.typing.BezierPath` +represents multiple Bézier curves. A :class:`~.typing.Spline` is when the Bézier curves in a :class:`~.typing.BezierPath` +forms a single connected curve. Manim also provides more specific type aliases when working with +quadratic or cubic curves, and they are prefixed with their respective type (e.g. :class:`~.typing.CubicBezierPoints`, +is a :class:`~.typing.BezierPoints` consisting of exactly 4 points representing a cubic Bézier curve). + + +Functions +--------- +Throughout the codebase, many different types of functions are used. The most obvious example +is a rate function, which takes in a float and outputs a float (``Callable[[float], float]``). +Another example is for overriding animations. One will often need to map a :class:`.Mobject` +to an overridden :class:`.Animation`, and for that we have the :class:`~.typing.FunctionOverride` type hint. + +:class:`~.typing.PathFuncType` and :class:`~.typing.MappingFunction` are more niche, but are related to moving objects +along a path, or applying functions. If you need to use it, you'll know. + + +Images +------ +There are several representations of images in Manim. The most common is +the representation as a NumPy array of floats representing the pixels of an image. +This is especially common when it comes to the OpenGL renderer. + +This is the use case of the :class:`~.typing.Image` type hint. Sometimes, Manim may use ``PIL.Image``, +in which case one should use that type hint instead. +Of course, if a more specific type of image is needed, it can be annotated as such. diff --git a/docs/source/contributing/docs/typings.rst b/docs/source/contributing/docs/typings.rst index befd557a2b..7cc14068c8 100644 --- a/docs/source/contributing/docs/typings.rst +++ b/docs/source/contributing/docs/typings.rst @@ -1,6 +1,6 @@ -============== -Adding Typings -============== +================== +Typing Conventions +================== .. warning:: This section is still a work in progress. diff --git a/docs/source/contributing/performance.rst b/docs/source/contributing/performance.rst index f16121c40c..09b3fa11e0 100644 --- a/docs/source/contributing/performance.rst +++ b/docs/source/contributing/performance.rst @@ -24,8 +24,8 @@ to the bottom of the file: .. code-block:: python with tempconfig({"quality": "medium_quality", "disable_caching": True}): - scene = SceneName() - scene.render() + manager = Manager(SceneName) + manager.render() Where ``SceneName`` is the name of the scene you want to run. You can then run the file directly, and can thus follow the instructions for most profilers. @@ -58,8 +58,8 @@ to ``square_to_circle.py``: with tempconfig({"quality": "medium_quality", "disable_caching": True}): - scene = SquareToCircle() - scene.render() + manager = Manager(SquareToCircle) + manager.render() Now run the following in the terminal: diff --git a/docs/source/guides/deep_dive.rst b/docs/source/guides/deep_dive.rst index 3ea950230f..8edee676c8 100644 --- a/docs/source/guides/deep_dive.rst +++ b/docs/source/guides/deep_dive.rst @@ -1,11 +1,11 @@ A deep dive into Manim's internals ================================== -**Author:** `Benjamin Hackl `__ +**Authors:** `Benjamin Hackl `__ and `Aarush Deshpande `__ .. admonition:: Disclaimer - This guide reflects the state of the library as of version ``v0.16.0`` + This guide reflects the state of the library as of version ``v0.20.0`` and primarily treats the Cairo renderer. The situation in the latest version of Manim might be different; in case of substantial deviations we will add a note below. @@ -84,7 +84,7 @@ discussing the contents of the following chapters on a very high level. to prepare a scene for rendering; right until the point where the user-overridden ``construct`` method is ran. This includes a brief discussion on using Manim's CLI versus other means of rendering (e.g., via Jupyter notebooks, or in your Python - script by calling the :meth:`.Scene.render` method yourself). + script by calling the :meth:`.Manager.render` method yourself). - `Mobject Initialization`_: For the second chapter we dive into creating and handling Mobjects, the basic elements that should be displayed in our scene. We discuss the :class:`.Mobject` base class, how there are essentially @@ -107,6 +107,25 @@ discussing the contents of the following chapters on a very high level. :meth:`.Scene.construct` has been run, the library combines the partial movie files to one video. +.. hint:: + + As we move forward, try to keep in mind the responsibilities of every + class we introduce. We'll talk more about them in detail, but here's a brief + overview + + * :class:`.Scene` is responsible for managing the classes :class:`Mobject`, :class:`.Animation`, + and :class:`.Camera`. + + * :class:`.Manager` is responsible for coordinating the :class:`.Scene`, :class:`.Renderer`, + and :class:`.FileWriter`. + + * :class:`.FileWriter` is responsible for writing frames and partial movie files, as well + as combining them all into a final movie file. + + * :class:`.Renderer` is an abstract class which has to be subclassed. + It's job is to take information related to the :class:`.Camera`, and the mobjects + on the :class:`.Scene` at a certain frame, and to return the pixels in a frame. + And with that, let us get *in medias res*. Preliminaries @@ -123,8 +142,8 @@ like :: with tempconfig({"quality": "medium_quality", "preview": True}): - scene = ToyExample() - scene.render() + manager = Manager(ToyExample) + manager.render() or whether you are rendering the code in a Jupyter notebook, you are still telling your python interpreter to import the library. The usual pattern used to do this is @@ -202,8 +221,8 @@ have created a file ``toy_example.py`` which looks like this:: self.play(FadeOut(blue_circle, small_dot)) with tempconfig({"quality": "medium_quality", "preview": True}): - scene = ToyExample() - scene.render() + manager = Manager(ToyExample) + manager.render() With such a file, the desired scene is rendered by simply running this Python script via ``python toy_example.py``. Then, as described above, the library @@ -218,10 +237,10 @@ dictionary, and upon leaving the context the original version of the configuration is restored. TL;DR: it provides a fancy way of temporarily setting configuration options. -Inside the context manager, two things happen: an actual ``ToyExample``-scene -object is instantiated, and the ``render`` method is called. Every way of using +Inside the context manager, two things happen: a :class:`.Manager` is created for +the ``ToyExample``-scene, and the ``render`` method is called. Every way of using Manim ultimately does something along of these lines, the library always instantiates -the scene object and then calls its ``render`` method. To illustrate that this +the manager of the scene object and then calls its ``render`` method. To illustrate that this really is the case, let us briefly look at the two most common ways of rendering scenes: @@ -243,54 +262,75 @@ and the code creating the scene class and calling its render method is located `here `__. -Now that we know that either way, a :class:`.Scene` object is created, let us investigate -what Manim does when that happens. When instantiating our scene object +Now that we know that either way, a :class:`.Manager` for a :class:`.Scene` object is created, let us investigate +what Manim does when that happens. When instantiating our manager :: - scene = ToyExample() + manager = Manager(ToyExample) -the ``Scene.__init__`` method is called, given that we did not implement our own initialization -method. Inspecting the corresponding code (see -`here `__) -reveals that ``Scene.__init__`` first sets several attributes of the scene objects that do not -depend on any configuration options set in ``config``. Then the scene inspects the value of -``config.renderer``, and based on its value, either instantiates a ``CairoRenderer`` or an -``OpenGLRenderer`` object and assigns it to its ``renderer`` attribute. +The :meth:`.Manager.__init__` method is called. Looking at the source code (`here `__), +we see that the :meth:`.Scene.__init__` method is called, +given that we did not implement our own initialization +method. Inspecting the corresponding code (see `here `__) +reveals that :class:`Scene.__init__` first sets several attributes of the scene objects that do not +depend on any configuration options set in ``config``. It then initializes it's :class:`.Camera`. +The purpose of a :class:`.Camera` is to keep track of what you can see in the scene. Think of it +as a pair of eyes, that limit how far you can look sideways and vertically. -The scene then asks its renderer to initialize the scene by calling +The :class:`.Scene` also sets up :attr:`.Scene.mobjects`. This attribute keeps track of all the :class:`.Mobject` +that have been added to the scene. -:: +The :class:`.Manager` then continues on to create a :class:`.Window`, which is the popopen interactive window, +and creates the renderer:: - self.renderer.init_scene(self) + self.renderer = self.create_renderer() + self.renderer.use_window() -Inspecting both the default Cairo renderer and the OpenGL renderer shows that the ``init_scene`` -method effectively makes the renderer instantiate a :class:`.SceneFileWriter` object, which -basically is Manim's interface to ``libav`` (FFMPEG) and actually writes the movie file. The Cairo -renderer (see the implementation `here `__) does not require any further initialization. The OpenGL renderer -does some additional setup to enable the realtime rendering preview window, which we do not go -into detail further here. +If you hover over :attr:`.Manager.renderer`, you might see that the type is a :class:`.RendererProtocol`. +A :class:`~typing.Protocol` is a contract for a class. It says that whatever the class is, it will implement +the methods defined inside the protocol. In this case, it means that the renderer will have all the methods +defined in :class:`.RendererProtocol`. -.. warning:: +.. note:: - Currently, there is a lot of interplay between a scene and its renderer. This is a flaw - in Manim's current architecture, and we are working on reducing this interdependency to - achieve a less convoluted code flow. + The point of using :class:`~typing.Protocol` is so that in the future, plugins + can swap out the renderer with their own version - either for speed, or for a different + behavior. -After the renderer has been instantiated and initialized its file writer, the scene populates -further initial attributes (notable mention: the ``mobjects`` attribute which keeps track -of the mobjects that have been added to the scene). It is then done with its instantiation -and ready to be rendered. + +For the rest of this article to take a concrete example, we'll use :class:`.OpenGLRenderer`. + +Finally, the :class:`.Manager` creates a :class:`.FileWriter`. This is the object that actually +writes the partial movie files. The rest of this article is concerned with the last line in our toy example script:: - scene.render() + manager.render() This is where the actual magic happens. +.. note:: + + TODO TO REVIEWERS - Replace this link with the proper permanent link + Inspecting the `implementation of the render method `__ -reveals that there are several hooks that can be used for pre- or postprocessing -a scene. Unsurprisingly, :meth:`.Scene.render` describes the full *render cycle* +we see that there are two passes of rendering. + +.. note:: + + As of the experimental branch at June 30th, 2024, two pass rendering + does not exist. This will proceed to explain the single pass rendering system. + +Looking around, we find that there are several hooks that can be used for pre- or postprocessing +a scene (check out :meth:`.Manager._setup`, and :meth:`.Manager._tear_down`). + +.. note:: + + You might notice :attr:`.Manager.virtual_animation_start_time` and :attr:`.Manager.real_animation_start_time` + when looking through :meth:`.Manager._setup`. These will be explained later. + +Unsurprisingly, :meth:`.Manager.render` describes the full *render cycle* of a scene. During this life cycle, there are three custom methods whose base implementation is empty and that can be overwritten to suit your purposes. In the order they are called, these customizable methods are: @@ -308,14 +348,14 @@ the order they are called, these customizable methods are: Python scripts). After these three methods are run, the animations have been fully rendered, -and Manim calls :meth:`.CairoRenderer.scene_finished` to gracefully +and Manim calls :meth:`.Manager.tear_down` to gracefully complete the rendering process. This checks whether any animations have been played -- and if so, it tells the :class:`.SceneFileWriter` to close the output file. If not, Manim assumes that a static image should be output which it then renders using the same strategy by calling the render loop (see below) once. -**Back in our toy example,** the call to :meth:`.Scene.render` first +**Back in our toy example,** the call to :meth:`.Manager.render` first triggers :meth:`.Scene.setup` (which only consists of ``pass``), followed by a call of :meth:`.Scene.construct`. At this point, our *animation script* is run, starting with the initialization of ``orange_square``. @@ -348,16 +388,12 @@ of :class:`.Mobject`, you will find that not too much happens in there: - and finally, ``init_colors`` is called. Digging deeper, you will find that :meth:`.Mobject.reset_points` simply -sets the ``points`` attribute of the mobject to an empty NumPy vector, +sets the ``points`` attribute of the mobject to an empty NumPy array, while the other two methods, :meth:`.Mobject.generate_points` and :meth:`.Mobject.init_colors` are just implemented as ``pass``. This makes sense: :class:`.Mobject` is not supposed to be used as -an *actual* object that is displayed on screen; in fact the camera -(which we will discuss later in more detail; it is the class that is, -for the Cairo renderer, responsible for "taking a picture" of the -current scene) does not process "pure" :class:`Mobjects <.Mobject>` -in any way, they *cannot* even appear in the rendered output. +an *actual* object that is displayed on screen. This is where different types of mobjects come into play. Roughly speaking, the Cairo renderer setup knows three different types of @@ -376,24 +412,24 @@ mobjects that can be rendered: As just mentioned, :class:`VMobjects <.VMobject>` represent vectorized mobjects. To render a :class:`.VMobject`, the camera looks at the -``points`` attribute of a :class:`.VMobject` and divides it into sets -of four points each. Each of these sets is then used to construct a -cubic Bézier curve with the first and last entry describing the -end points of the curve ("anchors"), and the second and third entry -describing the control points in between ("handles"). +:attr:`~.VMobject.points` attribute of a :class:`.VMobject` and divides it into sets +of three points each. Each of these sets is then used to construct a +quadratic Bézier curve with the first and last entry describing the +end points of the curve ("anchors"), and the second entry +describing the control points in between ("handle"). .. hint:: To learn more about Bézier curves, take a look at the excellent online textbook `A Primer on Bézier curves `__ by `Pomax `__ -- there is a playground representing - cubic Bézier curves `in §1 `__, + quadratic Bézier curves `in §1 `__, the red and yellow points are "anchors", and the green and blue points are "handles". In contrast to :class:`.Mobject`, :class:`.VMobject` can be displayed on screen (even though, technically, it is still considered a base class). To illustrate how points are processed, consider the following short example -of a :class:`.VMobject` with 8 points (and thus made out of 8/4 = 2 cubic +of a :class:`.VMobject` with 6 points (and thus made out of 6/3 = 2 cubic Bézier curves). The resulting :class:`.VMobject` is drawn in green. The handles are drawn as red dots with a line to their closest anchor. @@ -430,6 +466,7 @@ The handles are drawn as red dots with a line to their closest anchor. .. warning:: + Manually setting the points of your :class:`.VMobject` is usually discouraged; there are specialized methods that can take care of that for you -- but it might be relevant when implementing your own, @@ -561,59 +598,12 @@ is not a "flat" list of mobjects, but a list of mobjects which might contain mobjects themselves, and so on. Stepping through the code in :meth:`.Scene.add`, we see that first -it is checked whether we are currently using the OpenGL renderer -(which we are not) -- adding mobjects to the scene works slightly -different (and actually easier!) for the OpenGL renderer. Then, the -code branch for the Cairo renderer is entered and the list of so-called -foreground mobjects (which are rendered on top of all other mobjects) -is added to the list of passed mobjects. This is to ensure that the -foreground mobjects will stay above of the other mobjects, even after -adding the new ones. In our case, the list of foreground mobjects -is actually empty, and nothing changes. - -Next, :meth:`.Scene.restructure_mobjects` is called with the list -of mobjects to be added as the ``to_remove`` argument, which might -sound odd at first. Practically, this ensures that mobjects are not -added twice, as mentioned above: if they were present in the scene -``Scene.mobjects`` list before (even if they were contained as a -child of some other mobject), they are first removed from the list. -The way :meth:`.Scene.restrucutre_mobjects` works is rather aggressive: -It always operates on a given list of mobjects; in the ``add`` method -two different lists occur: the default one, ``Scene.mobjects`` (no extra -keyword argument is passed), and ``Scene.moving_mobjects`` (which we will -discuss later in more detail). It iterates through all of the members of -the list, and checks whether any of the mobjects passed in ``to_remove`` -are contained as children (in any nesting level). If so, **their parent -mobject is deconstructed** and their siblings are inserted directly -one level higher. Consider the following example:: - - >>> from manim import Scene, Square, Circle, Group - >>> test_scene = Scene() - >>> mob1 = Square() - >>> mob2 = Circle() - >>> mob_group = Group(mob1, mob2) - >>> test_scene.add(mob_group) - - >>> test_scene.mobjects - [Group] - >>> test_scene.restructure_mobjects(to_remove=[mob1]) - - >>> test_scene.mobjects - [Circle] - -Note that the group is disbanded and the circle moves into the -root layer of mobjects in ``test_scene.mobjects``. - -After the mobject list is "restructured", the mobject to be added -are simply appended to ``Scene.mobjects``. In our toy example, -the ``Scene.mobjects`` list is actually empty, so the -``restructure_mobjects`` method does not actually do anything. The -``orange_square`` is simply added to ``Scene.mobjects``, and as -the aforementioned ``Scene.moving_mobjects`` list is, at this point, -also still empty, nothing happens and :meth:`.Scene.add` returns. - -We will hear more about the ``moving_mobject`` list when we discuss -the render loop. Before we do that, let us look at the next line +we remove all the mobjects that are being added -- this is to make +sure we don't add a :class:`.Mobject` twice! After that, we can safely +add it to :attr:`.Scene.mobjects`. + +We will hear more from :class:`.Scene` soon. +Before we do that, let us look at the next line of code in our toy example, which includes the initialization of an animation class, :: @@ -642,11 +632,11 @@ the run time of animations is also fixed and known beforehand. The initialization of animations actually is not very exciting, :meth:`.Animation.__init__` merely sets some attributes derived from the passed keyword arguments and additionally ensures that -the ``Animation.starting_mobject`` and ``Animation.mobject`` +the :attr:`~Animation.starting_mobject` and :attr:`~.Animation.mobject` attributes are populated. Once the animation is played, the -``starting_mobject`` attribute holds an unmodified copy of the +:attr:`~.Animation.starting_mobject` attribute holds an unmodified copy of the mobject the animation is attached to; during the initialization -it is set to a placeholder mobject. The ``mobject`` attribute +it is set to a placeholder mobject. The :attr:`~.Animation.mobject` attribute is set to the mobject the animation is attached to. Animations have a few special methods which are called during the @@ -681,77 +671,80 @@ animation (like its ``run_time``, the ``rate_func``, etc.) are processed there -- and then the animation object is fully initialized and ready to be played. +The Animation Buffer +^^^^^^^^^^^^^^^^^^^^ +There's an attribute of animations that we have glossed +over, and that is :attr:`.Animation.buffer`, of type :class:`.SceneBuffer`. +The :attr:`~.Animation.buffer` is the animations way of communicating +with what happens on the scene. If you want to modify +the scene during the interpolation stage (outside of :meth:`~.Animation.begin` or :meth:`~.Animation.finish`), +the attribute :attr:`.Animation.apply_buffer` is what tells the scene that the buffer +should be processed. + +For example, an animation that adds a circle to the scene every frame might look like this + +.. code-block:: python + + class CircleAnimation(Animation): + def begin(self) -> None: + self.circles = VGroup() + + def interpolate(self, alpha: float) -> None: + # create and arrange the circles + self.circles.add(Circle()) + self.circles().arrange() + # add the new circle to the scene + self.buffer.add(self.circles[-1]) + # make sure the scene actually realizes something changed + self.apply_buffer = True + +Every time the :class:`.Scene` applies the buffer, it gets emptied out +for use the next time. + The ``play`` call: preparing to enter Manim's render loop ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We are finally there, the render loop is in our reach. Let us walk through the code that is run when :meth:`.Scene.play` is called. -.. hint:: +.. note:: - Recall that this article is specifically about the Cairo renderer. - Up to here, things were more or less the same for the OpenGL renderer - as well; while some base mobjects might be different, the control flow - and lifecycle of mobjects is still more or less the same. There are more - substantial differences when it comes to the rendering loop. + In the future, control will not be passed to the Manager. + Instead, the Scene will keep track of every animation and + at the very end, the Manager will render everything. As you will see when inspecting the method, :meth:`.Scene.play` almost -immediately passes over to the ``play`` method of the renderer, -in our case :class:`.CairoRenderer.play`. The one thing :meth:`.Scene.play` -takes care of is the management of subcaptions that you might have -passed to it (see the the documentation of :meth:`.Scene.play` and -:meth:`.Scene.add_subcaption` for more information). +immediately passes over to the :class:`~.Manager._play` method of the :class:`.Manager`. +The one thing :meth:`.Scene.play` does before that is preparing the animations. +Whenever :attr:`.Mobject.animate` is called, it creates a new object called a +:class:`._AnimationBuilder`. We have to make sure to convert that into an actual +animation by calling it's :meth:`._AnimationBuilder.build` method. +We also have to update the animations with the correct rate functions, lag ratios, +and run time. + +.. note:: + + Methods in :class:`.Manager` starting with an underscore ``_`` are intended to be + private, and are not guaranteed to be stable across versions of Manim. The :class:`.Manager` + class provides some "public" methods (methods not prefixed with ``_``) that can be overridden to + change the behavior of the program. .. warning:: - As has been said before, the communication between scene and renderer - is not in a very clean state at this point, so the following paragraphs - might be confusing if you don't run a debugger and step through the - code yourself a bit. - -Inside :meth:`.CairoRenderer.play`, the renderer first checks whether -it may skip rendering of the current play call. This might happen, for example, -when ``-s`` is passed to the CLI (i.e., only the last frame should be rendered), -or when the ``-n`` flag is passed and the current play call is outside of the -specified render bounds. The "skipping status" is updated in form of the -call to :meth:`.CairoRenderer.update_skipping_status`. - -Next, the renderer asks the scene to process the animations in the play -call so that renderer obtains all of the information it needs. To -be more concrete, :meth:`.Scene.compile_animation_data` is called, -which then takes care of several things: - -- The method processes all animations and the keyword arguments passed - to the initial :meth:`.Scene.play` call. In particular, this means - that it makes sure all arguments passed to the play call are actually - animations (or ``.animate`` syntax calls, which are also assembled to - be actual :class:`.Animation`-objects at that point). It also propagates - any animation-related keyword arguments (like ``run_time``, - or ``rate_func``) passed to :class:`.Scene.play` to each individual - animation. The processed animations are then stored in the ``animations`` - attribute of the scene (which the renderer later reads...). -- It adds all mobjects to which the animations that are played are - bound to to the scene (provided the animation is not an mobject-introducing - animation -- for these, the addition to the scene happens later). -- In case the played animation is a :class:`.Wait` animation (this is the - case in a :meth:`.Scene.wait` call), the method checks whether a static - image should be rendered, or whether the render loop should be processed - as usual (see :meth:`.Scene.should_update_mobjects` for the exact conditions, - basically it checks whether there are any time-dependent updater functions - and so on). -- Finally, the method determines the total run time of the play call (which - at this point is computed as the maximum of the run times of the passed - animations). This is stored in the ``duration`` attribute of the scene. - - -After the animation data has been compiled by the scene, the renderer -continues to prepare for entering the render loop. It now checks the -skipping status which has been determined before. If the renderer can -skip this play call, it does so: it sets the current play call hash (which -we will get back to in a moment) to ``None`` and increases the time of the -renderer by the determined animation run time. - -Otherwise, the renderer checks whether or not Manim's caching system should + Subcaptions and audio is still in progress + + +After the :class:`.Scene` has done all the processing of animations, +it hands out control to the :class:`.Manager`. The :class:`.Manager` +then updates the skipping status of the :class:`.Scene`. This makes sure +that if ``-s`` or ``-n`` is used for sections, the scene does the correct +thing. + +The next important line is:: + + self._write_hashed_movie_file() + +Here, the :class:`.Manager` checks whether or not Manim's caching system should be used. The idea of the caching system is simple: for every play call, a hash value is computed, which is then stored and upon re-rendering the scene, the hash is generated again and checked against the stored value. If it is the @@ -761,40 +754,28 @@ to learn more, the :func:`.get_hash_from_play_call` function in the :mod:`.utils.hashing` module is essentially the entry point to the caching mechanism. -In the event that the animation has to be rendered, the renderer asks -its :class:`.SceneFileWriter` to open an output container. The process +In the event that the animation has to be rendered, the manager asks +its :class:`.FileWriter` to open an output container. The process is started by a call to ``libav`` and opens a container to which rendered raw frames can be written. As long as the output is open, the container can be accessed via the ``output_container`` attribute of the file writer. + With the writing process in place, the renderer then asks the scene to "begin" the animations. First, it literally *begins* all of the animations by calling their -setup methods (:meth:`.Animation._setup_scene`, :meth:`.Animation.begin`). +setup methods (:meth:`.Animation.begin`). In doing so, the mobjects that are newly introduced by an animation (like via :class:`.Create` etc.) are added to the scene. Furthermore, the animation suspends updater functions being called on its mobject, and it sets its mobject to the state that corresponds to the first frame of the animation. -After this has happened for all animations in the current ``play`` call, -the Cairo renderer determines which of the scene's mobjects can be -painted statically to the background, and which ones have to be -redrawn every frame. It does so by calling -:meth:`.Scene.get_moving_and_static_mobjects`, and the resulting -partition of mobjects is stored in the corresponding ``moving_mobjects`` -and ``static_mobjects`` attributes. +.. note:: -.. NOTE:: + Implementation of figuring out which mobjects have to be redrawn + is still in progress. - The mechanism that determines static and moving mobjects is - specific for the Cairo renderer, the OpenGL renderer works differently. - Basically, moving mobjects are determined by checking whether they, - any of their children, or any of the mobjects "below" them (in the - sense of the order in which mobjects are processed in the scene) - either have an update function attached, or whether they appear - in one of the current animations. See the implementation of - :meth:`.Scene.get_moving_mobjects` for more details. Up to this very point, we did not actually render any (partial) image or movie files from the scene yet. This is, however, about to change. @@ -835,68 +816,28 @@ Time to render some frames. The render loop (for real this time) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Now we get to the meat of rendering, which happens in :meth:`.Manager._progress_through_animations`. -As mentioned above, due to the mechanism that determines static and moving -mobjects in the scene, the renderer knows which mobjects it can paint -statically to the background of the scene. Practically, this means that -it partially renders a scene (to produce a background image), and then -when iterating through the time progression of the animation only the -"moving mobjects" are re-painted on top of the static background. - -The renderer calls :meth:`.CairoRenderer.save_static_frame_data`, which -first checks whether there are currently any static mobjects, and if there -are, it updates the frame (only with the static mobjects; more about how -exactly this works in a moment) and then saves a NumPy array representing -the rendered frame in the ``static_image`` attribute. In our toy example, -there are no static mobjects, and so the ``static_image`` attribute is -simply set to ``None``. - -Next, the renderer asks the scene whether the current animation is -a "frozen frame" animation, which would mean that the renderer actually -does not have to repaint the moving mobjects in every frame of the time -progression. It can then just take the latest static frame, and display it -throughout the animation. - -.. NOTE:: - - An animation is considered a "frozen frame" animation if only a - static :class:`.Wait` animation is played. See the description - of :meth:`.Scene.compile_animation_data` above, or the - implementation of :meth:`.Scene.should_update_mobjects` for - more details. - -If this is not the case (just as in our toy example), the renderer -then calls the :meth:`.Scene.play_internal` method, which is the -integral part of the render loop (in which the library steps through -the time progression of the animation and renders the corresponding -frames). - -Within :meth:`.Scene.play_internal`, the following steps are performed: - -- The scene determines the run time of the animations by calling - :meth:`.Scene.get_run_time`. This method basically takes the maximum +- The manager determines the run time of the animations by calling + :meth:`.Manager._calc_run_time`. This method basically takes the maximum ``run_time`` attribute of all of the animations passed to the :meth:`.Scene.play` call. -- Then the *time progression* is constructed via the (internal) - :meth:`.Scene._get_animation_time_progression` method, which wraps - the actual :meth:`.Scene.get_time_progression` method. The time - progression is a ``tqdm`` `progress bar object `__ - for an iterator over ``np.arange(0, run_time, 1 / config.frame_rate)``. In +- Then, the progressbar is created by :meth:`.Manager._create_progressbar`, + which returns a ``tqdm`` `progress bar object `__ + object (from the ``tqdm`` library), or a fake progressbar if + :attr:`.ManimConfig.write_to_movie` is ``False``. +- Then the *time progression* is constructed via + :meth:`.Manager._calc_time_progression` method, which returns + ``np.arange(0, run_time, 1 / config.frame_rate)``. In other words, the time progression holds the time stamps (relative to the current animations, so starting at 0 and ending at the total animation run time, with the step size determined by the render frame rate) of the timeline where a new animation frame should be rendered. - Then the scene iterates over the time progression: for each time stamp ``t``, - :meth:`.Scene.update_to_time` is called, which ... - - - ... first computes the time passed since the last update (which might be 0, - especially for the initial call) and references it as ``dt``, - - then (in the order in which the animations are passed to :meth:`.Scene.play`) - calls :meth:`.Animation.update_mobjects` to trigger all updater functions that - are attached to the respective animation except for the "main mobject" of - the animation (that is, for example, for :class:`.Transform` the unmodified - copies of start and target mobject -- see :meth:`.Animation.get_all_mobjects_to_update` - for more details), + we find the time difference between the current and previous frame (AKA ``dt``). + We then update the animations in the scene using ``dt`` by + - iterating over each animation + - next, we update the animations mobjects - then the relative time progression with respect to the current animation is computed (``alpha = t / animation.run_time``), which is then used to update the state of the animation with a call to :meth:`.Animation.interpolate`. @@ -904,62 +845,29 @@ Within :meth:`.Scene.play_internal`, the following steps are performed: of all mobjects in the scene, all meshes, and finally those attached to the scene itself are run. -At this point, the internal (Python) state of all mobjects has been updated -to match the currently processed timestamp. If rendering should not be skipped, -then it is now time to *take a picture*! + After updating the animations, we pass ``dt`` to :meth:`.Manager._update_frame` which... + + - ... updates the total time passed + - Updates all the mobjects by calling :meth:`.Scene._update_mobjects`. This in turn + iterates over all the mobjects on the screen and updates them. + - After that, the current state of the scene is computed by :meth:`.Scene.get_state`, + which returns a :class:`.SceneState`. + - The state is then passed into :meth:`.Manager._render_frame`, which gets + the renderer to create the pixels. With :class:`.OpenGLRenderer`, this + also updates the window. :meth:`~.Manager._render_frame` also checks if it should write a frame, + and if so, writes a frame via the :class:`.FileWriter`. + - Finally, it uses a concept of virtual time vs real time to see + if the right amount of time has passed in the window. The virtual + time is the amount of time that is supposed to have passed (that is, ``t``). + The real time is how much time has actually passed in the window + (current time - start time of play). If the animations are progressing + faster than they would in real life, it will slow down the window by calling + :meth:`~.Manager._update_frame` with ``dt=0`` until that's no longer the case. + This is to make sure that animations never go too fast: it doesn't do anything if + animations are too slow! -.. NOTE:: - - The update of the internal state (iteration over the time progression) happens - *always* once :meth:`.Scene.play_internal` is entered. This ensures that even - if frames do not need to be rendered (because, e.g., the ``-n`` CLI flag has - been passed, something has been cached, or because we might be in a *Section* - with skipped rendering), updater functions still run correctly, and the state - of the first frame that *is* rendered is kept consistent. - -To render an image, the scene calls the corresponding method of its renderer, -:meth:`.CairoRenderer.render` and passes just the list of *moving mobjects* (remember, -the *static mobjects* are assumed to have already been painted statically to -the background of the scene). All of the hard work then happens when the renderer -updates its current frame via a call to :meth:`.CairoRenderer.update_frame`: - -First, the renderer prepares its :class:`.Camera` by checking whether the renderer -has a ``static_image`` different from ``None`` stored already. If so, it sets the -image as the *background image* of the camera via :meth:`.Camera.set_frame_to_background`, -and otherwise it just resets the camera via :meth:`.Camera.reset`. The camera is then -asked to capture the scene with a call to :meth:`.Camera.capture_mobjects`. - -Things get a bit technical here, and at some point it is more efficient to -delve into the implementation -- but here is a summary of what happens once the -camera is asked to capture the scene: - -- First, a flat list of mobjects is created (so submobjects get extracted from - their parents). This list is then processed in groups of the same type of - mobjects (e.g., a batch of vectorized mobjects, followed by a batch of image mobjects, - followed by more vectorized mobjects, etc. -- in many cases there will just be - one batch of vectorized mobjects). -- Depending on the type of the currently processed batch, the camera uses dedicated - *display functions* to convert the :class:`.Mobject` Python object to - a NumPy array stored in the camera's ``pixel_array`` attribute. - The most important example in that context is the display function for - vectorized mobjects, :meth:`.Camera.display_multiple_vectorized_mobjects`, - or the more particular (in case you did not add a background image to your - :class:`.VMobject`), :meth:`.Camera.display_multiple_non_background_colored_vmobjects`. - This method first gets the current Cairo context, and then, for every (vectorized) - mobject in the batch, calls :meth:`.Camera.display_vectorized`. There, - the actual background stroke, fill, and then stroke of the mobject is - drawn onto the context. See :meth:`.Camera.apply_stroke` and - :meth:`.Camera.set_cairo_context_color` for more details -- but it does not get - much deeper than that, in the latter method the actual Bézier curves - determined by the points of the mobject are drawn; this is where the low-level - interaction with Cairo happens. - -After all batches have been processed, the camera has an image representation -of the Scene at the current time stamp in form of a NumPy array stored in its -``pixel_array`` attribute. The renderer then takes this array and passes it to -its :class:`.SceneFileWriter`. This concludes one iteration of the render loop, -and once the time progression has been processed completely, a final bit -of cleanup is performed before the :meth:`.Scene.play_internal` call is completed. +At this point, the internal (Python) state of all mobjects has been updated +to match the currently processed timestamp. A TL;DR for the render loop, in the context of our toy example, reads as follows: @@ -968,23 +876,20 @@ A TL;DR for the render loop, in the context of our toy example, reads as follows medium render quality, the frame rate is 30 frames per second, and so the time progression with steps ``[0, 1/30, 2/30, ..., 89/30]`` is created. - In the internal render loop, each of these time stamps is processed: - there are no updater functions, so effectively the scene updates the + there are no updater functions, so effectively the manager updates the state of the transformation animation to the desired time stamp (for example, at time stamp ``t = 45/30``, the animation is completed to a rate of ``alpha = 0.5``). -- Then the scene asks the renderer to do its job. The renderer asks its camera - to capture the scene, the only mobject that needs to be processed at this point - is the main mobject attached to the transformation; the camera converts the - current state of the mobject to entries in a NumPy array. The renderer passes - this array to the file writer. +- Then the manager asks the renderer to do its job. The renderer then produces + the pixels, which are then fed into the :class:`.FileWriter`. - At the end of the loop, 90 frames have been passed to the file writer. Completing the render loop ^^^^^^^^^^^^^^^^^^^^^^^^^^ -The last few steps in the :meth:`.Scene.play_internal` call are not too +The last few steps in the :meth:`.Manager._play` call are not too exciting: for every animation, the corresponding :meth:`.Animation.finish` -and :meth:`.Animation.clean_up_from_scene` methods are called. +method is called. .. NOTE:: @@ -999,10 +904,7 @@ and :meth:`.Animation.clean_up_from_scene` methods are called. would be slightly longer than 1 second. We decided against this at some point. In the end, the time progression is closed (which completes the displayed progress bar) -in the terminal. With the closing of the time progression, the -:meth:`.Scene.play_internal` call is completed, and we return to the renderer, -which now orders the :class:`.SceneFileWriter` to close the output container that has -been opened for this animation: a partial movie file is written. +in the terminal. This pretty much concludes the walkthrough of a :class:`.Scene.play` call, and actually there is not too much more to say for our toy example either: at @@ -1025,5 +927,4 @@ which triggers the combination of the partial movie files into the final product And there you go! This is a more or less detailed description of how Manim works under the hood. While we did not discuss every single line of code in detail in this walkthrough, it should still give you a fairly good idea of how the general -structural design of the library and at least the Cairo rendering flow in particular -looks like. +structural design of the library looks like. diff --git a/docs/source/installation/macos.rst b/docs/source/installation/macos.rst index 8a0d2b817a..1edd21dd5f 100644 --- a/docs/source/installation/macos.rst +++ b/docs/source/installation/macos.rst @@ -17,46 +17,15 @@ follow `Homebrew's installation instructions (and is recommended to) be installed natively. -Required Dependencies ---------------------- +Installing Manim +---------------- -To install all required dependencies for installing Manim (namely: Python, -and some required Python packages), run: +As of July/2024, brew can install Manim including all required dependencies. +To install Manim: .. code-block:: bash - brew install py3cairo - -On *Apple Silicon* based machines (i.e., devices with the M1 chip or similar; if -you are unsure which processor you have check by opening the Apple menu, select -*About This Mac* and check the entry next to *Chip*), some additional dependencies -are required, namely: - -.. code-block:: bash - - brew install pango pkg-config scipy - -After all required dependencies are installed, simply run: - -.. code-block:: bash - - pip3 install manim - -to install Manim. - -.. note:: - - A frequent source for installation problems is if ``pip3`` - does not point to the correct Python installation on your system. - To check this, run ``pip3 -V``: for macOS Intel, the path should - start with ``/usr/local``, and for Apple Silicon with - ``/opt/homebrew``. If this is not the case, you either forgot - to modify your shell profile (``.zprofile``) during the installation - of Homebrew, or did not reload your shell (e.g., by opening a new - terminal) after doing so. It is also possible that some other - software (like Pycharm) changed the ``PATH`` variable – to fix this, - make sure that the Homebrew-related lines in your ``.zprofile`` are - at the very end of the file. + brew install manim .. _macos-optional-dependencies: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index c4c631b655..7bd8c0061c 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -106,7 +106,7 @@ specified in Poetry as: [tool.poetry.plugins."manim.plugins"] "name" = "object_reference" -.. versionremoved:: 0.19.0 +.. versionremoved:: 0.18.1 Plugins should be imported explicitly to be usable in user code. The plugin system will probably be refactored in the future to provide a more structured diff --git a/docs/source/tutorials/building_blocks.rst b/docs/source/tutorials/building_blocks.rst index a01874535e..d507d1c5fc 100644 --- a/docs/source/tutorials/building_blocks.rst +++ b/docs/source/tutorials/building_blocks.rst @@ -297,9 +297,9 @@ Creating a custom animation Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another. If you find yourself in that situation, then you can define your own custom animation. -You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate_mobject`. -The :meth:`~.Animation.interpolate_mobject` method receives alpha as a parameter that starts at 0 and changes throughout the animation. -So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate_mobject method. +You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate`. +The :meth:`~.Animation.interpolate` method receives alpha as a parameter that starts at 0 and changes throughout the animation. +So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate method. Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions. Let's say you start with a number and want to create a :class:`~.Transform` animation that transforms it to a target number. @@ -312,11 +312,11 @@ The class can have a constructor with three arguments, a :class:`~.DecimalNumber The constructor will pass the :class:`~.DecimalNumber` Mobject to the super constructor (in this case, the :class:`~.Animation` constructor) and will set start and end. The only thing that you need to do is to define how you want it to look at every step of the animation. -Manim provides you with the alpha value in the :meth:`~.Animation.interpolate_mobject` method based on frame rate of video, rate function, and run time of animation played. +Manim provides you with the alpha value in the :meth:`~.Animation.interpolate` method based on frame rate of video, rate function, and run time of animation played. The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation. For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation. -In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate_mobject` method of the ``Count`` animation. +In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate` method of the ``Count`` animation. Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation. * If alpha is 0, you want the value to be 50. @@ -331,7 +331,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:` .. manim:: CountingScene :ref_classes: Animation DecimalNumber - :ref_methods: Animation.interpolate_mobject Scene.play + :ref_methods: Animation.interpolate Scene.play class Count(Animation): def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None: @@ -341,7 +341,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:` self.start = start self.end = end - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: # Set value of DecimalNumber according to alpha value = self.start + (alpha * (self.end - self.start)) self.mobject.set_value(value) diff --git a/example_scenes/new_test_new.py b/example_scenes/new_test_new.py index 9d01c3f851..ff68c63aec 100644 --- a/example_scenes/new_test_new.py +++ b/example_scenes/new_test_new.py @@ -1,7 +1,8 @@ import time import numpy as np -import pyglet + +# import pyglet from pyglet.gl import Config from pyglet.window import Window @@ -64,7 +65,7 @@ # exit(0) renderer.use_window() - clock = pyglet.clock.get_default() + # clock = pyglet.clock.get_default() def update_circle(dt): vm.move_to((np.sin(dt) * 4, np.cos(dt) * 4, -1)) @@ -98,7 +99,7 @@ def on_mouse_motion(x, y, dx, dy): @win.event def on_draw(): - dt = clock.update_time() + # dt = clock.update_time() renderer.render(camera, [vm2, vm3, vm4, clock_mobject, vm]) # update_circle(counter) @@ -135,7 +136,7 @@ def on_resize(width, height): if virtual_time >= run_time: animation.finish() buffer = str(animation.buffer) - print(f"{buffer = }") + print(f"buffer = {buffer}") has_finished = True else: animation.update_mobjects(dt) diff --git a/example_scenes/test_new.py b/example_scenes/test_new.py index 06d3c8ba98..ee45d4359b 100644 --- a/example_scenes/test_new.py +++ b/example_scenes/test_new.py @@ -5,7 +5,7 @@ import manim.utils.color.manim_colors as col from manim._config import tempconfig -from manim.camera.camera import OpenGLCameraFrame +from manim.camera.camera import Camera from manim.constants import OUT, RIGHT, UP from manim.mobject.geometry.arc import Circle from manim.mobject.geometry.polygram import Square @@ -47,7 +47,7 @@ # print(vm.fill_color) # print(vm.stroke_color) - camera = OpenGLCameraFrame() + camera = Camera() camera.save_state() renderer.init_camera(camera) diff --git a/example_scenes/test_new_rendering.py b/example_scenes/test_new_rendering.py index 68485607ac..0cc6bc8152 100644 --- a/example_scenes/test_new_rendering.py +++ b/example_scenes/test_new_rendering.py @@ -4,10 +4,31 @@ class Test(Scene): def construct(self) -> None: s = Square() + self.add(s) + self.play(Rotate(s, PI / 2)) + self.play(FadeOut(s)) + sq = RegularPolygon(6) c = Circle() - st = Star(color=YELLOW, fill_color=YELLOW) - self.play(Succession(*[Create(x) for x in VGroup(s, c, st).arrange()])) + st = Star() + VGroup(sq, c, st).arrange() + self.play( + Succession( + Create(sq), + DrawBorderThenFill(c), + Create(st), + ) + ) + self.play(FadeOut(VGroup(*self.mobjects))) -with tempconfig({"renderer": "opengl", "preview": True, "parallel": False}): - Manager(Test).render() +if __name__ == "__main__": + with tempconfig( + { + "preview": True, + "write_to_movie": False, + "disable_caching": True, + "frame_rate": 60, + "disable_caching_warning": True, + } + ): + Manager(Test).render() diff --git a/manim/__init__.py b/manim/__init__.py index bb8699a27e..bd85300125 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -10,90 +10,89 @@ # Importing the config module should be the first thing we do, since other # modules depend on the global config dict for initialization. -from ._config import * +from manim._config import * # many scripts depend on this -> has to be loaded first -from .utils.commands import * +from manim.utils.commands import * # isort: on import numpy as np -from .animation.animation import * -from .animation.changing import * -from .animation.composition import * -from .animation.creation import * -from .animation.fading import * -from .animation.growing import * -from .animation.indication import * -from .animation.movement import * -from .animation.numbers import * -from .animation.rotation import * -from .animation.specialized import * -from .animation.speedmodifier import * -from .animation.transform import * -from .animation.transform_matching_parts import * -from .animation.updaters.mobject_update_utils import * -from .animation.updaters.update import * -from .constants import * -from .mobject.frame import * -from .mobject.geometry.arc import * -from .mobject.geometry.boolean_ops import * -from .mobject.geometry.labeled import * -from .mobject.geometry.line import * -from .mobject.geometry.polygram import * -from .mobject.geometry.shape_matchers import * -from .mobject.geometry.tips import * -from .mobject.graph import * -from .mobject.graphing.coordinate_systems import * -from .mobject.graphing.functions import * -from .mobject.graphing.number_line import * -from .mobject.graphing.probability import * -from .mobject.graphing.scale import * -from .mobject.logo import * -from .mobject.matrix import * -from .mobject.mobject import * -from .mobject.opengl.dot_cloud import * -from .mobject.opengl.opengl_point_cloud_mobject import * -from .mobject.svg.brace import * -from .mobject.svg.svg_mobject import * -from .mobject.table import * -from .mobject.text.code_mobject import * -from .mobject.text.numbers import * -from .mobject.text.tex_mobject import * -from .mobject.text.text_mobject import * -from .mobject.three_d.polyhedra import * -from .mobject.three_d.three_d_utils import * -from .mobject.three_d.three_dimensions import * -from .mobject.types.image_mobject import * -from .mobject.types.point_cloud_mobject import * -from .mobject.types.vectorized_mobject import * -from .mobject.value_tracker import * -from .mobject.vector_field import * -from .renderer.render_manager import * -from .scene.scene import * -from .scene.scene_file_writer import * -from .scene.section import * -from .scene.vector_space_scene import * -from .utils import color, rate_functions, unit -from .utils.bezier import * -from .utils.color import * -from .utils.config_ops import * -from .utils.debug import * -from .utils.file_ops import * -from .utils.images import * -from .utils.iterables import * -from .utils.paths import * -from .utils.rate_functions import * -from .utils.simple_functions import * -from .utils.sounds import * -from .utils.space_ops import * -from .utils.tex import * -from .utils.tex_templates import * +from manim.animation.animation import * +from manim.animation.changing import * +from manim.animation.composition import * +from manim.animation.creation import * +from manim.animation.fading import * +from manim.animation.growing import * +from manim.animation.indication import * +from manim.animation.movement import * +from manim.animation.numbers import * +from manim.animation.rotation import * +from manim.animation.specialized import * +from manim.animation.speedmodifier import * +from manim.animation.transform import * +from manim.animation.transform_matching_parts import * +from manim.animation.updaters.mobject_update_utils import * +from manim.animation.updaters.update import * +from manim.constants import * +from manim.file_writer import * +from manim.manager import * +from manim.mobject.frame import * +from manim.mobject.geometry.arc import * +from manim.mobject.geometry.boolean_ops import * +from manim.mobject.geometry.labeled import * +from manim.mobject.geometry.line import * +from manim.mobject.geometry.polygram import * +from manim.mobject.geometry.shape_matchers import * +from manim.mobject.geometry.tips import * +from manim.mobject.graph import * +from manim.mobject.graphing.coordinate_systems import * +from manim.mobject.graphing.functions import * +from manim.mobject.graphing.number_line import * +from manim.mobject.graphing.probability import * +from manim.mobject.graphing.scale import * +from manim.mobject.logo import * +from manim.mobject.matrix import * +from manim.mobject.mobject import * +from manim.mobject.opengl.dot_cloud import * +from manim.mobject.opengl.opengl_point_cloud_mobject import * +from manim.mobject.svg.brace import * +from manim.mobject.svg.svg_mobject import * +from manim.mobject.table import * +from manim.mobject.text.code_mobject import * +from manim.mobject.text.numbers import * +from manim.mobject.text.tex_mobject import * +from manim.mobject.text.text_mobject import * +from manim.mobject.three_d.polyhedra import * +from manim.mobject.three_d.three_d_utils import * +from manim.mobject.three_d.three_dimensions import * +from manim.mobject.types.image_mobject import * +from manim.mobject.types.point_cloud_mobject import * +from manim.mobject.types.vectorized_mobject import * +from manim.mobject.value_tracker import * +from manim.mobject.vector_field import * +from manim.scene.scene import * +from manim.scene.vector_space_scene import * +from manim.utils import color, rate_functions, unit +from manim.utils.bezier import * +from manim.utils.color import * +from manim.utils.config_ops import * +from manim.utils.debug import * +from manim.utils.file_ops import * +from manim.utils.images import * +from manim.utils.iterables import * +from manim.utils.paths import * +from manim.utils.rate_functions import * +from manim.utils.simple_functions import * +from manim.utils.sounds import * +from manim.utils.space_ops import * +from manim.utils.tex import * +from manim.utils.tex_templates import * try: from IPython import get_ipython - from .utils.ipython_magic import ManimMagic + from manim.utils.ipython_magic import ManimMagic except ImportError: pass else: @@ -101,4 +100,4 @@ if ipy is not None: ipy.register_magics(ManimMagic) -from .plugins import * +from manim.plugins import * diff --git a/manim/_config/default.cfg b/manim/_config/default.cfg index 9155d28dbd..0c7d6ddbf7 100644 --- a/manim/_config/default.cfg +++ b/manim/_config/default.cfg @@ -7,18 +7,18 @@ # Each of the following will be set to True if the corresponding CLI flag # is present when executing manim. If the flag is not present, they will -# be set to the value found here. For example, running manim with the -w -# flag will set WRITE_TO_MOVIE to True. However, since the default value -# of WRITE_TO_MOVIE defined in this file is also True, running manim -# without the -w value will also output a movie file. To change that, set -# WRITE_TO_MOVIE = False so that running manim without the -w flag will not -# generate a movie file. Note all of the following accept boolean values. +# be set to the value found here. For example, running manim with the --format=mp4 +# flag will set FORMAT to mp4. However, since the default value +# of FORMAT defined in this file is also mp4, running manim +# without the --format=mp4 value will also output an mp4 movie file. To change that, set +# FORMAT = webm so that running manim without the --format=mp4 flag will not +# generate an mp4 movie file. # --notify_outdated_version notify_outdated_version = True # -w, --write_to_movie -write_to_movie = True +write_to_movie = False format = mp4 @@ -94,7 +94,7 @@ text_dir = {media_dir}/texts partial_movie_dir = {video_dir}/partial_movie_files/{scene_name} # --renderer [cairo|opengl] -renderer = cairo +renderer = opengl # --enable_gui enable_gui = False diff --git a/manim/_config/utils.py b/manim/_config/utils.py index 17073796fa..c856aa4cd1 100644 --- a/manim/_config/utils.py +++ b/manim/_config/utils.py @@ -316,7 +316,6 @@ class MyScene(Scene): ... "write_to_movie", "zero_pad", "force_window", - "parallel", "no_latex_cleanup", "preview_command", } @@ -324,6 +323,19 @@ class MyScene(Scene): ... def __init__(self) -> None: self._d: dict[str, Any | None] = {k: None for k in self._OPTS} + def _warn_about_config_options(self) -> None: + """Warns about incorrect config options, or permutations of config options.""" + + logger = logging.getLogger("manim") + if self.format == "webm": + logger.warning( + "Output format set as webm, this can be slower than other formats", + ) + if not self.preview and not self.write_to_movie: + logger.warning( + "preview and write_to_movie disabled, this is a dry run. Try passing -p or -w." + ) + # behave like a dict def __iter__(self) -> Iterator[str]: return iter(self._d) @@ -590,7 +602,6 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self: "use_projection_stroke_shaders", "enable_wireframe", "force_window", - "parallel", "no_latex_cleanup", ]: setattr(self, key, parser["CLI"].getboolean(key, fallback=False)) @@ -939,6 +950,7 @@ def notify_outdated_version(self) -> bool: def notify_outdated_version(self, value: bool) -> None: self._set_boolean("notify_outdated_version", value) + # TODO: Rename to write_to_file @property def write_to_movie(self) -> bool: """Whether to render the scene to a movie file (-w).""" @@ -1054,18 +1066,7 @@ def format(self, val: str) -> None: val, [None, "png", "gif", "mp4", "mov", "webm"], ) - if self.format == "webm": - logging.getLogger("manim").warning( - "Output format set as webm, this can be slower than other formats", - ) - - @property - def in_parallel(self) -> None: - return self._d["parallel"] - - @in_parallel.setter - def in_parallel(self, val: bool) -> None: - self._set_boolean("parallel", val) + self.resolve_movie_file_extension(self.transparent) @property def ffmpeg_loglevel(self) -> str: diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 36be27764a..f06647a75c 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -2,6 +2,8 @@ from __future__ import annotations +import numpy as np + from manim.mobject.opengl.opengl_mobject import OpenGLMobject from .. import config, logger @@ -11,7 +13,7 @@ from ..mobject.opengl import opengl_mobject from ..utils.rate_functions import linear, smooth from .protocol import AnimationProtocol -from .scene_buffer import SceneBuffer +from .scene_buffer import SceneBuffer, SceneOperation __all__ = ["Animation", "Wait", "override_animation"] @@ -72,9 +74,9 @@ class Animation(AnimationProtocol): .. NOTE:: In the current implementation of this class, the specified rate function is applied - within :meth:`.Animation.interpolate_mobject` call as part of the call to + within :meth:`.Animation.interpolate` call as part of the call to :meth:`.Animation.interpolate_submobject`. For subclasses of :class:`.Animation` - that are implemented by overriding :meth:`interpolate_mobject`, the rate function + that are implemented by overriding :meth:`interpolate`, the rate function has to be applied manually (e.g., by passing ``self.rate_func(alpha)`` instead of just ``alpha``). @@ -135,7 +137,7 @@ def __init__( run_time: float = DEFAULT_ANIMATION_RUN_TIME, rate_func: Callable[[float], float] = smooth, reverse_rate_function: bool = False, - name: str | None = None, + name: str = "", remover: bool = False, # remove a mobject from the screen at end of animation suspend_mobject_updating: bool = True, introducer: bool = False, @@ -147,7 +149,7 @@ def __init__( self.run_time: float = run_time self.rate_func: Callable[[float], float] = rate_func self.reverse_rate_function: bool = reverse_rate_function - self.name: str | None = name + self.name: str = name self.remover: bool = remover self.introducer: bool = introducer self.suspend_mobject_updating: bool = suspend_mobject_updating @@ -218,7 +220,7 @@ def begin(self) -> None: # TODO: Figure out a way to check # if self.mobject in scene.get_mobject_family - if self.is_introducer(): + if self.introducer: self.buffer.add(self.mobject) def finish(self) -> None: @@ -231,15 +233,16 @@ def finish(self) -> None: if self.suspend_mobject_updating and self.mobject is not None: self.mobject.resume_updating() + # TODO: remove on_finish self._on_finish(self.buffer) - if self.is_remover(): + if self.remover: self.buffer.remove(self.mobject) - def create_starting_mobject(self) -> Mobject: + def create_starting_mobject(self) -> OpenGLMobject: # Keep track of where the mobject starts return self.mobject.copy() - def get_all_mobjects(self) -> Sequence[Mobject]: + def get_all_mobjects(self) -> Sequence[OpenGLMobject]: """Get all mobjects involved in the animation. Ordering must match the ordering of arguments to interpolate_submobject @@ -274,13 +277,19 @@ def process_subanimation_buffer(self, buffer: SceneBuffer): This is used in animations that are proxies around other animations, like :class:`.AnimationGroup` """ - self.buffer.remove(*buffer.to_remove) - for to_replace_pairs in buffer.to_replace: - self.buffer.replace(*to_replace_pairs) - self.buffer.add(*buffer.to_add) + for op, args, kwargs in buffer: + match op: + case SceneOperation.ADD: + self.buffer.add(*args, **kwargs) + case SceneOperation.REMOVE: + self.buffer.remove(*args, **kwargs) + case SceneOperation.REPLACE: + self.buffer.replace(*args, **kwargs) + case o: + raise NotImplementedError(f"Unknown operation {o}") buffer.clear() - def get_all_mobjects_to_update(self) -> list[Mobject]: + def get_all_mobjects_to_update(self) -> Sequence[OpenGLMobject]: """Get all mobjects to be updated during the animation. Returns @@ -307,19 +316,6 @@ def copy(self) -> Self: # TODO: stop using alpha as parameter name in different meanings. def interpolate(self, alpha: float) -> None: - """Set the animation progress. - - This method gets called for every frame during an animation. - - Parameters - ---------- - alpha - The relative time to set the animation to, 0 meaning the start, 1 meaning - the end. - """ - self.interpolate_mobject(alpha) - - def interpolate_mobject(self, alpha: float) -> None: """Interpolates the mobject of the :class:`Animation` based on alpha value. Parameters @@ -332,17 +328,16 @@ def interpolate_mobject(self, alpha: float) -> None: families = tuple(self.get_all_families_zipped()) for i, mobs in enumerate(families): sub_alpha = self.get_sub_alpha(alpha, i, len(families)) - self.interpolate_submobject(*mobs, sub_alpha) + self.interpolate_submobject(*mobs, sub_alpha) # type: ignore def interpolate_submobject( self, - submobject: Mobject, - starting_submobject: Mobject, + submobject: OpenGLMobject, + starting_submobject: OpenGLMobject, # target_copy: Mobject, #Todo: fix - signature of interpolate_submobject differs in Transform(). alpha: float, ) -> Animation: - # Typically implemented by subclass - raise NotImplementedError() + raise NotImplementedError("Implement in subclass") def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float: """Get the animation progress of any submobjects subanimation. @@ -368,13 +363,14 @@ def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float full_length = (num_submobjects - 1) * lag_ratio + 1 value = alpha * full_length lower = index * lag_ratio + raw_sub_alpha = np.clip((value - lower), 0, 1) if self.reverse_rate_function: - return self.rate_func(1 - (value - lower)) + return self.rate_func(1 - raw_sub_alpha) else: - return self.rate_func(value - lower) + return self.rate_func(raw_sub_alpha) # Getters and setters - def set_run_time(self, run_time: float) -> Animation: + def set_run_time(self, run_time: float) -> Self: """Set the run time of the animation. Parameters @@ -454,31 +450,12 @@ def set_name(self, name: str) -> Self: self.name = name return self - def is_remover(self) -> bool: - """Test if the animation is a remover. - - Returns - ------- - bool - ``True`` if the animation is a remover, ``False`` otherwise. - """ - return self.remover - - def is_introducer(self) -> bool: - """Test if the animation is an introducer. - - Returns - ------- - bool - ``True`` if the animation is an introducer, ``False`` otherwise. - """ - return self.introducer - def prepare_animation( anim: AnimationProtocol | mobject._AnimationBuilder - | opengl_mobject._AnimationBuilder, + | opengl_mobject._AnimationBuilder + | opengl_mobject.OpenGLMobject, ) -> Animation: r"""Returns either an unchanged animation, or the animation built from a passed animation factory. @@ -551,10 +528,8 @@ def __init__( self.duration: float = run_time self.stop_condition = stop_condition - self.is_static_wait: bool = frozen_frame + self.is_static_wait: bool = bool(frozen_frame) super().__init__(None, run_time=run_time, rate_func=rate_func, **kwargs) - # quick fix to work in opengl setting: - self.mobject.shader_wrapper_list = [] def begin(self) -> None: pass @@ -611,7 +586,7 @@ def construct(self): _F = TypeVar("_F", bound=Callable) def decorator(func: _F) -> _F: - func._override_animation = animation_class + func._override_animation = animation_class # type: ignore return func return decorator diff --git a/manim/animation/composition.py b/manim/animation/composition.py index fda3220366..0802850da6 100644 --- a/manim/animation/composition.py +++ b/manim/animation/composition.py @@ -16,10 +16,6 @@ from manim.utils.parameter_parsing import flatten_iterable_parameters from manim.utils.rate_functions import linear -from ..animation.animation import Animation, prepare_animation -from ..constants import RendererType -from ..mobject.mobject import Group, Mobject - if TYPE_CHECKING: from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup from manim.mobject.types.vectorized_mobject import VGroup @@ -70,7 +66,7 @@ def __init__( self.group = group if self.group is None: mobjects = remove_list_redundancies( - [anim.mobject for anim in self.animations if not anim.is_introducer()], + [anim.mobject for anim in self.animations if not anim.introducer], ) if config["renderer"] == RendererType.OPENGL: self.group = OpenGLGroup(*mobjects) @@ -234,10 +230,6 @@ def begin(self) -> None: ) self.update_active_animation(0) - for anim in self.animations: - if not anim.is_introducer() and anim.mobject is not None: - self.buffer.add(anim.mobject) - def finish(self) -> None: while self.active_animation is not None: self.next_animation() diff --git a/manim/animation/creation.py b/manim/animation/creation.py index 49a198a05e..1124575b99 100644 --- a/manim/animation/creation.py +++ b/manim/animation/creation.py @@ -475,7 +475,7 @@ def __init__( super().__init__(shapes, introducer=True, **kwargs) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: alpha = self.rate_func(alpha) for original_shape, shape in zip(self.shapes, self.mobject): shape.restore() @@ -529,7 +529,7 @@ def __init__( **kwargs, ) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: n_submobs = len(self.all_submobs) value = ( 1 - self.rate_func(alpha) diff --git a/manim/animation/fading.py b/manim/animation/fading.py index 5a5ee86197..f25cd44ade 100644 --- a/manim/animation/fading.py +++ b/manim/animation/fading.py @@ -27,14 +27,14 @@ def construct(self): from ..animation.transform import Transform from ..constants import ORIGIN -from ..mobject.mobject import Group, Mobject +from ..mobject.mobject import Group if TYPE_CHECKING: pass class _Fade(Transform): - """Fade :class:`~.Mobject` s in or out. + """Fade :class:`~.OpenGLMobject` s in or out. Parameters ---------- @@ -53,9 +53,9 @@ class _Fade(Transform): def __init__( self, - *mobjects: Mobject, + *mobjects: OpenGLMobject, shift: np.ndarray | None = None, - target_position: np.ndarray | Mobject | None = None, + target_position: np.ndarray | OpenGLMobject | None = None, scale: float = 1, **kwargs, ) -> None: @@ -69,7 +69,7 @@ def __init__( self.point_target = False if shift is None: if target_position is not None: - if isinstance(target_position, (Mobject, OpenGLMobject)): + if isinstance(target_position, OpenGLMobject): target_position = target_position.get_center() shift = target_position - mobject.get_center() self.point_target = True @@ -79,7 +79,7 @@ def __init__( self.scale_factor = scale super().__init__(mobject, **kwargs) - def _create_faded_mobject(self, fade_in: bool) -> Mobject: + def _create_faded_mobject(self, fade_in: bool) -> OpenGLMobject: """Create a faded, shifted and scaled copy of the mobject. Parameters @@ -89,7 +89,7 @@ def _create_faded_mobject(self, fade_in: bool) -> Mobject: Returns ------- - Mobject + OpenGLMobject The faded, shifted and scaled copy of the mobject. """ faded_mobject = self.mobject.copy() @@ -101,7 +101,7 @@ def _create_faded_mobject(self, fade_in: bool) -> Mobject: class FadeIn(_Fade): - """Fade in :class:`~.Mobject` s. + """Fade in :class:`~.OpenGLMobject` s. Parameters ---------- @@ -138,7 +138,7 @@ def construct(self): """ - def __init__(self, *mobjects: Mobject, **kwargs) -> None: + def __init__(self, *mobjects: OpenGLMobject, **kwargs) -> None: super().__init__(*mobjects, introducer=True, **kwargs) def create_target(self): @@ -149,7 +149,7 @@ def create_starting_mobject(self): class FadeOut(_Fade): - """Fade out :class:`~.Mobject` s. + """Fade out :class:`~.OpenGLMobject` s. Parameters ---------- @@ -186,7 +186,7 @@ def construct(self): """ - def __init__(self, *mobjects: Mobject, **kwargs) -> None: + def __init__(self, *mobjects: OpenGLMobject, **kwargs) -> None: super().__init__(*mobjects, remover=True, **kwargs) def create_target(self): diff --git a/manim/animation/movement.py b/manim/animation/movement.py index 2e61d2c182..24f627f292 100644 --- a/manim/animation/movement.py +++ b/manim/animation/movement.py @@ -126,7 +126,7 @@ def __init__( **kwargs, ) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: if hasattr(self, "last_alpha"): dt = self.virtual_time * ( self.rate_func(alpha) - self.rate_func(self.last_alpha) @@ -162,6 +162,6 @@ def __init__( mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs ) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: point = self.path.point_from_proportion(self.rate_func(alpha)) self.mobject.move_to(point) diff --git a/manim/animation/numbers.py b/manim/animation/numbers.py index 86bfe7154b..4f257170c1 100644 --- a/manim/animation/numbers.py +++ b/manim/animation/numbers.py @@ -31,7 +31,7 @@ def check_validity_of_input(self, decimal_mob: DecimalNumber) -> None: if not isinstance(decimal_mob, DecimalNumber): raise TypeError("ChangingDecimal can only take in a DecimalNumber") - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: self.mobject.set_value(self.number_update_func(self.rate_func(alpha))) diff --git a/manim/animation/rotation.py b/manim/animation/rotation.py index 7bdd42238a..6ae3c4e51f 100644 --- a/manim/animation/rotation.py +++ b/manim/animation/rotation.py @@ -4,49 +4,61 @@ __all__ = ["Rotating", "Rotate"] -from collections.abc import Sequence -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING import numpy as np -from ..animation.animation import Animation -from ..animation.transform import Transform -from ..constants import OUT, PI, TAU -from ..utils.rate_functions import linear +from manim.animation.animation import Animation +from manim.constants import ORIGIN, OUT, PI, TAU +from manim.utils.rate_functions import linear if TYPE_CHECKING: - from ..mobject.mobject import Mobject + from manim.mobject.opengl.opengl_mobject import OpenGLMobject + from manim.typing import RateFunc class Rotating(Animation): def __init__( self, - mobject: Mobject, + mobject: OpenGLMobject, + angle: float = TAU, axis: np.ndarray = OUT, - radians: np.ndarray = TAU, about_point: np.ndarray | None = None, about_edge: np.ndarray | None = None, - run_time: float = 5, - rate_func: Callable[[float], float] = linear, + run_time: float = 5.0, + rate_func: RateFunc = linear, + suspend_mobject_updating: bool = False, **kwargs, - ) -> None: + ): + super().__init__( + mobject, + run_time=run_time, + rate_func=rate_func, + suspend_mobject_updating=suspend_mobject_updating, + **kwargs, + ) + self.angle = angle self.axis = axis - self.radians = radians self.about_point = about_point self.about_edge = about_edge - super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs) - def interpolate_mobject(self, alpha: float) -> None: - self.mobject.become(self.starting_mobject) + def interpolate(self, alpha: float) -> None: + pairs = zip( + self.mobject.family_members_with_points(), + self.starting_mobject.family_members_with_points(), + ) + for sm1, sm2 in pairs: + sm1.points[:] = sm2.points + self.mobject.rotate( - self.rate_func(alpha) * self.radians, + self.rate_func(alpha) * self.angle, axis=self.axis, about_point=self.about_point, about_edge=self.about_edge, ) -class Rotate(Transform): +class Rotate(Rotating): """Animation that rotates a Mobject. Parameters @@ -67,7 +79,6 @@ class Rotate(Transform): Examples -------- .. manim:: UsingRotate - class UsingRotate(Scene): def construct(self): self.play( @@ -79,36 +90,23 @@ def construct(self): ), Rotate(Square(side_length=0.5), angle=2*PI, rate_func=linear), ) - """ def __init__( self, - mobject: Mobject, + mobject: OpenGLMobject, angle: float = PI, axis: np.ndarray = OUT, - about_point: Sequence[float] | None = None, - about_edge: Sequence[float] | None = None, + run_time: float = 1, + about_edge: np.ndarray = ORIGIN, **kwargs, - ) -> None: - if "path_arc" not in kwargs: - kwargs["path_arc"] = angle - if "path_arc_axis" not in kwargs: - kwargs["path_arc_axis"] = axis - self.angle = angle - self.axis = axis - self.about_edge = about_edge - self.about_point = about_point - if self.about_point is None: - self.about_point = mobject.get_center() - super().__init__(mobject, path_arc_centers=self.about_point, **kwargs) - - def create_target(self) -> Mobject: - target = self.mobject.copy() - target.rotate( - self.angle, - axis=self.axis, - about_point=self.about_point, - about_edge=self.about_edge, + ): + super().__init__( + mobject, + angle, + axis, + run_time=run_time, + about_edge=about_edge, + introducer=True, + **kwargs, ) - return target diff --git a/manim/animation/scene_buffer.py b/manim/animation/scene_buffer.py index 699d17da8d..d3dfe179ae 100644 --- a/manim/animation/scene_buffer.py +++ b/manim/animation/scene_buffer.py @@ -1,10 +1,21 @@ from __future__ import annotations -from typing import final +from collections.abc import Iterator +from enum import Enum +from typing import TYPE_CHECKING, Any, final -from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject +from manim.mobject.opengl.opengl_mobject import OpenGLMobject -__all__ = ["SceneBuffer"] +if TYPE_CHECKING: + from collections.abc import Sequence + +__all__ = ["SceneBuffer", "SceneOperation"] + + +class SceneOperation(Enum): + ADD = "add" + REMOVE = "remove" + REPLACE = "replace" @final @@ -19,42 +30,55 @@ class SceneBuffer: It is the scenes job to clear the buffer in between the beginning and end of animations. + + To iterate over the operations, simply iterate over the buffer. + + Example + ------- + + .. code-block:: pycon + + >>> buffer = SceneBuffer() + >>> buffer.add(Square()) + >>> buffer.remove(Circle()) + >>> buffer.replace(Square(), Circle()) + >>> for operation in buffer: + ... print(operation) + (SceneOperation.ADD, (Square(),), {}) + (SceneOperation.REMOVE, (Circle(),), {}) + (SceneOperation.REPLACE, (Square(), Circle()), {}) """ def __init__(self) -> None: - self.to_remove: list[Mobject] = [] - self.to_add: list[Mobject] = [] - self.to_replace: list[tuple[Mobject, ...]] = [] - self.deferred = False + self.operations: list[ + tuple[SceneOperation, Sequence[OpenGLMobject], dict[str, Any]] + ] = [] - def add(self, *mobs: Mobject) -> None: - self._check_deferred() - self.to_add.extend(mobs) + def add(self, *mobs: OpenGLMobject, **kwargs: Any) -> None: + """Add mobjects to the scene.""" + self.operations.append((SceneOperation.ADD, mobs, kwargs)) - def remove(self, *mobs: Mobject) -> None: - self._check_deferred() - self.to_remove.extend(mobs) + def remove(self, *mobs: OpenGLMobject, **kwargs: Any) -> None: + """Remove mobjects from the scene.""" + self.operations.append((SceneOperation.REMOVE, mobs, kwargs)) - def replace(self, mob: Mobject, *replacements: Mobject) -> None: - self._check_deferred() - self.to_replace.append((mob, *replacements)) + def replace( + self, mob: OpenGLMobject, *replacements: OpenGLMobject, **kwargs: Any + ) -> None: + """Replace a ``mob`` with ``replacements`` on the scene.""" + self.operations.append((SceneOperation.REPLACE, (mob, *replacements), kwargs)) def clear(self) -> None: - self.to_remove.clear() - self.to_add.clear() - - def deferred_clear(self) -> None: - """Clear ``self`` on next operation""" - self.deferred = True - - def _check_deferred(self) -> None: - if self.deferred: - self.clear() - self.deferred = False + """Clear the buffer.""" + self.operations.clear() def __str__(self) -> str: - to_add = self.to_add - to_remove = self.to_remove - return f"{type(self).__name__}({to_add=}, {to_remove=})" + operations = self.operations + return f"{type(self).__name__}({operations=})" __repr__ = __str__ + + def __iter__( + self, + ) -> Iterator[tuple[SceneOperation, Sequence[OpenGLMobject], dict[str, Any]]]: + return iter(self.operations) diff --git a/manim/animation/transform.py b/manim/animation/transform.py index 2fa9ed7e80..46e59675f4 100644 --- a/manim/animation/transform.py +++ b/manim/animation/transform.py @@ -2,6 +2,8 @@ from __future__ import annotations +from manim.typing import PathFuncType + __all__ = [ "Transform", "ReplacementTransform", @@ -125,12 +127,12 @@ def make_arc_path(start, end, arc_angle): def __init__( self, - mobject: Mobject | None, - target_mobject: Mobject | None = None, + mobject: OpenGLMobject | None, + target_mobject: OpenGLMobject | None = None, path_func: Callable | None = None, path_arc: float = 0, path_arc_axis: np.ndarray = OUT, - path_arc_centers: np.ndarray = None, + path_arc_centers: np.ndarray | None = None, replace_mobject_with_target_in_scene: bool = False, **kwargs, ) -> None: @@ -151,8 +153,8 @@ def __init__( self.replace_mobject_with_target_in_scene: bool = ( replace_mobject_with_target_in_scene ) - self.target_mobject: Mobject = ( - target_mobject if target_mobject is not None else Mobject() + self.target_mobject: OpenGLMobject = ( + target_mobject if target_mobject is not None else OpenGLMobject() ) super().__init__(mobject, **kwargs) @@ -171,19 +173,13 @@ def path_arc(self, path_arc: float) -> None: @property def path_func( self, - ) -> Callable[ - [Iterable[np.ndarray], Iterable[np.ndarray], float], - Iterable[np.ndarray], - ]: + ) -> PathFuncType: return self._path_func @path_func.setter def path_func( self, - path_func: Callable[ - [Iterable[np.ndarray], Iterable[np.ndarray], float], - Iterable[np.ndarray], - ], + path_func: PathFuncType, ) -> None: if path_func is not None: self._path_func = path_func @@ -193,13 +189,17 @@ def begin(self) -> None: # call so that the actual target_mobject stays # preserved. self.target_mobject = self.create_target() - self.target_copy = self.target_mobject.copy() # Note, this potentially changes the structure # of both mobject and target_mobject + if self.mobject.is_aligned_with(self.target_mobject): + self.target_copy = self.target_mobject + else: + self.target_copy = self.target_mobject.copy() self.mobject.align_data_and_family(self.target_copy) + super().begin() - def create_target(self) -> Mobject: + def create_target(self) -> OpenGLMobject: # Has no meaningful effect here, but may be useful # in subclasses return self.target_mobject @@ -212,7 +212,7 @@ def finish(self) -> None: self.buffer.remove(self.mobject) self.buffer.add(self.target_mobject) - def get_all_mobjects(self) -> Sequence[Mobject]: + def get_all_mobjects(self) -> Sequence[OpenGLMobject]: return [ self.mobject, self.starting_mobject, @@ -230,9 +230,9 @@ def get_all_families_zipped(self) -> Iterable[tuple]: # more precise typing? def interpolate_submobject( self, - submobject: Mobject, - starting_submobject: Mobject, - target_copy: Mobject, + submobject: OpenGLMobject, + starting_submobject: OpenGLMobject, + target_copy: OpenGLMobject, alpha: float, ) -> Transform: submobject.interpolate(starting_submobject, target_copy, alpha, self.path_func) @@ -287,7 +287,9 @@ def construct(self): """ - def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None: + def __init__( + self, mobject: OpenGLMobject, target_mobject: OpenGLMobject, **kwargs + ) -> None: super().__init__( mobject, target_mobject, replace_mobject_with_target_in_scene=True, **kwargs ) @@ -298,7 +300,9 @@ class TransformFromCopy(Transform): Performs a reversed Transform """ - def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None: + def __init__( + self, mobject: OpenGLMobject, target_mobject: OpenGLMobject, **kwargs + ) -> None: super().__init__(target_mobject, mobject, **kwargs) def interpolate(self, alpha: float) -> None: @@ -337,8 +341,8 @@ def construct(self): def __init__( self, - mobject: Mobject, - target_mobject: Mobject, + mobject: OpenGLMobject, + target_mobject: OpenGLMobject, path_arc: float = -np.pi, **kwargs, ) -> None: @@ -386,8 +390,8 @@ def construct(self): def __init__( self, - mobject: Mobject, - target_mobject: Mobject, + mobject: OpenGLMobject, + target_mobject: OpenGLMobject, path_arc: float = np.pi, **kwargs, ) -> None: @@ -420,11 +424,11 @@ def construct(self): """ - def __init__(self, mobject: Mobject, **kwargs) -> None: + def __init__(self, mobject: OpenGLMobject, **kwargs) -> None: self.check_validity_of_input(mobject) super().__init__(mobject, mobject.target, **kwargs) - def check_validity_of_input(self, mobject: Mobject) -> None: + def check_validity_of_input(self, mobject: OpenGLMobject) -> None: if not hasattr(mobject, "target"): raise ValueError( "MoveToTarget called on mobject" "without attribute 'target'", @@ -477,7 +481,7 @@ def check_validity_of_input(self, method: Callable) -> None: ) assert isinstance(method.__self__, (Mobject, OpenGLMobject)) - def create_target(self) -> Mobject: + def create_target(self) -> OpenGLMobject: method = self.method # Make sure it's a list so that args.pop() works args = list(self.method_args) @@ -523,7 +527,9 @@ def __init__( class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction): - def __init__(self, function: types.MethodType, mobject: Mobject, **kwargs) -> None: + def __init__( + self, function: types.MethodType, mobject: OpenGLMobject, **kwargs + ) -> None: self.function = function super().__init__(mobject.move_to, **kwargs) @@ -613,7 +619,9 @@ def __init__(self, mobject: Mobject, **kwargs) -> None: class ApplyFunction(Transform): - def __init__(self, function: types.MethodType, mobject: Mobject, **kwargs) -> None: + def __init__( + self, function: types.MethodType, mobject: OpenGLMobject, **kwargs + ) -> None: self.function = function super().__init__(mobject, **kwargs) @@ -684,6 +692,7 @@ def __init__(self, function: types.MethodType, mobject: Mobject, **kwargs) -> No super().__init__(method, function, **kwargs) def _init_path_func(self) -> None: + # TODO: this seems broken? func1 = self.function(complex(1)) self.path_arc = np.log(func1).imag super()._init_path_func() diff --git a/manim/animation/transform_matching_parts.py b/manim/animation/transform_matching_parts.py index e000c416fd..9758982fd1 100644 --- a/manim/animation/transform_matching_parts.py +++ b/manim/animation/transform_matching_parts.py @@ -143,7 +143,7 @@ def get_shape_map(self, mobject: Mobject) -> dict: key = self.get_mobject_key(sm) if key not in shape_map: if config["renderer"] == RendererType.OPENGL: - shape_map[key] = OpenGLVGroup() + shape_map[key] = VGroup() else: shape_map[key] = VGroup() shape_map[key].add(sm) diff --git a/manim/animation/updaters/mobject_update_utils.py b/manim/animation/updaters/mobject_update_utils.py index dee27ff398..a6d438ec29 100644 --- a/manim/animation/updaters/mobject_update_utils.py +++ b/manim/animation/updaters/mobject_update_utils.py @@ -4,11 +4,7 @@ __all__ = [ "assert_is_mobject_method", - "always", - "f_always", "always_redraw", - "always_shift", - "always_rotate", "turn_animation_into_updater", "cycle_animation", ] @@ -19,13 +15,11 @@ import numpy as np -from manim.constants import DEGREES, RIGHT from manim.mobject.mobject import Mobject -from manim.opengl import OpenGLMobject -from manim.utils.space_ops import normalize +from manim.mobject.opengl.opengl_mobject import OpenGLMobject if TYPE_CHECKING: - from manim.animation.animation import Animation + from manim.animation.protocol import AnimationProtocol def assert_is_mobject_method(method: Callable) -> None: @@ -34,32 +28,6 @@ def assert_is_mobject_method(method: Callable) -> None: assert isinstance(mobject, (Mobject, OpenGLMobject)) -def always(method: Callable, *args, **kwargs) -> Mobject: - assert_is_mobject_method(method) - mobject = method.__self__ - func = method.__func__ - mobject.add_updater(lambda m: func(m, *args, **kwargs)) - return mobject - - -def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mobject: - """ - More functional version of always, where instead - of taking in args, it takes in functions which output - the relevant arguments. - """ - assert_is_mobject_method(method) - mobject = method.__self__ - func = method.__func__ - - def updater(mob): - args = [arg_generator() for arg_generator in arg_generators] - func(mob, *args, **kwargs) - - mobject.add_updater(updater) - return mobject - - def always_redraw(func: Callable[[], Mobject]) -> Mobject: """Redraw the mobject constructed by a function every frame. @@ -105,80 +73,8 @@ def construct(self): return mob -def always_shift( - mobject: Mobject, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1 -) -> Mobject: - """A mobject which is continuously shifted along some direction - at a certain rate. - - Parameters - ---------- - mobject - The mobject to shift. - direction - The direction to shift. The vector is normalized, the specified magnitude - is not relevant. - rate - Length in Manim units which the mobject travels in one - second along the specified direction. - - Examples - -------- - - .. manim:: ShiftingSquare - - class ShiftingSquare(Scene): - def construct(self): - sq = Square().set_fill(opacity=1) - tri = Triangle() - VGroup(sq, tri).arrange(LEFT) - - # construct a square which is continuously - # shifted to the right - always_shift(sq, RIGHT, rate=5) - - self.add(sq) - self.play(tri.animate.set_fill(opacity=1)) - """ - mobject.add_updater(lambda m, dt: m.shift(dt * rate * normalize(direction))) - return mobject - - -def always_rotate(mobject: Mobject, rate: float = 20 * DEGREES, **kwargs) -> Mobject: - """A mobject which is continuously rotated at a certain rate. - - Parameters - ---------- - mobject - The mobject to be rotated. - rate - The angle which the mobject is rotated by - over one second. - kwags - Further arguments to be passed to :meth:`.Mobject.rotate`. - - Examples - -------- - - .. manim:: SpinningTriangle - - class SpinningTriangle(Scene): - def construct(self): - tri = Triangle().set_fill(opacity=1).set_z_index(2) - sq = Square().to_edge(LEFT) - - # will keep spinning while there is an animation going on - always_rotate(tri, rate=2*PI, about_point=ORIGIN) - - self.add(tri, sq) - self.play(sq.animate.to_edge(RIGHT), rate_func=linear, run_time=1) - """ - mobject.add_updater(lambda m, dt: m.rotate(dt * rate, **kwargs)) - return mobject - - def turn_animation_into_updater( - animation: Animation, cycle: bool = False, **kwargs + animation: AnimationProtocol, cycle: bool = False, **kwargs ) -> Mobject: """ Add an updater to the animation's mobject which applies @@ -227,5 +123,5 @@ def update(m: Mobject, dt: float): return mobject -def cycle_animation(animation: Animation, **kwargs) -> Mobject: +def cycle_animation(animation: AnimationProtocol, **kwargs) -> Mobject: return turn_animation_into_updater(animation, cycle=True, **kwargs) diff --git a/manim/animation/updaters/update.py b/manim/animation/updaters/update.py index ded160cff7..4422b0277f 100644 --- a/manim/animation/updaters/update.py +++ b/manim/animation/updaters/update.py @@ -33,12 +33,12 @@ def __init__( mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs ) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: self.update_function(self.mobject) class UpdateFromAlphaFunc(UpdateFromFunc): - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: self.update_function(self.mobject, self.rate_func(alpha)) @@ -51,7 +51,7 @@ def __init__(self, mobject: Mobject, tracked_mobject: Mobject, **kwargs) -> None ) super().__init__(mobject, **kwargs) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: target = self.tracked_mobject.get_center() location = self.mobject.get_center() self.mobject.shift(target - location + self.diff) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 54259a496f..cafbf97545 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -1,28 +1,18 @@ from __future__ import annotations -import itertools as it import math -from collections.abc import Iterable -from typing import Any -import moderngl import numpy as np -from PIL import Image from scipy.spatial.transform import Rotation -from manim import config, logger -from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint -from manim.renderer.shader_wrapper import ShaderWrapper -from manim.utils.color import BLACK, color_to_rgba +from manim._config import config +from manim.mobject.opengl.opengl_mobject import OpenGLMobject from ..constants import * -from ..utils.simple_functions import fdiv from ..utils.space_ops import normalize class Camera(OpenGLMobject): - fps: int = 30 - def __init__( self, frame_shape: tuple[float, float] = (config.frame_width, config.frame_height), @@ -156,355 +146,3 @@ def get_implied_camera_location(self) -> np.ndarray: to_camera = self.get_inverse_camera_rotation_matrix()[2] dist = self.get_focal_distance() return self.get_center() + dist * to_camera - - -# TODO: This is already ported to the renderer and now is useless, leavefor now for compoatibilty reasons -class OpenGLCamera: - def __init__( - self, - ctx: moderngl.Context | None = None, - background_image: str | None = None, - frame_config: dict = {}, - pixel_width: int = config.pixel_width, - pixel_height: int = config.pixel_height, - fps: int = config.frame_rate, - # Note: frame height and width will be resized to match the pixel aspect rati - background_color=BLACK, - background_opacity: float = 1.0, - # Points in vectorized mobjects with norm greater - # than this value will be rescaled - max_allowable_norm: float = 1.0, - image_mode: str = "RGBA", - n_channels: int = 4, - pixel_array_dtype: type = np.uint8, - light_source_position: np.ndarray = np.array([-10, 10, 10]), - # Although vector graphics handle antialiasing fine - # without multisampling, for 3d scenes one might want - # to set samples to be greater than 0. - samples: int = 0, - ) -> None: - self.background_image = background_image - self.pixel_width = pixel_width - self.pixel_height = pixel_height - self.fps = fps - self.max_allowable_norm = max_allowable_norm - self.image_mode = image_mode - self.n_channels = n_channels - self.pixel_array_dtype = pixel_array_dtype - self.light_source_position = light_source_position - self.samples = samples - - self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max - self.background_color: list[float] = list( - color_to_rgba(background_color, background_opacity) - ) - self.init_frame(**frame_config) - self.init_context(ctx) - self.init_shaders() - self.init_textures() - self.init_light_source() - self.refresh_perspective_uniforms() - # A cached map from mobjects to their associated list of render groups - # so that these render groups are not regenerated unnecessarily for static - # mobjects - self.mob_to_render_groups: dict = {} - - def init_frame(self, **config) -> None: - self.frame = OpenGLCameraFrame(**config) - - def init_context(self, ctx: moderngl.Context | None = None) -> None: - if ctx is None: - ctx = moderngl.create_standalone_context() - fbo = self.get_fbo(ctx, 0) - else: - fbo = ctx.detect_framebuffer() - - self.ctx = ctx - self.fbo = fbo - self.set_ctx_blending() - - # For multisample antisampling - fbo_msaa = self.get_fbo(ctx, self.samples) - fbo_msaa.use() - self.fbo_msaa = fbo_msaa - - def set_ctx_blending(self, enable: bool = True) -> None: - if enable: - self.ctx.enable(moderngl.BLEND) - else: - self.ctx.disable(moderngl.BLEND) - - def set_ctx_depth_test(self, enable: bool = True) -> None: - if enable: - self.ctx.enable(moderngl.DEPTH_TEST) - else: - self.ctx.disable(moderngl.DEPTH_TEST) - - def init_light_source(self) -> None: - self.light_source = OpenGLPoint(self.light_source_position) - - # Methods associated with the frame buffer - def get_fbo(self, ctx: moderngl.Context, samples: int = 0) -> moderngl.Framebuffer: - pw = self.pixel_width - ph = self.pixel_height - return ctx.framebuffer( - color_attachments=ctx.texture( - (pw, ph), components=self.n_channels, samples=samples - ), - depth_attachment=ctx.depth_renderbuffer((pw, ph), samples=samples), - ) - - def clear(self) -> None: - self.fbo.clear(*self.background_color) - self.fbo_msaa.clear(*self.background_color) - - def reset_pixel_shape(self, new_width: int, new_height: int) -> None: - self.pixel_width = new_width - self.pixel_height = new_height - self.refresh_perspective_uniforms() - - def get_raw_fbo_data(self, dtype: str = "f1") -> bytes: - # Copy blocks from the fbo_msaa to the drawn fbo using Blit - # pw, ph = (self.pixel_width, self.pixel_height) - # gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo_msaa.glo) - # gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.fbo.glo) - # gl.glBlitFramebuffer( - # 0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR - # ) - - self.ctx.copy_framebuffer(self.fbo, self.fbo_msaa) - return self.fbo.read( - viewport=self.fbo.viewport, - components=self.n_channels, - dtype=dtype, - ) - - def get_image(self) -> Image.Image: - return Image.frombytes( - "RGBA", - self.get_pixel_shape(), - self.get_raw_fbo_data(), - "raw", - "RGBA", - 0, - -1, - ) - - def get_pixel_array(self) -> np.ndarray: - raw = self.get_raw_fbo_data(dtype="f4") - flat_arr = np.frombuffer(raw, dtype="f4") - arr = flat_arr.reshape([*reversed(self.fbo.size), self.n_channels]) - arr = arr[::-1] - # Convert from float - return (self.rgb_max_val * arr).astype(self.pixel_array_dtype) - - def get_texture(self): - texture = self.ctx.texture( - size=self.fbo.size, components=4, data=self.get_raw_fbo_data(), dtype="f4" - ) - return texture - - # Getting camera attributes - def get_pixel_shape(self) -> tuple[int, int]: - return self.fbo.viewport[2:4] - # return (self.pixel_width, self.pixel_height) - - def get_pixel_width(self) -> int: - return self.get_pixel_shape()[0] - - def get_pixel_height(self) -> int: - return self.get_pixel_shape()[1] - - def get_frame_height(self) -> float: - return self.frame.get_height() - - def get_frame_width(self) -> float: - return self.frame.get_width() - - def get_frame_shape(self) -> tuple[float, float]: - return (self.get_frame_width(), self.get_frame_height()) - - def get_frame_center(self) -> np.ndarray: - return self.frame.get_center() - - def get_location(self) -> tuple[float, float, float] | np.ndarray: - return self.frame.get_implied_camera_location() - - def resize_frame_shape(self, fixed_dimension: bool = False) -> None: - """ - Changes frame_shape to match the aspect ratio - of the pixels, where fixed_dimension determines - whether frame_height or frame_width - remains fixed while the other changes accordingly. - """ - pixel_height = self.get_pixel_height() - pixel_width = self.get_pixel_width() - frame_height = self.get_frame_height() - frame_width = self.get_frame_width() - aspect_ratio = fdiv(pixel_width, pixel_height) - if not fixed_dimension: - frame_height = frame_width / aspect_ratio - else: - frame_width = aspect_ratio * frame_height - self.frame.set_height(frame_height) - self.frame.set_width(frame_width) - - # Rendering - def capture(self, *mobjects: OpenGLMobject) -> None: - self.refresh_perspective_uniforms() - for mobject in mobjects: - for render_group in self.get_render_group_list(mobject): - self.render(render_group) - - def render(self, render_group: dict[str, Any]) -> None: - shader_wrapper: ShaderWrapper = render_group["shader_wrapper"] - shader_program = render_group["prog"] - self.set_shader_uniforms(shader_program, shader_wrapper) - self.set_ctx_depth_test(shader_wrapper.depth_test) - render_group["vao"].render(int(shader_wrapper.render_primitive)) - if render_group["single_use"]: - self.release_render_group(render_group) - - def get_render_group_list(self, mobject: OpenGLMobject) -> Iterable[dict[str, Any]]: - if mobject.is_changing(): - return self.generate_render_group_list(mobject) - - # Otherwise, cache result for later use - key = id(mobject) - if key not in self.mob_to_render_groups: - self.mob_to_render_groups[key] = list( - self.generate_render_group_list(mobject) - ) - return self.mob_to_render_groups[key] - - def generate_render_group_list( - self, mobject: OpenGLMobject - ) -> Iterable[dict[str, Any]]: - return ( - self.get_render_group(sw, single_use=mobject.is_changing()) - for sw in mobject.get_shader_wrapper_list() - ) - - def get_render_group( - self, shader_wrapper: ShaderWrapper, single_use: bool = True - ) -> dict[str, Any]: - # Data buffers - vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes()) - if shader_wrapper.vert_indices is None: - ibo = None - else: - vert_index_data = shader_wrapper.vert_indices.astype("i4").tobytes() - if vert_index_data: - ibo = self.ctx.buffer(vert_index_data) - else: - ibo = None - - # Program an vertex array - shader_program, vert_format = self.get_shader_program(shader_wrapper) # type: ignore - vao = self.ctx.vertex_array( - program=shader_program, - content=[(vbo, vert_format, *shader_wrapper.vert_attributes)], - index_buffer=ibo, - ) - return { - "vbo": vbo, - "ibo": ibo, - "vao": vao, - "prog": shader_program, - "shader_wrapper": shader_wrapper, - "single_use": single_use, - } - - def release_render_group(self, render_group: dict[str, Any]) -> None: - for key in ["vbo", "ibo", "vao"]: - if render_group[key] is not None: - render_group[key].release() - - def refresh_static_mobjects(self) -> None: - for render_group in it.chain(*self.mob_to_render_groups.values()): - self.release_render_group(render_group) - self.mob_to_render_groups = {} - - # Shaders - def init_shaders(self) -> None: - # Initialize with the null id going to None - self.id_to_shader_program: dict[int, tuple[moderngl.Program, str] | None] = { - hash(""): None - } - - def get_shader_program( - self, shader_wrapper: ShaderWrapper - ) -> tuple[moderngl.Program, str] | None: - sid = shader_wrapper.get_program_id() - if sid not in self.id_to_shader_program: - # Create shader program for the first time, then cache - # in the id_to_shader_program dictionary - program = self.ctx.program(**shader_wrapper.get_program_code()) - vert_format = moderngl.detect_format( - program, shader_wrapper.vert_attributes - ) - self.id_to_shader_program[sid] = (program, vert_format) - - return self.id_to_shader_program[sid] - - def set_shader_uniforms( - self, - shader: moderngl.Program, - shader_wrapper: ShaderWrapper, - ) -> None: - for name, path in shader_wrapper.texture_paths.items(): - tid = self.get_texture_id(path) - shader[name].value = tid - for name, value in it.chain( - self.perspective_uniforms.items(), shader_wrapper.uniforms.items() - ): - if name in shader: - if isinstance(value, np.ndarray) and value.ndim > 0: - value = tuple(value) - shader[name].value = value - else: - logger.debug(f"Uniform {name} not found in shader {shader}") - - def refresh_perspective_uniforms(self) -> None: - frame = self.frame - # Orient light - rotation = frame.get_inverse_camera_rotation_matrix() - offset = frame.get_center() - light_pos = np.dot(rotation, self.light_source.get_location() + offset) - cam_pos = self.frame.get_implied_camera_location() # TODO - - self.perspective_uniforms = { - "frame_shape": frame.get_shape(), - "pixel_shape": self.get_pixel_shape(), - "camera_offset": tuple(offset), - "camera_rotation": tuple(np.array(rotation).T.flatten()), - "camera_position": tuple(cam_pos), - "light_source_position": tuple(light_pos), - "focal_distance": frame.get_focal_distance(), - } - - def init_textures(self) -> None: - self.n_textures: int = 0 - self.path_to_texture: dict[str, tuple[int, moderngl.Texture]] = {} - - def get_texture_id(self, path: str) -> int: - if path not in self.path_to_texture: - if self.n_textures == 15: # I have no clue why this is needed - self.n_textures += 1 - tid = self.n_textures - self.n_textures += 1 - im = Image.open(path).convert("RGBA") - texture = self.ctx.texture( - size=im.size, - components=len(im.getbands()), - data=im.tobytes(), - ) - texture.use(location=tid) - self.path_to_texture[path] = (tid, texture) - return self.path_to_texture[path][0] - - def release_texture(self, path: str): - tid_and_texture = self.path_to_texture.pop(path, None) - if tid_and_texture: - tid_and_texture[1].release() - return self diff --git a/manim/cli/checkhealth/commands.py b/manim/cli/checkhealth/commands.py index d6873755f2..587174823b 100644 --- a/manim/cli/checkhealth/commands.py +++ b/manim/cli/checkhealth/commands.py @@ -6,11 +6,12 @@ from __future__ import annotations import sys +import timeit import click import cloup -from .checks import HEALTH_CHECKS +from manim.cli.checkhealth.checks import HEALTH_CHECKS __all__ = ["checkhealth"] @@ -62,7 +63,7 @@ def checkhealth(): import manim as mn class CheckHealthDemo(mn.Scene): - def construct(self): + def _inner_construct(self): banner = mn.ManimBanner().shift(mn.UP * 0.5) self.play(banner.create()) self.wait(0.5) @@ -79,5 +80,13 @@ def construct(self): mn.FadeOut(text_tex_group, shift=mn.DOWN), ) + def construct(self): + self.execution_time = timeit.timeit(self._inner_construct, number=1) + with mn.tempconfig({"preview": True, "disable_caching": True}): - CheckHealthDemo().render() + manager = mn.Manager(CheckHealthDemo) + manager.render() + + click.echo( + f"Scene rendered in {manager.scene.execution_time:.2f} seconds." + ) diff --git a/manim/cli/render/commands.py b/manim/cli/render/commands.py index bd378e7c0e..3fe8421005 100644 --- a/manim/cli/render/commands.py +++ b/manim/cli/render/commands.py @@ -18,14 +18,15 @@ import cloup -from ... import __version__, config, console, error_console, logger -from ..._config import tempconfig -from ...constants import EPILOG, RendererType -from ...utils.module_ops import scene_classes_from_file -from .ease_of_access_options import ease_of_access_options -from .global_options import global_options -from .output_options import output_options -from .render_options import render_options +from manim import __version__, config, console, error_console, logger +from manim._config import tempconfig +from manim.cli.render.ease_of_access_options import ease_of_access_options +from manim.cli.render.global_options import global_options +from manim.cli.render.output_options import output_options +from manim.cli.render.render_options import render_options +from manim.constants import EPILOG, RendererType +from manim.manager import Manager +from manim.utils.module_ops import scene_classes_from_file __all__ = ["render"] @@ -95,10 +96,10 @@ def __repr__(self): while keep_running: for SceneClass in scene_classes_from_file(file): with tempconfig({}): - scene = SceneClass() - rerun = scene.render() + manager = Manager(SceneClass) + rerun = manager.render() if rerun or config["write_all"]: - scene.num_plays = 0 + manager.scene.num_plays = 0 continue else: keep_running = False diff --git a/manim/cli/render/global_options.py b/manim/cli/render/global_options.py index f8a05dfaf5..d7424dd1ee 100644 --- a/manim/cli/render/global_options.py +++ b/manim/cli/render/global_options.py @@ -99,7 +99,6 @@ def validate_gui_location(ctx, param, value): help="Renders animations without outputting image or video files and disables the window", default=False, ), - option("--parallel", default=True, help="Renders all animations in parallel"), option( "--no_latex_cleanup", is_flag=True, diff --git a/manim/cli/render/output_options.py b/manim/cli/render/output_options.py index a7613f1565..8af7fadabc 100644 --- a/manim/cli/render/output_options.py +++ b/manim/cli/render/output_options.py @@ -21,10 +21,11 @@ help="Zero padding for PNG file names.", ), option( + "-w", "--write_to_movie", is_flag=True, default=None, - help="Write the video rendered with opengl to a file.", + help="Write the video to a file.", ), option( "--media_dir", diff --git a/manim/event_handler/event_dispatcher.py b/manim/event_handler/event_dispatcher.py index 050379b5e1..5c8f7f663e 100644 --- a/manim/event_handler/event_dispatcher.py +++ b/manim/event_handler/event_dispatcher.py @@ -1,13 +1,14 @@ from __future__ import annotations import numpy as np +from typing_extensions import Any, Self from manim.event_handler.event_listener import EventListener from manim.event_handler.event_type import EventType class EventDispatcher: - def __init__(self): + def __init__(self) -> None: self.event_listeners: dict[EventType, list[EventListener]] = { event_type: [] for event_type in EventType } @@ -16,12 +17,12 @@ def __init__(self): self.pressed_keys: set[int] = set() self.draggable_object_listeners: list[EventListener] = [] - def add_listener(self, event_listener: EventListener): + def add_listener(self, event_listener: EventListener) -> Self: assert isinstance(event_listener, EventListener) self.event_listeners[event_listener.event_type].append(event_listener) return self - def remove_listener(self, event_listener: EventListener): + def remove_listener(self, event_listener: EventListener) -> Self: assert isinstance(event_listener, EventListener) try: while event_listener in self.event_listeners[event_listener.event_type]: @@ -31,7 +32,7 @@ def remove_listener(self, event_listener: EventListener): pass return self - def dispatch(self, event_type: EventType, **event_data): + def dispatch(self, event_type: EventType, **event_data: Any) -> bool | None: if event_type == EventType.MouseMotionEvent: self.mouse_point = event_data["point"] elif event_type == EventType.MouseDragEvent: diff --git a/manim/event_handler/event_listener.py b/manim/event_handler/event_listener.py index 9c923d4d57..22a9ea0767 100644 --- a/manim/event_handler/event_listener.py +++ b/manim/event_handler/event_listener.py @@ -1,33 +1,33 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable if TYPE_CHECKING: - from typing import Callable + from typing_extensions import Any - import manim.mobject.opengl.opengl_mobject as glmob from manim.event_handler.event_type import EventType + from manim.mobject.opengl.opengl_mobject import OpenGLMobject class EventListener: def __init__( self, - mobject: glmob.OpenGLMobject, + mobject: OpenGLMobject, event_type: EventType, - event_callback: Callable[[glmob.OpenGLMobject, dict[str, str]], None], - ): + event_callback: Callable[[OpenGLMobject, dict[str, str]], None], + ) -> None: self.mobject = mobject self.event_type = event_type self.callback = event_callback - def __eq__(self, o: object) -> bool: + def __eq__(self, other: Any) -> bool: return_val = False - if isinstance(o, EventListener): + if isinstance(other, EventListener): try: return_val = ( - self.callback == o.callback - and self.mobject == o.mobject - and self.event_type == o.event_type + self.callback == other.callback + and self.mobject == other.mobject + and self.event_type == other.event_type ) except Exception: pass diff --git a/manim/event_handler/window.py b/manim/event_handler/window.py new file mode 100644 index 0000000000..57846bd36e --- /dev/null +++ b/manim/event_handler/window.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod + + +class WindowABC(ABC): + is_closing: bool + + @abstractmethod + def swap_buffers(self) -> None: ... + + @abstractmethod + def close(self) -> None: ... + + @abstractmethod + def clear(self) -> None: ... diff --git a/manim/file_writer/__init__.py b/manim/file_writer/__init__.py new file mode 100644 index 0000000000..c3dd8a7056 --- /dev/null +++ b/manim/file_writer/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .file_writer import FileWriter +from .sections import * diff --git a/manim/scene/scene_file_writer.py b/manim/file_writer/file_writer.py similarity index 83% rename from manim/scene/scene_file_writer.py rename to manim/file_writer/file_writer.py index 4c39c6d58b..74d766719e 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/file_writer/file_writer.py @@ -1,18 +1,25 @@ +"""The interface between scenes and ffmpeg.""" + from __future__ import annotations -__all__ = ["SceneFileWriter"] +__all__ = ["FileWriter"] import json import shutil from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import av import numpy as np +import srt +from PIL import Image from pydub import AudioSegment -from tqdm import tqdm as ProgressDisplay -from manim import config +from manim import __version__ +from manim._config import config, logger +from manim._config.logger_utils import set_file_logger +from manim.file_writer.protocols import FileWriterProtocol +from manim.file_writer.sections import DefaultSectionType, Section from manim.utils.file_ops import ( add_extension_if_not_present, add_version_before_extension, @@ -25,56 +32,46 @@ from manim.utils.sounds import get_full_sound_file_path if TYPE_CHECKING: - from PIL.Image import Image + from manim.typing import PixelArray - from manim.scene import Scene +class FileWriter(FileWriterProtocol): + """ + FileWriter is the object that actually writes the animations + played, into video files, using FFMPEG. + This is mostly for Manim's internal use. You will rarely, if ever, + have to use the methods for this class, unless tinkering with the very + fabric of Manim's reality. -class SceneFileWriter: - def __init__( - self, - scene: Scene, - write_to_movie: bool = False, - break_into_partial_movies: bool = False, - save_pngs: bool = False, # TODO, this currently does nothing - png_mode: str = "RGBA", - save_last_frame: bool = False, - movie_file_extension: str = ".mp4", - # What python file is generating this scene - input_file_path: str = "", - # Where should this be written - output_directory: str | None = None, - file_name: str | None = None, - open_file_upon_completion: bool = False, - show_file_location_upon_completion: bool = False, - quiet: bool = False, - total_frames: int = 0, - progress_description_len: int = 40, - ): - self.scene: Scene = scene - self.write_to_movie = write_to_movie - self.break_into_partial_movies = break_into_partial_movies - self.save_pngs = save_pngs - self.png_mode = png_mode - self.save_last_frame = save_last_frame - self.movie_file_extension = movie_file_extension - self.input_file_path = input_file_path - self.output_directory = output_directory - self.file_name = file_name - self.open_file_upon_completion = open_file_upon_completion - self.show_file_location_upon_completion = show_file_location_upon_completion - self.quiet = quiet - self.total_frames = total_frames - self.progress_description_len = progress_description_len - - # State during file writing - self.writing_process: sp.Popen | None = None - self.progress_display: ProgressDisplay | None = None - self.ended_with_interrupt: bool = False - self.init_output_directories() + Attributes + ---------- + sections : list of :class:`.Section` + used to segment scene + + sections_output_dir : :class:`pathlib.Path` + where are section videos stored + + output_name : str + name of movie without extension and basis for section video names + + Some useful attributes are: + "write_to_movie" (bool=False) + Whether or not to write the animations into a video file. + "movie_file_extension" (str=".mp4") + The file-type extension of the outputted video. + "partial_movie_files" + List of all the partial-movie files. + + """ + + force_output_as_scene_name = False + + def __init__(self, scene_name: str) -> None: + self.init_output_directories(scene_name) self.init_audio() self.frame_count = 0 - self.partial_movie_files: list[str] = [] + self.num_plays = 0 + self.partial_movie_files: list[str | None] = [] self.subcaptions: list[srt.Subtitle] = [] self.sections: list[Section] = [] # first section gets automatically created for convenience @@ -83,7 +80,7 @@ def __init__( name="autocreated", type=DefaultSectionType.NORMAL, skip_animations=False ) - def init_output_directories(self, scene_name): + def init_output_directories(self, scene_name: str) -> None: """Initialise output directories. Notes @@ -101,7 +98,7 @@ def init_output_directories(self, scene_name): else: module_name = "" - if SceneFileWriter.force_output_as_scene_name: + if self.force_output_as_scene_name: self.output_name = Path(scene_name) elif config["output_file"] and not config["write_all"]: self.output_name = config.get_dir("output_file") @@ -193,7 +190,7 @@ def next_section(self, name: str, type: str, skip_animations: bool) -> None: ), ) - def add_partial_movie_file(self, hash_animation: str): + def add_partial_movie_file(self, hash_animation: str | None) -> None: """Adds a new partial movie file path to `scene.partial_movie_files` and current section from a hash. This method will compute the path from the hash. In addition to that it adds the new animation to the current section. @@ -213,12 +210,12 @@ def add_partial_movie_file(self, hash_animation: str): else: new_partial_movie_file = str( self.partial_movie_directory - / f"{hash_animation}{config['movie_file_extension']}" + / f"{hash_animation}{config.movie_file_extension}" ) self.partial_movie_files.append(new_partial_movie_file) self.sections[-1].partial_movie_files.append(new_partial_movie_file) - def get_resolution_directory(self): + def get_resolution_directory(self) -> str: """Get the name of the resolution directory directly containing the video file. @@ -243,15 +240,21 @@ def get_resolution_directory(self): :class:`str` The name of the directory. """ - pixel_height = config["pixel_height"] - frame_rate = config["frame_rate"] + pixel_height = config.pixel_height + frame_rate = config.frame_rate return f"{pixel_height}p{frame_rate}" # Sound def init_audio(self) -> None: - self.includes_sound: bool = False + """ + Preps the writer for adding audio to the movie. + """ + self.includes_sound = False def create_audio_segment(self) -> None: + """ + Creates an empty, silent, Audio Segment. + """ self.audio_segment = AudioSegment.silent() def add_audio_segment( @@ -260,15 +263,31 @@ def add_audio_segment( time: float | None = None, gain_to_background: float | None = None, ) -> None: + """ + This method adds an audio segment from an + AudioSegment type object and suitable parameters. + + Parameters + ---------- + new_segment + The audio segment to add + + time + the timestamp at which the + sound should be added. + + gain_to_background + The gain of the segment from the background. + """ if not self.includes_sound: self.includes_sound = True self.create_audio_segment() segment = self.audio_segment - curr_end = segment.duration_seconds + curr_end: float = segment.duration_seconds if time is None: time = curr_end if time < 0: - raise Exception("Adding sound at timestamp < 0") + raise ValueError("Adding sound at timestamp < 0") new_end = time + new_segment.duration_seconds diff = new_end - curr_end @@ -288,16 +307,37 @@ def add_sound( sound_file: str, time: float | None = None, gain: float | None = None, - gain_to_background: float | None = None, + **kwargs: Any, ) -> None: + """ + This method adds an audio segment from a sound file. + + Parameters + ---------- + sound_file + The path to the sound file. + + time + The timestamp at which the audio should be added. + + gain + The gain of the given audio segment. + + **kwargs + This method uses add_audio_segment, so any keyword arguments + used there can be referenced here. + + """ file_path = get_full_sound_file_path(sound_file) new_segment = AudioSegment.from_file(file_path) if gain: new_segment = new_segment.apply_gain(gain) - self.add_audio_segment(new_segment, time, gain_to_background) + self.add_audio_segment(new_segment, time, **kwargs) # Writers - def begin_animation(self, allow_write: bool = False, file_path=None): + def begin_animation( + self, allow_write: bool = False, file_path: str | None = None + ) -> None: """ Used internally by manim to stream the animation to FFMPEG for displaying or writing to a file. @@ -310,7 +350,7 @@ def begin_animation(self, allow_write: bool = False, file_path=None): if write_to_movie() and allow_write: self.open_partial_movie_stream(file_path=file_path) - def end_animation(self, allow_write: bool = False): + def end_animation(self, allow_write: bool = False) -> None: """ Internally used by Manim to stop streaming to FFMPEG gracefully. @@ -322,10 +362,9 @@ def end_animation(self, allow_write: bool = False): """ if write_to_movie() and allow_write: self.close_partial_movie_stream() + self.num_plays += 1 - def write_frame( - self, frame_or_renderer: np.ndarray | OpenGLRenderer, num_frames: int = 1 - ): + def write_frame(self, frame: PixelArray, num_frames: int = 1) -> None: """ Used internally by Manim to write a frame to the FFMPEG input buffer. @@ -338,11 +377,6 @@ def write_frame( The number of times to write frame. """ if write_to_movie(): - frame: np.ndarray = ( - frame_or_renderer.get_frame() - if config.renderer == RendererType.OPENGL - else frame_or_renderer - ) for _ in range(num_frames): # Notes: precomputing reusing packets does not work! # I.e., you cannot do `packets = encode(...)` @@ -354,47 +388,43 @@ def write_frame( for packet in self.video_stream.encode(av_frame): self.video_container.mux(packet) - if is_png_format() and not config["dry_run"]: - image: Image = ( - frame_or_renderer.get_image() - if config.renderer == RendererType.OPENGL - else Image.fromarray(frame_or_renderer) - ) + if is_png_format() and not config.dry_run: + image = Image.fromarray(frame) target_dir = self.image_file_path.parent / self.image_file_path.stem extension = self.image_file_path.suffix self.output_image( image, target_dir, extension, - config["zero_pad"], + config.zero_pad, ) - def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool): + def output_image( + self, image: Image.Image, target_dir: str | Path, ext: str, zero_pad: int + ) -> None: if zero_pad: image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}") else: image.save(f"{target_dir}{self.frame_count}{ext}") self.frame_count += 1 - def save_final_image(self, image: np.ndarray): + def save_image(self, image: PixelArray) -> None: """ - The name is a misnomer. This method saves the image - passed to it as an in the default image directory. + Saves an image in the default image directory. Parameters ---------- image The pixel array of the image to save. """ - if config["dry_run"]: - return if not config["output_file"]: self.image_file_path = add_version_before_extension(self.image_file_path) - image.save(self.image_file_path) + image_processed = Image.fromarray(image) + image_processed.save(self.image_file_path) self.print_file_ready_message(self.image_file_path) - def finish(self): + def finish(self) -> None: """ Finishes writing to the FFMPEG buffer or writing images to output directory. @@ -404,8 +434,6 @@ def finish(self): frame in the default image directory. """ if write_to_movie(): - if hasattr(self, "writing_process"): - self.writing_process.terminate() self.combine_to_movie() if config.save_sections: self.combine_to_section_videos() @@ -419,18 +447,18 @@ def finish(self): if self.subcaptions: self.write_subcaption_file() - def open_partial_movie_stream(self, file_path=None): + def open_partial_movie_stream(self, file_path: str | None = None) -> None: """Open a container holding a video stream. This is used internally by Manim initialize the container holding the video stream of a partial movie file. """ if file_path is None: - file_path = self.partial_movie_files[self.renderer.num_plays] + file_path = self.partial_movie_files[self.num_plays] self.partial_movie_file_path = file_path - fps = config["frame_rate"] - if fps == int(fps): # fps is integer + fps = config.frame_rate + if fps == int(fps): fps = int(fps) partial_movie_file_codec = "libx264" @@ -463,7 +491,7 @@ def open_partial_movie_stream(self, file_path=None): self.video_container = video_container self.video_stream = stream - def close_partial_movie_stream(self): + def close_partial_movie_stream(self) -> None: """Close the currently opened video container. Used internally by Manim to first flush the remaining packages @@ -476,11 +504,11 @@ def close_partial_movie_stream(self): self.video_container.close() logger.info( - f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s", + f"Animation {self.num_plays} : Partial movie file written in %(path)s", {"path": f"'{self.partial_movie_file_path}'"}, ) - def is_already_cached(self, hash_invocation: str): + def is_already_cached(self, hash_invocation: str) -> bool: """Will check if a file named with `hash_invocation` exists. Parameters @@ -505,9 +533,9 @@ def combine_files( self, input_files: list[str], output_file: Path, - create_gif=False, - includes_sound=False, - ): + create_gif: bool = False, + includes_sound: bool = False, + ) -> None: file_list = self.partial_movie_directory / "partial_movie_file_list.txt" logger.debug( f"Partial movie files to combine ({len(input_files)} files): %(p)s", @@ -605,7 +633,7 @@ def combine_files( partial_movies_input.close() output_container.close() - def combine_to_movie(self): + def combine_to_movie(self) -> None: """Used internally by Manim to combine the separate partial movie files that make up a Scene into a single video file for that Scene. @@ -730,16 +758,16 @@ def combine_to_section_videos(self) -> None: with (self.sections_output_dir / f"{self.output_name}.json").open("w") as file: json.dump(sections_index, file, indent=4) - def clean_cache(self): + def clean_cache(self) -> None: """Will clean the cache by removing the oldest partial_movie_files.""" cached_partial_movies = [ (self.partial_movie_directory / file_name) for file_name in self.partial_movie_directory.iterdir() if file_name != "partial_movie_file_list.txt" ] - if len(cached_partial_movies) > config["max_files_cached"]: + if len(cached_partial_movies) > config.max_files_cached: number_files_to_delete = ( - len(cached_partial_movies) - config["max_files_cached"] + len(cached_partial_movies) - config.max_files_cached ) oldest_files_to_delete = sorted( cached_partial_movies, @@ -748,11 +776,11 @@ def clean_cache(self): for file_to_delete in oldest_files_to_delete: file_to_delete.unlink() logger.info( - f"The partial movie directory is full (> {config['max_files_cached']} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)." + f"The partial movie directory is full (> {config.max_files_cached} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)." " You can change this behaviour by changing max_files_cached in config.", ) - def flush_cache_directory(self): + def flush_cache_directory(self) -> None: """Delete all the cached partial movie files""" cached_partial_movies = [ self.partial_movie_directory / file_name @@ -766,7 +794,7 @@ def flush_cache_directory(self): {"par_dir": self.partial_movie_directory}, ) - def write_subcaption_file(self): + def write_subcaption_file(self) -> None: """Writes the subcaption file.""" if config.output_file is None: return @@ -774,7 +802,8 @@ def write_subcaption_file(self): subcaption_file.write_text(srt.compose(self.subcaptions), encoding="utf-8") logger.info(f"Subcaption file has been written as {subcaption_file}") - def print_file_ready_message(self, file_path): + def print_file_ready_message(self, file_path: str | Path) -> None: """Prints the "File Ready" message to STDOUT.""" - config["output_file"] = file_path - logger.info("\nFile ready at %(file_path)s\n", {"file_path": f"'{file_path}'"}) + + config.output_file = str(file_path) + logger.info(f"\nFile ready at {str(file_path)!r}\n") diff --git a/manim/file_writer/protocols.py b/manim/file_writer/protocols.py new file mode 100644 index 0000000000..5b08d0f38e --- /dev/null +++ b/manim/file_writer/protocols.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import Protocol + +from manim.typing import PixelArray + + +class FileWriterProtocol(Protocol): + """Protocol for a file writer. + + This is mainly useful for testing purposes, to create + a mock file writer. However, it can be used in plugins. + """ + + num_plays: int + + def __init__(self, scene_name: str) -> None: ... + + def begin_animation(self, allow_write: bool = False) -> None: ... + + def end_animation(self, allow_write: bool = False) -> None: ... + + def is_already_cached(self, hash_invocation: str) -> bool: ... + + def add_partial_movie_file(self, hash_animation: str) -> None: ... + + def write_frame(self, frame: PixelArray) -> None: ... + + def finish(self) -> None: ... + + def save_image(self, image: PixelArray) -> None: ... diff --git a/manim/scene/section.py b/manim/file_writer/sections.py similarity index 99% rename from manim/scene/section.py rename to manim/file_writer/sections.py index a9e4ba2aba..838d534277 100644 --- a/manim/scene/section.py +++ b/manim/file_writer/sections.py @@ -100,5 +100,5 @@ def get_dict(self, sections_dir: Path) -> dict[str, Any]: **video_metadata, ) - def __repr__(self): + def __repr__(self) -> str: return f"
" diff --git a/manim/manager.py b/manim/manager.py new file mode 100644 index 0000000000..28bf271c77 --- /dev/null +++ b/manim/manager.py @@ -0,0 +1,413 @@ +from __future__ import annotations + +__all__ = ["Manager"] + +import contextlib +import platform +import time +from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING, Callable, Generic, TypeVar + +import numpy as np +from tqdm import tqdm + +from manim import config, logger +from manim.event_handler.window import WindowABC +from manim.file_writer import FileWriter +from manim.plugins import Hooks, plugins +from manim.scene.scene import Scene, SceneState +from manim.utils.exceptions import EndSceneEarlyException +from manim.utils.hashing import get_hash_from_play_call + +if TYPE_CHECKING: + import numpy.typing as npt + from typing_extensions import Any + + from manim.animation.protocol import AnimationProtocol + from manim.file_writer.protocols import FileWriterProtocol + from manim.renderer.renderer import RendererProtocol + +Scene_co = TypeVar("Scene_co", covariant=True, bound=Scene) + + +class Manager(Generic[Scene_co]): + """ + The Brain of Manim + + .. admonition:: Warning for Developers + + Only methods of this class that are not prefixed with an + underscore (``_``) are stable. If you override any of the + ``_`` methods, consider pinning your version of Manim. + + Usage + ----- + + .. code-block:: python + + class Manimation(Scene): + def construct(self): + self.play(FadeIn(Circle())) + + + Manager(Manimation).render() + """ + + def __init__(self, scene_cls: type[Scene_co]) -> None: + # scene + self.scene: Scene_co = scene_cls(manager=self) + + if not isinstance(self.scene, Scene): + raise ValueError(f"{self.scene!r} is not an instance of Scene") + + self.time = 0.0 + + # Initialize window, if applicable + self.window = self.create_window() + + # this must be done AFTER instantiating a window + self.renderer = self.create_renderer() + self.renderer.use_window() + + # file writer + self.file_writer: FileWriterProtocol = self.create_file_writer() + self._write_files = config.write_to_movie + + # keep these as instance methods so subclasses + # have access to everything + def create_renderer(self) -> RendererProtocol: + """Create and return a renderer instance. + + This can be overridden in subclasses (plugins), if more processing + is needed. + + Returns + ------- + An instance of a renderer + """ + renderer = plugins.renderer() + if config.preview: + renderer.use_window() + return renderer + + def create_window(self) -> WindowABC | None: + """Create and return a window instance. + + This can be overridden in subclasses (plugins), if more + processing is needed. + + Returns + ------- + A window if previewing, else None + """ + return plugins.window() if config.preview else None + + def create_file_writer(self) -> FileWriterProtocol: + """Create and returna file writer instance. + + This can be overridden in subclasses (plugins), if more + processing is needed. + + Returns + ------- + A file writer satisfying :class:`.FileWriterProtocol` + """ + return FileWriter(self.scene.get_default_scene_name()) + + def setup(self) -> None: + """Set up processes and manager""" + + self.scene.setup() + + # these are used for making sure it feels like the correct + # amount of time has passed in the window instead of rendering + # at full speed + self.virtual_animation_start_time = 0.0 + self.real_animation_start_time = time.perf_counter() + + def render(self) -> None: + """ + Entry point to running a Manim class + + Example + ------- + + .. code-block:: python + + class MyScene(Scene): + def construct(self): + self.play(Create(Circle())) + + + with tempconfig({"preview": True}): + Manager(MyScene).render() + """ + config._warn_about_config_options() + self._render_first_pass() + self._render_second_pass() + + def _render_first_pass(self) -> None: + """ + Temporarily use the normal single pass + rendering system + """ + self.setup() + + with contextlib.suppress(EndSceneEarlyException): + self.scene.construct() + self.post_contruct() + self._interact() + + self.tear_down() + + def _render_second_pass(self) -> None: + """ + In the future, this method could be used + for two pass rendering + """ + ... + + def post_contruct(self) -> None: + """Run post-construct hooks, and clean up the file writer.""" + for hook in plugins.hooks[Hooks.POST_CONSTRUCT]: + hook(self) + + if self.file_writer.num_plays: + self.file_writer.finish() + # otherwise no animations were played + elif config.write_to_movie or config.save_last_frame: + self.render_state(write_to_file=False) + # FIXME: for some reason the OpenGLRenderer does not give out the + # correct frame values here + frame = self.renderer.get_pixels() + # NOTE: add hooks for post-processing (e.g. gaussian blur)? + self.file_writer.save_image(frame) + + self._write_files = False + + def tear_down(self) -> None: + """Tear down the scene and the window.""" + + self.scene.tear_down() + + if config.save_last_frame: + self._update_frame(0) + + if self.window is not None: + self.window.close() + self.window = None + + def _interact(self) -> None: + """Live interaction with the Window""" + + if self.window is None: + return + logger.info( + "\nTips: Using the keys `d`, `f`, or `z` " + "you can interact with the scene. " + "Press `command + q` or `esc` to quit" + ) + # TODO: Replace with actual dt instead + # of hardcoded dt + dt = 1 / config.frame_rate + while not self.window.is_closing: + self._update_frame(dt) + + def _update_frame(self, dt: float, *, write_to_file: bool | None = None) -> None: + """Update the current frame by ``dt`` + + Parameters + ---------- + dt : the time in between frames + write_to_file : Whether to write the result to the output stream. + Default value checks :attr:`_write_files` to see if it should be written. + """ + self.time += dt + self.scene._update_mobjects(dt) + self.scene.time = self.time + + if self.window is not None: + self.window.clear() + + self.render_state(write_to_file=write_to_file) + + if self.window is not None: + self.window.swap_buffers() + # This recursively updates the window with dt=0 until the correct + # amount of time has passed + # TODO: do ^ better with less overhead + vt = self.time - self.virtual_animation_start_time + rt = time.perf_counter() - self.real_animation_start_time + if rt < vt: + self._update_frame(0, write_to_file=False) + + def _play(self, *animations: AnimationProtocol) -> None: + """Play a bunch of animations""" + + if self.window is not None: + self.real_animation_start_time = time.perf_counter() + self.virtual_animation_start_time = self.time + + self._write_hashed_movie_file(animations) + + self.scene.begin_animations(animations) + self._progress_through_animations(animations) + self.scene.finish_animations(animations) + + self.scene.post_play() + + self.file_writer.end_animation(allow_write=self._write_files) + + def _write_hashed_movie_file(self, animations: Sequence[AnimationProtocol]) -> None: + """Compute the hash of a self.play call, and write it to a file + + Essentially, a series of methods that need to be called to successfully + render a frame. + """ + if not config.write_to_movie: + return + + if config.disable_caching: + if not config.disable_caching_warning: + logger.info("Caching disabled...") + hash_current_play = f"uncached_{self.file_writer.num_plays:05}" + else: + hash_current_play = get_hash_from_play_call( + self.scene, + self.scene.camera, + animations, + self.scene.mobjects, + ) + if self.file_writer.is_already_cached(hash_current_play): + logger.info( + f"Animation {self.file_writer.num_plays} : Using cached data (hash : {hash_current_play})" + ) + # TODO: think about how to skip + raise NotImplementedError( + "Skipping cached animations is not implemented yet" + ) + + self.file_writer.add_partial_movie_file(hash_current_play) + self.file_writer.begin_animation(allow_write=self._write_files) + + def _create_progressbar( + self, total: float, description: str, **kwargs: Any + ) -> tqdm | contextlib.nullcontext[NullProgressBar]: + """Create a progressbar""" + + if not config.write_to_movie or not config.progress_bar: + return contextlib.nullcontext(NullProgressBar()) + else: + return tqdm( + total=total, + unit="frames", + desc=description % {"num": self.file_writer.num_plays}, + ascii=True if platform.system() == "Windows" else None, + leave=config.progress_bar == "leave", + disable=config.progress_bar == "none", + **kwargs, + ) + + # TODO: change to a single wait animation + def _wait( + self, + duration: float, + *, + stop_condition: Callable[[], bool] | None = None, + ) -> None: + self.scene.pre_play() + + self._write_hashed_movie_file(animations=[]) + + update_mobjects = self.scene.should_update_mobjects() + condition = stop_condition or (lambda: False) + + progression = self._calc_time_progression(duration) + with self._create_progressbar( + progression.shape[0], "Waiting %(num)d: " + ) as progress: + last_t = 0 + for t in progression: + dt, last_t = t - last_t, t + if update_mobjects: + self._update_frame(dt) + if condition(): + progress.update(duration - t) + break + else: + # if we don't need to update mobjects + # we can just leave the mobjects on the window + # and increment the time + # but we still have to write frames + self.time += dt + self.write_frame() + progress.update(1) + self.scene.post_play() + + self.file_writer.end_animation(allow_write=self._write_files) + + def _progress_through_animations( + self, animations: Sequence[AnimationProtocol] + ) -> None: + last_t = 0.0 + run_time = self._calc_runtime(animations) + progression = self._calc_time_progression(run_time) + with self._create_progressbar( + progression.shape[0], + f"Animation %(num)d: {animations[0]}{', etc.' if len(animations) > 1 else ''}", + ) as progress: + for t in progression: + dt, last_t = t - last_t, t + self.scene._update_animations(animations, t, dt) + self._update_frame(dt) + progress.update(1) + + def _calc_time_progression(self, run_time: float) -> npt.NDArray[np.float64]: + """Compute the time values at which to evaluate the animation""" + + return np.arange(0, run_time, 1 / config.frame_rate) + + def _calc_runtime(self, animations: Iterable[AnimationProtocol]) -> float: + """Calculate the runtime of an iterable of animations. + + .. warning:: + + If animations is a generator, this will consume the generator. + """ + return max(animation.get_run_time() for animation in animations) + + def render_state(self, write_to_file: bool | None = None) -> None: + """Render the current state of the scene. + + Any extra kwargs are passed to :meth:`_render_frame`. + """ + state = self.scene.get_state() + self._render_frame(state, write_file=write_to_file) + + def _render_frame( + self, state: SceneState, *, write_file: bool | None = None + ) -> None: + """Renders a frame based on a state, and writes it to a file. + + Any extra kwargs are passed to :meth:`write_frame`. + """ + + # render the frame to the window + # TODO: change self.scene.camera to state.camera + self.renderer.render(self.scene.camera, state.mobjects) + + should_write = write_file if write_file is not None else self._write_files + if should_write: + self.write_frame() + + def write_frame(self) -> None: + """Take a frame from the renderer and write it in the file writer.""" + + frame = self.renderer.get_pixels() + self.file_writer.write_frame(frame) + + +class NullProgressBar: + """Fake progressbar.""" + + def update(self, _: Any) -> None: ... diff --git a/manim/mobject/geometry/arc.py b/manim/mobject/geometry/arc.py index b4dc876edf..09dbcd1c57 100644 --- a/manim/mobject/geometry/arc.py +++ b/manim/mobject/geometry/arc.py @@ -275,7 +275,7 @@ def get_start(self) -> Point3D: else: return super().get_start() - def get_length(self) -> np.floating: + def get_length(self) -> float: start, end = self.get_start_and_end() return np.linalg.norm(start - end) diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index 8c5c3a73f4..d9253f1def 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -42,8 +42,8 @@ class Line(TipableVMobject): def __init__( self, - start: Point3D = LEFT, - end: Point3D = RIGHT, + start: Point3D | Mobject = LEFT, + end: Point3D | Mobject = RIGHT, buff: float = 0, path_arc: float | None = None, **kwargs, @@ -64,22 +64,38 @@ def generate_points(self) -> None: def set_points_by_ends( self, - start: Point3D, - end: Point3D, + start: Point3D | Mobject, + end: Point3D | Mobject, buff: float = 0, path_arc: float = 0, ) -> None: + """Sets the points of the line based on its start and end points. + Unlike :meth:`put_start_and_end_on`, this method respects `self.buff` and + Mobject bounding boxes. + + Parameters + ---------- + start + The start point or Mobject of the line. + end + The end point or Mobject of the line. + buff + The empty space between the start and end of the line, by default 0. + path_arc + The angle of a circle spanned by this arc, by default 0 which is a straight line. + """ + self._set_start_and_end_attrs(start, end) if path_arc: arc = ArcBetweenPoints(self.start, self.end, angle=self.path_arc) self.set_points(arc.points) else: - self.set_points_as_corners([start, end]) + self.set_points_as_corners([self.start, self.end]) self._account_for_buff(buff) init_points = generate_points - def _account_for_buff(self, buff: float) -> Self: + def _account_for_buff(self, buff: float) -> Self | None: if buff == 0: return # @@ -94,7 +110,9 @@ def _account_for_buff(self, buff: float) -> Self: self.pointwise_become_partial(self, buff_proportion, 1 - buff_proportion) return self - def _set_start_and_end_attrs(self, start: Point3D, end: Point3D) -> None: + def _set_start_and_end_attrs( + self, start: Point3D | Mobject, end: Point3D | Mobject + ) -> None: # If either start or end are Mobjects, this # gives their centers rough_start = self._pointify(start) diff --git a/manim/mobject/geometry/polygram.py b/manim/mobject/geometry/polygram.py index f43bc1d28f..4d1194eba4 100644 --- a/manim/mobject/geometry/polygram.py +++ b/manim/mobject/geometry/polygram.py @@ -23,9 +23,8 @@ from manim.constants import * from manim.mobject.geometry.arc import ArcBetweenPoints -from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.types.vectorized_mobject import VGroup from manim.utils.color import BLUE, WHITE, ParsableManimColor from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices @@ -37,7 +36,7 @@ from manim.utils.color import ParsableManimColor -class Polygram(VMobject, metaclass=ConvertToOpenGL): +class Polygram(OpenGLVMobject): """A generalized :class:`Polygon`, allowing for disconnected sets of edges. Parameters @@ -249,17 +248,17 @@ def construct(self): if evenly_distribute_anchors: # Determine the average length of each curve - nonZeroLengthArcs = [arc for arc in arcs if len(arc.points) > 4] - if len(nonZeroLengthArcs): + non_zero_length_arcs = [arc for arc in arcs if len(arc.points) > 4] + if len(non_zero_length_arcs): totalArcLength = sum( - [arc.get_arc_length() for arc in nonZeroLengthArcs] + [arc.get_arc_length() for arc in non_zero_length_arcs] ) totalCurveCount = ( - sum([len(arc.points) for arc in nonZeroLengthArcs]) / 4 + sum([len(arc.points) for arc in non_zero_length_arcs]) / 4 ) - averageLengthPerCurve = totalArcLength / totalCurveCount + average_length_per_curve = totalArcLength / totalCurveCount else: - averageLengthPerCurve = 1 + average_length_per_curve = 1 # To ensure that we loop through starting with last arcs = [arcs[-1], *arcs[:-1]] @@ -273,7 +272,7 @@ def construct(self): # Make sure anchors are evenly distributed, if necessary if evenly_distribute_anchors: line.insert_n_curves( - ceil(line.get_length() / averageLengthPerCurve) + ceil(line.get_length() / average_length_per_curve) # type: ignore ) new_points.extend(line.points) @@ -720,7 +719,7 @@ def __init__(self, corner_radius: float | list[float] = 0.5, **kwargs): self.round_corners(self.corner_radius) -class Cutout(VMobject, metaclass=ConvertToOpenGL): +class Cutout(OpenGLVMobject): """A shape with smaller cutouts. Parameters diff --git a/manim/mobject/graph.py b/manim/mobject/graph.py index d54c1e0457..72a26d27db 100644 --- a/manim/mobject/graph.py +++ b/manim/mobject/graph.py @@ -561,6 +561,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL): all other configuration options for a vertex. edge_type The mobject class used for displaying edges in the scene. + Must be a subclass of :class:`~.Line` for default updaters to work. edge_config Either a dictionary containing keyword arguments to be passed to the class specified via ``edge_type``, or a dictionary whose @@ -1559,7 +1560,12 @@ def _populate_edge_dict( def update_edges(self, graph): for (u, v), edge in graph.edges.items(): # Undirected graph has a Line edge - edge.put_start_and_end_on(graph[u].get_center(), graph[v].get_center()) + edge.set_points_by_ends( + graph[u].get_center(), + graph[v].get_center(), + buff=self._edge_config.get("buff", 0), + path_arc=self._edge_config.get("path_arc", 0), + ) def __repr__(self: Graph) -> str: return f"Undirected graph on {len(self.vertices)} vertices and {len(self.edges)} edges" @@ -1768,10 +1774,15 @@ def update_edges(self, graph): deformed. """ for (u, v), edge in graph.edges.items(): - edge_type = type(edge) tip = edge.pop_tips()[0] - new_edge = edge_type(self[u], self[v], **self._edge_config[(u, v)]) - edge.become(new_edge) + # Passing the Mobject instead of the vertex makes the tip + # stop on the bounding box of the vertex. + edge.set_points_by_ends( + graph[u], + graph[v], + buff=self._edge_config.get("buff", 0), + path_arc=self._edge_config.get("path_arc", 0), + ) edge.add_tip(tip) def __repr__(self: DiGraph) -> str: diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 38083ae42e..c8b6642182 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -21,11 +21,10 @@ import numpy as np +from manim import config, logger +from manim.constants import * from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL - -from .. import config, logger -from ..constants import * -from ..utils.color import ( +from manim.utils.color import ( BLACK, WHITE, YELLOW_C, @@ -34,28 +33,27 @@ color_gradient, interpolate_color, ) -from ..utils.exceptions import MultiAnimationOverrideException -from ..utils.iterables import list_update, remove_list_redundancies -from ..utils.paths import straight_path -from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix +from manim.utils.exceptions import MultiAnimationOverrideException +from manim.utils.iterables import list_update, remove_list_redundancies +from manim.utils.paths import straight_path +from manim.utils.space_ops import angle_between_vectors, normalize, rotation_matrix if TYPE_CHECKING: from typing_extensions import Self, TypeAlias + from manim.animation.animation import Animation from manim.typing import ( FunctionOverride, - Image, ManimFloat, ManimInt, MappingFunction, PathFuncType, + PixelArray, Point3D, Point3D_Array, Vector3D, ) - from ..animation.animation import Animation - TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object] NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object] Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater @@ -149,7 +147,7 @@ def _assert_valid_submobjects(self, submobjects: Iterable[Mobject]) -> Self: return self._assert_valid_submobjects_internal(submobjects, Mobject) def _assert_valid_submobjects_internal( - self, submobjects: list[Mobject], mob_class: type[Mobject] + self, submobjects: Iterable[Mobject], mob_class: type[Mobject] ) -> Self: for i, submob in enumerate(submobjects): if not isinstance(submob, mob_class): @@ -825,9 +823,9 @@ def apply_over_attr_arrays(self, func: MappingFunction) -> Self: # Displaying - def get_image(self, camera=None) -> Image: + def get_image(self, camera=None) -> PixelArray: if camera is None: - from ..camera.cairo_camera import CairoCamera as Camera + from manim.camera.cairo_camera import CairoCamera as Camera camera = Camera() camera.capture_mobject(self) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index afdcc63bd5..03dc056ac8 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -11,7 +11,7 @@ from dataclasses import dataclass from functools import partialmethod, wraps from math import ceil -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generic import moderngl import numpy as np @@ -50,26 +50,36 @@ from collections.abc import Iterable, Sequence from typing import Any, Callable, Union - from typing_extensions import Self, TypeAlias + import numpy.typing as npt + from typing_extensions import Concatenate, ParamSpec, Self, TypeAlias + + from manim.animation.animation import Animation + from manim.renderer.renderer import RendererData + from manim.typing import PathFuncType, Point3D, Point3D_Array TimeBasedUpdater: TypeAlias = Callable[ ["OpenGLMobject", float], "OpenGLMobject | None" ] - NonTimeUpdater: TypeAlias = Callable[["OpenGLMobject"], "OpenGLMobject" | None] + NonTimeUpdater: TypeAlias = Callable[["OpenGLMobject"], "OpenGLMobject | None"] Updater: TypeAlias = Union[TimeBasedUpdater, NonTimeUpdater] PointUpdateFunction: TypeAlias = Callable[[np.ndarray], np.ndarray] - from manim.renderer.renderer import RendererData - from manim.typing import PathFuncType - T = TypeVar("T", bound=RendererData) - _F = TypeVar("_F", bound=Callable[..., Any]) + M = TypeVar("M", bound="OpenGLMobject") + T = TypeVar("T") + P = ParamSpec("P") + + +R = TypeVar("R", bound="RendererData") +T_co = TypeVar("T_co", covariant=True, bound="OpenGLMobject") UNIFORM_DTYPE = np.float64 -def stash_mobject_pointers(func: _F) -> _F: +def stash_mobject_pointers( + func: Callable[Concatenate[M, P], T], +) -> Callable[Concatenate[M, P], T]: @wraps(func) - def wrapper(self, *args, **kwargs): + def wrapper(self: M, *args: P.args, **kwargs: P.kwargs): uncopied_attrs = ["parents", "target", "saved_state"] stash = {} for attr in uncopied_attrs: @@ -105,7 +115,9 @@ class MobjectStatus: points_changed: bool = False -class OpenGLMobject: +# it's generic in its renderer, which is a little bit cursed +# In the future, it should be replaced with a RendererData protocol +class OpenGLMobject(Generic[R]): """Mathematical Object: base class for objects that can be displayed on screen. Attributes @@ -166,7 +178,8 @@ def __init__( self.data: dict[str, np.ndarray] = {} self.uniforms: dict[str, float | np.ndarray] = {} - self.renderer_data: T | None = None + # TODO replace with protocol + self.renderer_data: R | None = None self.status = MobjectStatus() self.init_data() @@ -304,9 +317,9 @@ def init_points(self): # Typically implemented in subclass, unless purposefully left blank pass - def set_data(self, data): - for key in data: - self.data[key] = data[key] + def set_data(self, data: dict[str, Any]): + for key, value in data.items(): + self.data[key] = value return self def set_uniforms(self, uniforms): @@ -316,8 +329,12 @@ def set_uniforms(self, uniforms): self.uniforms[key] = value return self + # https://github.com/python/typing/issues/802 + # so we hack around it by doing | Self + # but this causes issues in Scene.play which only + # accepts _AnimationBuilder/Animations, not Mobjects @property - def animate(self) -> _AnimationBuilder: + def animate(self) -> _AnimationBuilder[Self] | Self: """Used to animate the application of a method. .. warning:: @@ -669,13 +686,15 @@ def refresh_bounding_box(self, recurse_down=False, recurse_up=True): parent.refresh_bounding_box() return self - def are_points_touching(self, points, buff: float = 0) -> np.ndarray: + def are_points_touching( + self, points: Point3D_Array, buff: float = 0 + ) -> npt.NDArray[bool]: bb = self.get_bounding_box() mins = bb[0] - buff maxs = bb[2] + buff return ((points >= mins) * (points <= maxs)).all(1) - def is_point_touching(self, point, buff=MED_SMALL_BUFF): + def is_point_touching(self, point: Point3D, buff: float = MED_SMALL_BUFF) -> bool: return self.are_points_touching(np.array(point, ndmin=2), buff)[0] def is_touching(self, mobject: OpenGLMobject, buff: float = 1e-2) -> bool: @@ -709,13 +728,22 @@ def __len__(self) -> int: def split(self) -> list[OpenGLMobject]: return self.submobjects - def assemble_family(self) -> Self: + def note_changed_family(self) -> Self: + """Updates bounding boxes and updater statuses. + + This used to be called ``assemble_family`` + + .. warning:: + + Remove the above remark about ``assemble_family`` before experimental + is merged, it's a note to MrDiver and other devs + """ sub_families = (sm.get_family() for sm in self.submobjects) self.family = [self, *uniq_chain(*sub_families)] self.refresh_has_updater_status() self.refresh_bounding_box() for parent in self.parents: - parent.assemble_family() + parent.note_changed_family() return self def get_family(self, recurse=True) -> list[OpenGLMobject]: @@ -817,7 +845,7 @@ def add( self.submobjects.append(mobject) if self not in mobject.parents: mobject.parents.append(self) - self.assemble_family() + self.note_changed_family() return self def remove(self, *mobjects: OpenGLMobject, reassemble: bool = True) -> Self: @@ -848,7 +876,7 @@ def remove(self, *mobjects: OpenGLMobject, reassemble: bool = True) -> Self: if self in mobject.parents: mobject.parents.remove(self) if reassemble: - self.assemble_family() + self.note_changed_family() return self def add_to_back(self, *mobjects: OpenGLMobject) -> Self: @@ -905,7 +933,7 @@ def replace_submobject(self, index, new_submob): old_submob.parents.remove(self) self.submobjects[index] = new_submob new_submob.parents.append(self) - self.assemble_family() + self.note_changed_family() return self def insert_submobject(self, index: int, mobject: OpenGLMobject): @@ -934,7 +962,7 @@ def insert_submobject(self, index: int, mobject: OpenGLMobject): if mobject not in self.submobjects: self.submobjects.insert(index, mobject) - self.assemble_family() + self.note_changed_family() return self def set_submobjects(self, submobject_list: list[OpenGLMobject]): @@ -1316,7 +1344,7 @@ def construct(self): for submob in self.submobjects: submob.shuffle(recurse=True) random.shuffle(self.submobjects) - self.assemble_family() + self.note_changed_family() return self def reverse_submobjects(self, recursive=False): @@ -1344,7 +1372,7 @@ def construct(self): if recursive: for submob in self.submobjects: submob.reverse_submobjects(recursive=True) - self.assemble_family() + self.note_changed_family() # Copying @@ -1512,6 +1540,7 @@ def get_grid( def init_updaters(self) -> None: self.time_based_updaters: list[TimeBasedUpdater] = [] self.non_time_updaters: list[NonTimeUpdater] = [] + # so that we don't have to refind updaters self.has_updaters: bool = False self.updating_suspended: bool = False @@ -2635,6 +2664,16 @@ def get_mobject_type_class(): # Alignment + def is_aligned_with(self, mobject: OpenGLMobject) -> bool: + return ( + len(self.data) == len(mobject.data) + and len(self.submobjects) == len(mobject.submobjects) + and all( + sm1.is_aligned_with(sm2) + for sm1, sm2 in zip(self.submobjects, mobject.submobjects) + ) + ) + def align_data_and_family(self, mobject): self.align_family(mobject) self.align_data(mobject) @@ -3152,8 +3191,8 @@ def set_location(self, new_loc): self.set_points(np.array(new_loc, ndmin=2, dtype=float)) -class _AnimationBuilder: - def __init__(self, mobject): +class _AnimationBuilder(Generic[T_co]): + def __init__(self, mobject: T_co): self.mobject = mobject self.mobject.generate_target() @@ -3165,7 +3204,7 @@ def __init__(self, mobject): self.cannot_pass_args = False self.anim_args = {} - def __call__(self, **kwargs) -> _AnimationBuilder: + def __call__(self, **kwargs) -> _AnimationBuilder[T_co]: if self.cannot_pass_args: raise ValueError( "Animation arguments must be passed before accessing methods and can only be passed once", @@ -3176,7 +3215,7 @@ def __call__(self, **kwargs) -> _AnimationBuilder: return self - def __getattr__(self, method_name): + def __getattr__(self, method_name: str): method = getattr(self.mobject.target, method_name) has_overridden_animation = hasattr(method, "_override_animate") @@ -3204,7 +3243,7 @@ def update_target(*method_args, **method_kwargs): return update_target - def build(self): + def build(self) -> Animation: from manim.animation.transform import _MethodAnimation if self.overridden_animation: diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index 6fd0ae074b..0be16b77d3 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -16,15 +16,15 @@ ) from manim.utils.bezier import ( bezier, + bezier_remap, get_quadratic_approximation_of_cubic, get_smooth_cubic_bezier_handle_points, get_smooth_quadratic_bezier_handle_points, integer_interpolate, interpolate, inverse_interpolate, - partial_quadratic_bezier_points, + partial_bezier_points, proportions_along_bezier_curve_for_point, - quadratic_bezier_remap, ) from manim.utils.color import * from manim.utils.deprecation import deprecated @@ -94,9 +94,11 @@ def __init__( fill_color = color if stroke_color is None: stroke_color = color - self.fill_color: Iterable[ManimColor] = listify(ManimColor.parse(fill_color)) + self.fill_color: Sequence[ManimColor] = listify(ManimColor.parse(fill_color)) self.set_fill(opacity=fill_opacity) - self.stroke_color = listify(ManimColor.parse(stroke_color)) + self.stroke_color: Sequence[ManimColor] = listify( + ManimColor.parse(stroke_color) + ) self.set_stroke(opacity=stroke_opacity) if stroke_width is None: stroke_width = DEFAULT_STROKE_WIDTH @@ -115,7 +117,7 @@ def __init__( super().__init__(**kwargs) # self.refresh_unit_normal() - def get_group_class(self): + def get_group_class(self) -> type[OpenGLVGroup]: # type: ignore return OpenGLVGroup @staticmethod @@ -207,7 +209,7 @@ def set_rgba_array( def set_fill( self, - color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, + color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, opacity: float | None = None, recurse: bool = True, ) -> Self: @@ -276,7 +278,7 @@ def set_stroke( def set_backstroke( self, - color: Color | Iterable[Color] | None = None, + color: ManimColor | Iterable[ManimColor] | None = None, width: float | Iterable[float] = 3, background: bool = True, ) -> Self: @@ -292,11 +294,9 @@ def align_stroke_width_data_to_points(self, recurse: bool = True) -> None: def set_style( self, fill_color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, - fill_opacity: float | Iterable[float] | None = None, - fill_rgba: np.ndarray | None = None, + fill_opacity: float | None = None, stroke_color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, stroke_opacity: float | Iterable[float] | None = None, - stroke_rgba: np.ndarray | None = None, stroke_width: float | Iterable[float] | None = None, stroke_background: bool = True, reflectiveness: float | None = None, @@ -564,7 +564,7 @@ def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, recurse=True): alphas = np.linspace(0, 1, n + 1) new_points.extend( [ - partial_quadratic_bezier_points(tup, a1, a2) + partial_bezier_points(tup, a1, a2) for a1, a2 in zip(alphas, alphas[1:]) ], ) @@ -639,7 +639,8 @@ def change_anchor_mode(self, mode) -> Self: elif mode == "jagged": new_subpath[1::nppc] = 0.5 * (anchors[:-1] + anchors[1:]) submob.append_points(new_subpath) - submob.refresh_triangulation() + # TODO: not implemented + # submob.refresh_triangulation() return self def make_smooth(self): @@ -1197,9 +1198,9 @@ def get_unit_normal(self) -> np.ndarray: return normal # Alignment - def align_points(self, vmobject): + def align_points(self, vmobject: OpenGLVMobject) -> Self: # TODO: This shortcut can be a bit over eager. What if they have the same length, but different subpath lengths? - if self.get_num_points() == len(vmobject.points): + if self.get_num_points() == vmobject.get_num_points(): return for mob in self, vmobject: @@ -1273,14 +1274,13 @@ def insert_n_curves(self, n: int, recurse=True) -> Self: return self def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarray: - """Given an array of k points defining a bezier curves - (anchors and handles), returns points defining exactly - k + n bezier curves. + """Given an array of 3k points defining a Bézier curve (anchors and + handles), return 3(k+n) points defining exactly k + n Bézier curves. Parameters ---------- n - Number of desired curves. + Number of desired curves to insert. points Starting points. @@ -1289,34 +1289,16 @@ def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarra np.ndarray Points generated. """ - nppc = self.n_points_per_curve + if len(points) == 1: + nppc = self.n_points_per_curve return np.repeat(points, nppc * n, 0) - - bezier_groups = self.get_bezier_tuples_from_points(points) - norms = np.array([get_norm(bg[nppc - 1] - bg[0]) for bg in bezier_groups]) - total_norm = sum(norms) - # Calculate insertions per curve (ipc) - if total_norm < 1e-6: - ipc = [n] + [0] * (len(bezier_groups) - 1) - else: - ipc = np.round(n * norms / sum(norms)).astype(int) - - diff = n - sum(ipc) - for _ in range(diff): - ipc[np.argmin(ipc)] += 1 - for _ in range(-diff): - ipc[np.argmax(ipc)] -= 1 - - new_points = [] - for group, n_inserts in zip(bezier_groups, ipc): - # What was once a single quadratic curve defined - # by "group" will now be broken into n_inserts + 1 - # smaller quadratic curves - alphas = np.linspace(0, 1, n_inserts + 2) - for a1, a2 in zip(alphas, alphas[1:]): - new_points += partial_quadratic_bezier_points(group, a1, a2) - return np.vstack(new_points) + bezier_tuples = self.get_bezier_tuples_from_points(points) + current_number_of_curves = len(bezier_tuples) + new_number_of_curves = current_number_of_curves + n + new_bezier_tuples = bezier_remap(bezier_tuples, new_number_of_curves) + new_points = new_bezier_tuples.reshape(-1, 3) + return new_points def interpolate_color(self, mobject1, mobject2, alpha): attrs = [ @@ -1396,7 +1378,7 @@ def pointwise_become_partial( return self if lower_index == upper_index: self.append_points( - partial_quadratic_bezier_points( + partial_bezier_points( bezier_triplets[lower_index], lower_residue, upper_residue, @@ -1404,24 +1386,18 @@ def pointwise_become_partial( ) else: self.append_points( - partial_quadratic_bezier_points( - bezier_triplets[lower_index], lower_residue, 1 - ), + partial_bezier_points(bezier_triplets[lower_index], lower_residue, 1), ) inner_points = bezier_triplets[lower_index + 1 : upper_index] if len(inner_points) > 0: if remap: - new_triplets = quadratic_bezier_remap( - inner_points, num_quadratics - 2 - ) + new_triplets = bezier_remap(inner_points, num_quadratics - 2) else: new_triplets = bezier_triplets self.append_points(np.asarray(new_triplets).reshape(-1, 3)) self.append_points( - partial_quadratic_bezier_points( - bezier_triplets[upper_index], 0, upper_residue - ), + partial_bezier_points(bezier_triplets[upper_index], 0, upper_residue), ) return self @@ -1448,21 +1424,6 @@ def get_subcurve(self, a: float, b: float) -> Self: # Related to triangulation - def set_points(self, points): - super().set_points(points) - return self - - def append_points(self, points): - return super().append_points(points) - - def reverse_points(self): - return super().reverse_points() - - def set_data(self, data): - super().set_data(data) - return self - - # TODO, how to be smart about tangents here? def apply_function(self, function, make_smooth=False, **kwargs): super().apply_function(function, **kwargs) if self.make_smooth_after_applying_functions or make_smooth: @@ -1566,14 +1527,14 @@ def set_z(self, z: float) -> Self: return self @deprecated( - since="0.18.2", - until="0.19.0", + since="0.20.0", + until="0.21.0", message="OpenGL has no concept of z_index. Use set_z instead", ) def set_z_index(self, z: float) -> Self: return self.set_z(z) - def add(self, *vmobjects: OpenGLVMobject): # type: ignore + def add(self, *vmobjects: OpenGLVMobject): """Checks if all passed elements are an instance of OpenGLVMobject and then add them to submobjects Parameters diff --git a/manim/mobject/svg/brace.py b/manim/mobject/svg/brace.py index 17429bb729..600e841f65 100644 --- a/manim/mobject/svg/brace.py +++ b/manim/mobject/svg/brace.py @@ -5,6 +5,7 @@ __all__ = ["Brace", "BraceLabel", "ArcBrace", "BraceText", "BraceBetweenPoints"] from collections.abc import Sequence +from typing import TYPE_CHECKING import numpy as np import svgelements as se @@ -24,6 +25,10 @@ from ...utils.color import BLACK from ..svg.svg_mobject import VMobjectFromSVGPath +if TYPE_CHECKING: + from manim.typing import Point3D, Vector3D + from manim.utils.color.core import ParsableManimColor + __all__ = ["Brace", "BraceBetweenPoints", "BraceLabel", "ArcBrace"] @@ -65,13 +70,13 @@ def construct(self): def __init__( self, mobject: Mobject, - direction: Sequence[float] | None = DOWN, - buff=0.2, - sharpness=2, - stroke_width=0, - fill_opacity=1.0, - background_stroke_width=0, - background_stroke_color=BLACK, + direction: Vector3D | None = DOWN, + buff: float = 0.2, + sharpness: float = 2, + stroke_width: float = 0, + fill_opacity: float = 1.0, + background_stroke_width: float = 0, + background_stroke_color: ParsableManimColor = BLACK, **kwargs, ): path_string_template = ( @@ -125,7 +130,20 @@ def __init__( for mob in mobject, self: mob.rotate(angle, about_point=ORIGIN) - def put_at_tip(self, mob, use_next_to=True, **kwargs): + def put_at_tip(self, mob: Mobject, use_next_to: bool = True, **kwargs): + """Puts the given mobject at the brace tip. + + Parameters + ---------- + mob + The mobject to be placed at the tip. + use_next_to + If true, then :meth:`next_to` is used to place the mobject at the + tip. + kwargs + Any additional keyword arguments are passed to :meth:`next_to` which + is used to put the mobject next to the brace tip. + """ if use_next_to: mob.next_to(self.get_tip(), np.round(self.get_direction()), **kwargs) else: @@ -136,16 +154,45 @@ def put_at_tip(self, mob, use_next_to=True, **kwargs): return self def get_text(self, *text, **kwargs): + """Places the text at the brace tip. + + Parameters + ---------- + text + The text to be placed at the brace tip. + kwargs + Any additional keyword arguments are passed to :meth:`.put_at_tip` which + is used to position the text at the brace tip. + + Returns + ------- + :class:`~.Tex` + """ text_mob = Tex(*text) self.put_at_tip(text_mob, **kwargs) return text_mob def get_tex(self, *tex, **kwargs): + """Places the tex at the brace tip. + + Parameters + ---------- + tex + The tex to be placed at the brace tip. + kwargs + Any further keyword arguments are passed to :meth:`.put_at_tip` which + is used to position the tex at the brace tip. + + Returns + ------- + :class:`~.MathTex` + """ tex_mob = MathTex(*tex) self.put_at_tip(tex_mob, **kwargs) return tex_mob def get_tip(self): + """Returns the point at the brace tip.""" # Returns the position of the seventh point in the path, which is the tip. if config["renderer"] == "opengl": return self.points[34] @@ -153,6 +200,7 @@ def get_tip(self): return self.points[28] # = 7*4 def get_direction(self): + """Returns the direction from the center to the brace tip.""" vect = self.get_tip() - self.get_center() return vect / np.linalg.norm(vect) @@ -269,9 +317,9 @@ def construct(self): def __init__( self, - point_1: Sequence[float] | None, - point_2: Sequence[float] | None, - direction: Sequence[float] | None = ORIGIN, + point_1: Point3D | None, + point_2: Point3D | None, + direction: Vector3D | None = ORIGIN, **kwargs, ): if all(direction == ORIGIN): diff --git a/manim/mobject/three_d/three_dimensions.py b/manim/mobject/three_d/three_dimensions.py index def02d23a8..d7d10bad95 100644 --- a/manim/mobject/three_d/three_dimensions.py +++ b/manim/mobject/three_d/three_dimensions.py @@ -30,9 +30,9 @@ from manim.mobject.geometry.arc import Circle from manim.mobject.geometry.polygram import Square from manim.mobject.mobject import * -from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup from manim.utils.color import ( ManimColor, ParsableManimColor, @@ -41,12 +41,12 @@ from manim.utils.space_ops import normalize, perpendicular_bisector, z_to_vector -class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL): +class ThreeDVMobject(OpenGLVMobject): def __init__(self, shade_in_3d: bool = True, **kwargs): super().__init__(shade_in_3d=shade_in_3d, **kwargs) -class Surface(VGroup, metaclass=ConvertToOpenGL): +class Surface(VGroup): """Creates a Parametric Surface using a checkerboard pattern. Parameters @@ -613,17 +613,18 @@ def __init__( **kwargs, ) # used for rotations + self.new_height = height self._current_theta = 0 self._current_phi = 0 - + self.base_circle = Circle( + radius=base_radius, + color=self.fill_color, + fill_opacity=self.fill_opacity, + stroke_width=0, + ) + self.base_circle.shift(height * IN) + self._set_start_and_end_attributes(direction) if show_base: - self.base_circle = Circle( - radius=base_radius, - color=self.fill_color, - fill_opacity=self.fill_opacity, - stroke_width=0, - ) - self.base_circle.shift(height * IN) self.add(self.base_circle) self._rotate_to_direction() @@ -653,6 +654,12 @@ def func(self, u: float, v: float) -> np.ndarray: ], ) + def get_start(self) -> np.ndarray: + return self.start_point.get_center() + + def get_end(self) -> np.ndarray: + return self.end_point.get_center() + def _rotate_to_direction(self) -> None: x, y, z = self.direction @@ -707,6 +714,15 @@ def get_direction(self) -> np.ndarray: """ return self.direction + def _set_start_and_end_attributes(self, direction): + normalized_direction = direction * np.linalg.norm(direction) + + start = self.base_circle.get_center() + end = start + normalized_direction * self.new_height + self.start_point = VectorizedPoint(start) + self.end_point = VectorizedPoint(end) + self.add(self.start_point, self.end_point) + class Cylinder(Surface): """A cylinder, defined by its height, radius and direction, @@ -1149,14 +1165,20 @@ def __init__( self.end - height * self.direction, **kwargs, ) - self.cone = Cone( - direction=self.direction, base_radius=base_radius, height=height, **kwargs + direction=self.direction, + base_radius=base_radius, + height=height, + **kwargs, ) self.cone.shift(end) - self.add(self.cone) + self.end_point = VectorizedPoint(end) + self.add(self.end_point, self.cone) self.set_color(color) + def get_end(self) -> np.ndarray: + return self.end_point.get_center() + class Torus(Surface): """A torus. diff --git a/manim/mobject/types/point_cloud_mobject.py b/manim/mobject/types/point_cloud_mobject.py index 4f43dbf597..04da9e75b6 100644 --- a/manim/mobject/types/point_cloud_mobject.py +++ b/manim/mobject/types/point_cloud_mobject.py @@ -23,7 +23,7 @@ ) from ...utils.iterables import stretch_array_to_length -__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot", "Point"] +__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot"] class PMobject(Mobject, metaclass=ConvertToOpenGL): @@ -199,7 +199,7 @@ def align_points_with_larger(self, larger_mobject): def get_point_mobject(self, center=None): if center is None: center = self.get_center() - return Point(center) + return PMobject().set_points([center]) def interpolate_color(self, mobject1, mobject2, alpha): self.rgbas = interpolate(mobject1.rgbas, mobject2.rgbas, alpha) @@ -348,37 +348,3 @@ def generate_points(self): ) ], ) - - -class Point(PMobject): - """A mobject representing a point. - - Examples - -------- - - .. manim:: ExamplePoint - :save_last_frame: - - class ExamplePoint(Scene): - def construct(self): - colorList = [RED, GREEN, BLUE, YELLOW] - for i in range(200): - point = Point(location=[0.63 * np.random.randint(-4, 4), 0.37 * np.random.randint(-4, 4), 0], color=np.random.choice(colorList)) - self.add(point) - for i in range(200): - point = Point(location=[0.37 * np.random.randint(-4, 4), 0.63 * np.random.randint(-4, 4), 0], color=np.random.choice(colorList)) - self.add(point) - self.add(point) - """ - - def __init__(self, location=ORIGIN, color=BLACK, **kwargs): - self.location = location - super().__init__(color=color, **kwargs) - - def init_points(self): - self.reset_points() - self.generate_points() - self.set_points([self.location]) - - def generate_points(self): - self.add_points([self.location]) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index e1e480ce83..da5a455819 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -32,7 +32,8 @@ from ...mobject.mobject import Mobject from ...utils.bezier import ( bezier, - get_smooth_handle_points, + bezier_remap, + get_smooth_cubic_bezier_handle_points, integer_interpolate, interpolate, partial_bezier_points, @@ -46,6 +47,7 @@ if TYPE_CHECKING: from typing_extensions import Self + # TODO # - Change cubic curve groups to have 4 points instead of 3 # - Change sub_path idea accordingly @@ -923,8 +925,8 @@ def change_anchor_mode(self, mode: str): # The append is needed as the last element is not reached when slicing with numpy. anchors = np.append(subpath[::nppcc], subpath[-1:], 0) if mode == "smooth": - h1, h2 = get_smooth_handle_points(anchors) - elif mode == "jagged": + h1, h2 = get_smooth_cubic_bezier_handle_points(anchors) + else: # mode == "jagged" # The following will make the handles aligned with the anchors, thus making the bezier curve a segment a1 = anchors[:-1] a2 = anchors[1:] @@ -1577,40 +1579,11 @@ def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarra if len(points) == 1: nppcc = self.n_points_per_cubic_curve return np.repeat(points, nppcc * n, 0) - bezier_quads = self.get_cubic_bezier_tuples_from_points(points) - curr_num = len(bezier_quads) - target_num = curr_num + n - # This is an array with values ranging from 0 - # up to curr_num, with repeats such that - # it's total length is target_num. For example, - # with curr_num = 10, target_num = 15, this would - # be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9] - repeat_indices = (np.arange(target_num, dtype="i") * curr_num) // target_num - - # If the nth term of this list is k, it means - # that the nth curve of our path should be split - # into k pieces. - # In the above example our array had the following elements - # [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9] - # We have two 0s, one 1, two 2s and so on. - # The split factors array would hence be: - # [2, 1, 2, 1, 2, 1, 2, 1, 2, 1] - split_factors = np.zeros(curr_num, dtype="i") - for val in repeat_indices: - split_factors[val] += 1 - - new_points = np.zeros((0, self.dim)) - for quad, sf in zip(bezier_quads, split_factors): - # What was once a single cubic curve defined - # by "quad" will now be broken into sf - # smaller cubic curves - alphas = np.linspace(0, 1, sf + 1) - for a1, a2 in zip(alphas, alphas[1:]): - new_points = np.append( - new_points, - partial_bezier_points(quad, a1, a2), - axis=0, - ) + bezier_tuples = self.get_cubic_bezier_tuples_from_points(points) + current_number_of_curves = len(bezier_tuples) + new_number_of_curves = current_number_of_curves + n + new_bezier_tuples = bezier_remap(bezier_tuples, new_number_of_curves) + new_points = new_bezier_tuples.reshape(-1, 3) return new_points def align_rgbas(self, vmobject): @@ -1657,61 +1630,92 @@ def pointwise_become_partial( vmobject: VMobject, a: float, b: float, - ): - """Given two bounds a and b, transforms the points of the self vmobject into the points of the vmobject - passed as parameter with respect to the bounds. Points here stand for control points of the bezier curves (anchors and handles) + ) -> Self: + """Given a 2nd :class:`.VMobject` ``vmobject``, a lower bound ``a`` and + an upper bound ``b``, modify this :class:`.VMobject`'s points to + match the portion of the Bézier spline described by ``vmobject.points`` + with the parameter ``t`` between ``a`` and ``b``. Parameters ---------- vmobject - The vmobject that will serve as a model. + The :class:`.VMobject` that will serve as a model. a - upper-bound. + The lower bound for ``t``. b - lower-bound + The upper bound for ``t`` Returns ------- - :class:`VMobject` - ``self`` + :class:`.VMobject` + The :class:`.VMobject` itself, after the transformation. + + Raises + ------ + TypeError + If ``vmobject`` is not an instance of :class:`VMobject`. """ - assert isinstance(vmobject, VMobject) + if not isinstance(vmobject, VMobject): + raise TypeError( + f"Expected a VMobject, got value {vmobject} of type " + f"{type(vmobject).__name__}." + ) # Partial curve includes three portions: - # - A middle section, which matches the curve exactly - # - A start, which is some ending portion of an inner cubic - # - An end, which is the starting portion of a later inner cubic + # - A middle section, which matches the curve exactly. + # - A start, which is some ending portion of an inner cubic. + # - An end, which is the starting portion of a later inner cubic. if a <= 0 and b >= 1: self.set_points(vmobject.points) return self - bezier_quads = vmobject.get_cubic_bezier_tuples() - num_cubics = len(bezier_quads) - - # The following two lines will compute which bezier curves of the given mobject need to be processed. - # The residue basically indicates de proportion of the selected bezier curve that have to be selected. - # Ex : if lower_index is 3, and lower_residue is 0.4, then the algorithm will append to the points 0.4 of the third bezier curve - lower_index, lower_residue = integer_interpolate(0, num_cubics, a) - upper_index, upper_residue = integer_interpolate(0, num_cubics, b) - - self.clear_points() - if num_cubics == 0: + num_curves = vmobject.get_num_curves() + if num_curves == 0: + self.clear_points() return self + + # The following two lines will compute which Bézier curves of the given Mobject must be processed. + # The residue indicates the proportion of the selected Bézier curve which must be selected. + # + # Example: if num_curves is 10, a is 0.34 and b is 0.78, then: + # - lower_index is 3 and lower_residue is 0.4, which means the algorithm will look at the 3rd Bézier + # and select its part which ranges from t=0.4 to t=1. + # - upper_index is 7 and upper_residue is 0.8, which means the algorithm will look at the 7th Bézier + # and select its part which ranges from t=0 to t=0.8. + lower_index, lower_residue = integer_interpolate(0, num_curves, a) + upper_index, upper_residue = integer_interpolate(0, num_curves, b) + + nppc = self.n_points_per_curve + # If both indices coincide, get a part of a single Bézier curve. if lower_index == upper_index: - self.append_points( - partial_bezier_points( - bezier_quads[lower_index], - lower_residue, - upper_residue, - ), + # Look at the "lower_index"-th Bézier curve and select its part from + # t=lower_residue to t=upper_residue. + self.points = partial_bezier_points( + vmobject.points[nppc * lower_index : nppc * (lower_index + 1)], + lower_residue, + upper_residue, ) else: - self.append_points( - partial_bezier_points(bezier_quads[lower_index], lower_residue, 1), + # Allocate space for (upper_index-lower_index+1) Bézier curves. + self.points = np.empty((nppc * (upper_index - lower_index + 1), self.dim)) + # Look at the "lower_index"-th Bezier curve and select its part from + # t=lower_residue to t=1. This is the first curve in self.points. + self.points[:nppc] = partial_bezier_points( + vmobject.points[nppc * lower_index : nppc * (lower_index + 1)], + lower_residue, + 1, ) - for quad in bezier_quads[lower_index + 1 : upper_index]: - self.append_points(quad) - self.append_points( - partial_bezier_points(bezier_quads[upper_index], 0, upper_residue), + # If there are more curves between the "lower_index"-th and the + # "upper_index"-th Béziers, add them all to self.points. + self.points[nppc:-nppc] = vmobject.points[ + nppc * (lower_index + 1) : nppc * upper_index + ] + # Look at the "upper_index"-th Bézier curve and select its part from + # t=0 to t=upper_residue. This is the last curve in self.points. + self.points[-nppc:] = partial_bezier_points( + vmobject.points[nppc * upper_index : nppc * (upper_index + 1)], + 0, + upper_residue, ) + return self def get_subcurve(self, a: float, b: float) -> VMobject: @@ -1882,7 +1886,7 @@ def __str__(self): f"submobject{'s' if len(self.submobjects) > 0 else ''}" ) - def add(self, *vmobjects: VMobject): + def add(self, *vmobjects: OpenGLVMobject): """Checks if all passed elements are an instance of VMobject and then add them to submobjects Parameters @@ -1930,8 +1934,7 @@ def construct(self): (gr-circle_red).animate.shift(RIGHT) ) """ - if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in vmobjects): - raise TypeError("All submobjects must be of type VMobject") + # leave here because the docstring is useful return super().add(*vmobjects) def __add__(self, vmobject): @@ -2146,7 +2149,7 @@ def remove(self, key: typing.Hashable): my_dict.remove("square") """ if key not in self.submob_dict: - raise KeyError("The given key '%s' is not present in the VDict" % str(key)) + raise KeyError(f"The given key '{key}' is not present in the VDict") super().remove(self.submob_dict[key]) del self.submob_dict[key] return self diff --git a/manim/opengl/__init__.py b/manim/opengl/__init__.py deleted file mode 100644 index 79f9c112df..0000000000 --- a/manim/opengl/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -try: - from dearpygui import dearpygui as dpg -except ImportError: - pass - - -from manim.mobject.opengl.dot_cloud import * -from manim.mobject.opengl.opengl_image_mobject import * -from manim.mobject.opengl.opengl_mobject import * -from manim.mobject.opengl.opengl_point_cloud_mobject import * -from manim.mobject.opengl.opengl_surface import * -from manim.mobject.opengl.opengl_three_dimensions import * -from manim.mobject.opengl.opengl_vectorized_mobject import * - -from ..utils.opengl import * diff --git a/manim/plugins/__init__.py b/manim/plugins/__init__.py index 06314895e1..23e8c4e137 100644 --- a/manim/plugins/__init__.py +++ b/manim/plugins/__init__.py @@ -2,9 +2,12 @@ from manim import config, logger +from .plugin_config import Hooks, plugins from .plugins_flags import get_plugins, list_plugins __all__ = [ + "plugins", + "Hooks", "get_plugins", "list_plugins", ] diff --git a/manim/plugins/plugin_config.py b/manim/plugins/plugin_config.py new file mode 100644 index 0000000000..54a2807730 --- /dev/null +++ b/manim/plugins/plugin_config.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from collections.abc import Callable +from enum import Enum +from typing import TYPE_CHECKING + +from pydantic import BaseModel + +from manim.event_handler.window import WindowABC +from manim.renderer.opengl_renderer import OpenGLRenderer +from manim.renderer.opengl_renderer_window import Window +from manim.renderer.renderer import RendererProtocol + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + from manim.manager import Manager + + HookFunction: TypeAlias = Callable[[Manager], object] + + +__all__ = ( + "plugins", + "Hooks", +) + + +class Hooks(Enum): + POST_CONSTRUCT = "post_construct" + + +class PluginConfig(BaseModel): + """Plugin abilities that should be customizable by the user. + + Parameters + ---------- + renderer : The renderer class to use for rendering scenes. + window: The window class to use for displaying the scene. + + Examples + -------- + + .. code-block:: pycon + + >>> from manim import plugins + >>> plugins.renderer.__name__ + 'OpenGLRenderer' + >>> class MyRenderer(OpenGLRenderer): + ... '''My custom renderer + ... + ... All this actually has to do is implement + ... the RendererProtocol. + ... ''' + >>> plugins.renderer = MyRenderer + >>> plugins.renderer.__name__ + 'MyRenderer' + >>> plugins.renderer = 3 + Traceback (most recent call last): + ... + pydantic_core._pydantic_core.ValidationError: 1 validation error for PluginConfig + renderer + Input should be a subclass of RendererProtocol [type=is_subclass_of, input_value=3, input_type=int] + For further information visit https://errors.pydantic.dev/2.8/v/is_subclass_of + """ + + class Config: + # runtime check Protocols (must be runtime_checkable Protocols) + allow_arbitrary_types = True + # validate setting attributes + validate_assignment = True + extra = "forbid" + + renderer: type[RendererProtocol] + window: type[WindowABC] + + # not included in pydantic because Manager is undefined + # due to circular imports and __future__.annotations + # instead we do validation manually via :meth:`.register` + _hooks: dict[Hooks, list[HookFunction]] = {hook: [] for hook in Hooks} + + @property + def hooks(self) -> dict[Hooks, list[HookFunction]]: + return self._hooks + + def register(self, hooks: dict[Hooks, list[HookFunction]]) -> None: + """Register hooks to run at specific points in the program.""" + + for hook, functions in hooks.items(): + if not all(callable(func) for func in functions): + raise ValueError("All hooks must be callables!") + if not isinstance(hook, Hooks): + raise ValueError( + f"Unknown hook type {hook}, must be an instance of enum {Hooks}" + ) + self._hooks[hook].extend(functions) + + +plugins = PluginConfig(renderer=OpenGLRenderer, window=Window) diff --git a/manim/plugins/plugins_flags.py b/manim/plugins/plugins_flags.py index 3733ac3f3f..db44a52976 100644 --- a/manim/plugins/plugins_flags.py +++ b/manim/plugins/plugins_flags.py @@ -2,14 +2,9 @@ from __future__ import annotations -import sys +from importlib.metadata import entry_points from typing import Any -if sys.version_info < (3, 10): - from importlib_metadata import entry_points -else: - from importlib.metadata import entry_points - from manim import console __all__ = ["list_plugins"] diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index abd719cc5d..99c8663453 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -4,13 +4,12 @@ import numpy as np +from manim import config, logger +from manim.camera.camera import Camera +from manim.mobject.mobject import Mobject +from manim.utils.exceptions import EndSceneEarlyException from manim.utils.hashing import get_hash_from_play_call - -from .. import config, logger -from ..mobject.mobject import Mobject -from ..scene.scene_file_writer import SceneFileWriter -from ..utils.exceptions import EndSceneEarlyException -from ..utils.iterables import list_update +from manim.utils.iterables import list_update if typing.TYPE_CHECKING: import types @@ -32,7 +31,7 @@ class CairoRenderer: def __init__( self, - file_writer_class=SceneFileWriter, + file_writer_class=None, camera_class=None, skip_animations=False, **kwargs, diff --git a/manim/renderer/opengl_file_writer.py b/manim/renderer/opengl_file_writer.py deleted file mode 100644 index f78e965ced..0000000000 --- a/manim/renderer/opengl_file_writer.py +++ /dev/null @@ -1,467 +0,0 @@ -from __future__ import annotations - -import os -import platform -import shutil -import subprocess as sp -import sys -from pathlib import Path -from typing import TYPE_CHECKING - -import numpy as np -from pydub import AudioSegment -from tqdm import tqdm as ProgressDisplay - -from manim import config -from manim._config import logger as log -from manim.utils.file_ops import ( - add_extension_if_not_present, - get_sorted_integer_files, - guarantee_existence, -) -from manim.utils.sounds import get_full_sound_file_path - -if TYPE_CHECKING: - from PIL.Image import Image - - -class FileWriter: - def __init__( - self, - file_name: str, - write_to_movie: bool = False, - break_into_partial_movies: bool = False, - save_pngs: bool = False, # TODO, this currently does nothing - png_mode: str = "RGBA", - save_last_frame: bool = False, - movie_file_extension: str = ".mp4", - # What python file is generating this scene - input_file_path: str = "", - # Where should this be written - output_directory: str | None = None, - open_file_upon_completion: bool = False, - show_file_location_upon_completion: bool = False, - quiet: bool = False, - total_frames: int = 0, - progress_description_len: int = 40, - ): - self.frames: list[Image] = [] - self.write_to_movie = write_to_movie - self.break_into_partial_movies = break_into_partial_movies - self.save_pngs = save_pngs - self.png_mode = png_mode - self.save_last_frame = save_last_frame - self.movie_file_extension = movie_file_extension - self.input_file_path = input_file_path - self.output_directory = output_directory - self.file_name = file_name - self.open_file_upon_completion = open_file_upon_completion - self.show_file_location_upon_completion = show_file_location_upon_completion - self.quiet = quiet - self.total_frames = total_frames - self.progress_description_len = progress_description_len - - # State during file writing - self.writing_process: sp.Popen | None = None - self.progress_display: ProgressDisplay | None = None - self.ended_with_interrupt: bool = False - self.init_output_directories() - self.init_audio() - - # Output directories and files - def init_output_directories(self) -> None: - out_dir = self.output_directory or "" - scene_name = Path(self.file_name) - if self.save_last_frame: - image_dir = guarantee_existence(Path(out_dir) / "images") - image_file = add_extension_if_not_present(scene_name, ".png") - self.image_file_path = Path(image_dir) / image_file - if self.write_to_movie: - movie_dir = guarantee_existence(Path(out_dir) / "videos") - movie_file = add_extension_if_not_present( - scene_name, self.movie_file_extension - ) - self.movie_file_path = Path(movie_dir) / movie_file - if self.break_into_partial_movies: - self.partial_movie_directory = guarantee_existence( - Path(movie_dir) / "partial_movie_files" / scene_name, - ) - # A place to save mobjects - self.saved_mobject_directory = Path(out_dir) / "mobjects" / scene_name - - def add_frames(self, *frames: Image) -> None: - self.frames.extend(frames) - - def get_default_module_directory(self) -> str: - path, _ = os.path.splitext(self.input_file_path) - path = path.removeprefix("_") - return path - - # Directory getters - def get_image_file_path(self) -> str: - return self.image_file_path - - # Sound - def init_audio(self) -> None: - self.includes_sound: bool = False - - def create_audio_segment(self) -> None: - self.audio_segment = AudioSegment.silent() - - def add_audio_segment( - self, - new_segment: AudioSegment, - time: float | None = None, - gain_to_background: float | None = None, - ) -> None: - if not self.includes_sound: - self.includes_sound = True - self.create_audio_segment() - segment = self.audio_segment - curr_end = segment.duration_seconds - if time is None: - time = curr_end - if time < 0: - raise Exception("Adding sound at timestamp < 0") - - new_end = time + new_segment.duration_seconds - diff = new_end - curr_end - if diff > 0: - segment = segment.append( - AudioSegment.silent(int(np.ceil(diff * 1000))), - crossfade=0, - ) - self.audio_segment = segment.overlay( - new_segment, - position=int(1000 * time), - gain_during_overlay=gain_to_background, - ) - - def add_sound( - self, - sound_file: str, - time: float | None = None, - gain: float | None = None, - gain_to_background: float | None = None, - ) -> None: - file_path = get_full_sound_file_path(sound_file) - new_segment = AudioSegment.from_file(file_path) - if gain: - new_segment = new_segment.apply_gain(gain) - self.add_audio_segment(new_segment, time, gain_to_background) - - # Writers - def begin(self) -> None: - if not self.break_into_partial_movies and self.write_to_movie: - self.open_movie_pipe(self.get_movie_file_path()) - - def begin_animation(self) -> None: - if self.break_into_partial_movies and self.write_to_movie: - # self.open_movie_pipe(self.get_next_partial_movie_path()) - ... - - def end_animation(self) -> None: - if self.break_into_partial_movies and self.write_to_movie: - # self.close_movie_pipe() - ... - - def finish(self) -> None: - if self.write_to_movie: - if self.break_into_partial_movies: - self.combine_movie_files() - else: - self.close_movie_pipe() - if self.includes_sound: - self.add_sound_to_video() - self.print_file_ready_message(self.get_movie_file_path()) - if self.save_last_frame: - self.save_final_image(self.scene.get_image()) - if self.should_open_file(): - self.open_file() - - def open_movie_pipe(self, file_path: str) -> None: - stem, ext = os.path.splitext(file_path) - self.final_file_path = file_path - self.temp_file_path = stem + "_temp" + ext - - fps = self.scene.camera.fps - width, height = self.scene.camera.get_pixel_shape() - - command = [ - config.ffmpeg_executable, - "-y", # overwrite output file if it exists - "-f", - "rawvideo", - "-s", - f"{width}x{height}", # size of one frame - "-pix_fmt", - "rgba", - "-r", - str(fps), # frames per second - "-i", - "-", # The input comes from a pipe - "-vf", - "vflip", - "-an", # Tells FFMPEG not to expect any audio - "-loglevel", - "error", - ] - if self.movie_file_extension == ".mov": - # This is if the background of the exported - # video should be transparent. - command += [ - "-vcodec", - "prores_ks", - ] - elif self.movie_file_extension != ".gif": - command += [ - "-vcodec", - "libx264", - "-pix_fmt", - "yuv420p", - ] - command += [self.temp_file_path] - self.writing_process = sp.Popen(command, stdin=sp.PIPE) - - if self.total_frames > 0 and not self.quiet: - self.progress_display = ProgressDisplay( - range(self.total_frames), - # bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}", - leave=False, - ascii=True if platform.system() == "Windows" else None, - dynamic_ncols=True, - ) - self.set_progress_display_description() - - def has_progress_display(self): - return self.progress_display is not None - - def set_progress_display_description( - self, file: str = "", sub_desc: str = "" - ) -> None: - if self.progress_display is None: - return - - desc_len = self.progress_description_len - if not file: - file = os.path.split(self.get_movie_file_path())[1] - full_desc = f"{file} {sub_desc}" - if len(full_desc) > desc_len: - full_desc = full_desc[: desc_len - 3] + "..." - else: - full_desc += " " * (desc_len - len(full_desc)) - self.progress_display.set_description(full_desc) - - def write_frame(self, frame: Image) -> None: - if self.write_to_movie: - self.writing_process.stdin.write(frame.tobytes("utf-8")) - if self.progress_display is not None: - self.progress_display.update() - - def close_movie_pipe(self) -> None: - self.writing_process.stdin.close() - self.writing_process.wait() - self.writing_process.terminate() - if self.progress_display is not None: - self.progress_display.close() - - if not self.ended_with_interrupt: - shutil.move(self.temp_file_path, self.final_file_path) - else: - self.movie_file_path = self.temp_file_path - - def combine_movie_files(self) -> None: - kwargs = { - "remove_non_integer_files": True, - "extension": self.movie_file_extension, - } - if self.scene.start_at_animation_number is not None: - kwargs["min_index"] = self.scene.start_at_animation_number - if self.scene.end_at_animation_number is not None: - kwargs["max_index"] = self.scene.end_at_animation_number - else: - kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1 - partial_movie_files = get_sorted_integer_files( - self.partial_movie_directory, **kwargs - ) - if len(partial_movie_files) == 0: - log.warning("No animations in this scene") - return - - # Write a file partial_file_list.txt containing all - # partial movie files - file_list = Path(self.partial_movie_directory) / "partial_movie_file_list.txt" - with open(file_list, "w") as fp: - for pf_path in partial_movie_files: - if os.name == "nt": - pf_path = pf_path.replace("\\", "/") - fp.write(f"file '{pf_path}'\n") - - movie_file_path = self.get_movie_file_path() - commands = [ - config.ffmpeg_executable, - "-y", # overwrite output file if it exists - "-f", - "concat", - "-safe", - "0", - "-i", - file_list, - "-loglevel", - "error", - "-c", - "copy", - movie_file_path, - ] - if not self.includes_sound: - commands.insert(-1, "-an") - - combine_process = sp.Popen(commands) - combine_process.wait() - - def add_sound_to_video(self) -> None: - movie_file_path = self.get_movie_file_path() - stem, ext = os.path.splitext(movie_file_path) - sound_file_path = stem + ".wav" - # Makes sure sound file length will match video file - self.add_audio_segment(AudioSegment.silent(0)) - self.audio_segment.export( - sound_file_path, - bitrate="312k", - ) - temp_file_path = stem + "_temp" + ext - commands = [ - config.ffmpeg_executable, - "-i", - movie_file_path, - "-i", - sound_file_path, - "-y", # overwrite output file if it exists - "-c:v", - "copy", - "-c:a", - "aac", - "-b:a", - "320k", - # select video stream from first file - "-map", - "0:v:0", - # select audio stream from second file - "-map", - "1:a:0", - "-loglevel", - "error", - # "-shortest", - temp_file_path, - ] - sp.call(commands) - shutil.move(temp_file_path, movie_file_path) - os.remove(sound_file_path) - - def save_final_image(self, image: Image) -> None: - file_path = self.get_image_file_path() - image.save(file_path) - self.print_file_ready_message(file_path) - - def print_file_ready_message(self, file_path: str) -> None: - if not self.quiet: - log.info(f"File ready at {file_path}") - - def should_open_file(self) -> bool: - return any( - ( - self.show_file_location_upon_completion, - self.open_file_upon_completion, - ) - ) - - def combine_to_section_videos(self) -> None: - """Concatenate partial movie files for each section.""" - - self.finish_last_section() - sections_index: list[dict[str, Any]] = [] - for section in self.sections: - # only if section does want to be saved - if section.video is not None: - logger.info(f"Combining partial files for section '{section.name}'") - self.combine_files( - section.get_clean_partial_movie_files(), - self.sections_output_dir / section.video, - ) - sections_index.append(section.get_dict(self.sections_output_dir)) - with (self.sections_output_dir / f"{self.output_name}.json").open("w") as file: - json.dump(sections_index, file, indent=4) - - def clean_cache(self): - """Will clean the cache by removing the oldest partial_movie_files.""" - cached_partial_movies = [ - (self.partial_movie_directory / file_name) - for file_name in self.partial_movie_directory.iterdir() - if file_name != "partial_movie_file_list.txt" - ] - if len(cached_partial_movies) > config["max_files_cached"]: - number_files_to_delete = ( - len(cached_partial_movies) - config["max_files_cached"] - ) - oldest_files_to_delete = sorted( - cached_partial_movies, - key=lambda path: path.stat().st_atime, - )[:number_files_to_delete] - for file_to_delete in oldest_files_to_delete: - file_to_delete.unlink() - logger.info( - f"The partial movie directory is full (> {config['max_files_cached']} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)." - " You can change this behaviour by changing max_files_cached in config.", - ) - - def flush_cache_directory(self): - """Delete all the cached partial movie files""" - cached_partial_movies = [ - self.partial_movie_directory / file_name - for file_name in self.partial_movie_directory.iterdir() - if file_name != "partial_movie_file_list.txt" - ] - for f in cached_partial_movies: - f.unlink() - logger.info( - f"Cache flushed. {len(cached_partial_movies)} file(s) deleted in %(par_dir)s.", - {"par_dir": self.partial_movie_directory}, - ) - - def open_file(self) -> None: - if self.quiet: - curr_stdout = sys.stdout - sys.stdout = open(os.devnull, "w") - - current_os = platform.system() - file_paths = [] - - if self.save_last_frame: - file_paths.append(self.get_image_file_path()) - if self.write_to_movie: - file_paths.append(self.get_movie_file_path()) - - for file_path in file_paths: - if current_os == "Windows": - os.startfile(file_path) - else: - commands = [] - if current_os == "Linux": - commands.append("xdg-open") - elif current_os.startswith("CYGWIN"): - commands.append("cygstart") - else: # Assume macOS - commands.append("open") - - if self.show_file_location_upon_completion: - commands.append("-R") - - commands.append(file_path) - - FNULL = open(os.devnull, "w") - sp.call(commands, stdout=FNULL, stderr=sp.STDOUT) - FNULL.close() - - if self.quiet: - sys.stdout.close() - sys.stdout = curr_stdout diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 0f0f78c872..e7df1e4afd 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -11,7 +11,8 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject from manim.renderer.buffers.buffer import STD140BufferFormat from manim.renderer.opengl_shader_program import load_shader_program_by_folder -from manim.renderer.renderer import ImageType, Renderer, RendererData, RendererProtocol +from manim.renderer.renderer import Renderer, RendererData, RendererProtocol +from manim.typing import PixelArray from manim.utils.iterables import listify from manim.utils.space_ops import cross2d, earclip_triangulation, z_to_vector @@ -121,7 +122,7 @@ def get_triangulation(self: OpenGLVMobject, normal_vector=None): # Triangulate inner_verts = points[inner_vert_indices] - inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] + inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] # type: ignore tri_indices = np.hstack([indices, inner_tri_indices]) self.triangulation = tri_indices @@ -194,9 +195,8 @@ def get_available_uniforms(prog): def write_uniforms(prog, uniforms): for name in prog: member = prog[name] - if isinstance(member, gl.Uniform): - if name in uniforms: - member.value = uniforms[name] + if isinstance(member, gl.Uniform) and name in uniforms: + member.value = uniforms[name] @staticmethod def bind_to_uniform_block(uniform_buffer_object: gl.Buffer, idx: int = 0): @@ -214,7 +214,6 @@ def __init__( background_color: c.ManimColor = color.BLACK, background_opacity: float = 1.0, background_image: str | None = None, - substitute_output_fbo: gl.Framebuffer | None = None, ) -> None: super().__init__() self.pixel_width = pixel_width @@ -226,7 +225,7 @@ def __init__( # Initializing Context logger.debug("Initializing OpenGL context and framebuffers") - self.ctx: gl.Context = gl.create_context() + self.ctx: gl.Context = gl.create_context(standalone=not config.preview) # Those are the actual buffers that are used for rendering self.stencil_texture = self.ctx.texture( @@ -373,7 +372,7 @@ def post_render(self): format = gl.detect_format(self.render_texture_program, frame_data.dtype.names) vao = self.ctx.vertex_array( program=self.render_texture_program, - content=[(vbo, format, *frame_data.dtype.names)], + content=[(vbo, format, *frame_data.dtype.names)], # type: ignore ) self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo) self.render_target_texture.use(0) @@ -408,12 +407,15 @@ def render_program(self, prog, data, indices=None): # return data, data_size def render_image(self, mob): - raise NotImplementedError # TODO + raise NotImplementedError def render_previous(self, camera: Camera) -> None: raise NotImplementedError - def render_vmobject(self, mob: OpenGLVMobject) -> None: # type: ignore + def render_mesh(self, mob) -> None: + raise NotImplementedError + + def render_vmobject(self, mob: OpenGLVMobject) -> None: self.stencil_buffer_fbo.use() self.stencil_buffer_fbo.clear() self.render_target_fbo.use() @@ -512,10 +514,13 @@ def enable_depth(mob): np.array(range(len(sub.points))), ) - def get_pixels(self) -> ImageType: + def get_pixels(self) -> PixelArray: raw = self.output_fbo.read(components=4, dtype="f1", clamp=True) # RGBA, floats - buf = np.frombuffer(raw, dtype=np.uint8).reshape((1080, 1920, -1)) - return buf + y, x = self.output_fbo.viewport[2:4] + buf = np.frombuffer(raw, dtype=np.uint8).reshape((x, y, 4)) + # this actually has the right type (uint8) but due to + # numpy typing being bad, we have to type: ignore it + return buf[::-1] # type: ignore class GLVMobjectManager: diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index 84ce080df7..2366421dd8 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -2,16 +2,18 @@ import moderngl_window as mglw import numpy as np -from moderngl_window.context.pyglet.window import Window as FunWindow +from moderngl_window.context.pyglet.window import Window as PygletWindow from moderngl_window.timers.clock import Timer from screeninfo import get_monitors -from .. import __version__, config +from manim import __version__, config +from manim.event_handler.window import WindowABC __all__ = ["Window"] -class Window(FunWindow): +class Window(PygletWindow, WindowABC): + name = "Manim Community" fullscreen: bool = False resizable: bool = False gl_version: tuple[int, int] = (3, 3) diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py deleted file mode 100644 index 9319f1a96e..0000000000 --- a/manim/renderer/render_manager.py +++ /dev/null @@ -1,248 +0,0 @@ -from __future__ import annotations - -import time -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, Callable - -import numpy as np - -from manim import config, logger -from manim.constants import RendererType -from manim.renderer.cairo_renderer import CairoRenderer -from manim.utils.exceptions import EndSceneEarlyException - -from ..scene.scene import Scene, SceneState -from .opengl_file_writer import FileWriter -from .opengl_renderer import OpenGLRenderer -from .opengl_renderer_window import Window - -if TYPE_CHECKING: - from manim.animation.protocol import AnimationProtocol - - from ..camera.camera import Camera - from .renderer import RendererProtocol - -__all__ = ("Manager",) - - -class Manager: - """ - The Brain of Manim - - .. note:: - - The only method of this class officially guaranteed to be - stable is :meth:`~.Manager.render`. Any other methods documented - are purely for development - - Usage - ----- - - .. code-block:: python - - class Manimation(Scene): - def construct(self): - self.play(FadeIn(Circle())) - - - Manager(Manimation).render() - """ - - def __init__(self, scene_cls: type[Scene]) -> None: - # scene - self.scene: Scene = scene_cls(self) - - if not isinstance(self.scene, Scene): - raise ValueError(f"{self.scene!r} is not an instance of Scene") - - self.time = 0 - - # Initialize window, if applicable - if config.preview: - self.window = Window() - else: - self.window = None - - # this must be done AFTER instantiating a window - self.renderer = self.create_renderer() - self.renderer.use_window() - - # file writer - self.file_writer = FileWriter(self.scene.get_default_scene_name()) # TODO - - @property - def camera(self) -> Camera: - return self.scene.camera - - def create_renderer(self) -> RendererProtocol: - match config.renderer: - case RendererType.OPENGL: - return OpenGLRenderer() - - case RendererType.CAIRO: - return CairoRenderer() - - case rendertype: - raise ValueError(f"Invalid Config Renderer type {rendertype}") - - def _setup(self) -> None: - """Set up processes and manager""" - if self.file_writer.has_progress_display(): - self.scene.show_animation_progress = False - - self.scene.setup() - - self.virtual_animation_start_time = 0 - self.real_animation_start_time = time.perf_counter() - - def render(self) -> None: - """ - Entry point to running a Manim class - - Example - ------- - - .. code-block:: python - - class MyScene(Scene): - def construct(self): - self.play(Create(Circle())) - - - with tempconfig({"preview": True}): - Manager(MyScene).render() - """ - self._render_first_pass() - self._render_second_pass() - self._interact() - - def _render_first_pass(self) -> None: - """ - Temporarily use the normal single pass - rendering system - """ - self._setup() - - try: - self.scene.construct() - self._interact() - except EndSceneEarlyException: - pass - except KeyboardInterrupt: - # Get rid keyboard interrupt symbols - print("", end="\r") - self.file_writer.ended_with_interrupt = True - self._tear_down() - - def _render_second_pass(self) -> None: - """ - In the future, this method could be used - for two pass rendering - """ - ... - - def _tear_down(self): - self.scene.tear_down() - - if config.save_last_frame: - self._update_frame(0) - - self.file_writer.finish() - - if self.window is not None: - self.window.close() - self.window = None - - def _interact(self) -> None: - if self.window is None: - return - logger.info( - "\nTips: Using the keys `d`, `f`, or `z` " - + "you can interact with the scene. " - + "Press `command + q` or `esc` to quit" - ) - self.scene.skip_animations = False - self.scene.refresh_static_mobjects() - while not self.window.is_closing: - # TODO: Replace with actual dt instead - # of hardcoded dt - dt = 1 / self.camera.fps - self._update_frame(dt) - - def _update_frame(self, dt: float): - self.time += dt - self.scene._update_mobjects(dt) - - if self.window is not None: - self.window.clear() - - state = self.scene.get_state() - self._render_frame(state) - - if self.window is not None: - self.window.swap_buffers() - vt = self.time - self.virtual_animation_start_time - rt = time.perf_counter() - self.real_animation_start_time - if rt < vt: - self._update_frame(0) - - def _play(self, *animations: AnimationProtocol): - self.scene.pre_play() - - if self.window is not None: - self.real_animation_start_time = time.perf_counter() - self.virtual_animation_start_time = self.time - - self.scene.begin_animations(animations) - self._progress_through_animations(animations) - self.scene.finish_animations(animations) - - if self.scene.skip_animations and self.window is not None: - self._update_frame(dt=0) - - self.scene.post_play() - - def _wait( - self, duration: float, *, stop_condition: Callable[[], bool] | None = None - ): - self.scene.pre_play() - - update_mobjects = ( - self.scene.should_update_mobjects() - ) # TODO: this method needs to be implemented - condition = stop_condition or (lambda: False) - - last_t = 0 - for t in self._calc_time_progression(duration): - if update_mobjects: - dt, last_t = t - last_t, t - self._update_frame(dt) - if condition(): - break - else: - self.renderer.render_previous(self.camera) - self.scene.post_play() - - def _progress_through_animations(self, animations: Iterable[AnimationProtocol]): - last_t = 0 - run_time = self._calc_runtime(animations) - for t in self._calc_time_progression(run_time): - dt, last_t = t - last_t, t - self.scene._update_animations(animations, t, dt) - self._update_frame(dt) - - def _calc_time_progression(self, run_time: float) -> Iterable[float]: - return np.arange(0, run_time, 1 / self.camera.fps) - - def _calc_runtime(self, animations: Iterable[AnimationProtocol]): - return max(animation.get_run_time() for animation in animations) - - def _render_frame(self, state: SceneState) -> Any | None: - """Renders a frame based on a state, and writes it to a file""" - data = self._send_scene_to_renderer(state) - # result = self.file_writer.write(data) - - def _send_scene_to_renderer(self, state: SceneState): - """Renders the State""" - result = self.renderer.render(self.scene.camera, state.mobjects) - return result diff --git a/manim/renderer/renderer.py b/manim/renderer/renderer.py index 666d86e347..537145c40d 100644 --- a/manim/renderer/renderer.py +++ b/manim/renderer/renderer.py @@ -1,10 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Protocol - -import numpy as np -from typing_extensions import TypeAlias +from typing import TYPE_CHECKING, Protocol, runtime_checkable from manim._config import logger from manim.mobject.opengl.opengl_mobject import OpenGLMobject @@ -12,11 +9,10 @@ from manim.mobject.types.image_mobject import ImageMobject if TYPE_CHECKING: - from collections.abc import Callable, Iterable, Sequence + from collections.abc import Iterable from manim.camera.camera import Camera - -ImageType: TypeAlias = np.ndarray + from manim.typing import PixelArray class RendererData: @@ -24,13 +20,19 @@ class RendererData: class Renderer(ABC): + """Abstract class that handles dispatching mobjects to their specialized mobjects. + + Specifically, it maps :class:`.OpenGLVMobject` to :meth:`render_vmobject`, :class:`.ImageMobject` + to :meth:`render_image`, etc. + """ + def __init__(self): self.capabilities = [ - (OpenGLVMobject, self.render_vmobject), # type: ignore - (ImageMobject, self.render_image), # type: ignore + (OpenGLVMobject, self.render_vmobject), + (ImageMobject, self.render_image), ] - def render(self, camera, renderables: Iterable[OpenGLMobject]) -> None: # Image + def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None: self.pre_render(camera) for mob in renderables: for type_, render_func in self.capabilities: @@ -45,11 +47,11 @@ def render(self, camera, renderables: Iterable[OpenGLMobject]) -> None: # Image @abstractmethod def pre_render(self, camera: Camera): - raise NotImplementedError + """Actions before rendering any :class:`.OpenGLMobject`""" @abstractmethod def post_render(self): - raise NotImplementedError + """Actions before rendering any :class:`.OpenGLMobject`""" @abstractmethod def render_vmobject(self, mob: OpenGLVMobject): @@ -60,28 +62,23 @@ def render_image(self, mob: ImageMobject): raise NotImplementedError +# Note: runtime checking is slow, +# but it only happens once or twice so it should be fine +@runtime_checkable class RendererProtocol(Protocol): - capabilities: Sequence[ - tuple[type[OpenGLMobject], Callable[[type[OpenGLMobject]], object]] - ] - - def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None: ... - - def render_previous(self, camera: Camera) -> None: ... - - def pre_render(self, camera) -> object: ... - - def post_render(self) -> object: ... - - def use_window(self): ... - - def render_vmobject(self, mob: OpenGLVMobject) -> object: ... + """The Protocol a renderer must implement to be used in :class:`.Manager`.""" - def render_mesh(self, mob) -> None: ... + def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None: + """Render a group of Mobjects""" + ... - def render_image(self, mob: ImageMobject) -> None: ... + def use_window(self) -> None: + """Hook called after instantiation.""" + ... - def get_pixels(self) -> ImageType: ... + def get_pixels(self) -> PixelArray: + """Get the pixels that should be written to a file.""" + ... # NOTE: The user should expect depth between renderers not to be handled discussed at 03.09.2023 Between jsonv and MrDiver @@ -97,7 +94,7 @@ def get_pixels(self) -> ImageType: ... # def add(img1, img2): # raise NotImplementedError -# def subtract(*images: List[Image]): +# def subtract(*images: List[PixelArray]): # raise NotImplementedError # def mix(): diff --git a/manim/scene/scene.py b/manim/scene/scene.py index a8b0fb1c46..cfba6b4727 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -1,42 +1,30 @@ from __future__ import annotations -import inspect -import os import random -from collections import OrderedDict -from collections.abc import Sequence +from collections import OrderedDict, deque from typing import TYPE_CHECKING import numpy as np -from IPython.terminal import pt_inputhooks -from IPython.terminal.embed import InteractiveShellEmbed from pyglet.window import key -from tqdm import tqdm as ProgressDisplay -from manim import logger +from manim import config, logger from manim.animation.animation import prepare_animation +from manim.animation.scene_buffer import SceneBuffer, SceneOperation from manim.camera.camera import Camera from manim.constants import DEFAULT_WAIT_TIME from manim.event_handler import EVENT_DISPATCHER from manim.event_handler.event_type import EventType -from manim.mobject.frame import FullScreenRectangle from manim.mobject.mobject import Group, Point, _AnimationBuilder -from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject +from manim.mobject.opengl.opengl_mobject import OpenGLMobject from manim.mobject.types.vectorized_mobject import VGroup, VMobject -from manim.utils.color import RED -from manim.utils.deprecation import deprecated -from manim.utils.exceptions import EndSceneEarlyException -from manim.utils.family_ops import extract_mobject_family_members from manim.utils.iterables import list_difference_update -from manim.utils.module_ops import get_module if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Iterable, Reversible, Sequence from typing import Any, Callable - from manim.animation.protocol import AnimationProtocol as Animation - from manim.animation.scene_buffer import SceneBuffer - from manim.renderer.render_manager import Manager + from manim.animation.protocol import AnimationProtocol + from manim.manager import Manager # TODO: these keybindings should be made configurable @@ -48,55 +36,52 @@ class Scene: - random_seed: int = 0 + """The Canvas of Manim. + + You can use it by putting the following into a + file ``manimation.py`` + + .. manim:: SceneWithSettings + + class SceneWithSettings(Scene): + # set configuration attributes + random_seed = 1 + + # all the action happens here + def construct(self): + self.play(Create(ManimBanner())) + + And then run ``manim -p manimation.py``. To write the result to a file, + do ``manim -w manimation.py``. + + Attributes + ---------- + + random_seed : The seed for random and numpy.random + pan_sensitivity : + """ + + random_seed: int | None = None pan_sensitivity: float = 3.0 max_num_saved_states: int = 50 - default_camera_config: dict = {} - default_window_config: dict = {} - default_file_writer_config: dict = {} - def __init__( - self, - manager: Manager | None = None, - window_config: dict = {}, - camera_config: dict = {}, - skip_animations: bool = False, - always_update_mobjects: bool = False, - start_at_animation_number: int | None = None, - end_at_animation_number: int | None = None, - leave_progress_bars: bool = False, - preview: bool = True, - presenter_mode: bool = False, - show_animation_progress: bool = False, - embed_exception_mode: str = "", - embed_error_sound: bool = False, - ): - self.skip_animations = skip_animations - self.always_update_mobjects = always_update_mobjects - self.start_at_animation_number = start_at_animation_number - self.end_at_animation_number = end_at_animation_number - self.leave_progress_bars = leave_progress_bars - self.preview = preview - self.presenter_mode = presenter_mode - self.show_animation_progress = show_animation_progress - self.embed_exception_mode = embed_exception_mode - self.embed_error_sound = embed_error_sound + always_update_mobjects: bool = False + start_at_animation_number: int = 0 + end_at_animation_number: int | None = None + presenter_mode: bool = False + embed_exception_mode: str = "" + embed_error_sound: bool = False + def __init__(self, manager: Manager): # Core state of the scene self.camera: Camera = Camera() - self.camera.save_state() self.manager = manager - self.mobjects: list[Mobject] = [] - self.id_to_mobject_map: dict[int, Mobject] = {} + self.mobjects: list[OpenGLMobject] = [] self.num_plays: int = 0 + # the time is updated by the manager self.time: float = 0 - self.skip_time: float = 0 - self.original_skipping_status: bool = self.skip_animations - self.undo_stack = [] - self.redo_stack = [] - - if self.start_at_animation_number is not None: - self.skip_animations = True + self.undo_stack: deque[SceneState] = deque() + self.redo_stack: list[SceneState] = [] # Items associated with interaction self.mouse_point = Point() @@ -123,21 +108,18 @@ def get_default_scene_name(self) -> str: return name def process_buffer(self, buffer: SceneBuffer) -> None: - self.remove(*buffer.to_remove) - for to_replace_pairs in buffer.to_replace: - self.replace(*to_replace_pairs) - self.add(*buffer.to_add) + for op, args, kwargs in buffer: + match op: + case SceneOperation.ADD: + self.add(*args, **kwargs) + case SceneOperation.REMOVE: + self.remove(*args, **kwargs) + case SceneOperation.REPLACE: + self.replace(*args, **kwargs) + case o: + raise NotImplementedError(f"Unknown operation {o}") buffer.clear() - @deprecated(message="Use Manager(Scene).render()") - @classmethod - def run(cls) -> None: - from ..renderer.render_manager import Manager - - return Manager(cls).render() - - render = run - def setup(self) -> None: """ This method is used to set up scenes to do any setup @@ -149,109 +131,13 @@ def construct(self) -> None: The entrypoint to animations in Manim. Should be overridden in the subclass to produce animations """ + raise RuntimeError("Could not find the construct method, did you misspell it?") def tear_down(self) -> None: """ This method is used to clean up scenes """ - def embed( - self, - close_scene_on_exit: bool = True, - show_animation_progress: bool = True, - ) -> None: - if not self.preview: - return # Embed is only relevant with a preview - self.stop_skipping() - self.update_frame() - self.save_state() - self.show_animation_progress = show_animation_progress - - # Create embedded IPython terminal to be configured - shell = InteractiveShellEmbed.instance() - - # Use the locals namespace of the caller - caller_frame = inspect.currentframe().f_back - local_ns = dict(caller_frame.f_locals) - - # Add a few custom shortcuts - local_ns.update( - play=self.play, - wait=self.wait, - add=self.add, - remove=self.remove, - clear=self.clear, - save_state=self.save_state, - undo=self.undo, - redo=self.redo, - i2g=self.i2g, - i2m=self.i2m, - ) - - # Enables gui interactions during the embed - def inputhook(context): - while not context.input_is_ready(): - if not self.is_window_closing(): - self.update_frame(dt=0) - if self.is_window_closing(): - shell.ask_exit() - - pt_inputhooks.register("manim", inputhook) - shell.enable_gui("manim") - - # This is hacky, but there's an issue with ipython which is that - # when you define lambda's or list comprehensions during a shell session, - # they are not aware of local variables in the surrounding scope. Because - # That comes up a fair bit during scene construction, to get around this, - # we (admittedly sketchily) update the global namespace to match the local - # namespace, since this is just a shell session anyway. - shell.events.register( - "pre_run_cell", lambda: shell.user_global_ns.update(shell.user_ns) - ) - - # Operation to run after each ipython command - def post_cell_func(): - self.refresh_static_mobjects() - if not self.is_window_closing(): - self.update_frame(dt=0, ignore_skipping=True) - self.save_state() - - shell.events.register("post_run_cell", post_cell_func) - - # Flash border, and potentially play sound, on exceptions - def custom_exc(shell, etype, evalue, tb, tb_offset=None): - # still show the error don't just swallow it - shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) - if self.embed_error_sound: - os.system("printf '\a'") - rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0) - rect.fix_in_frame() - - from manim.animation.fading import FadeIn - from manim.utils.rate_functions import there_and_back - - self.play( - FadeIn(rect, run_time=0.5, rate_func=there_and_back, remover=True) - ) - - shell.set_custom_exc((Exception,), custom_exc) - - # Set desired exception mode - shell.magic(f"xmode {self.embed_exception_mode}") - - # Launch shell - shell( - local_ns=local_ns, - # Pretend like we're embedding in the caller function, not here - stack_depth=2, - # Specify that the present module is the caller's, not here - module=get_module(caller_frame.f_globals["__file__"]), - ) - - # End scene when exiting an embed - if close_scene_on_exit: - raise EndSceneEarlyException() - # Only these methods should touch the camera # Related to updating @@ -269,77 +155,29 @@ def should_update_mobjects(self) -> bool: """ # always rerender by returning True # TODO: Apply caching here - return True - # wait_animation = self.animations[0] - # if wait_animation.is_static_wait is None: - # should_update = ( - # self.always_update_mobjects - # or self.updaters - # or wait_animation.stop_condition is not None - # or any( - # mob.has_time_based_updater() - # for mob in self.get_mobject_family_members() - # ) - # ) - # wait_animation.is_static_wait = not should_update - # return not wait_animation.is_static_wait + return self.always_update_mobjects or any( + mob.has_updaters for mob in self.mobjects + ) def has_time_based_updaters(self) -> bool: return any( - [ - sm.has_time_based_updater() - for mob in self.mobjects - for sm in mob.get_family() - ] + sm.has_time_based_updater() + for mob in self.mobjects + for sm in mob.get_family() ) - # Related to time - - def get_time(self) -> float: - return self.time - - def increment_time(self, dt: float) -> None: - self.time += dt - # Related to internal mobject organization - def get_top_level_mobjects(self) -> list[Mobject]: - # Return only those which are not in the family - # of another mobject from the scene - mobjects = self.get_mobjects() - families = [m.get_family() for m in mobjects] - - def is_top_level(mobject): - num_families = sum([(mobject in family) for family in families]) - return num_families == 1 - - return list(filter(is_top_level, mobjects)) - - def get_mobject_family_members(self) -> list[Mobject]: - return extract_mobject_family_members(self.mobjects) - - def add(self, *new_mobjects: Mobject): + def add(self, *new_mobjects: OpenGLMobject): """ Mobjects will be displayed, from background to foreground in the order with which they are added. """ self.remove(*new_mobjects) self.mobjects += new_mobjects - self.id_to_mobject_map.update( - {id(sm): sm for m in new_mobjects for sm in m.get_family()} - ) return self - def add_mobjects_among(self, values: Iterable): - """ - This is meant mostly for quick prototyping, - e.g. to add all mobjects defined up to a point, - call self.add_mobjects_among(locals().values()) - """ - self.add(*filter(lambda m: isinstance(m, Mobject), values)) - return self - - def remove(self, *mobjects_to_remove: Mobject): + def remove(self, *mobjects_to_remove: OpenGLMobject): """ Removes anything in mobjects from scenes mobject list, but in the event that one of the items to be removed is a member of the family of an item in mobject_list, @@ -356,19 +194,20 @@ def remove(self, *mobjects_to_remove: Mobject): self.mobjects = list_difference_update(self.mobjects, mob.get_family()) return self - def replace(self, mobject: Mobject, *replacements: Mobject): - """Replace one mobject in the scene with another, preserving draw order. + def replace(self, mobject: OpenGLMobject, *replacements: OpenGLMobject): + """Replace one Mobject in the scene with one or more other Mobjects, + preserving draw order. - If ``old_mobject`` is a submobject of some other Mobject (e.g. a - :class:`.Group`), the new_mobject will replace it inside the group, - without otherwise changing the parent mobject. + If ``mobject`` is a submobject of some other :class:`OpenGLMobject` + (e.g. a :class:`.Group`), the ``replacements`` will replace it inside + the group, without otherwise changing the parent mobject. Parameters ---------- - old_mobject + mobject The mobject to be replaced. Must be present in the scene. - new_mobject - A mobject which must not already be in the scene. + replacements + One or more Mobjects which must not already be in the scene. """ if mobject in self.mobjects: @@ -426,73 +265,31 @@ def remove_updater(self, func: Callable[[float], None]) -> None: """ self.updaters = [f for f in self.updaters if f is not func] - def restructure_mobjects( - self, - to_remove: Mobject, - mobject_list_name: str = "mobjects", - extract_families: bool = True, - ): - """ - tl:wr - If your scene has a Group(), and you removed a mobject from the Group, - this dissolves the group and puts the rest of the mobjects directly - in self.mobjects or self.foreground_mobjects. - - In cases where the scene contains a group, e.g. Group(m1, m2, m3), but one - of its submobjects is removed, e.g. scene.remove(m1), the list of mobjects - will be edited to contain other submobjects, but not m1, e.g. it will now - insert m2 and m3 to where the group once was. - - Parameters - ---------- - to_remove - The Mobject to remove. - - mobject_list_name - The list of mobjects ("mobjects", "foreground_mobjects" etc) to remove from. - - extract_families - Whether the mobject's families should be recursively extracted. - - Returns - ------- - Scene - The Scene mobject with restructured Mobjects. - """ - if extract_families: - to_remove = extract_mobject_family_members( - to_remove, - use_z_index=self.renderer.camera.use_z_index, - ) - _list = getattr(self, mobject_list_name) - new_list = self.get_restructured_mobject_list(_list, to_remove) - setattr(self, mobject_list_name, new_list) - - def bring_to_front(self, *mobjects: Mobject): + def bring_to_front(self, *mobjects: OpenGLMobject): self.add(*mobjects) return self - def bring_to_back(self, *mobjects: Mobject): + def bring_to_back(self, *mobjects: OpenGLMobject): self.remove(*mobjects) - self.mobjects = list(mobjects) + self.mobjects + self.mobjects = [*mobjects, *self.mobjects] return self def clear(self): - self.mobjects = [] + self.mobjects.clear() return self - def get_mobjects(self) -> list[Mobject]: + def get_mobjects(self) -> Sequence[OpenGLMobject]: return list(self.mobjects) - def get_mobject_copies(self) -> list[Mobject]: + def get_mobject_copies(self) -> Sequence[OpenGLMobject]: return [m.copy() for m in self.mobjects] def point_to_mobject( self, point: np.ndarray, - search_set: Iterable[Mobject] | None = None, + search_set: Reversible[OpenGLMobject] | None = None, buff: float = 0, - ) -> Mobject | None: + ) -> OpenGLMobject | None: """ E.g. if clicking on the scene, this returns the top layer mobject under a given point @@ -510,76 +307,23 @@ def get_group(self, *mobjects): else: return Group(*mobjects) - def id_to_mobject(self, id_value): - return self.id_to_mobject_map[id_value] - - def ids_to_group(self, *id_values): - return self.get_group( - *filter(lambda x: x is not None, map(self.id_to_mobject, id_values)) - ) - - def i2g(self, *id_values): - return self.ids_to_group(*id_values) - - def i2m(self, id_value): - return self.id_to_mobject(id_value) - # Related to skipping - def update_skipping_status(self) -> None: - if (self.start_at_animation_number is not None) and ( - self.num_plays == self.start_at_animation_number - ): - self.skip_time = self.time - if not self.original_skipping_status: - self.stop_skipping() - if (self.end_at_animation_number is not None) and ( - self.num_plays >= self.end_at_animation_number - ): - raise EndSceneEarlyException() - - def stop_skipping(self) -> None: - self.virtual_animation_start_time = self.time - self.skip_animations = False - # Methods associated with running animations + def pre_play(self) -> None: + """To be implemented in subclasses.""" - def get_wait_time_progression( - self, duration: float, stop_condition: Callable[[], bool] | None = None - ) -> list[float] | np.ndarray | ProgressDisplay: - kw: dict[str, Any] = {"desc": f"{self.num_plays} Waiting"} - if stop_condition is not None: - kw["n_iterations"] = -1 # So it doesn't show % progress - kw["override_skip_animations"] = True - return self.get_time_progression(duration, **kw) - - def pre_play(self): # Doesn't exist in Main - if self.presenter_mode and self.num_plays == 0: - self.hold_loop() - - self.update_skipping_status() - - # if not self.skip_animations: - # self.file_writer.begin_animation() - - self.refresh_static_mobjects() - - def post_play(self): - # if not self.skip_animations: - # self.manager.file_writer.end_animation() - + def post_play(self) -> None: self.num_plays += 1 - def refresh_static_mobjects(self) -> None: - # self.camera.refresh_static_mobjects() - ... - - def begin_animations(self, animations: Iterable[Animation]) -> None: + def begin_animations(self, animations: Iterable[AnimationProtocol]) -> None: for animation in animations: animation.begin() self.process_buffer(animation.buffer) - def _update_animations(self, animations: Iterable[Animation], t: float, dt: float): + def _update_animations( + self, animations: Iterable[AnimationProtocol], t: float, dt: float + ): for animation in animations: animation.update_mobjects(dt) alpha = t / animation.get_run_time() @@ -588,19 +332,16 @@ def _update_animations(self, animations: Iterable[Animation], t: float, dt: floa self.process_buffer(animation.buffer) animation.apply_buffer = False - def finish_animations(self, animations: Iterable[Animation]) -> None: + def finish_animations(self, animations: Iterable[AnimationProtocol]) -> None: for animation in animations: animation.finish() self.process_buffer(animation.buffer) - if self.skip_animations: - self._update_mobjects(self.manager._calc_runtime(animations)) - else: - self._update_mobjects(0) - def play( self, - *proto_animations: Animation | _AnimationBuilder, + # the OpenGLMobject is a side-effect of the return type of animate, it will + # raise a ValueError + *proto_animations: AnimationProtocol | _AnimationBuilder | OpenGLMobject, run_time: float | None = None, rate_func: Callable[[float], float] | None = None, lag_ratio: float | None = None, @@ -608,6 +349,7 @@ def play( if len(proto_animations) == 0: logger.warning("Called Scene.play with no animations") return + # build animationbuilders animations = [prepare_animation(x) for x in proto_animations] for anim in animations: anim.update_rate_info(run_time, rate_func, lag_ratio) @@ -623,8 +365,6 @@ def wait( ignore_presenter_mode: bool = False, ): self.manager._wait(duration, stop_condition=stop_condition) - # self.pre_play() - # self.update_mobjects(dt=0) # Any problems with this? # if ( # self.presenter_mode # and not self.skip_animations @@ -633,35 +373,10 @@ def wait( # if note: # logger.info(note) # self.hold_loop() - # else: - # time_progression = self.get_wait_time_progression(duration, stop_condition) - # last_t = 0 - # for t in time_progression: - # dt = t - last_t - # last_t = t - # self.update_frame(dt) - # self.emit_frame() - # if stop_condition is not None and stop_condition(): - # break - # self.refresh_static_mobjects() - # self.post_play() def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60): self.wait(max_time, stop_condition=stop_condition) - def force_skipping(self): - self.original_skipping_status = self.skip_animations - self.skip_animations = True - return self - - def revert_to_original_skipping_status(self): - if hasattr(self, "original_skipping_status"): - self.skip_animations = self.original_skipping_status - return self - - def emit_frame(self) -> None: - pass - def add_sound( self, sound_file: str, @@ -669,13 +384,10 @@ def add_sound( gain: float | None = None, gain_to_background: float | None = None, ): - if self.skip_animations: - return - time = self.get_time() + time_offset + raise NotImplementedError("TODO") + time = self.time + time_offset self.file_writer.add_sound(sound_file, time, gain, gain_to_background) - # Helpers for interactive development - def get_state(self) -> SceneState: return SceneState(self) @@ -683,7 +395,7 @@ def restore_state(self, scene_state: SceneState): scene_state.restore_scene(self) def save_state(self) -> None: - if not self.preview: + if not config.preview: return state = self.get_state() if self.undo_stack and state.mobjects_match(self.undo_stack[-1]): @@ -691,40 +403,19 @@ def save_state(self) -> None: self.redo_stack = [] self.undo_stack.append(state) if len(self.undo_stack) > self.max_num_saved_states: - self.undo_stack.pop(0) + self.undo_stack.popleft() def undo(self): if self.undo_stack: self.redo_stack.append(self.get_state()) self.restore_state(self.undo_stack.pop()) - self.refresh_static_mobjects() def redo(self): if self.redo_stack: self.undo_stack.append(self.get_state()) self.restore_state(self.redo_stack.pop()) - self.refresh_static_mobjects() # TODO: reimplement checkpoint feature with CE's section API - - def save_mobject_to_file( - self, mobject: Mobject, file_path: str | None = None - ) -> None: - return - if file_path is None: - file_path = self.file_writer.get_saved_mobject_path(mobject) - if file_path is None: - return - mobject.save_to_file(file_path) - - def load_mobject(self, file_name): - if os.path.exists(file_name): - path = file_name - else: - directory = self.file_writer.get_saved_mobject_directory() - path = os.path.join(directory, file_name) - return Mobject.load(path) - # Event handling def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None: @@ -840,7 +531,6 @@ def on_key_press(self, symbol: int, modifiers: int) -> None: self.hold_on_wait = False def on_resize(self, width: int, height: int) -> None: - # self.camera.reset_pixel_shape(width, height) pass def on_show(self) -> None: @@ -854,7 +544,7 @@ def on_close(self) -> None: class SceneState: - def __init__(self, scene: Scene, ignore: list[Mobject] | None = None) -> None: + def __init__(self, scene: Scene, ignore: list[OpenGLMobject] | None = None) -> None: self.time = scene.time self.num_plays = scene.num_plays self.camera = scene.camera.copy() @@ -873,7 +563,7 @@ def __init__(self, scene: Scene, ignore: list[Mobject] | None = None) -> None: self.mobjects_to_copies[mob] = mob.copy() @property - def mobjects(self) -> Sequence[Mobject]: + def mobjects(self) -> Sequence[OpenGLMobject]: return tuple(self.mobjects_to_copies.keys()) def __eq__(self, state: Any) -> bool: diff --git a/manim/typing.py b/manim/typing.py index 8111ca7398..ac31d9d456 100644 --- a/manim/typing.py +++ b/manim/typing.py @@ -77,10 +77,10 @@ "FunctionOverride", "PathFuncType", "MappingFunction", - "Image", - "GrayscaleImage", - "RGBImage", - "RGBAImage", + "PixelArray", + "GrayscalePixelArray", + "RGBPixelArray", + "RGBAPixelArray", "StrPath", "StrOrBytesPath", ] @@ -576,13 +576,16 @@ MappingFunction: TypeAlias = Callable[[Point3D], Point3D] """A function mapping a `Point3D` to another `Point3D`.""" +RateFunc: TypeAlias = Callable[[float], float] +r"""A rate function :math:`f: [0, 1] \to [0, 1]`.""" + """ [CATEGORY] Image types """ -Image: TypeAlias = npt.NDArray[ManimInt] +PixelArray: TypeAlias = npt.NDArray[ManimInt] """``shape: (height, width) | (height, width, 3) | (height, width, 4)`` A rasterized image with a height of ``height`` pixels and a width of @@ -595,24 +598,24 @@ `RGBA_Array_Int`. """ -GrayscaleImage: TypeAlias = Image +GrayscalePixelArray: TypeAlias = PixelArray """``shape: (height, width)`` -A 100% opaque grayscale `Image`, where every pixel value is a +A 100% opaque grayscale `PixelArray`, where every pixel value is a `ManimInt` indicating its lightness (black -> gray -> white). """ -RGBImage: TypeAlias = Image +RGBPixelArray: TypeAlias = PixelArray """``shape: (height, width, 3)`` -A 100% opaque `Image` in color, where every pixel value is an +A 100% opaque `PixelArray` in color, where every pixel value is an `RGB_Array_Int` object. """ -RGBAImage: TypeAlias = Image +RGBAPixelArray: TypeAlias = PixelArray """``shape: (height, width, 4)`` -An `Image` in color where pixels can be transparent. Every pixel +A `PixelArray` in color where pixels can be transparent. Every pixel value is an `RGBA_Array_Int` object. """ diff --git a/manim/utils/bezier.py b/manim/utils/bezier.py index 4bdea9f895..7609908bad 100644 --- a/manim/utils/bezier.py +++ b/manim/utils/bezier.py @@ -2,25 +2,19 @@ from __future__ import annotations -from collections.abc import Iterable - __all__ = [ "bezier", "partial_bezier_points", - "partial_quadratic_bezier_points", "split_bezier", - "subdivide_quadratic_bezier", "subdivide_bezier", "bezier_remap", - "quadratic_bezier_remap", "interpolate", "integer_interpolate", "mid", "inverse_interpolate", "match_interpolate", - "get_smooth_handle_points", + "get_smooth_quadratic_bezier_handle_points", "get_smooth_cubic_bezier_handle_points", - "diag_to_matrix", "is_closed", "proportions_along_bezier_curve_for_point", "point_lies_on_bezier", @@ -32,12 +26,9 @@ from typing import TYPE_CHECKING, Any, Callable, overload import numpy as np -from scipy import linalg from manim.typing import PointDType - -from ..utils.simple_functions import choose -from ..utils.space_ops import cross2d, find_intersection +from manim.utils.simple_functions import choose if TYPE_CHECKING: import numpy.typing as npt @@ -45,10 +36,10 @@ from manim.typing import ( BezierPoints, BezierPoints_Array, - ColVector, MatrixMN, Point3D, Point3D_Array, + QuadraticBezierPoints_Array, ) # l is a commonly used name in linear algebra @@ -315,27 +306,6 @@ def partial_bezier_points(points: BezierPoints, a: float, b: float) -> BezierPoi return arr -def partial_quadratic_bezier_points(points, a, b): - points = np.asarray(points, dtype=np.float64) - if a == 1: - return 3 * [points[-1]] - - def curve(t): - return ( - points[0] * (1 - t) * (1 - t) - + 2 * points[1] * t * (1 - t) - + points[2] * t * t - ) - - # bezier(points) - h0 = curve(a) if a > 0 else points[0] - h2 = curve(b) if b < 1 else points[2] - h1_prime = (1 - a) * points[1] + a * points[2] - end_prop = (b - a) / (1.0 - a) - h1 = (1 - end_prop) * h0 + end_prop * h1_prime - return [h0, h1, h2] - - def split_bezier(points: BezierPoints, t: float) -> Point3D_Array: r"""Split a Bézier curve at argument ``t`` into two curves. @@ -651,93 +621,6 @@ def split_bezier(points: BezierPoints, t: float) -> Point3D_Array: return arr.reshape(2 * N, dim) -def split_quadratic_bezier(points: np.ndarray, t: float) -> np.ndarray: - """Split a quadratic Bézier curve at argument ``t`` into two quadratic curves. - - Parameters - ---------- - points - The control points of the bezier curve - has shape ``[a1, h1, b1]`` - - t - The ``t``-value at which to split the Bézier curve - - Returns - ------- - The two Bézier curves as a list of tuples, - has the shape ``[a1, h1, b1], [a2, h2, b2]`` - """ - a1, h1, a2 = points - s1 = interpolate(a1, h1, t) - s2 = interpolate(h1, a2, t) - p = interpolate(s1, s2, t) - - return np.array([a1, s1, p, p, s2, a2]) - - -def subdivide_quadratic_bezier(points: Iterable[float], n: int) -> np.ndarray: - """Subdivide a quadratic Bézier curve into ``n`` subcurves which have the same shape. - - The points at which the curve is split are located at the - arguments :math:`t = i/n` for :math:`i = 1, ..., n-1`. - - Parameters - ---------- - points - The control points of the Bézier curve in form ``[a1, h1, b1]`` - - n - The number of curves to subdivide the Bézier curve into - - Returns - ------- - The new points for the Bézier curve in the form ``[a1, h1, b1, a2, h2, b2, ...]`` - - .. image:: /_static/bezier_subdivision_example.png - - """ - beziers = np.empty((n, 3, 3)) - current = points - for j in range(0, n): - i = n - j - tmp = split_quadratic_bezier(current, 1 / i) - beziers[j] = tmp[:3] - current = tmp[3:] - return beziers.reshape(-1, 3) - - -def subdivide_quadratic_bezier(points: Iterable[float], n: int) -> np.ndarray: - """Subdivide a quadratic Bézier curve into ``n`` subcurves which have the same shape. - - The points at which the curve is split are located at the - arguments :math:`t = i/n` for :math:`i = 1, ..., n-1`. - - Parameters - ---------- - points - The control points of the Bézier curve in form ``[a1, h1, b1]`` - - n - The number of curves to subdivide the Bézier curve into - - Returns - ------- - The new points for the Bézier curve in the form ``[a1, h1, b1, a2, h2, b2, ...]`` - - .. image:: /_static/bezier_subdivision_example.png - - """ - beziers = np.empty((n, 3, 3)) - current = points - for j in range(0, n): - i = n - j - tmp = split_quadratic_bezier(current, 1 / i) - beziers[j] = tmp[:3] - current = tmp[3:] - return beziers.reshape(-1, 3) - - # Memos explained in subdivide_bezier docstring SUBDIVISION_MATRICES = [{} for i in range(4)] @@ -1038,62 +921,6 @@ def bezier_remap( return new_tuples -def quadratic_bezier_remap( - triplets: Iterable[Iterable[float]], new_number_of_curves: int -): - """Remaps the number of curves to a higher amount by splitting bezier curves - - Parameters - ---------- - triplets - The triplets of the quadratic bezier curves to be remapped shape(n, 3, 3) - - new_number_of_curves - The number of curves that the output will contain. This needs to be higher than the current number. - - Returns - ------- - The new triplets for the quadratic bezier curves. - """ - difference = new_number_of_curves - len(triplets) - if difference <= 0: - return triplets - new_triplets = np.zeros((new_number_of_curves, 3, 3)) - idx = 0 - for triplet in triplets: - if difference > 0: - tmp_noc = int(np.ceil(difference / len(triplets))) + 1 - tmp = subdivide_quadratic_bezier(triplet, tmp_noc).reshape(-1, 3, 3) - for i in range(tmp_noc): - new_triplets[idx + i] = tmp[i] - difference -= tmp_noc - 1 - idx += tmp_noc - else: - new_triplets[idx] = triplet - idx += 1 - return new_triplets - - """ - This is an alternate version of the function just for documentation purposes - -------- - - difference = new_number_of_curves - len(triplets) - if difference <= 0: - return triplets - new_triplets = [] - for triplet in triplets: - if difference > 0: - tmp_noc = int(np.ceil(difference / len(triplets))) + 1 - tmp = subdivide_quadratic_bezier(triplet, tmp_noc).reshape(-1, 3, 3) - for i in range(tmp_noc): - new_triplets.append(tmp[i]) - difference -= tmp_noc - 1 - else: - new_triplets.append(triplet) - return new_triplets - """ - - # Linear interpolation variants @@ -1292,11 +1119,8 @@ def match_interpolate( ) -def get_smooth_quadratic_bezier_handle_points(points: FloatArray) -> FloatArray: - """ - Figuring out which bezier curves most smoothly connect a sequence of points. - - Given three successive points, P0, P1 and P2, you can compute that by defining +def get_smooth_quadratic_bezier_handle_points(points: Point3D_Array) -> Point3D_Array: + """Given three successive points, P0, P1 and P2, you can compute that by defining h = (1/4) P0 + P1 - (1/4)P2, the bezier curve defined by (P0, h, P1) will pass through the point P2. @@ -1308,7 +1132,8 @@ def get_smooth_quadratic_bezier_handle_points(points: FloatArray) -> FloatArray: and use the midpoint between the two. """ if len(points) == 2: - return midpoint(*points) + return 0.5 * (points[0] + points[1]) + smooth_to_right, smooth_to_left = ( 0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:] for ps in (points, points[::-1]) ) @@ -1324,225 +1149,637 @@ def get_smooth_quadratic_bezier_handle_points(points: FloatArray) -> FloatArray: def get_smooth_cubic_bezier_handle_points( - points: Point3D_Array, -) -> tuple[BezierPoints, BezierPoints]: - points = np.asarray(points) - num_handles = len(points) - 1 - dim = points.shape[1] - if num_handles < 1: - return np.zeros((0, dim)), np.zeros((0, dim)) - # Must solve 2*num_handles equations to get the handles. - # l and u are the number of lower an upper diagonal rows - # in the matrix to solve. - l, u = 2, 1 - # diag is a representation of the matrix in diagonal form - # See https://www.particleincell.com/2012/bezier-splines/ - # for how to arrive at these equations - diag: MatrixMN = np.zeros((l + u + 1, 2 * num_handles)) - diag[0, 1::2] = -1 - diag[0, 2::2] = 1 - diag[1, 0::2] = 2 - diag[1, 1::2] = 1 - diag[2, 1:-2:2] = -2 - diag[3, 0:-3:2] = 1 - # last - diag[2, -2] = -1 - diag[1, -1] = 2 - # This is the b as in Ax = b, where we are solving for x, - # and A is represented using diag. However, think of entries - # to x and b as being points in space, not numbers - b: Point3D_Array = np.zeros((2 * num_handles, dim)) - b[1::2] = 2 * points[1:] - b[0] = points[0] - b[-1] = points[-1] - - def solve_func(b: ColVector) -> ColVector | MatrixMN: - return linalg.solve_banded((l, u), diag, b) # type: ignore - - use_closed_solve_function = is_closed(points) - if use_closed_solve_function: - # Get equations to relate first and last points - matrix = diag_to_matrix((l, u), diag) - # last row handles second derivative - matrix[-1, [0, 1, -2, -1]] = [2, -1, 1, -2] - # first row handles first derivative - matrix[0, :] = np.zeros(matrix.shape[1]) - matrix[0, [0, -1]] = [1, 1] - b[0] = 2 * points[0] - b[-1] = np.zeros(dim) - - def closed_curve_solve_func(b: ColVector) -> ColVector | MatrixMN: - return linalg.solve(matrix, b) # type: ignore - - handle_pairs = np.zeros((2 * num_handles, dim)) - for i in range(dim): - if use_closed_solve_function: - handle_pairs[:, i] = closed_curve_solve_func(b[:, i]) - else: - handle_pairs[:, i] = solve_func(b[:, i]) - return handle_pairs[0::2], handle_pairs[1::2] - - -def get_smooth_handle_points( - points: BezierPoints, -) -> tuple[BezierPoints, BezierPoints]: - """Given some anchors (points), compute handles so the resulting bezier curve is smooth. + anchors: Point3D_Array, +) -> tuple[Point3D_Array, Point3D_Array]: + """Given an array of anchors for a cubic spline (array of connected cubic + Bézier curves), compute the 1st and 2nd handle for every curve, so that + the resulting spline is smooth. Parameters ---------- - points - Anchors. + anchors + Anchors of a cubic spline. Returns ------- - typing.Tuple[np.ndarray, np.ndarray] - Computed handles. + :class:`tuple` [:class:`~.Point3D_Array`, :class:`~.Point3D_Array`] + A tuple of two arrays: one containing the 1st handle for every curve in + the cubic spline, and the other containing the 2nd handles. """ - # NOTE points here are anchors. - points = np.asarray(points) - num_handles = len(points) - 1 - dim = points.shape[1] - if num_handles < 1: + anchors = np.asarray(anchors) + n_anchors = anchors.shape[0] + + # If there's a single anchor, there's no Bézier curve. + # Return empty arrays. + if n_anchors == 1: + dim = anchors.shape[1] return np.zeros((0, dim)), np.zeros((0, dim)) - # Must solve 2*num_handles equations to get the handles. - # l and u are the number of lower an upper diagonal rows - # in the matrix to solve. - l, u = 2, 1 - # diag is a representation of the matrix in diagonal form - # See https://www.particleincell.com/2012/bezier-splines/ - # for how to arrive at these equations - diag: MatrixMN = np.zeros((l + u + 1, 2 * num_handles)) - diag[0, 1::2] = -1 - diag[0, 2::2] = 1 - diag[1, 0::2] = 2 - diag[1, 1::2] = 1 - diag[2, 1:-2:2] = -2 - diag[3, 0:-3:2] = 1 - # last - diag[2, -2] = -1 - diag[1, -1] = 2 - # This is the b as in Ax = b, where we are solving for x, - # and A is represented using diag. However, think of entries - # to x and b as being points in space, not numbers - b = np.zeros((2 * num_handles, dim)) - b[1::2] = 2 * points[1:] - b[0] = points[0] - b[-1] = points[-1] - - def solve_func(b: ColVector) -> ColVector | MatrixMN: - return linalg.solve_banded((l, u), diag, b) # type: ignore - - use_closed_solve_function = is_closed(points) - if use_closed_solve_function: - # Get equations to relate first and last points - matrix = diag_to_matrix((l, u), diag) - # last row handles second derivative - matrix[-1, [0, 1, -2, -1]] = [2, -1, 1, -2] - # first row handles first derivative - matrix[0, :] = np.zeros(matrix.shape[1]) - matrix[0, [0, -1]] = [1, 1] - b[0] = 2 * points[0] - b[-1] = np.zeros(dim) - - def closed_curve_solve_func(b: ColVector) -> ColVector | MatrixMN: - return linalg.solve(matrix, b) # type: ignore - - handle_pairs = np.zeros((2 * num_handles, dim)) - for i in range(dim): - if use_closed_solve_function: - handle_pairs[:, i] = closed_curve_solve_func(b[:, i]) - else: - handle_pairs[:, i] = solve_func(b[:, i]) - return handle_pairs[0::2], handle_pairs[1::2] - - -def diag_to_matrix( - l_and_u: tuple[int, int], diag: npt.NDArray[Any] -) -> npt.NDArray[Any]: + + # If there are only two anchors (thus only one pair of handles), + # they can only be an interpolation of these two anchors with alphas + # 1/3 and 2/3, which will draw a straight line between the anchors. + if n_anchors == 2: + return interpolate(anchors[0], anchors[1], np.array([[1 / 3], [2 / 3]])) + + # Handle different cases depending on whether the points form a closed + # curve or not + curve_is_closed = is_closed(anchors) + if curve_is_closed: + return get_smooth_closed_cubic_bezier_handle_points(anchors) + else: + return get_smooth_open_cubic_bezier_handle_points(anchors) + + +CP_CLOSED_MEMO = np.array([1 / 3]) +UP_CLOSED_MEMO = np.array([1 / 3]) + + +def get_smooth_closed_cubic_bezier_handle_points( + anchors: Point3D_Array, +) -> tuple[Point3D_Array, Point3D_Array]: + r"""Special case of :func:`get_smooth_cubic_bezier_handle_points`, + when the ``anchors`` form a closed loop. + + .. note:: + A system of equations must be solved to get the first handles of + every Bézier curve (referred to as :math:`H_1`). + Then :math:`H_2` (the second handles) can be obtained separately. + + .. seealso:: + The equations were obtained from: + + * `Conditions on control points for continuous curvature. (2016). Jaco Stuifbergen. `_ + + In general, if there are :math:`N+1` anchors, there will be :math:`N` Bézier curves + and thus :math:`N` pairs of handles to find. We must solve the following + system of equations for the 1st handles (example for :math:`N = 5`): + + .. math:: + \begin{pmatrix} + 4 & 1 & 0 & 0 & 1 \\ + 1 & 4 & 1 & 0 & 0 \\ + 0 & 1 & 4 & 1 & 0 \\ + 0 & 0 & 1 & 4 & 1 \\ + 1 & 0 & 0 & 1 & 4 + \end{pmatrix} + \begin{pmatrix} + H_{1,0} \\ + H_{1,1} \\ + H_{1,2} \\ + H_{1,3} \\ + H_{1,4} + \end{pmatrix} + = + \begin{pmatrix} + 4A_0 + 2A_1 \\ + 4A_1 + 2A_2 \\ + 4A_2 + 2A_3 \\ + 4A_3 + 2A_4 \\ + 4A_4 + 2A_5 + \end{pmatrix} + + which will be expressed as :math:`RH_1 = D`. + + :math:`R` is almost a tridiagonal matrix, so we could use Thomas' algorithm. + + .. seealso:: + `Tridiagonal matrix algorithm. Wikipedia. `_ + + However, :math:`R` has ones at the opposite corners. A solution to this is + the first decomposition proposed in the link below, with :math:`\alpha = 1`: + + .. seealso:: + `Tridiagonal matrix algorithm # Variants. Wikipedia. `_ + + .. math:: + R + = + \begin{pmatrix} + 4 & 1 & 0 & 0 & 1 \\ + 1 & 4 & 1 & 0 & 0 \\ + 0 & 1 & 4 & 1 & 0 \\ + 0 & 0 & 1 & 4 & 1 \\ + 1 & 0 & 0 & 1 & 4 + \end{pmatrix} + &= + \begin{pmatrix} + 3 & 1 & 0 & 0 & 0 \\ + 1 & 4 & 1 & 0 & 0 \\ + 0 & 1 & 4 & 1 & 0 \\ + 0 & 0 & 1 & 4 & 1 \\ + 0 & 0 & 0 & 1 & 3 + \end{pmatrix} + + + \begin{pmatrix} + 1 & 0 & 0 & 0 & 1 \\ + 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 \\ + 1 & 0 & 0 & 0 & 1 + \end{pmatrix} + \\ + & + \\ + &= + \begin{pmatrix} + 3 & 1 & 0 & 0 & 0 \\ + 1 & 4 & 1 & 0 & 0 \\ + 0 & 1 & 4 & 1 & 0 \\ + 0 & 0 & 1 & 4 & 1 \\ + 0 & 0 & 0 & 1 & 3 + \end{pmatrix} + + + \begin{pmatrix} + 1 \\ + 0 \\ + 0 \\ + 0 \\ + 1 + \end{pmatrix} + \begin{pmatrix} + 1 & 0 & 0 & 0 & 1 + \end{pmatrix} + \\ + & + \\ + &= + T + uv^t + + We decompose :math:`R = T + uv^t`, where :math:`T` is a tridiagonal matrix, and + :math:`u, v` are :math:`N`-D vectors such that :math:`u_0 = u_{N-1} = v_0 = v_{N-1} = 1`, + and :math:`u_i = v_i = 0, \forall i \in \{1, ..., N-2\}`. + + Thus: + + .. math:: + RH_1 &= D \\ + \Rightarrow (T + uv^t)H_1 &= D + + If we find a vector :math:`q` such that :math:`Tq = u`: + + .. math:: + \Rightarrow (T + Tqv^t)H_1 &= D \\ + \Rightarrow T(I + qv^t)H_1 &= D \\ + \Rightarrow H_1 &= (I + qv^t)^{-1} T^{-1} D + + According to Sherman-Morrison's formula: + + .. seealso:: + `Sherman-Morrison's formula. Wikipedia. `_ + + .. math:: + (I + qv^t)^{-1} = I - \frac{1}{1 + v^tq} qv^t + + If we find :math:`Y = T^{-1} D`, or in other words, if we solve for + :math:`Y` in :math:`TY = D`: + + .. math:: + H_1 &= (I + qv^t)^{-1} T^{-1} D \\ + &= (I + qv^t)^{-1} Y \\ + &= (I - \frac{1}{1 + v^tq} qv^t) Y \\ + &= Y - \frac{1}{1 + v^tq} qv^tY + + Therefore, we must solve for :math:`q` and :math:`Y` in :math:`Tq = u` and :math:`TY = D`. + As :math:`T` is now tridiagonal, we shall use Thomas' algorithm. + + Define: + + * :math:`a = [a_0, \ a_1, \ ..., \ a_{N-2}]` as :math:`T`'s lower diagonal of :math:`N-1` elements, + such that :math:`a_0 = a_1 = ... = a_{N-2} = 1`, so this diagonal is filled with ones; + * :math:`b = [b_0, \ b_1, \ ..., \ b_{N-2}, \ b_{N-1}]` as :math:`T`'s main diagonal of :math:`N` elements, + such that :math:`b_0 = b_{N-1} = 3`, and :math:`b_1 = b_2 = ... = b_{N-2} = 4`; + * :math:`c = [c_0, \ c_1, \ ..., \ c_{N-2}]` as :math:`T`'s upper diagonal of :math:`N-1` elements, + such that :math:`c_0 = c_1 = ... = c_{N-2} = 1`: this diagonal is also filled with ones. + + If, according to Thomas' algorithm, we define: + + .. math:: + c'_0 &= \frac{c_0}{b_0} & \\ + c'_i &= \frac{c_i}{b_i - a_{i-1} c'_{i-1}}, & \quad \forall i \in \{1, ..., N-2\} \\ + & & \\ + u'_0 &= \frac{u_0}{b_0} & \\ + u'_i &= \frac{u_i - a_{i-1} u'_{i-1}}{b_i - a_{i-1} c'_{i-1}}, & \quad \forall i \in \{1, ..., N-1\} \\ + & & \\ + D'_0 &= \frac{1}{b_0} D_0 & \\ + D'_i &= \frac{1}{b_i - a_{i-1} c'_{i-1}} (D_i - a_{i-1} D'_{i-1}), & \quad \forall i \in \{1, ..., N-1\} + + Then: + + .. math:: + c'_0 &= \frac{1}{3} & \\ + c'_i &= \frac{1}{4 - c'_{i-1}}, & \quad \forall i \in \{1, ..., N-2\} \\ + & & \\ + u'_0 &= \frac{1}{3} & \\ + u'_i &= \frac{-u'_{i-1}}{4 - c'_{i-1}} = -c'_i u'_{i-1}, & \quad \forall i \in \{1, ..., N-2\} \\ + u'_{N-1} &= \frac{1 - u'_{N-2}}{3 - c'_{N-2}} & \\ + & & \\ + D'_0 &= \frac{1}{3} (4A_0 + 2A_1) & \\ + D'_i &= \frac{1}{4 - c'_{i-1}} (4A_i + 2A_{i+1} - D'_{i-1}) & \\ + &= c_i (4A_i + 2A_{i+1} - D'_{i-1}), & \quad \forall i \in \{1, ..., N-2\} \\ + D'_{N-1} &= \frac{1}{3 - c'_{N-2}} (4A_{N-1} + 2A_N - D'_{N-2}) & + + Finally, we can do Backward Substitution to find :math:`q` and :math:`Y`: + + .. math:: + q_{N-1} &= u'_{N-1} & \\ + q_i &= u'_{i} - c'_i q_{i+1}, & \quad \forall i \in \{0, ..., N-2\} \\ + & & \\ + Y_{N-1} &= D'_{N-1} & \\ + Y_i &= D'_i - c'_i Y_{i+1}, & \quad \forall i \in \{0, ..., N-2\} + + With those values, we can finally calculate :math:`H_1 = Y - \frac{1}{1 + v^tq} qv^tY`. + Given that :math:`v_0 = v_{N-1} = 1`, and :math:`v_1 = v_2 = ... = v_{N-2} = 0`, its dot products + with :math:`q` and :math:`Y` are respectively :math:`v^tq = q_0 + q_{N-1}` and + :math:`v^tY = Y_0 + Y_{N-1}`. Thus: + + .. math:: + H_1 = Y - \frac{1}{1 + q_0 + q_{N-1}} q(Y_0 + Y_{N-1}) + + Once we have :math:`H_1`, we can get :math:`H_2` (the array of second handles) as follows: + + .. math:: + H_{2, i} &= 2A_{i+1} - H_{1, i+1}, & \quad \forall i \in \{0, ..., N-2\} \\ + H_{2, N-1} &= 2A_0 - H_{1, 0} & + + Because the matrix :math:`R` always follows the same pattern (and thus :math:`T, u, v` as well), + we can define a memo list for :math:`c'` and :math:`u'` to avoid recalculation. We cannot + memoize :math:`D` and :math:`Y`, however, because they are always different matrices. We + cannot make a memo for :math:`q` either, but we can calculate it faster because :math:`u'` + can be memoized. + + Parameters + ---------- + anchors + Anchors of a closed cubic spline. + + Returns + ------- + :class:`tuple` [:class:`~.Point3D_Array`, :class:`~.Point3D_Array`] + A tuple of two arrays: one containing the 1st handle for every curve in + the closed cubic spline, and the other containing the 2nd handles. """ - Converts array whose rows represent diagonal - entries of a matrix into the matrix itself. - See scipy.linalg.solve_banded + global CP_CLOSED_MEMO + global UP_CLOSED_MEMO + + A = np.asarray(anchors) + N = A.shape[0] - 1 + dim = A.shape[1] + + # Calculate cp (c prime) and up (u prime) with help from + # CP_CLOSED_MEMO and UP_CLOSED_MEMO. + len_memo = CP_CLOSED_MEMO.size + if len_memo < N - 1: + cp = np.empty(N - 1) + up = np.empty(N - 1) + cp[:len_memo] = CP_CLOSED_MEMO + up[:len_memo] = UP_CLOSED_MEMO + # Forward Substitution 1 + # Calculate up (at the same time we calculate cp). + for i in range(len_memo, N - 1): + cp[i] = 1 / (4 - cp[i - 1]) + up[i] = -cp[i] * up[i - 1] + CP_CLOSED_MEMO = cp + UP_CLOSED_MEMO = up + else: + cp = CP_CLOSED_MEMO[: N - 1] + up = UP_CLOSED_MEMO[: N - 1] + + # The last element of u' is different + cp_last_division = 1 / (3 - cp[N - 2]) + up_last = cp_last_division * (1 - up[N - 2]) + + # Backward Substitution 1 + # Calculate q. + q = np.empty((N, dim)) + q[N - 1] = up_last + for i in range(N - 2, -1, -1): + q[i] = up[i] - cp[i] * q[i + 1] + + # Forward Substitution 2 + # Calculate Dp (D prime). + Dp = np.empty((N, dim)) + AUX = 4 * A[:N] + 2 * A[1:] # Vectorize the sum for efficiency. + Dp[0] = AUX[0] / 3 + for i in range(1, N - 1): + Dp[i] = cp[i] * (AUX[i] - Dp[i - 1]) + Dp[N - 1] = cp_last_division * (AUX[N - 1] - Dp[N - 2]) + + # Backward Substitution + # Calculate Y, which is defined as a view of Dp for efficiency + # and semantic convenience at the same time. + Y = Dp + # Y[N-1] = Dp[N-1] (redundant) + for i in range(N - 2, -1, -1): + Y[i] = Dp[i] - cp[i] * Y[i + 1] + + # Calculate H1. + H1 = Y - 1 / (1 + q[0] + q[N - 1]) * q * (Y[0] + Y[N - 1]) + + # Calculate H2. + H2 = np.empty((N, dim)) + H2[0 : N - 1] = 2 * A[1:N] - H1[1:N] + H2[N - 1] = 2 * A[N] - H1[0] + + return H1, H2 + + +CP_OPEN_MEMO = np.array([0.5]) + + +def get_smooth_open_cubic_bezier_handle_points( + anchors: Point3D_Array, +) -> tuple[Point3D_Array, Point3D_Array]: + r"""Special case of :func:`get_smooth_cubic_bezier_handle_points`, + when the ``anchors`` do not form a closed loop. + + .. note:: + A system of equations must be solved to get the first handles of + every Bèzier curve (referred to as :math:`H_1`). + Then :math:`H_2` (the second handles) can be obtained separately. + + .. seealso:: + The equations were obtained from: + + * `Smooth Bézier Spline Through Prescribed Points. (2012). Particle in Cell Consulting LLC. `_ + * `Conditions on control points for continuous curvature. (2016). Jaco Stuifbergen. `_ + + .. warning:: + The equations in the first webpage have some typos which were corrected in the comments. + + In general, if there are :math:`N+1` anchors, there will be :math:`N` Bézier curves + and thus :math:`N` pairs of handles to find. We must solve the following + system of equations for the 1st handles (example for :math:`N = 5`): + + .. math:: + \begin{pmatrix} + 2 & 1 & 0 & 0 & 0 \\ + 1 & 4 & 1 & 0 & 0 \\ + 0 & 1 & 4 & 1 & 0 \\ + 0 & 0 & 1 & 4 & 1 \\ + 0 & 0 & 0 & 2 & 7 + \end{pmatrix} + \begin{pmatrix} + H_{1,0} \\ + H_{1,1} \\ + H_{1,2} \\ + H_{1,3} \\ + H_{1,4} + \end{pmatrix} + = + \begin{pmatrix} + A_0 + 2A_1 \\ + 4A_1 + 2A_2 \\ + 4A_2 + 2A_3 \\ + 4A_3 + 2A_4 \\ + 8A_4 + A_5 + \end{pmatrix} + + which will be expressed as :math:`TH_1 = D`. + :math:`T` is a tridiagonal matrix, so the system can be solved in :math:`O(N)` + operations. Here we shall use Thomas' algorithm or the tridiagonal matrix + algorithm. + + .. seealso:: + `Tridiagonal matrix algorithm. Wikipedia. `_ + + Define: + + * :math:`a = [a_0, \ a_1, \ ..., \ a_{N-2}]` as :math:`T`'s lower diagonal of :math:`N-1` elements, + such that :math:`a_0 = a_1 = ... = a_{N-3} = 1`, and :math:`a_{N-2} = 2`; + * :math:`b = [b_0, \ b_1, \ ..., \ b_{N-2}, \ b_{N-1}]` as :math:`T`'s main diagonal of :math:`N` elements, + such that :math:`b_0 = 2`, :math:`b_1 = b_2 = ... = b_{N-2} = 4`, and :math:`b_{N-1} = 7`; + * :math:`c = [c_0, \ c_1, \ ..., \ c_{N-2}]` as :math:`T`'s upper diagonal of :math:`{N-1}` elements, + such that :math:`c_0 = c_1 = ... = c_{N-2} = 1`: this diagonal is filled with ones. + + If, according to Thomas' algorithm, we define: + + .. math:: + c'_0 &= \frac{c_0}{b_0} & \\ + c'_i &= \frac{c_i}{b_i - a_{i-1} c'_{i-1}}, & \quad \forall i \in \{1, ..., N-2\} \\ + & & \\ + D'_0 &= \frac{1}{b_0} D_0 & \\ + D'_i &= \frac{1}{b_i - a_{i-1} c'{i-1}} (D_i - a_{i-1} D'_{i-1}), & \quad \forall i \in \{1, ..., N-1\} + + Then: + + .. math:: + c'_0 &= 0.5 & \\ + c'_i &= \frac{1}{4 - c'_{i-1}}, & \quad \forall i \in \{1, ..., N-2\} \\ + & & \\ + D'_0 &= 0.5A_0 + A_1 & \\ + D'_i &= \frac{1}{4 - c'_{i-1}} (4A_i + 2A_{i+1} - D'_{i-1}) & \\ + &= c_i (4A_i + 2A_{i+1} - D'_{i-1}), & \quad \forall i \in \{1, ..., N-2\} \\ + D'_{N-1} &= \frac{1}{7 - 2c'_{N-2}} (8A_{N-1} + A_N - 2D'_{N-2}) & + + Finally, we can do Backward Substitution to find :math:`H_1`: + + .. math:: + H_{1, N-1} &= D'_{N-1} & \\ + H_{1, i} &= D'_i - c'_i H_{1, i+1}, & \quad \forall i \in \{0, ..., N-2\} + + Once we have :math:`H_1`, we can get :math:`H_2` (the array of second handles) as follows: + + .. math:: + H_{2, i} &= 2A_{i+1} - H_{1, i+1}, & \quad \forall i \in \{0, ..., N-2\} \\ + H_{2, N-1} &= 0.5A_N + 0.5H_{1, N-1} & + + As the matrix :math:`T` always follows the same pattern, we can define a memo list + for :math:`c'` to avoid recalculation. We cannot do the same for :math:`D`, however, + because it is always a different matrix. + + Parameters + ---------- + anchors + Anchors of an open cubic spline. + + Returns + ------- + :class:`tuple` [:class:`~.Point3D_Array`, :class:`~.Point3D_Array`] + A tuple of two arrays: one containing the 1st handle for every curve in + the open cubic spline, and the other containing the 2nd handles. """ - l, u = l_and_u - dim = diag.shape[1] - matrix = np.zeros((dim, dim)) - for i in range(l + u + 1): - np.fill_diagonal( - matrix[max(0, i - u) :, max(0, u - i) :], - diag[i, max(0, u - i) :], - ) - return matrix + global CP_OPEN_MEMO + + A = np.asarray(anchors) + N = A.shape[0] - 1 + dim = A.shape[1] + + # Calculate cp (c prime) with help from CP_OPEN_MEMO. + len_memo = CP_OPEN_MEMO.size + if len_memo < N - 1: + cp = np.empty(N - 1) + cp[:len_memo] = CP_OPEN_MEMO + for i in range(len_memo, N - 1): + cp[i] = 1 / (4 - cp[i - 1]) + CP_OPEN_MEMO = cp + else: + cp = CP_OPEN_MEMO[: N - 1] + + # Calculate Dp (D prime). + Dp = np.empty((N, dim)) + Dp[0] = 0.5 * A[0] + A[1] + AUX = 4 * A[1 : N - 1] + 2 * A[2:N] # Vectorize the sum for efficiency. + for i in range(1, N - 1): + Dp[i] = cp[i] * (AUX[i - 1] - Dp[i - 1]) + Dp[N - 1] = (1 / (7 - 2 * cp[N - 2])) * (8 * A[N - 1] + A[N] - 2 * Dp[N - 2]) + # Backward Substitution. + # H1 (array of the first handles) is defined as a view of Dp for efficiency + # and semantic convenience at the same time. + H1 = Dp + # H1[N-1] = Dp[N-1] (redundant) + for i in range(N - 2, -1, -1): + H1[i] = Dp[i] - cp[i] * H1[i + 1] -# Given 4 control points for a cubic bezier curve (or arrays of such) -# return control points for 2 quadratics (or 2n quadratics) approximating them. + # Calculate H2. + H2 = np.empty((N, dim)) + H2[0 : N - 1] = 2 * A[1:N] - H1[1:N] + H2[N - 1] = 0.5 * (A[N] + H1[N - 1]) + + return H1, H2 + + +@overload def get_quadratic_approximation_of_cubic( a0: Point3D, h0: Point3D, h1: Point3D, a1: Point3D -) -> BezierPoints: - a0 = np.array(a0, ndmin=2) - h0 = np.array(h0, ndmin=2) - h1 = np.array(h1, ndmin=2) - a1 = np.array(a1, ndmin=2) - # Tangent vectors at the start and end. - T0 = h0 - a0 - T1 = a1 - h1 - - # Search for inflection points. If none are found, use the - # midpoint as a cut point. - # Based on http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html - has_infl = np.ones(len(a0), dtype=bool) - - p = h0 - a0 - q = h1 - 2 * h0 + a0 - r = a1 - 3 * h1 + 3 * h0 - a0 - - a = cross2d(q, r) - b = cross2d(p, r) - c = cross2d(p, q) - - disc = b * b - 4 * a * c - has_infl &= disc > 0 - sqrt_disc = np.sqrt(np.abs(disc)) - settings = np.seterr(all="ignore") - ti_bounds = [] - for sgn in [-1, +1]: - ti = (-b + sgn * sqrt_disc) / (2 * a) - ti[a == 0] = (-c / b)[a == 0] - ti[(a == 0) & (b == 0)] = 0 - ti_bounds.append(ti) - ti_min, ti_max = ti_bounds - np.seterr(**settings) - ti_min_in_range = has_infl & (0 < ti_min) & (ti_min < 1) - ti_max_in_range = has_infl & (0 < ti_max) & (ti_max < 1) - - # Choose a value of t which starts at 0.5, - # but is updated to one of the inflection points - # if they lie between 0 and 1 - - t_mid = 0.5 * np.ones(len(a0)) - t_mid[ti_min_in_range] = ti_min[ti_min_in_range] - t_mid[ti_max_in_range] = ti_max[ti_max_in_range] - - m, n = a0.shape - t_mid = t_mid.repeat(n).reshape((m, n)) - - # Compute bezier point and tangent at the chosen value of t (these are vectorized) - mid = bezier([a0, h0, h1, a1])(t_mid) # type: ignore - Tm = bezier([h0 - a0, h1 - h0, a1 - h1])(t_mid) # type: ignore - - # Intersection between tangent lines at end points - # and tangent in the middle - i0 = find_intersection(a0, T0, mid, Tm) - i1 = find_intersection(a1, T1, mid, Tm) - - m, n = np.shape(a0) - result = np.zeros((6 * m, n)) +) -> QuadraticBezierPoints_Array: ... + + +@overload +def get_quadratic_approximation_of_cubic( + a0: Point3D_Array, + h0: Point3D_Array, + h1: Point3D_Array, + a1: Point3D_Array, +) -> QuadraticBezierPoints_Array: ... + + +def get_quadratic_approximation_of_cubic(a0, h0, h1, a1): + r"""If ``a0``, ``h0``, ``h1`` and ``a1`` are the control points of a cubic + Bézier curve, approximate the curve with two quadratic Bézier curves and + return an array of 6 points, where the first 3 points represent the first + quadratic curve and the last 3 represent the second one. + + Otherwise, if ``a0``, ``h0``, ``h1`` and ``a1`` are _arrays_ of :math:`N` + points representing :math:`N` cubic Bézier curves, return an array of + :math:`6N` points where each group of :math:`6` consecutive points + approximates each of the :math:`N` curves in a similar way as above. + + .. note:: + If the cubic spline given by the original cubic Bézier curves is + smooth, this algorithm will generate a quadratic spline which is also + smooth. + + If a cubic Bézier is given by + + .. math:: + C(t) = (1-t)^3 A_0 + 3(1-t)^2 t H_0 + 3(1-t)t^2 H_1 + t^3 A_1 + + where :math:`A_0`, :math:`H_0`, :math:`H_1` and :math:`A_1` are its + control points, then this algorithm should generate two quadratic + Béziers given by + + .. math:: + Q_0(t) &= (1-t)^2 A_0 + 2(1-t)t M_0 + t^2 K \\ + Q_1(t) &= (1-t)^2 K + 2(1-t)t M_1 + t^2 A_1 + + where :math:`M_0` and :math:`M_1` are the respective handles to be + found for both curves, and :math:`K` is the end anchor of the 1st curve + and the start anchor of the 2nd, which must also be found. + + To solve for :math:`M_0`, :math:`M_1` and :math:`K`, three conditions + can be imposed: + + 1. :math:`Q_0'(0) = \frac{1}{2}C'(0)`. The derivative of the first + quadratic curve at :math:`t = 0` should be proportional to that of + the original cubic curve, also at :math:`t = 0`. Because the cubic + curve is split into two parts, it is necessary to divide this by + two: the speed of a point travelling through the curve should be + half of the original. This gives: + + .. math:: + Q_0'(0) &= \frac{1}{2}C'(0) \\ + 2(M_0 - A_0) &= \frac{3}{2}(H_0 - A_0) \\ + 2M_0 - 2A_0 &= \frac{3}{2}H_0 - \frac{3}{2}A_0 \\ + 2M_0 &= \frac{3}{2}H_0 + \frac{1}{2}A_0 \\ + M_0 &= \frac{1}{4}(3H_0 + A_0) + + 2. :math:`Q_1'(1) = \frac{1}{2}C'(1)`. The derivative of the second + quadratic curve at :math:`t = 1` should be half of that of the + original cubic curve for the same reasons as above, also at + :math:`t = 1`. This gives: + + .. math:: + Q_1'(1) &= \frac{1}{2}C'(1) \\ + 2(A_1 - M_1) &= \frac{3}{2}(A_1 - H_1) \\ + 2A_1 - 2M_1 &= \frac{3}{2}A_1 - \frac{3}{2}H_1 \\ + -2M_1 &= -\frac{1}{2}A_1 - \frac{3}{2}H_1 \\ + M_1 &= \frac{1}{4}(3H_1 + A_1) + + 3. :math:`Q_0'(1) = Q_1'(0)`. The derivatives of both quadratic curves + should match at the point :math:`K`, in order for the final spline + to be smooth. This gives: + + .. math:: + Q_0'(1) &= Q_1'(0) \\ + 2(K - M_0) &= 2(M_1 - K) \\ + 2K - 2M_0 &= 2M_1 - 2K \\ + 4K &= 2M_0 + 2M_1 \\ + K &= \frac{1}{2}(M_0 + M_1) + + This is sufficient to find proper control points for the quadratic + Bézier curves. + + Parameters + ---------- + a0 + The start anchor of a single cubic Bézier curve, or an array of + :math:`N` start anchors for :math:`N` curves. + h0 + The first handle of a single cubic Bézier curve, or an array of + :math:`N` first handles for :math:`N` curves. + h1 + The second handle of a single cubic Bézier curve, or an array of + :math:`N` second handles for :math:`N` curves. + a1 + The end anchor of a single cubic Bézier curve, or an array of + :math:`N` end anchors for :math:`N` curves. + + Returns + ------- + result + An array containing either 6 points for 2 quadratic Bézier curves + approximating the original cubic curve, or :math:`6N` points for + :math:`2N` quadratic curves approximating :math:`N` cubic curves. + + Raises + ------ + ValueError + If ``a0``, ``h0``, ``h1`` and ``a1`` have different dimensions, or + if their number of dimensions is not 1 or 2. + """ + a0 = np.asarray(a0) + h0 = np.asarray(h0) + h1 = np.asarray(h1) + a1 = np.asarray(a1) + + if all(arr.ndim == 1 for arr in (a0, h0, h1, a1)): + num_curves, dim = 1, a0.shape[0] + elif all(arr.ndim == 2 for arr in (a0, h0, h1, a1)): + num_curves, dim = a0.shape + else: + raise ValueError("All arguments must be Point3D or Point3D_Array.") + + m0 = 0.25 * (3 * h0 + a0) + m1 = 0.25 * (3 * h1 + a1) + k = 0.5 * (m0 + m1) + + result = np.empty((6 * num_curves, dim)) result[0::6] = a0 - result[1::6] = i0 - result[2::6] = mid - result[3::6] = mid - result[4::6] = i1 + result[1::6] = m0 + result[2::6] = k + result[3::6] = k + result[4::6] = m1 result[5::6] = a1 return result diff --git a/manim/utils/caching.py b/manim/utils/caching.py index d19ba8c0e2..da3d59d711 100644 --- a/manim/utils/caching.py +++ b/manim/utils/caching.py @@ -30,8 +30,6 @@ def handle_caching_play(func: Callable[..., None]): # method has to be deleted. def wrapper(self, scene, *args, **kwargs): - self.skip_animations = self._original_skipping_status - self.update_skipping_status() animations = scene.compile_animations(*args, **kwargs) scene.add_mobjects_from_animations(animations) if self.skip_animations: diff --git a/manim/utils/docbuild/autoaliasattr_directive.py b/manim/utils/docbuild/autoaliasattr_directive.py index 6cb4137833..ba42bd1ec4 100644 --- a/manim/utils/docbuild/autoaliasattr_directive.py +++ b/manim/utils/docbuild/autoaliasattr_directive.py @@ -16,7 +16,7 @@ __all__ = ["AliasAttrDocumenter"] -ALIAS_DOCS_DICT, DATA_DICT = parse_module_attributes() +ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT = parse_module_attributes() ALIAS_LIST = [ alias_name for module_dict in ALIAS_DOCS_DICT.values() @@ -100,10 +100,11 @@ class AliasAttrDocumenter(Directive): def run(self) -> list[nodes.Element]: module_name = self.arguments[0] - # Slice module_name[6:] to remove the "manim." prefix which is # not present in the keys of the DICTs - module_alias_dict = ALIAS_DOCS_DICT.get(module_name[6:], None) - module_attrs_list = DATA_DICT.get(module_name[6:], None) + module_name = module_name.removeprefix("manim.") + module_alias_dict = ALIAS_DOCS_DICT.get(module_name, None) + module_attrs_list = DATA_DICT.get(module_name, None) + module_typevars = TYPEVAR_DICT.get(module_name, None) content = nodes.container() @@ -161,6 +162,11 @@ def run(self) -> list[nodes.Element]: for A in ALIAS_LIST: alias_doc = alias_doc.replace(f"`{A}`", f":class:`~.{A}`") + # also hyperlink the TypeVars from that module + if module_typevars is not None: + for T in module_typevars: + alias_doc = alias_doc.replace(f"`{T}`", f":class:`{T}`") + # Add all the lines with 4 spaces behind, to consider all the # documentation as a paragraph INSIDE the `.. class::` block doc_lines = alias_doc.split("\n") @@ -172,6 +178,37 @@ def run(self) -> list[nodes.Element]: self.state.nested_parse(unparsed, 0, alias_container) category_alias_container += alias_container + # then add the module TypeVars section + if module_typevars is not None: + module_typevars_section = nodes.section(ids=[f"{module_name}.typevars"]) + content += module_typevars_section + + # Use a rubric (title-like), just like in `module.rst` + module_typevars_section += nodes.rubric(text="TypeVar's") + + # name: str + # definition: TypeVarDict = dict[str, str] + for name, definition in module_typevars.items(): + # Using the `.. class::` directive is CRUCIAL, since + # function/method parameters are always annotated via + # classes - therefore Sphinx expects a class + unparsed = ViewList( + [ + f".. class:: {name}", + "", + " .. parsed-literal::", + "", + f" {definition}", + "", + ] + ) + + # Parse the reST text into a fresh container + # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest + typevar_container = nodes.container() + self.state.nested_parse(unparsed, 0, typevar_container) + module_typevars_section += typevar_container + # Then, add the traditional "Module Attributes" section if module_attrs_list is not None: module_attrs_section = nodes.section(ids=[f"{module_name}.data"]) diff --git a/manim/utils/docbuild/manim_directive.py b/manim/utils/docbuild/manim_directive.py index da4b496ecf..b6af8ee00d 100644 --- a/manim/utils/docbuild/manim_directive.py +++ b/manim/utils/docbuild/manim_directive.py @@ -82,6 +82,7 @@ def construct(self): import csv import itertools as it +import os import re import shutil import sys @@ -296,7 +297,7 @@ def run(self) -> list[nodes.Element]: code = [ "from manim import *", *user_code, - f"{clsname}().render()", + f"Manager({clsname}).render()", ] try: @@ -350,7 +351,7 @@ def run(self) -> list[nodes.Element]: rendering_times_file_path = Path("../rendering_times.csv") -def _write_rendering_stats(scene_name: str, run_time: str, file_name: str) -> None: +def _write_rendering_stats(scene_name: str, run_time: float, file_name: str) -> None: with rendering_times_file_path.open("a") as file: csv.writer(file).writerow( [ diff --git a/manim/utils/docbuild/module_parsing.py b/manim/utils/docbuild/module_parsing.py index 5166ff4801..57ac9a56aa 100644 --- a/manim/utils/docbuild/module_parsing.py +++ b/manim/utils/docbuild/module_parsing.py @@ -26,6 +26,10 @@ classified by category in different `AliasCategoryDict` objects. """ +ModuleTypeVarDict: TypeAlias = dict[str, str] +"""Dictionary containing every :class:`TypeVar` defined in a module.""" + + AliasDocsDict: TypeAlias = dict[str, ModuleLevelAliasDict] """Dictionary which, for every module in Manim, contains documentation about their module-level attributes which are explicitly defined as @@ -39,8 +43,12 @@ explicitly defined as :class:`TypeAlias`. """ +TypeVarDict: TypeAlias = dict[str, ModuleTypeVarDict] +"""A dictionary mapping module names to dictionaries of :class:`TypeVar` objects.""" + ALIAS_DOCS_DICT: AliasDocsDict = {} DATA_DICT: DataDict = {} +TYPEVAR_DICT: TypeVarDict = {} MANIM_ROOT = Path(__file__).resolve().parent.parent.parent @@ -50,7 +58,7 @@ # ruff: noqa: E721 -def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]: +def parse_module_attributes() -> tuple[AliasDocsDict, DataDict, TypeVarDict]: """Read all files, generate Abstract Syntax Trees from them, and extract useful information about the type aliases defined in the files: the category they belong to, their definition and their @@ -58,19 +66,24 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]: Returns ------- - ALIAS_DOCS_DICT : `AliasDocsDict` + ALIAS_DOCS_DICT : :class:`AliasDocsDict` A dictionary containing the information from all the type - aliases in Manim. See `AliasDocsDict` for more information. + aliases in Manim. See :class:`AliasDocsDict` for more information. - DATA_DICT : `DataDict` + DATA_DICT : :class:`DataDict` A dictionary containing the names of all DOCUMENTED module-level attributes which are not a :class:`TypeAlias`. + + TYPEVAR_DICT : :class:`TypeVarDict` + A dictionary containing the definitions of :class:`TypeVar` objects, + organized by modules. """ global ALIAS_DOCS_DICT global DATA_DICT + global TYPEVAR_DICT - if ALIAS_DOCS_DICT or DATA_DICT: - return ALIAS_DOCS_DICT, DATA_DICT + if ALIAS_DOCS_DICT or DATA_DICT or TYPEVAR_DICT: + return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT for module_path in MANIM_ROOT.rglob("*.py"): module_name = module_path.resolve().relative_to(MANIM_ROOT) @@ -85,6 +98,9 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]: category_dict: AliasCategoryDict | None = None alias_info: AliasInfo | None = None + # For storing TypeVars + module_typevars: ModuleTypeVarDict = {} + # For storing regular module attributes data_list: list[str] = [] data_name: str | None = None @@ -172,6 +188,19 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]: alias_info = category_dict[alias_name] continue + # Check if it is a typing.TypeVar + elif ( + type(node) is ast.Assign + and type(node.targets[0]) is ast.Name + and type(node.value) is ast.Call + and type(node.value.func) is ast.Name + and node.value.func.id.endswith("TypeVar") + ): + module_typevars[node.targets[0].id] = ast.unparse( + node.value + ).replace("_", r"\_") + continue + # If here, the node is not a TypeAlias definition alias_info = None @@ -185,7 +214,9 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]: else: target = None - if type(target) is ast.Name: + if type(target) is ast.Name and not ( + type(node) is ast.Assign and target.id not in module_typevars + ): data_name = target.id else: data_name = None @@ -194,5 +225,7 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]: ALIAS_DOCS_DICT[module_name] = module_dict if len(data_list) > 0: DATA_DICT[module_name] = data_list + if module_typevars: + TYPEVAR_DICT[module_name] = module_typevars - return ALIAS_DOCS_DICT, DATA_DICT + return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT diff --git a/manim/utils/family_ops.py b/manim/utils/family_ops.py index 3260af920f..8b0aaa70ea 100644 --- a/manim/utils/family_ops.py +++ b/manim/utils/family_ops.py @@ -7,15 +7,26 @@ "restructure_list_to_exclude_certain_family_members", ] +from typing import TYPE_CHECKING -def extract_mobject_family_members(mobject_list, only_those_with_points=False): +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject + + +def extract_mobject_family_members( + mobject_list: Iterable[Mobject], only_those_with_points: bool = False +) -> Sequence[Mobject]: result = list(it.chain(*(mob.get_family() for mob in mobject_list))) if only_those_with_points: result = [mob for mob in result if mob.has_points()] return result -def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove): +def restructure_list_to_exclude_certain_family_members( + mobject_list: Iterable[Mobject], to_remove: Iterable[Mobject] +) -> Sequence[Mobject]: """ Removes anything in to_remove from mobject_list, but in the event that one of the items to be removed is a member of the family of an item in mobject_list, @@ -43,8 +54,8 @@ def add_safe_mobjects_from_list(list_to_examine, set_to_remove): def recursive_mobject_remove( - mobjects: List[Mobject], to_remove: Set[Mobject] -) -> Tuple[List[Mobject], bool]: + mobjects: list[Mobject], to_remove: set[Mobject] +) -> tuple[Sequence[Mobject], bool]: """ Takes in a list of mobjects, together with a set of mobjects to remove. The first component of what's removed is a new list such that any mobject diff --git a/manim/utils/file_ops.py b/manim/utils/file_ops.py index 5bb8e95e92..d8e28e1515 100644 --- a/manim/utils/file_ops.py +++ b/manim/utils/file_ops.py @@ -30,11 +30,9 @@ import numpy as np if TYPE_CHECKING: - from ..scene.scene_file_writer import SceneFileWriter + from manim.scene.scene_file_writer import SceneFileWriter -from manim import __version__, config, logger - -from .. import console +from manim import __version__, config, console, logger def is_mp4_format() -> bool: @@ -314,10 +312,12 @@ def get_sorted_integer_files( full_path = os.path.join(directory, file) if index_str.isdigit(): index = int(index_str) - if remove_indices_greater_than is not None: - if index > remove_indices_greater_than: - os.remove(full_path) - continue + if ( + remove_indices_greater_than is not None + and index > remove_indices_greater_than + ): + os.remove(full_path) + continue if extension is not None and not file.endswith(extension): continue if index >= min_index and index < max_index: @@ -325,4 +325,4 @@ def get_sorted_integer_files( elif remove_non_integer_files: os.remove(full_path) indexed_files.sort(key=lambda p: p[0]) - return list(map(lambda p: os.path.join(directory, p[1]), indexed_files)) + return [os.path.join(directory, p[1]) for p in indexed_files] diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 9557ba18f1..6286282acd 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -2,27 +2,31 @@ from __future__ import annotations -import collections import copy import inspect import json import typing import zlib +from collections.abc import Callable, Hashable from time import perf_counter from types import FunctionType, MappingProxyType, MethodType, ModuleType from typing import Any import numpy as np -from manim.animation.animation import Animation -from manim.camera.camera import Camera -from manim.mobject.mobject import Mobject - from .. import config, logger if typing.TYPE_CHECKING: + from typing_extensions import TypeVar + + from manim.animation.protocol import AnimationProtocol + from manim.camera.camera import Camera + from manim.mobject.opengl.opengl_mobject import OpenGLMobject from manim.scene.scene import Scene + T = TypeVar("T") + S = TypeVar("S", default=str) + __all__ = ["KEYS_TO_FILTER_OUT", "get_hash_from_play_call", "get_json"] # Sometimes there are elements that are not suitable for hashing (too long or @@ -59,7 +63,7 @@ def reset_already_processed(cls): cls._already_processed.clear() @classmethod - def check_already_processed_decorator(cls: _Memoizer, is_method: bool = False): + def check_already_processed_decorator(cls, is_method: bool = False): """Decorator to handle the arguments that goes through the decorated function. Returns _ALREADY_PROCESSED_PLACEHOLDER if the obj has been processed, or lets the decorated function call go ahead. @@ -102,7 +106,7 @@ def check_already_processed(cls, obj: Any) -> Any: return cls._handle_already_processed(obj, lambda x: x) @classmethod - def mark_as_processed(cls, obj: Any) -> None: + def mark_as_processed(cls, obj: Any) -> str: """Marks an object as processed. Parameters @@ -131,7 +135,7 @@ def _handle_already_processed( # It makes no sense (and it'd slower) to memoize objects of these primitive # types. Hence, we simply return the object. return obj - if isinstance(obj, collections.abc.Hashable): + if isinstance(obj, Hashable): try: return cls._return(obj, hash, default_function) except TypeError: @@ -144,11 +148,11 @@ def _handle_already_processed( @classmethod def _return( cls, - obj: typing.Any, + obj: T, obj_to_membership_sign: typing.Callable[[Any], int], - default_func, + default_func: Callable[[T], str], memoizing=True, - ) -> str | Any: + ) -> str: obj_membership_sign = obj_to_membership_sign(obj) if obj_membership_sign in cls._already_processed: return cls.ALREADY_PROCESSED_PLACEHOLDER @@ -173,7 +177,7 @@ def _return( class _CustomEncoder(json.JSONEncoder): - def default(self, obj: Any): + def default(self, o: Any): """ This method is used to serialize objects to JSON format. @@ -196,11 +200,11 @@ def default(self, obj: Any): Python object that JSON encoder will recognize """ - if not (isinstance(obj, ModuleType)) and isinstance( - obj, + if not (isinstance(o, ModuleType)) and isinstance( + o, (MethodType, FunctionType), ): - cvars = inspect.getclosurevars(obj) + cvars = inspect.getclosurevars(o) cvardict = {**copy.copy(cvars.globals), **copy.copy(cvars.nonlocals)} for i in list(cvardict): # NOTE : All module types objects are removed, because otherwise it @@ -208,7 +212,7 @@ def default(self, obj: Any): if isinstance(cvardict[i], ModuleType): del cvardict[i] try: - code = inspect.getsource(obj) + code = inspect.getsource(o) except (OSError, TypeError): # This happens when rendering videos included in the documentation # within doctests and should be replaced by a solution avoiding @@ -216,23 +220,23 @@ def default(self, obj: Any): # See https://github.com/ManimCommunity/manim/pull/402. code = "" return self._cleaned_iterable({"code": code, "nonlocals": cvardict}) - elif isinstance(obj, np.ndarray): - if obj.size > 1000: - obj = np.resize(obj, (100, 100)) - return f"TRUNCATED ARRAY: {repr(obj)}" + elif isinstance(o, np.ndarray): + if o.size > 1000: + o = np.resize(o, (100, 100)) + return f"TRUNCATED ARRAY: {repr(o)}" # We return the repr and not a list to avoid the JsonEncoder to iterate over it. - return repr(obj) - elif hasattr(obj, "__dict__"): - temp = getattr(obj, "__dict__") + return repr(o) + elif hasattr(o, "__dict__"): + temp = getattr(o, "__dict__") # MappingProxy is scene-caching nightmare. It contains all of the object methods and attributes. We skip it as the mechanism will at some point process the object, but instantiated. # Indeed, there is certainly no case where scene-caching will receive only a non instancied object, as this is never used in the library or encouraged to be used user-side. if isinstance(temp, MappingProxyType): return "MappingProxy" return self._cleaned_iterable(temp) - elif isinstance(obj, np.uint8): - return int(obj) + elif isinstance(o, np.uint8): + return int(o) # Serialize it with only the type of the object. You can change this to whatever string when debugging the serialization process. - return str(type(obj)) + return str(type(o)) def _cleaned_iterable(self, iterable: typing.Iterable[Any]): """Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format. @@ -325,8 +329,8 @@ def get_json(obj: dict): def get_hash_from_play_call( scene_object: Scene, camera_object: Camera, - animations_list: typing.Iterable[Animation], - current_mobjects_list: typing.Iterable[Mobject], + animations_list: typing.Iterable[AnimationProtocol], + current_mobjects_list: typing.Iterable[OpenGLMobject], ) -> str: """Take the list of animations and a list of mobjects and output their hashes. This is meant to be used for `scene.play` function. diff --git a/manim/utils/ipython_magic.py b/manim/utils/ipython_magic.py index 334da7021f..3677f4af86 100644 --- a/manim/utils/ipython_magic.py +++ b/manim/utils/ipython_magic.py @@ -10,6 +10,7 @@ from manim import config, logger, tempconfig from manim.__main__ import main +from manim.manager import Manager from manim.renderer.shader import shader_program_cache __all__ = ["ManimMagic"] @@ -127,19 +128,20 @@ def construct(self): args = main(modified_args, standalone_mode=False, prog_name="manim") with tempconfig(local_ns.get("config", {})): config.digest_args(args) + manager: Manager | None = None try: SceneClass = local_ns[config["scene_names"][0]] - scene = SceneClass() - scene.render() + manager = Manager(SceneClass) + manager.render() finally: # Shader cache becomes invalid as the context is destroyed shader_program_cache.clear() # Close OpenGL window here instead of waiting for the main thread to # finish causing the window to stay open and freeze - if scene.window is not None: - scene.window.close() + if manager is not None and manager.window is not None: + manager.window.close() if config["output_file"] is None: logger.info("No output file produced") @@ -166,7 +168,11 @@ def construct(self): # set explicitly. embed = "google.colab" in str(get_ipython()) - if file_type.startswith("image"): + if file_type is None: + raise Exception( + "Could not guess file type, please contact the developers" + ) + elif file_type.startswith("image"): result = Image(filename=config["output_file"]) else: result = Video( diff --git a/manim/utils/module_ops.py b/manim/utils/module_ops.py index 03f297030d..32f8e4a121 100644 --- a/manim/utils/module_ops.py +++ b/manim/utils/module_ops.py @@ -6,15 +6,24 @@ import sys import types import warnings -from pathlib import Path +from typing import TYPE_CHECKING + +from manim import config, console, constants, logger +from manim.file_writer import FileWriter + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from pathlib import Path + + from typing_extensions import Any + + from manim.scene.scene import Scene -from .. import config, console, constants, logger -from ..scene.scene_file_writer import SceneFileWriter __all__ = ["scene_classes_from_file"] -def get_module(file_name: Path): +def get_module(file_name: Path) -> types.ModuleType: if str(file_name) == "-": module = types.ModuleType("input_scenes") logger.info( @@ -56,10 +65,10 @@ def get_module(file_name: Path): raise FileNotFoundError(f"{file_name} not found") -def get_scene_classes_from_module(module): - from ..scene.scene import Scene +def get_scene_classes_from_module(module: types.ModuleType) -> list[type[Scene]]: + from manim.scene.scene import Scene - def is_child_scene(obj, module): + def is_child_scene(obj: Any, module: types.ModuleType) -> bool: return ( inspect.isclass(obj) and issubclass(obj, Scene) @@ -73,7 +82,7 @@ def is_child_scene(obj, module): ] -def get_scenes_to_render(scene_classes): +def get_scenes_to_render(scene_classes: Sequence[type[Scene]]) -> list[type[Scene]]: if not scene_classes: logger.error(constants.NO_SCENE_MESSAGE) return [] @@ -97,9 +106,9 @@ def get_scenes_to_render(scene_classes): return prompt_user_for_choice(scene_classes) -def prompt_user_for_choice(scene_classes): +def prompt_user_for_choice(scene_classes: Iterable[type[Scene]]) -> list[type[Scene]]: num_to_class = {} - SceneFileWriter.force_output_as_scene_name = True + FileWriter.force_output_as_scene_name = True for count, scene_class in enumerate(scene_classes, 1): name = scene_class.__name__ console.print(f"{count}: {name}", style="logging.level.info") @@ -125,8 +134,8 @@ def prompt_user_for_choice(scene_classes): def scene_classes_from_file( - file_path: Path, require_single_scene=False, full_list=False -): + file_path: Path, require_single_scene: bool = False, full_list: bool = False +) -> type[Scene] | list[type[Scene]]: module = get_module(file_path) all_scene_classes = get_scene_classes_from_module(module) if full_list: diff --git a/manim/utils/simple_functions.py b/manim/utils/simple_functions.py index b64808e6fc..f73b15ce6d 100644 --- a/manim/utils/simple_functions.py +++ b/manim/utils/simple_functions.py @@ -114,9 +114,7 @@ def clip(a, min_a, max_a): return a -def fdiv( - a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None -) -> Scalable: +def fdiv(a: float, b: float, zero_over_zero_value: float | None = None) -> float: if zero_over_zero_value is not None: out = np.full_like(a, zero_over_zero_value) where = np.logical_or(a != 0, b != 0) diff --git a/manim/utils/sounds.py b/manim/utils/sounds.py index 5e0ea060f3..9621f4f4d2 100644 --- a/manim/utils/sounds.py +++ b/manim/utils/sounds.py @@ -6,13 +6,14 @@ "get_full_sound_file_path", ] +from pathlib import Path -from .. import config -from ..utils.file_ops import seek_full_path_from_defaults +from manim import config +from manim.utils.file_ops import seek_full_path_from_defaults # Still in use by add_sound() function in scene_file_writer.py -def get_full_sound_file_path(sound_file_name): +def get_full_sound_file_path(sound_file_name: str) -> Path: return seek_full_path_from_defaults( sound_file_name, default_dir=config.get_dir("assets_dir"), diff --git a/manim/utils/space_ops.py b/manim/utils/space_ops.py index 012d199ad1..1981bd708e 100644 --- a/manim/utils/space_ops.py +++ b/manim/utils/space_ops.py @@ -301,16 +301,6 @@ def get_norm(vector: np.ndarray) -> float: return np.linalg.norm(vector) -def normalize(vect: list[float], fall_back: list[float] | None = None) -> np.ndarray: - norm = get_norm(vect) - if norm > 0: - return np.array(vect) / norm - elif fall_back is not None: - return np.array(fall_back) - else: - return np.zeros(len(vect)) - - def z_to_vector(vector: np.ndarray) -> np.ndarray: """ Returns some matrix in SO(3) which takes the z-axis to the @@ -373,12 +363,16 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float: ) -def normalize(vect: np.ndarray | tuple[float], fall_back=None) -> np.ndarray: - norm = np.linalg.norm(vect) +def normalize( + vect: npt.NDArray[float], fall_back: npt.NDArray[float] | None = None +) -> npt.NDArray[float]: + norm = get_norm(vect) if norm > 0: return np.array(vect) / norm + elif fall_back is not None: + return np.array(fall_back) else: - return fall_back or np.zeros(len(vect)) + return np.zeros(len(vect)) def normalize_along_axis(array: np.ndarray, axis: np.ndarray) -> np.ndarray: diff --git a/manim/utils/testing/_frames_testers.py b/manim/utils/testing/_frames_testers.py index be0bc38447..1b274c18c0 100644 --- a/manim/utils/testing/_frames_testers.py +++ b/manim/utils/testing/_frames_testers.py @@ -7,9 +7,12 @@ import numpy as np from manim import logger +from manim.typing import PixelArray from ._show_diff import show_diff_helper +__all__ = ["_FramesTester", "_ControlDataWriter"] + FRAME_ABSOLUTE_TOLERANCE = 1.01 FRAME_MISMATCH_RATIO_TOLERANCE = 1e-5 @@ -37,7 +40,7 @@ def testing(self): f"when there are {self._number_frames} control frames for this test." ) - def check_frame(self, frame_number: int, frame: np.ndarray): + def check_frame(self, frame_number: int, frame: PixelArray): assert frame_number < self._number_frames, ( f"The tested scene is at frame number {frame_number} " f"when there are {self._number_frames} control frames." @@ -74,7 +77,7 @@ def check_frame(self, frame_number: int, frame: np.ndarray): self._frames[frame_number], self._file_path.name, ) - raise e + raise e from e class _ControlDataWriter(_FramesTester): @@ -84,7 +87,7 @@ def __init__(self, file_path: Path, size_frame: tuple) -> None: self._number_frames_written: int = 0 # Actually write a frame. - def check_frame(self, index: int, frame: np.ndarray): + def check_frame(self, frame_number: int, frame: np.ndarray): frame = frame[np.newaxis, ...] self.frames = np.concatenate((self.frames, frame)) self._number_frames_written += 1 diff --git a/manim/utils/testing/_show_diff.py b/manim/utils/testing/_show_diff.py index 0cb2aab0f5..e1fb28d475 100644 --- a/manim/utils/testing/_show_diff.py +++ b/manim/utils/testing/_show_diff.py @@ -5,6 +5,8 @@ import numpy as np +__all__ = ["show_diff_helper"] + def show_diff_helper( frame_number: int, diff --git a/manim/utils/testing/_test_class_makers.py b/manim/utils/testing/_test_class_makers.py index ac5886d494..a44153c826 100644 --- a/manim/utils/testing/_test_class_makers.py +++ b/manim/utils/testing/_test_class_makers.py @@ -2,76 +2,61 @@ from typing import Callable +from manim.file_writer.protocols import FileWriterProtocol from manim.scene.scene import Scene -from manim.scene.scene_file_writer import SceneFileWriter from ._frames_testers import _FramesTester +__all__ = ["_make_test_scene_class", "_make_scene_file_writer_class"] + def _make_test_scene_class( base_scene: type[Scene], - construct_test: Callable[[Scene], None], - test_renderer, + construct_test: Callable[[Scene], object], ) -> type[Scene]: class _TestedScene(base_scene): - def __init__(self, *args, **kwargs): - super().__init__(renderer=test_renderer, *args, **kwargs) - def construct(self): + from manim import config + construct_test(self) # Manim hack to render the very last frame (normally the last frame is not the very end of the animation) - if self.animations is not None: - self.update_to_time(self.get_run_time(self.animations)) - self.renderer.render(self, 1, self.moving_mobjects) + self.wait(1 / config.frame_rate) return _TestedScene -def _make_test_renderer_class(from_renderer): - # Just for inheritance. - class _TestRenderer(from_renderer): - pass - - return _TestRenderer - - -class DummySceneFileWriter(SceneFileWriter): +class DummySceneFileWriter(FileWriterProtocol): """Delegate of SceneFileWriter used to test the frames.""" - def __init__(self, renderer, scene_name, **kwargs): - super().__init__(renderer, scene_name, **kwargs) - self.i = 0 + def __init__(self, scene_name: str): + # we still need num_plays to satisfy the protocol + self.num_plays = 0 + self.frames = 0 - def init_output_directories(self, scene_name): + def begin_animation(self, allow_write: bool = False): pass - def add_partial_movie_file(self, hash_animation): - pass - - def begin_animation(self, allow_write=True): - pass + def end_animation(self, allow_write: bool = False): + self.num_plays += 1 - def end_animation(self, allow_write): - pass + def is_already_cached(self, hash_invocation: str) -> bool: + return False - def combine_to_movie(self): + def add_partial_movie_file(self, hash_animation: str) -> None: pass - def combine_to_section_videos(self): - pass + def write_frame(self, frame): + self.frames += 1 - def clean_cache(self): + def finish(self): pass - def write_frame(self, frame_or_renderer, num_frames=1): - self.i += 1 - -def _make_scene_file_writer_class(tester: _FramesTester) -> type[SceneFileWriter]: +def _make_scene_file_writer_class(tester: _FramesTester) -> type[FileWriterProtocol]: class TestSceneFileWriter(DummySceneFileWriter): - def write_frame(self, frame_or_renderer, num_frames=1): - tester.check_frame(self.i, frame_or_renderer) - super().write_frame(frame_or_renderer, num_frames=num_frames) + def write_frame(self, frame): + tester.check_frame(self.frames, frame) + super().write_frame(frame) return TestSceneFileWriter diff --git a/manim/utils/testing/frames_comparison.py b/manim/utils/testing/frames_comparison.py index e3814ea696..9d3f7ed89c 100644 --- a/manim/utils/testing/frames_comparison.py +++ b/manim/utils/testing/frames_comparison.py @@ -5,37 +5,31 @@ from pathlib import Path from typing import Callable -import cairo import pytest -from _pytest.fixtures import FixtureRequest -from manim import Scene +from manim import Manager, Scene from manim._config import tempconfig from manim._config.utils import ManimConfig -from manim.camera.three_d_camera import ThreeDCamera -from manim.renderer.cairo_renderer import CairoRenderer -from manim.scene.three_d_scene import ThreeDScene from ._frames_testers import _ControlDataWriter, _FramesTester from ._test_class_makers import ( DummySceneFileWriter, _make_scene_file_writer_class, - _make_test_renderer_class, _make_test_scene_class, ) +__all__ = ["frames_comparison"] + SCENE_PARAMETER_NAME = "scene" _tests_root_dir_path = Path(__file__).absolute().parents[2] PATH_CONTROL_DATA = _tests_root_dir_path / Path("control_data", "graphical_units_data") -MIN_CAIRO_VERSION = 11800 def frames_comparison( - func=None, + func: Callable[..., object] | None = None, *, last_frame: bool = True, - renderer_class=CairoRenderer, - base_scene=Scene, + base_scene: type[Scene] = Scene, **custom_config, ): """Compares the frames generated by the test with control frames previously registered. @@ -44,14 +38,12 @@ def frames_comparison( control frames for a given test, pass ``--set_test`` flag to pytest while running the test. - Note that this decorator can be use with or without parentheses. + Note that this decorator can be used with or without parentheses. Parameters ---------- last_frame whether the test should test the last frame, by default True. - renderer_class - The base renderer to use (OpenGLRenderer/CairoRenderer), by default CairoRenderer base_scene The base class for the scene (ThreeDScene, etc.), by default Scene @@ -65,8 +57,8 @@ def decorator_maker(tested_scene_construct): SCENE_PARAMETER_NAME not in inspect.getfullargspec(tested_scene_construct).args ): - raise Exception( - f"Invalid graphical test function test function : must have '{SCENE_PARAMETER_NAME}'as one of the parameters.", + raise ValueError( + f"Invalid graphical test function test function : must have {SCENE_PARAMETER_NAME!r} as one of the parameters.", ) # Exclude "scene" from the argument list of the signature. @@ -74,22 +66,17 @@ def decorator_maker(tested_scene_construct): functools.partial(tested_scene_construct, scene=None), ) - if "__module_test__" not in tested_scene_construct.__globals__: - raise Exception( + module_name = tested_scene_construct.__globals__.get("__module_test__") + if module_name is None: + raise AttributeError( "There is no module test name indicated for the graphical unit test. You have to declare __module_test__ in the test file.", ) - module_name = tested_scene_construct.__globals__.get("__module_test__") - test_name = tested_scene_construct.__name__[len("test_") :] + + test_name = tested_scene_construct.__name__.removeprefix("test_") @functools.wraps(tested_scene_construct) # The "request" parameter is meant to be used as a fixture by pytest. See below. - def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): - # check for cairo version - if ( - renderer_class is CairoRenderer - and cairo.cairo_version() < MIN_CAIRO_VERSION - ): - pytest.skip("Cairo version is too old. Skipping cairo graphical tests.") + def wrapper(*args, request: pytest.FixtureRequest, tmp_path, **kwargs): # Wraps the test_function to a construct method, to "freeze" the eventual additional arguments (parametrizations fixtures). construct = functools.partial(tested_scene_construct, *args, **kwargs) @@ -99,23 +86,21 @@ def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): # Example: if "length" is parametrized from 0 to 20, the kwargs # will be once with {"length" : 1}, etc. test_name_with_param = test_name + "_".join( - f"_{str(tup[0])}[{str(tup[1])}]" for tup in kwargs.items() + f"_{k}[{v}]" for k, v in kwargs.items() ) config_tests = _config_test(last_frame) - config_tests["text_dir"] = tmp_path - config_tests["tex_dir"] = tmp_path + config_tests.text_dir = tmp_path + config_tests.tex_dir = tmp_path if last_frame: - config_tests["frame_rate"] = 1 - config_tests["dry_run"] = True + config_tests.frame_rate = 1 + else: + config_tests.write_to_movie = True setting_test = request.config.getoption("--set_test") - try: - test_file_path = tested_scene_construct.__globals__["__file__"] - except Exception: - test_file_path = None + test_file_path = tested_scene_construct.__globals__.get("__file__") real_test = _make_test_comparing_frames( file_path=_control_data_path( test_file_path, @@ -125,7 +110,6 @@ def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): ), base_scene=base_scene, construct=construct, - renderer_class=renderer_class, is_set_test_data_test=setting_test, last_frame=last_frame, show_diff=request.config.getoption("--show_diff"), @@ -146,13 +130,13 @@ def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): inspect.Parameter("tmp_path", inspect.Parameter.KEYWORD_ONLY), ] new_sig = old_sig.replace(parameters=parameters) - wrapper.__signature__ = new_sig + wrapper.__signature__ = new_sig # type: ignore # Reach a bit into pytest internals to hoist the marks from our wrapped # function. - setattr(wrapper, "pytestmark", []) + wrapper.pytestmark = [] # type: ignore # Do we really need this? new_marks = getattr(tested_scene_construct, "pytestmark", []) - wrapper.pytestmark = new_marks + wrapper.pytestmark = new_marks # type: ignore return wrapper # Case where the decorator is called with and without parentheses. @@ -165,8 +149,7 @@ def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): def _make_test_comparing_frames( file_path: Path, base_scene: type[Scene], - construct: Callable[[Scene], None], - renderer_class: type, # Renderer type, there is no superclass renderer yet ..... + construct: Callable[[Scene], object], is_set_test_data_test: bool, last_frame: bool, show_diff: bool, @@ -203,30 +186,20 @@ def _make_test_comparing_frames( if not last_frame else DummySceneFileWriter ) - testRenderer = _make_test_renderer_class(renderer_class) def real_test(): with frames_tester.testing(): - sceneTested = _make_test_scene_class( + scene_tested: type[Scene] = _make_test_scene_class( base_scene=base_scene, construct_test=construct, - # NOTE this is really ugly but it's due to the very bad design of the two renderers. - # If you pass a custom renderer to the Scene, the Camera class given as an argument in the Scene - # is not passed to the renderer. See __init__ of Scene. - # This potentially prevents OpenGL testing. - test_renderer=( - testRenderer(file_writer_class=file_writer_class) - if base_scene is not ThreeDScene - else testRenderer( - file_writer_class=file_writer_class, - camera_class=ThreeDCamera, - ) - ), # testRenderer(file_writer_class=file_writer_class), ) - scene_tested = sceneTested(skip_animations=True) - scene_tested.render() + manager = Manager(scene_tested) + manager.file_writer = file_writer_class( + manager.scene.get_default_scene_name() + ) + manager.render() if last_frame: - frames_tester.check_frame(-1, scene_tested.renderer.get_frame()) + frames_tester.check_frame(-1, manager.renderer.get_pixels()) return real_test diff --git a/poetry.lock b/poetry.lock index b60bd2a69b..fbabecfded 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -11,6 +11,17 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "4.4.0" @@ -184,60 +195,60 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "av" -version = "12.1.0" +version = "12.2.0" description = "Pythonic bindings for FFmpeg's libraries." optional = false python-versions = ">=3.8" files = [ - {file = "av-12.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0df2ad330ccf63ed8192d637306f13123cdf1c06717168d1de8b9a084d62f70"}, - {file = "av-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e66ad48dc3f618cf4a75cc14dd7e119d1151ff3c13b9b064014c79bad20df85"}, - {file = "av-12.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0e8fbbe3cffd04dcbfaf7f9e0469c8c9d3ae962728487aae0dbbac9ebb62567"}, - {file = "av-12.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c24d21b116e3af45e2f4b3a7ff1c96ae9a266bcde33a689ace0c52888e74d9"}, - {file = "av-12.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eff59d1eb0ba263e9efe8e460ca239c6ee2285f1b92c6b3c64f002c1b2ffd56"}, - {file = "av-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:09f8bd1fd124e389a266c770d209b5b4333f69c4b5a66b9aa2d09a561b0b54ab"}, - {file = "av-12.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e4c409639699d75e85a5b4b9fbb0538388bb009c8b426f7976b218731815e645"}, - {file = "av-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f624a61d8062bb7128a4b0af018ef5c7642acff2af7cea1bb6cc5aa663954b77"}, - {file = "av-12.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c61635e959dd50857f1ae3ad28984ce813688262672a5188376686dd293333"}, - {file = "av-12.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f8dcf20ecdfed62cb8b31790d3f394c76f05d5d58d5cc516f7b37c8608b78e2"}, - {file = "av-12.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebb11aba1ef2acb945713be5f4f7a359439230dc566243c354dddb2b06361367"}, - {file = "av-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a309994db77f632b606fe22c5bac03302e3dbe48d53c195abc435ccc56192746"}, - {file = "av-12.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:08401e59a9e33a42511d28cf1fdc570c31d3416426a2d73f4f4aaaaca5945c54"}, - {file = "av-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:efd45e3aa1e478ccbaafd84baf7d95d660b9cef30d850816129fd37d76813589"}, - {file = "av-12.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab553ce72c631477181d6c08c6e710afa44fa3452e61b82d9a75be07b1b2fef"}, - {file = "av-12.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:555f3240306ff02169ff209b152f97b071b57957868c3004c65e25c28130d593"}, - {file = "av-12.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07706499489f2047b54a4675dd04e2cf88322caef904b7b6eb03f480e682cf15"}, - {file = "av-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f669f5fb2515e9a4c9ee05b24ffbe3168d33c241bda93c84c8e384ca682a5cde"}, - {file = "av-12.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:876302ee793a457a03c4faa8281012671bb52dec843062bec59d6f0ae3735ba6"}, - {file = "av-12.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e6ad88e1e61e65c69d92ff1db8826686f913f147b427c99aa3202b027e766128"}, - {file = "av-12.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49a8f88b26d3d25140633a8ec48328a9467bbe001d01c54472394484cdb60b10"}, - {file = "av-12.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97873f344344b9b6aef786b22b57fb42c6eaa4ea0798d2020c5ed061f29ab3d6"}, - {file = "av-12.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf4c54354580abbea9390e23a471a346e9a4b4ca19c6929ad11a59d525e2ad3"}, - {file = "av-12.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:dc1a82e7d43495be6d34b50fd917989a72de7c3a7434d8ec72af0952c1ad4ea3"}, - {file = "av-12.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41d13494401bd3968255f7f9af2af203c30b684efc5a7ed92ebe9ec37f9f9264"}, - {file = "av-12.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc36f7b74e88db8e73fa69dc869331da74abc4f034ecd55f85f6232fcdddca60"}, - {file = "av-12.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff7a43ce921f2cc3c794810b148c4fa2cfd7ff10f4404072c94cf57b39b13d"}, - {file = "av-12.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce02915698d605c19c372314b7894033a451e838300d0a45c2708a550044e2d1"}, - {file = "av-12.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eadd5c7c374c9ff889a9116802cdda7ef9d574b623338f4045effc0f3f3c2cbc"}, - {file = "av-12.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f32893849fe34300f3cec51c4ae71c45b0acac448d36336d3452a5bb4f7e11bf"}, - {file = "av-12.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0a2a8693fdaa3bbb00255cda388f110f7a0b00817470a8cd8f1aa5c8dcbc3c9"}, - {file = "av-12.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:615f440856cbc5b96b8ae52c75ba722f082b898c3ab837eae024a06a0914e8a6"}, - {file = "av-12.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257fe519b0ffb4e900b737515137fb9ae0490edca7d70818b6c71c3cd79994ca"}, - {file = "av-12.1.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04afe8f9005bb42f95717bcfbb22a8950b4b942a862444edb1f0bab71ea702e9"}, - {file = "av-12.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63cbeaedc0184094b7d36bd4267cd61e6c69c18cb3464cc726ce6a8a438ac87a"}, - {file = "av-12.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0a0e056baa87037f932d12de3d3f258cbc4284d18d85099ccd845b333ac1bb91"}, - {file = "av-12.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7d549c2e6e9035022ea2280b781150a8c81acc4a03c69bde20b2f53262041a88"}, - {file = "av-12.1.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b1e02715cbb985b0efe6b6aaf134f9d1fee760822a07fd19e995a8e461909f4"}, - {file = "av-12.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b348264ba26152d7b06f2aaf0b2a11c90b13c628a447f6daa2a6770b9443fb0"}, - {file = "av-12.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6a3b3e4138cd1977f14e3d16c5f89979de8efa251d7558e2dc10a51cfcc0100"}, - {file = "av-12.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:105b017958eb5b6a128a5399200a4ec2b1040c2047e0b5f5e3714cd64fe7046e"}, - {file = "av-12.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:00596e53db3082193142e32fbdf47349724221de117645b0ed8fcaaec508adf4"}, - {file = "av-12.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed7c48d2d79961d70ea59f44fcff453bb2444a152793f80d2ceaa17af4331b9c"}, - {file = "av-12.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2c486adf83fc5b8e444efcc32f3eef27eefd6d0966ef68607d41205adcd8ec0"}, - {file = "av-12.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe9475dd2c8bea47338d5e90d6a45a28930d0fe3820ed2d3d09dfbb3316d476"}, - {file = "av-12.1.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0130a8391aa258eee60da3c09d69eb5c9480f14a9f1b1b5312336bac879edd2a"}, - {file = "av-12.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669f206cfdd5696d0edf2c81c5d220acc40b4153b71cf6662618c376e00b6d3a"}, - {file = "av-12.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e322533f585c2e8df07aa708c594fcb67f5f27a2f8b4107a7e6a6f90606190c7"}, - {file = "av-12.1.0.tar.gz", hash = "sha256:67adab9fdabcb8a86bd542787196580e38ed4132331ee9e82234b23cea9546b3"}, + {file = "av-12.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b05248d522b846338d149d887ac299e877f6a01b489b49385ca4790ea03be5fe"}, + {file = "av-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f1b30c26baf3e000bb5942d97c2d85ea692d8dd8847ba88fd43065ef5de71a3c"}, + {file = "av-12.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2df91ca71fbb1ad9823a843acf6c90ff00599520dbbd3fcc44cf267e8bdbd51e"}, + {file = "av-12.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5165110107765c70572e8c68bca39b1f495ea34e0aed5635ee5b3badd87f3699"}, + {file = "av-12.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eeff2e5872a0310d6061d0faa5c378c71f8b664924329c05c8a0de9ae700124"}, + {file = "av-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bceba179304744998a011c3fd8530091e75037f9665dbb59a0dd1d2dd42c794b"}, + {file = "av-12.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f74c9987798d45302b86e94b3bdb6009e9a509c57971b5599aae4353f5b8c5c0"}, + {file = "av-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d17e4110efe449842d12bc11166141b5ca08ce821b7529f02ed9dc3b73c9291"}, + {file = "av-12.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a7cc709c0c231f282f1b26a76b42d63100a8bc088522cfd06c500ed4b6c660c"}, + {file = "av-12.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:433589001c309d4ab655f0085b47bcab89e5ddeb6472993d9ef92e05eabbbe8e"}, + {file = "av-12.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:505955d5c2c9b7e0034fad7d0b57df6944adda9e42a606c9964ca5693c9b090d"}, + {file = "av-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:a9ca519ae6525a88637b73a1adae87235ac9a141a8d0b0c26a75cd4182c12a9f"}, + {file = "av-12.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f0026b52b9981b15d71f7f09ecd32587d9b99dd350588f8e17ce726662b466c5"}, + {file = "av-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e60a6da4eb5200900b37716dd971676a1dfb8731b53e8f49a3e630b772f5c97"}, + {file = "av-12.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d143d10ce726ced812eb0d4834841349027efd082c13d45a42f6914aae402ca6"}, + {file = "av-12.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbe1ace6ebaae1de41f3b0412367eec42188a381fae83669d4b18e1340648839"}, + {file = "av-12.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8280def29f209ce294b0aa5045a66c1027b8ba2ba5b82e1f66e8a910ff1c1120"}, + {file = "av-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:6740ae90ffd7fe6e7439beab6494c966cd46739943d3046ea01021d4bc2b36e6"}, + {file = "av-12.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f7248e1d75cf2a154146eae48b8aa18d4b2b4ae5e19d97148a7e494581f7d30"}, + {file = "av-12.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dfcd81555b60ab2868cc4e6ef2f0e5d6d0cee13ebe2d5be37d0fd7f10c4c7fc4"}, + {file = "av-12.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a3e4241f28e125d1a3b3496a0b4e26b8890c71b5e03081bbf3649a34bb9448e"}, + {file = "av-12.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99f2bfa082199e758bf4daea993c134a9109710c92e8f19cd644716577f1171b"}, + {file = "av-12.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0da53f16d7cf936bd631eb91afcf54f092484e08afe7e2bf492afafdf133472"}, + {file = "av-12.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:92a7541ade9bd562b598cc6ec17745dff2b4697a7e88bb47e51d22a248f610cb"}, + {file = "av-12.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f61b1d8335c065ba7f03f0bd95f35bef35317a0073a5ded21663f6c664b8127"}, + {file = "av-12.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9d88a567f17614605613d9905952baa4281988591a76fd270dc0a54eec6595c7"}, + {file = "av-12.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c263791697a5a60798a67fda10f4ba5f81dc9b541595f82bfdcc2566ee866e32"}, + {file = "av-12.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c9553e2a537d89123589f2b16b82e491968774c5426a11bb95762cd6a0aa384"}, + {file = "av-12.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e31bc61d1036be12400a35f8fd73fe274ef013f3e183e63a6a3786069b26bc"}, + {file = "av-12.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:f69b455952d26909cd350dba5967d2e1c2a3c6fa3129e04308fdc59c18e0a6dd"}, + {file = "av-12.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8a8184c226d887c5fe8512b93ae10838faeca4ee044a61ab4f2bbd69c29c16f1"}, + {file = "av-12.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:42b0e6fe67a449dd78d1e81dda177c0e1985cd76f539e46e819607b68c0a72f3"}, + {file = "av-12.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac681467309d292ac0e2364210861875167badfb2b567eb98a9bb5d6ba3d3438"}, + {file = "av-12.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1d8264a2096a84a5b23f0e43f25d4f976f34b6af2939d29c7334bd50f043eb8"}, + {file = "av-12.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78da95e8fd83ff113467807883cef039506ef4fb1a31106cf8ac056dfa9a3f"}, + {file = "av-12.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0586a5ed90ce4d010fc9884d3e2bac869cb4e0ee85a3ac1e26d5d01280446d87"}, + {file = "av-12.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cd41871196411c1290780f995b427a0dbfbc15ce7c53a729860beb4a29e77d6"}, + {file = "av-12.2.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b4d098eb97c5bcd2d8a8fe4d3ed8b2f33e8a0936372713185b5958831152329c"}, + {file = "av-12.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df2f3620d5ae95f06417a046673d8b3d31f21f38b8ba038c7fc5cbc0e33a158a"}, + {file = "av-12.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfcb66943386175d92700e459cb3295a1cf2c6ef033b83b43b42d904f3839834"}, + {file = "av-12.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d604718802b1c9644808f5f172d1cf1e45e9769ee7e5710de0afbc9f9f512f08"}, + {file = "av-12.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3b49726898439583cf7805fc78563ee491a700ab79f5aae4297da03d5f9ffa62"}, + {file = "av-12.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8eb2428d56cb5e690b4014e9c39fe8c9fbb909bb5585132327b8c467d1a59c72"}, + {file = "av-12.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:01eebcdfc9b98cec21d36e43e38fb34f970eec66b47fdec88cd434e1b8e69883"}, + {file = "av-12.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d913fa71dbb9436798ffbbfedec62db12f50c8483311dfcd9b76ffaaf7383955"}, + {file = "av-12.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9995c36a31102901f67f11306af3e972ea2b6290296fd35a6334ea0325bf20"}, + {file = "av-12.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29c835f4bb89cbb80571ab1e18ad344c4642148626dd133d27a95dbd1e94637b"}, + {file = "av-12.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:909ddd39a9c08d5599560b95bdf610043bc66e9c2943c8230de62c0cc9ac8152"}, + {file = "av-12.2.0.tar.gz", hash = "sha256:460670325bbd64b7a6774b6bda05985cbe4582d111f81993bfbb08058f8c0484"}, ] [[package]] @@ -341,13 +352,13 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -645,63 +656,63 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.5.3" +version = "7.6.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, - {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, - {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, - {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, - {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, - {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, - {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, - {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, - {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, - {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, - {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, - {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, - {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, - {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, + {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, + {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, + {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, + {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, + {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, + {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, + {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, + {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, + {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, + {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, + {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, + {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, ] [package.dependencies] @@ -875,33 +886,33 @@ files = [ [[package]] name = "debugpy" -version = "1.8.1" +version = "1.8.2" description = "An implementation of the Debug Adapter Protocol for Python" optional = true python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, - {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, - {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, - {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, - {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, - {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, - {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, - {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, - {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, - {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, - {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, - {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, - {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, - {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, - {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, - {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, - {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, - {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, - {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, - {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, - {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, - {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, + {file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"}, + {file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"}, + {file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"}, + {file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"}, + {file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"}, + {file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"}, + {file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"}, + {file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"}, + {file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"}, + {file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"}, + {file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"}, + {file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"}, + {file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"}, + {file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"}, + {file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"}, + {file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"}, + {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"}, + {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"}, + {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"}, + {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"}, + {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"}, + {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"}, ] [[package]] @@ -1023,13 +1034,13 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.15.3" +version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, - {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] @@ -1090,17 +1101,17 @@ test = ["pytest"] [[package]] name = "flake8-comprehensions" -version = "3.14.0" +version = "3.15.0" description = "A flake8 plugin to help you write better list/set/dict comprehensions." optional = false python-versions = ">=3.8" files = [ - {file = "flake8_comprehensions-3.14.0-py3-none-any.whl", hash = "sha256:7b9d07d94aa88e62099a6d1931ddf16c344d4157deedf90fe0d8ee2846f30e97"}, - {file = "flake8_comprehensions-3.14.0.tar.gz", hash = "sha256:81768c61bfc064e1a06222df08a2580d97de10cb388694becaf987c331c6c0cf"}, + {file = "flake8_comprehensions-3.15.0-py3-none-any.whl", hash = "sha256:b7e027bbb52be2ceb779ee12484cdeef52b0ad3c1fcb8846292bdb86d3034681"}, + {file = "flake8_comprehensions-3.15.0.tar.gz", hash = "sha256:923c22603e0310376a6b55b03efebdc09753c69f2d977755cba8bb73458a5d4d"}, ] [package.dependencies] -flake8 = ">=3.0,<3.2.0 || >3.2.0" +flake8 = ">=3,<3.2 || >3.2" [[package]] name = "flake8-docstrings" @@ -1178,53 +1189,53 @@ flake8 = ">=3.7" [[package]] name = "fonttools" -version = "4.53.0" +version = "4.53.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.53.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20"}, - {file = "fonttools-4.53.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d"}, - {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6"}, - {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5"}, - {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9"}, - {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca"}, - {file = "fonttools-4.53.0-cp310-cp310-win32.whl", hash = "sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068"}, - {file = "fonttools-4.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68"}, - {file = "fonttools-4.53.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec"}, - {file = "fonttools-4.53.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749"}, - {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161"}, - {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee"}, - {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6"}, - {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e"}, - {file = "fonttools-4.53.0-cp311-cp311-win32.whl", hash = "sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005"}, - {file = "fonttools-4.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796"}, - {file = "fonttools-4.53.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a"}, - {file = "fonttools-4.53.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4"}, - {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06"}, - {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d"}, - {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109"}, - {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2"}, - {file = "fonttools-4.53.0-cp312-cp312-win32.whl", hash = "sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea"}, - {file = "fonttools-4.53.0-cp312-cp312-win_amd64.whl", hash = "sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380"}, - {file = "fonttools-4.53.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12"}, - {file = "fonttools-4.53.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4"}, - {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce"}, - {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f"}, - {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206"}, - {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd"}, - {file = "fonttools-4.53.0-cp38-cp38-win32.whl", hash = "sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af"}, - {file = "fonttools-4.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b"}, - {file = "fonttools-4.53.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac"}, - {file = "fonttools-4.53.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d"}, - {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1"}, - {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f"}, - {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64"}, - {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0"}, - {file = "fonttools-4.53.0-cp39-cp39-win32.whl", hash = "sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9"}, - {file = "fonttools-4.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2"}, - {file = "fonttools-4.53.0-py3-none-any.whl", hash = "sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4"}, - {file = "fonttools-4.53.0.tar.gz", hash = "sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, + {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, + {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, + {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, + {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, + {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, + {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, + {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, + {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, + {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, + {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, + {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, + {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, ] [package.extras] @@ -1434,13 +1445,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.36" +version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, - {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, ] [package.extras] @@ -1470,13 +1481,13 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.2.0" +version = "8.0.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.2.0-py3-none-any.whl", hash = "sha256:04e4aad329b8b948a5711d394fa8759cb80f009225441b4f2a02bd4d8e5f426c"}, - {file = "importlib_metadata-7.2.0.tar.gz", hash = "sha256:3ff4519071ed42740522d494d04819b666541b9752c43012f85afb2cc220fcc6"}, + {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, + {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, ] [package.dependencies] @@ -1518,13 +1529,13 @@ files = [ [[package]] name = "ipykernel" -version = "6.29.4" +version = "6.29.5" description = "IPython Kernel for Jupyter" optional = true python-versions = ">=3.8" files = [ - {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, - {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, ] [package.dependencies] @@ -1688,13 +1699,13 @@ files = [ [[package]] name = "jsonschema" -version = "4.22.0" +version = "4.23.0" description = "An implementation of JSON Schema validation for Python" optional = true python-versions = ">=3.8" files = [ - {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, - {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, ] [package.dependencies] @@ -1709,11 +1720,11 @@ rfc3339-validator = {version = "*", optional = true, markers = "extra == \"forma rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} rpds-py = ">=0.7.1" uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=24.6.0", optional = true, markers = "extra == \"format-nongpl\""} [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] [[package]] name = "jsonschema-specifications" @@ -1869,13 +1880,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.2.2" +version = "4.2.3" description = "JupyterLab computational environment" optional = true python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.2.2-py3-none-any.whl", hash = "sha256:59ee9b839f43308c3dfd55d72d1f1a299ed42a7f91f2d1afe9c12a783f9e525f"}, - {file = "jupyterlab-4.2.2.tar.gz", hash = "sha256:a534b6a25719a92a40d514fb133a9fe8f0d9981b0bbce5d8a5fcaa33344a3038"}, + {file = "jupyterlab-4.2.3-py3-none-any.whl", hash = "sha256:0b59d11808e84bb84105c73364edfa867dd475492429ab34ea388a52f2e2e596"}, + {file = "jupyterlab-4.2.3.tar.gz", hash = "sha256:df6e46969ea51d66815167f23d92f105423b7f1f06fa604d4f44aeb018c82c7b"}, ] [package.dependencies] @@ -2254,40 +2265,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.0" +version = "3.9.1" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, - {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, - {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, - {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, - {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, - {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, - {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, - {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, - {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, - {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, - {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, - {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, - {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, - {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, - {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, - {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, - {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, - {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, - {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, - {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, - {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, - {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, - {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, - {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, - {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, + {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, + {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, + {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, + {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, + {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, + {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, + {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, + {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, + {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, ] [package.dependencies] @@ -2807,84 +2818,95 @@ ptyprocess = ">=0.5" [[package]] name = "pillow" -version = "10.3.0" +version = "10.4.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -3057,30 +3079,6 @@ files = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -[[package]] -name = "pycairo" -version = "1.26.1" -description = "Python interface for cairo" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycairo-1.26.1-cp310-cp310-win32.whl", hash = "sha256:b93b9e3072826a346f1f79cb1becc403d1ba4a3971cad61d144db0fe6dcb6be8"}, - {file = "pycairo-1.26.1-cp310-cp310-win_amd64.whl", hash = "sha256:acfc76924ed668d8fea50f6cc6097b9a57ef6cd3dc3f2fa20814380d639a6dd2"}, - {file = "pycairo-1.26.1-cp310-cp310-win_arm64.whl", hash = "sha256:067191315c3b4d09cad1ec57cdb8fc1d72e2574e89389c268a94f22d4fa98b5f"}, - {file = "pycairo-1.26.1-cp311-cp311-win32.whl", hash = "sha256:56a29623aa7b4adbde5024c61ff001455b5a3def79e512742ea45ab36c3fe24b"}, - {file = "pycairo-1.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:8d2889e03a095de5da9e68a589b691a3ada09d60ef18b5fc1b1b99f2a7794297"}, - {file = "pycairo-1.26.1-cp311-cp311-win_arm64.whl", hash = "sha256:7a307111de345304ed8eadd7f81ebd7fb1fc711224aa314a4e8e33af7dfa3d27"}, - {file = "pycairo-1.26.1-cp312-cp312-win32.whl", hash = "sha256:5cc1808e9e30ccd0f4d84ba7700db5aab5673f8b6b901760369ebb88a0823436"}, - {file = "pycairo-1.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:36131a726f568b2dbc5e78ff50fbaa379e69db00614d46d66b1e4289caf9b1ce"}, - {file = "pycairo-1.26.1-cp312-cp312-win_arm64.whl", hash = "sha256:5577b51543ea4c283c15f436d891e9eaf6fd43fe74882adb032fba2c271f3fe9"}, - {file = "pycairo-1.26.1-cp38-cp38-win32.whl", hash = "sha256:27ec7b42c58af35dc11352881262dce4254378b0f11be0959d1c13edb4539d2c"}, - {file = "pycairo-1.26.1-cp38-cp38-win_amd64.whl", hash = "sha256:27357994d277b3fd10a45e9ef58f80a4cb5e3291fe76c5edd58d2d06335eb8e7"}, - {file = "pycairo-1.26.1-cp39-cp39-win32.whl", hash = "sha256:e68300d1c2196d1d34de3432885ae9ff78e10426fa16f765742a11c6f8fe0a71"}, - {file = "pycairo-1.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:ce049930e294c29b53c68dcaab3df97cc5de7eb1d3d8e8a9f5c77e7164cd6e85"}, - {file = "pycairo-1.26.1-cp39-cp39-win_arm64.whl", hash = "sha256:22e1db531d4ed3167a98f0ea165bfa2a30df9d6eb22361c38158c031065999a4"}, - {file = "pycairo-1.26.1.tar.gz", hash = "sha256:a11b999ce55b798dbf13516ab038e0ce8b6ec299b208d7c4e767a6f7e68e8430"}, -] - [[package]] name = "pycodestyle" version = "2.11.1" @@ -3103,6 +3101,126 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pydantic" +version = "2.8.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pydocstyle" version = "6.3.0" @@ -3702,136 +3820,137 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.18.1" +version = "0.19.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = true python-versions = ">=3.8" files = [ - {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, - {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, - {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, - {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, - {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, - {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, - {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, - {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, - {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, - {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, - {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, - {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, - {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, + {file = "rpds_py-0.19.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4"}, + {file = "rpds_py-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582"}, + {file = "rpds_py-0.19.0-cp310-none-win32.whl", hash = "sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336"}, + {file = "rpds_py-0.19.0-cp310-none-win_amd64.whl", hash = "sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec"}, + {file = "rpds_py-0.19.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81"}, + {file = "rpds_py-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952"}, + {file = "rpds_py-0.19.0-cp311-none-win32.whl", hash = "sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf"}, + {file = "rpds_py-0.19.0-cp311-none-win_amd64.whl", hash = "sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67"}, + {file = "rpds_py-0.19.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68"}, + {file = "rpds_py-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248"}, + {file = "rpds_py-0.19.0-cp312-none-win32.whl", hash = "sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600"}, + {file = "rpds_py-0.19.0-cp312-none-win_amd64.whl", hash = "sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4"}, + {file = "rpds_py-0.19.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca"}, + {file = "rpds_py-0.19.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae"}, + {file = "rpds_py-0.19.0-cp38-none-win32.whl", hash = "sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4"}, + {file = "rpds_py-0.19.0-cp38-none-win_amd64.whl", hash = "sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1"}, + {file = "rpds_py-0.19.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c"}, + {file = "rpds_py-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be"}, + {file = "rpds_py-0.19.0-cp39-none-win32.whl", hash = "sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb"}, + {file = "rpds_py-0.19.0-cp39-none-win_amd64.whl", hash = "sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521"}, + {file = "rpds_py-0.19.0.tar.gz", hash = "sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9"}, ] [[package]] name = "ruff" -version = "0.4.10" +version = "0.5.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, - {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, - {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, - {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, - {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, - {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, + {file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"}, + {file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"}, + {file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"}, + {file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"}, + {file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"}, + {file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"}, + {file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"}, ] [[package]] @@ -3909,18 +4028,18 @@ win32 = ["pywin32"] [[package]] name = "setuptools" -version = "70.1.0" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = true python-versions = ">=3.8" files = [ - {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, - {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -4379,13 +4498,13 @@ files = [ [[package]] name = "types-docutils" -version = "0.21.0.20240423" +version = "0.21.0.20240711" description = "Typing stubs for docutils" optional = false python-versions = ">=3.8" files = [ - {file = "types-docutils-0.21.0.20240423.tar.gz", hash = "sha256:7716ec6c68b5179b7ba1738cace2f1326e64df9f44b7ab08d9904d32c23fc15f"}, - {file = "types_docutils-0.21.0.20240423-py3-none-any.whl", hash = "sha256:7f6e84ba8fcd2454c5b8bb8d77384d091a901929cc2b31079316e10eb346580a"}, + {file = "types-docutils-0.21.0.20240711.tar.gz", hash = "sha256:646f239e7afd88304995f444d3ea39639166c9445580ae5dcaf5515974c050ed"}, + {file = "types_docutils-0.21.0.20240711-py3-none-any.whl", hash = "sha256:149d822bd02d77954f4e04d8e87fcd039db2101dbf564a1cd1b0fabbd543277b"}, ] [[package]] @@ -4427,13 +4546,13 @@ files = [ [[package]] name = "types-setuptools" -version = "70.0.0.20240524" +version = "70.3.0.20240710" description = "Typing stubs for setuptools" optional = false python-versions = ">=3.8" files = [ - {file = "types-setuptools-70.0.0.20240524.tar.gz", hash = "sha256:e31fee7b9d15ef53980526579ac6089b3ae51a005a281acf97178e90ac71aff6"}, - {file = "types_setuptools-70.0.0.20240524-py3-none-any.whl", hash = "sha256:8f5379b9948682d72a9ab531fbe52932e84c4f38deda570255f9bae3edd766bc"}, + {file = "types-setuptools-70.3.0.20240710.tar.gz", hash = "sha256:842cbf399812d2b65042c9d6ff35113bbf282dee38794779aa1f94e597bafc35"}, + {file = "types_setuptools-70.3.0.20240710-py3-none-any.whl", hash = "sha256:bd0db2a4b9f2c49ac5564be4e0fb3125c4c46b1f73eafdcbceffa5b005cceca4"}, ] [[package]] @@ -4480,13 +4599,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.2" +version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] [package.dependencies] @@ -4696,4 +4815,4 @@ jupyterlab = ["jupyterlab", "notebook"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "ece51446d00fbede0088ce9da767749704e5600c27fad0ad154005a1f0e89e0b" +content-hash = "bae9ec79173ee7ebe7a2a206b4d02c893bb961b615c6537ecf2206a843537acc" diff --git a/pyproject.toml b/pyproject.toml index abf53073f8..b7612b00e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ networkx = ">=2.6" notebook = { version = ">=6.0.0", optional = true } numpy = ">=1.26" Pillow = ">=9.1" -pycairo = ">=1.13,<2.0.0" pyopengl = "^3.1.6" pydub = ">=0.20.0" Pygments = ">=2.0.0" @@ -58,6 +57,7 @@ svgelements = ">=1.8.0" tqdm = ">=4.0.0" typing-extensions = ">=4.0.0" watchdog = ">=2.0.0" +pydantic = "^2.8.0" [tool.poetry.extras] jupyterlab = ["jupyterlab", "notebook"] @@ -103,6 +103,7 @@ types-Pygments = "^2.17.0.0" [tool.pytest.ini_options] markers = "slow: Mark the test as slow. Can be skipped with --skip_slow" addopts = "--no-cov-on-fail --cov=manim --cov-report xml --cov-report term -n auto --dist=loadfile --durations=0" +doctest_optionflags = "IGNORE_EXCEPTION_DETAIL" [tool.isort] profile = "black" diff --git a/tests/conftest.py b/tests/conftest.py index 5b9edb656a..740c2ecdcf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import pytest -from manim import config, tempconfig +import manim def pytest_addoption(parser): @@ -45,6 +45,20 @@ def pytest_collection_modifyitems(config, items): item.add_marker(slow_skip) +@pytest.fixture +def config(): + saved = manim.config.copy() + # we need to return the actual config so that tests + # using tempconfig pass + yield manim.config + manim.config.update(saved) + + +@pytest.fixture +def dry_run(config): + config.dry_run = True + + @pytest.fixture(scope="session") def python_version(): # use the same python executable as it is running currently @@ -63,9 +77,9 @@ def reset_cfg_file(): @pytest.fixture def using_opengl_renderer(): - """Standard fixture for running with opengl that makes tests use a standard_config.cfg with a temp dir.""" - with tempconfig({"renderer": "opengl"}): - yield - # as a special case needed to manually revert back to cairo - # due to side effects of setting the renderer - config.renderer = "cairo" + """Standard fixture for running with opengl that makes tests use a standard_config.cfg with a temp dir. + + .. warning:: + + As of experimental, this fixture is deprecated and should not be using + """ diff --git a/tests/helpers/graphical_units.py b/tests/helpers/graphical_units.py index 75fb00343a..0bb6d3bcd3 100644 --- a/tests/helpers/graphical_units.py +++ b/tests/helpers/graphical_units.py @@ -7,11 +7,11 @@ import numpy as np -from manim import config, logger +from manim import Manager, logger from manim.scene.scene import Scene -def set_test_scene(scene_object: type[Scene], module_name: str): +def set_test_scene(scene_object: type[Scene], module_name: str, config): """Function used to set up the test data for a new feature. This will basically set up a pre-rendered frame for a scene. This is meant to be used only when setting up tests. Please refer to the wiki. @@ -29,29 +29,29 @@ def set_test_scene(scene_object: type[Scene], module_name: str): set_test_scene(DotTest, "geometry") """ - config["write_to_movie"] = False - config["disable_caching"] = True - config["format"] = "png" - config["pixel_height"] = 480 - config["pixel_width"] = 854 - config["frame_rate"] = 15 + config.write_to_movie = False + config.disable_caching = True + config.format = "png" + config.pixel_height = 480 + config.pixel_width = 854 + config.frame_rate = 15 with tempfile.TemporaryDirectory() as tmpdir: temp_path = Path(tmpdir) config["text_dir"] = temp_path / "text" config["tex_dir"] = temp_path / "tex" - scene = scene_object(skip_animations=True) - scene.render() - data = scene.renderer.get_frame() + manager = Manager(scene_object) + manager.render() + data = manager.renderer.get_pixels() assert not np.all( data == np.array([0, 0, 0, 255]), - ), f"Control data generated for {str(scene)} only contains empty pixels." + ), f"Control data generated for {manager.scene!s} only contains empty pixels." assert data.shape == (480, 854, 4) tests_directory = Path(__file__).absolute().parent.parent path_control_data = Path(tests_directory) / "control_data" / "graphical_units_data" path = Path(path_control_data) / module_name if not path.is_dir(): path.mkdir(parents=True) - np.savez_compressed(path / str(scene), frame_data=data) - logger.info(f"Test data for {str(scene)} saved in {path}\n") + np.savez_compressed(path / str(manager.scene), frame_data=data) + logger.info(f"Test data for {str(manager.scene)} saved in {path}\n") diff --git a/tests/module/animation/test_animation.py b/tests/module/animation/test_animation.py index 67579c4fbf..abe8340294 100644 --- a/tests/module/animation/test_animation.py +++ b/tests/module/animation/test_animation.py @@ -2,7 +2,7 @@ import pytest -from manim import FadeIn, Scene, config +from manim import FadeIn, Manager, Scene @pytest.mark.parametrize( @@ -10,13 +10,15 @@ [0, -1], ) def test_animation_forbidden_run_time(run_time): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene with pytest.raises(ValueError, match="Please set the run_time to be positive"): test_scene.play(FadeIn(None, run_time=run_time)) -def test_animation_run_time_shorter_than_frame_rate(caplog): - test_scene = Scene() +def test_animation_run_time_shorter_than_frame_rate(caplog, config): + manager = Manager(Scene) + test_scene = manager.scene test_scene.play(FadeIn(None, run_time=1 / (config.frame_rate + 1))) assert ( "Original run time of FadeIn(Mobject) is shorter than current frame rate" @@ -26,7 +28,8 @@ def test_animation_run_time_shorter_than_frame_rate(caplog): @pytest.mark.parametrize("frozen_frame", [False, True]) def test_wait_run_time_shorter_than_frame_rate(caplog, frozen_frame): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene test_scene.wait(1e-9, frozen_frame=frozen_frame) assert ( "Original run time of Wait(Mobject) is shorter than current frame rate" diff --git a/tests/module/animation/test_composition.py b/tests/module/animation/test_composition.py index 89eb22658b..e62029c59a 100644 --- a/tests/module/animation/test_composition.py +++ b/tests/module/animation/test_composition.py @@ -4,15 +4,24 @@ import pytest -from manim.animation.animation import Animation, Wait -from manim.animation.composition import AnimationGroup, Succession -from manim.animation.creation import Create, Write -from manim.animation.fading import FadeIn, FadeOut -from manim.constants import DOWN, UP -from manim.mobject.geometry.arc import Circle -from manim.mobject.geometry.line import Line -from manim.mobject.geometry.polygram import RegularPolygon, Square -from manim.scene.scene import Scene +from manim import ( + DOWN, + UP, + Animation, + AnimationGroup, + Circle, + Create, + FadeIn, + FadeOut, + Line, + Manager, + RegularPolygon, + Scene, + Square, + Succession, + Wait, + Write, +) def test_succession_timing(): @@ -22,7 +31,6 @@ def test_succession_timing(): animation_4s = FadeOut(line, shift=DOWN, run_time=4.0) succession = Succession(animation_1s, animation_4s) assert succession.get_run_time() == 5.0 - succession._setup_scene(MagicMock()) succession.begin() assert succession.active_index == 0 # The first animation takes 20% of the total run time. @@ -138,7 +146,8 @@ def test_animationgroup_with_wait(): def test_animationgroup_is_passing_remover_to_animations( animation_remover, animation_group_remover ): - scene = Scene() + manager = Manager(Scene) + scene = manager.scene sqr_animation = Create(Square(), remover=animation_remover) circ_animation = Write(Circle(), remover=animation_remover) animation_group = AnimationGroup( @@ -153,7 +162,8 @@ def test_animationgroup_is_passing_remover_to_animations( def test_animationgroup_is_passing_remover_to_nested_animationgroups(): - scene = Scene() + manager = Manager(Scene) + scene = manager.scene sqr_animation = Create(Square()) circ_animation = Write(Circle(), remover=True) polygon_animation = Create(RegularPolygon(5)) diff --git a/tests/module/animation/test_creation.py b/tests/module/animation/test_creation.py index 3208ad1b41..d2a0b08666 100644 --- a/tests/module/animation/test_creation.py +++ b/tests/module/animation/test_creation.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from manim import AddTextLetterByLetter, Text, config +from manim import AddTextLetterByLetter, Text def test_non_empty_text_creation(): @@ -25,7 +25,7 @@ def test_whitespace_text_creation(): AddTextLetterByLetter(Text(" ")) -def test_run_time_for_non_empty_text(): +def test_run_time_for_non_empty_text(config): """Ensure the run_time is calculated correctly for non-empty text.""" s = Text("Hello") run_time_per_char = 0.1 diff --git a/tests/module/mobject/mobject/test_copy.py b/tests/module/mobject/mobject/test_copy.py index ce9f472f7e..39c6afd94c 100644 --- a/tests/module/mobject/mobject/test_copy.py +++ b/tests/module/mobject/mobject/test_copy.py @@ -2,7 +2,7 @@ from pathlib import Path -from manim import BraceLabel, Mobject, config +from manim import BraceLabel, Mobject def test_mobject_copy(): @@ -18,7 +18,7 @@ def test_mobject_copy(): assert orig.submobjects[i] is not copy.submobjects[i] -def test_bracelabel_copy(tmp_path): +def test_bracelabel_copy(tmp_path, config): """Test that a copy is a deepcopy.""" # For this test to work, we need to tweak some folders temporarily original_text_dir = config["text_dir"] diff --git a/tests/module/mobject/mobject/test_opengl_metaclass.py b/tests/module/mobject/mobject/test_opengl_metaclass.py index bc172bfa38..930a5616da 100644 --- a/tests/module/mobject/mobject/test_opengl_metaclass.py +++ b/tests/module/mobject/mobject/test_opengl_metaclass.py @@ -1,20 +1,20 @@ from __future__ import annotations -from manim import Mobject, config, tempconfig +from manim import Mobject from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from manim.mobject.opengl.opengl_mobject import OpenGLMobject -def test_metaclass_registry(): +def test_metaclass_registry(config): class SomeTestMobject(Mobject, metaclass=ConvertToOpenGL): pass assert SomeTestMobject in ConvertToOpenGL._converted_classes - with tempconfig({"renderer": "opengl"}): - assert OpenGLMobject in SomeTestMobject.__bases__ - assert Mobject not in SomeTestMobject.__bases__ + config.renderer = "opengl" + assert OpenGLMobject in SomeTestMobject.__bases__ + assert Mobject not in SomeTestMobject.__bases__ - config.renderer = "cairo" - assert Mobject in SomeTestMobject.__bases__ - assert OpenGLMobject not in SomeTestMobject.__bases__ + config.renderer = "cairo" + assert Mobject in SomeTestMobject.__bases__ + assert OpenGLMobject not in SomeTestMobject.__bases__ diff --git a/tests/module/mobject/mobject/test_set_attr.py b/tests/module/mobject/mobject/test_set_attr.py index f9cdd40fe6..9edfb93bc4 100644 --- a/tests/module/mobject/mobject/test_set_attr.py +++ b/tests/module/mobject/mobject/test_set_attr.py @@ -2,13 +2,11 @@ import numpy as np -from manim import RendererType, config from manim.constants import RIGHT from manim.mobject.geometry.polygram import Square -def test_Data(): - config.renderer = RendererType.OPENGL +def test_Data(using_opengl_renderer): a = Square().move_to(RIGHT) data_bb = a.data["bounding_box"] np.testing.assert_array_equal( @@ -39,6 +37,3 @@ def test_Data(): ) np.testing.assert_array_equal(a.bounding_box, data_bb) - config.renderer = ( - RendererType.CAIRO - ) # needs to be here or else the following cairo tests fail diff --git a/tests/module/mobject/test_graph.py b/tests/module/mobject/test_graph.py index b23ccff622..fd75bde432 100644 --- a/tests/module/mobject/test_graph.py +++ b/tests/module/mobject/test_graph.py @@ -2,7 +2,7 @@ import pytest -from manim import DiGraph, Graph, Scene, Text, tempconfig +from manim import DiGraph, Graph, Manager, Scene, Text, tempconfig from manim.mobject.graph import _layouts @@ -93,7 +93,8 @@ def test_graph_remove_edges(): def test_custom_animation_mobject_list(): G = Graph([1, 2, 3], [(1, 2), (2, 3)]) - scene = Scene() + manager = Manager(Scene) + scene = manager.scene scene.add(G) assert scene.mobjects == [G] with tempconfig({"dry_run": True, "quality": "low_quality"}): diff --git a/tests/module/mobject/text/test_texmobject.py b/tests/module/mobject/text/test_texmobject.py index c8d4f51f84..52cc1554cc 100644 --- a/tests/module/mobject/text/test_texmobject.py +++ b/tests/module/mobject/text/test_texmobject.py @@ -5,15 +5,15 @@ import numpy as np import pytest -from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, config, tempconfig +from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, tempconfig -def test_MathTex(): +def test_MathTex(config): MathTex("a^2 + b^2 = c^2") assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists() -def test_SingleStringMathTex(): +def test_SingleStringMathTex(config): SingleStringMathTex("test") assert Path(config.media_dir, "Tex", "8ce17c7f5013209f.svg").exists() @@ -27,7 +27,7 @@ def test_double_braces_testing(text_input, length_sub): assert len(t1.submobjects) == length_sub -def test_tex(): +def test_tex(config): Tex("The horse does not eat cucumber salad.") assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists() @@ -45,7 +45,7 @@ def test_tex_temp_directory(tmpdir, monkeypatch): assert Path("media", "Tex", "c3945e23e546c95a.svg").exists() -def test_percent_char_rendering(): +def test_percent_char_rendering(config): Tex(r"\%") assert Path(config.media_dir, "Tex", "4a583af4d19a3adf.tex").exists() @@ -194,7 +194,7 @@ def test_error_in_nested_context(capsys): \end{align} """ - with pytest.raises(ValueError) as err: + with pytest.raises(ValueError): Tex(invalid_tex) stdout = str(capsys.readouterr().out) @@ -202,25 +202,25 @@ def test_error_in_nested_context(capsys): assert r"\begin{frame}" not in stdout -def test_tempconfig_resetting_tex_template(): +def test_tempconfig_resetting_tex_template(config): my_template = TexTemplate() my_template.preamble = "Custom preamble!" - tex_template_config_value = config.tex_template with tempconfig({"tex_template": my_template}): assert config.tex_template.preamble == "Custom preamble!" assert config.tex_template.preamble != "Custom preamble!" -def test_tex_garbage_collection(tmpdir, monkeypatch): +def test_tex_garbage_collection(tmpdir, monkeypatch, config): monkeypatch.chdir(tmpdir) Path(tmpdir, "media").mkdir() + config.media_dir = "media" - with tempconfig({"media_dir": "media"}): - tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex - assert Path("media", "Tex", "d771330b76d29ffb.tex").exists() - assert not Path("media", "Tex", "d771330b76d29ffb.log").exists() + tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex + assert Path("media", "Tex", "d771330b76d29ffb.tex").exists() + assert not Path("media", "Tex", "d771330b76d29ffb.log").exists() + + config.no_latex_cleanup = True - with tempconfig({"media_dir": "media", "no_latex_cleanup": True}): - tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex - assert Path("media", "Tex", "da27670a37b08799.log").exists() + tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex + assert Path("media", "Tex", "da27670a37b08799.log").exists() diff --git a/tests/module/scene/test_auto_zoom.py b/tests/module/scene/test_auto_zoom.py index 76c17231fe..4e7e849c5e 100644 --- a/tests/module/scene/test_auto_zoom.py +++ b/tests/module/scene/test_auto_zoom.py @@ -10,7 +10,8 @@ def test_zoom(): s2.set_x(10) with tempconfig({"dry_run": True, "quality": "low_quality"}): - scene = MovingCameraScene() + manager = Manager(MovingCameraScene) + scene = manager.scene scene.add(s1, s2) scene.play(scene.camera.auto_zoom([s1, s2])) diff --git a/tests/module/scene/test_scene.py b/tests/module/scene/test_scene.py index 0790b7ece6..998f909ded 100644 --- a/tests/module/scene/test_scene.py +++ b/tests/module/scene/test_scene.py @@ -4,104 +4,105 @@ import pytest -from manim import Circle, FadeIn, Group, Mobject, Scene, Square, tempconfig +from manim import Circle, FadeIn, Group, Manager, Mobject, Scene, Square from manim.animation.animation import Wait -def test_scene_add_remove(): - with tempconfig({"dry_run": True}): - scene = Scene() - assert len(scene.mobjects) == 0 - scene.add(Mobject()) - assert len(scene.mobjects) == 1 - scene.add(*(Mobject() for _ in range(10))) - assert len(scene.mobjects) == 11 - - # Check that adding a mobject twice does not actually add it twice - repeated = Mobject() - scene.add(repeated) - assert len(scene.mobjects) == 12 - scene.add(repeated) - assert len(scene.mobjects) == 12 - - # Check that Scene.add() returns the Scene (for chained calls) - assert scene.add(Mobject()) is scene - to_remove = Mobject() - scene = Scene() - scene.add(to_remove) - scene.add(*(Mobject() for _ in range(10))) - assert len(scene.mobjects) == 11 - scene.remove(to_remove) - assert len(scene.mobjects) == 10 - scene.remove(to_remove) - assert len(scene.mobjects) == 10 - - # Check that Scene.remove() returns the instance (for chained calls) - assert scene.add(Mobject()) is scene - - -def test_scene_time(): - with tempconfig({"dry_run": True}): - scene = Scene() - assert scene.renderer.time == 0 - scene.wait(2) - assert scene.renderer.time == 2 - scene.play(FadeIn(Circle()), run_time=0.5) - assert pytest.approx(scene.renderer.time) == 2.5 - scene.renderer._original_skipping_status = True - scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped. - assert pytest.approx(scene.renderer.time) == 7.5 - - -def test_subcaption(): - with tempconfig({"dry_run": True}): - scene = Scene() - scene.add_subcaption("Testing add_subcaption", duration=1, offset=0) - scene.wait() - scene.play( - Wait(), - run_time=2, - subcaption="Testing Scene.play subcaption interface", - subcaption_duration=1.5, - subcaption_offset=0.5, - ) - subcaptions = scene.renderer.file_writer.subcaptions - assert len(subcaptions) == 2 - assert subcaptions[0].start == datetime.timedelta(seconds=0) - assert subcaptions[0].end == datetime.timedelta(seconds=1) - assert subcaptions[0].content == "Testing add_subcaption" - assert subcaptions[1].start == datetime.timedelta(seconds=1.5) - assert subcaptions[1].end == datetime.timedelta(seconds=3) - assert subcaptions[1].content == "Testing Scene.play subcaption interface" - - -def test_replace(): +def test_scene_add_remove(dry_run): + manager = Manager(Scene) + scene = manager.scene + assert len(scene.mobjects) == 0 + scene.add(Mobject()) + assert len(scene.mobjects) == 1 + scene.add(*(Mobject() for _ in range(10))) + assert len(scene.mobjects) == 11 + + # Check that adding a mobject twice does not actually add it twice + repeated = Mobject() + scene.add(repeated) + assert len(scene.mobjects) == 12 + scene.add(repeated) + assert len(scene.mobjects) == 12 + + # Check that Scene.add() returns the Scene (for chained calls) + assert scene.add(Mobject()) is scene + to_remove = Mobject() + manager = Manager(Scene) + scene = manager.scene + scene.add(to_remove) + scene.add(*(Mobject() for _ in range(10))) + assert len(scene.mobjects) == 11 + scene.remove(to_remove) + assert len(scene.mobjects) == 10 + scene.remove(to_remove) + assert len(scene.mobjects) == 10 + + # Check that Scene.remove() returns the instance (for chained calls) + assert scene.add(Mobject()) is scene + + +def test_scene_time(dry_run): + manager = Manager(Scene) + scene = manager.scene + assert scene.renderer.time == 0 + scene.wait(2) + assert scene.renderer.time == 2 + scene.play(FadeIn(Circle()), run_time=0.5) + assert pytest.approx(scene.renderer.time) == 2.5 + scene.renderer._original_skipping_status = True + scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped. + assert pytest.approx(scene.renderer.time) == 7.5 + + +def test_subcaption(dry_run): + manager = Manager(Scene) + scene = manager.scene + scene.add_subcaption("Testing add_subcaption", duration=1, offset=0) + scene.wait() + scene.play( + Wait(), + run_time=2, + subcaption="Testing Scene.play subcaption interface", + subcaption_duration=1.5, + subcaption_offset=0.5, + ) + subcaptions = scene.renderer.file_writer.subcaptions + assert len(subcaptions) == 2 + assert subcaptions[0].start == datetime.timedelta(seconds=0) + assert subcaptions[0].end == datetime.timedelta(seconds=1) + assert subcaptions[0].content == "Testing add_subcaption" + assert subcaptions[1].start == datetime.timedelta(seconds=1.5) + assert subcaptions[1].end == datetime.timedelta(seconds=3) + assert subcaptions[1].content == "Testing Scene.play subcaption interface" + + +def test_replace(dry_run): def assert_names(mobjs, names): assert len(mobjs) == len(names) for i in range(0, len(mobjs)): assert mobjs[i].name == names[i] - with tempconfig({"dry_run": True}): - scene = Scene() + manager = Manager(Scene) + scene = manager.scene - first = Mobject(name="first") - second = Mobject(name="second") - third = Mobject(name="third") - fourth = Mobject(name="fourth") + first = Mobject(name="first") + second = Mobject(name="second") + third = Mobject(name="third") + fourth = Mobject(name="fourth") - scene.add(first) - scene.add(Group(second, third, name="group")) - scene.add(fourth) - assert_names(scene.mobjects, ["first", "group", "fourth"]) - assert_names(scene.mobjects[1], ["second", "third"]) + scene.add(first) + scene.add(Group(second, third, name="group")) + scene.add(fourth) + assert_names(scene.mobjects, ["first", "group", "fourth"]) + assert_names(scene.mobjects[1], ["second", "third"]) - alpha = Mobject(name="alpha") - beta = Mobject(name="beta") + alpha = Mobject(name="alpha") + beta = Mobject(name="beta") - scene.replace(first, alpha) - assert_names(scene.mobjects, ["alpha", "group", "fourth"]) - assert_names(scene.mobjects[1], ["second", "third"]) + scene.replace(first, alpha) + assert_names(scene.mobjects, ["alpha", "group", "fourth"]) + assert_names(scene.mobjects[1], ["second", "third"]) - scene.replace(second, beta) - assert_names(scene.mobjects, ["alpha", "group", "fourth"]) - assert_names(scene.mobjects[1], ["beta", "third"]) + scene.replace(second, beta) + assert_names(scene.mobjects, ["alpha", "group", "fourth"]) + assert_names(scene.mobjects[1], ["beta", "third"]) diff --git a/tests/module/scene/test_sound.py b/tests/module/scene/test_sound.py index cfd67ed2df..10383eb203 100644 --- a/tests/module/scene/test_sound.py +++ b/tests/module/scene/test_sound.py @@ -4,7 +4,7 @@ import wave from pathlib import Path -from manim import Scene +from manim import Manager, Scene def test_add_sound(tmpdir): @@ -19,5 +19,6 @@ def test_add_sound(tmpdir): f.close() - scene = Scene() + manager = Manager(Scene) + scene = manager.scene scene.add_sound(sound_loc) diff --git a/tests/module/scene/test_threed_scene.py b/tests/module/scene/test_threed_scene.py index 24f6f26330..7e055b65cc 100644 --- a/tests/module/scene/test_threed_scene.py +++ b/tests/module/scene/test_threed_scene.py @@ -1,8 +1,9 @@ -from manim import Circle, Square, ThreeDScene +from manim import Circle, Manager, Square, ThreeDScene def test_fixed_mobjects(): - scene = ThreeDScene() + manager = Manager(ThreeDScene) + scene = manager.scene s = Square() c = Circle() scene.add_fixed_in_frame_mobjects(s, c) diff --git a/tests/module/utils/test_bezier.py b/tests/module/utils/test_bezier.py index 7e1351c961..e7e02a89ee 100644 --- a/tests/module/utils/test_bezier.py +++ b/tests/module/utils/test_bezier.py @@ -5,8 +5,11 @@ from _split_matrices import SPLIT_MATRICES from _subdivision_matrices import SUBDIVISION_MATRICES +from manim.typing import ManimFloat from manim.utils.bezier import ( _get_subdivision_matrix, + get_quadratic_approximation_of_cubic, + get_smooth_cubic_bezier_handle_points, partial_bezier_points, split_bezier, subdivide_bezier, @@ -95,3 +98,120 @@ def test_subdivide_bezier() -> None: subdivide_bezier(points, n_divisions), subdivision_matrix @ points, ) + + +def test_get_smooth_cubic_bezier_handle_points() -> None: + """Test that :func:`.get_smooth_cubic_bezier_handle_points` returns the + correct handles, both for open and closed Bézier splines. + """ + open_curve_corners = np.array( + [ + [1, 1, 0], + [-1, 1, 1], + [-1, -1, 2], + [1, -1, 1], + ], + dtype=ManimFloat, + ) + h1, h2 = get_smooth_cubic_bezier_handle_points(open_curve_corners) + assert np.allclose( + h1, + np.array( + [ + [1 / 5, 11 / 9, 13 / 45], + [-7 / 5, 5 / 9, 64 / 45], + [-3 / 5, -13 / 9, 91 / 45], + ] + ), + ) + assert np.allclose( + h2, + np.array( + [ + [-3 / 5, 13 / 9, 26 / 45], + [-7 / 5, -5 / 9, 89 / 45], + [1 / 5, -11 / 9, 68 / 45], + ] + ), + ) + + closed_curve_corners = np.array( + [ + [1, 1, 0], + [-1, 1, 1], + [-1, -1, 2], + [1, -1, 1], + [1, 1, 0], + ], + dtype=ManimFloat, + ) + h1, h2 = get_smooth_cubic_bezier_handle_points(closed_curve_corners) + assert np.allclose( + h1, + np.array( + [ + [1 / 2, 3 / 2, 0], + [-3 / 2, 1 / 2, 3 / 2], + [-1 / 2, -3 / 2, 2], + [3 / 2, -1 / 2, 1 / 2], + ] + ), + ) + assert np.allclose( + h2, + np.array( + [ + [-1 / 2, 3 / 2, 1 / 2], + [-3 / 2, -1 / 2, 2], + [1 / 2, -3 / 2, 3 / 2], + [3 / 2, 1 / 2, 0], + ] + ), + ) + + +def test_get_quadratic_approximation_of_cubic() -> None: + C = np.array( + [ + [-5, 2, 0], + [-4, 2, 0], + [-3, 2, 0], + [-2, 2, 0], + [-2, 2, 0], + [-7 / 3, 4 / 3, 0], + [-8 / 3, 2 / 3, 0], + [-3, 0, 0], + [-3, 0, 0], + [-1 / 3, -1, 0], + [7 / 3, -2, 0], + [5, -3, 0], + ] + ) + a0, h0, h1, a1 = C[::4], C[1::4], C[2::4], C[3::4] + + Q = get_quadratic_approximation_of_cubic(a0, h0, h1, a1) + assert np.allclose( + Q, + np.array( + [ + [-5, 2, 0], + [-17 / 4, 2, 0], + [-7 / 2, 2, 0], + [-7 / 2, 2, 0], + [-11 / 4, 2, 0], + [-2, 2, 0], + [-2, 2, 0], + [-9 / 4, 3 / 2, 0], + [-5 / 2, 1, 0], + [-5 / 2, 1, 0], + [-11 / 4, 1 / 2, 0], + [-3, 0, 0], + [-3, 0, 0], + [-1, -3 / 4, 0], + [1, -3 / 2, 0], + [1, -3 / 2, 0], + [3, -9 / 4, 0], + [5, -3, 0], + ] + ), + ) diff --git a/tests/module/utils/test_color.py b/tests/module/utils/test_color.py index c3d468328b..a2e442d696 100644 --- a/tests/module/utils/test_color.py +++ b/tests/module/utils/test_color.py @@ -2,7 +2,7 @@ import numpy as np -from manim import BLACK, Mobject, Scene, VMobject +from manim import BLACK, Manager, Mobject, Scene, VMobject def test_import_color(): @@ -12,7 +12,8 @@ def test_import_color(): def test_background_color(): - S = Scene() + manager = Manager(Scene) + S = manager.scene S.camera.background_color = "#ff0000" S.renderer.update_frame(S) np.testing.assert_array_equal( diff --git a/tests/module/utils/test_units.py b/tests/module/utils/test_units.py index 6c2cba1ac1..5f2995e626 100644 --- a/tests/module/utils/test_units.py +++ b/tests/module/utils/test_units.py @@ -3,13 +3,13 @@ import numpy as np import pytest -from manim import PI, X_AXIS, Y_AXIS, Z_AXIS, config +from manim import PI, X_AXIS, Y_AXIS, Z_AXIS from manim.utils.unit import Degrees, Munits, Percent, Pixels -def test_units(): +def test_units(config): # make sure we are using the right frame geometry - assert config.pixel_width == 1920 + config.pixel_width = 1920 np.testing.assert_allclose(config.frame_height, 8.0) diff --git a/tests/opengl/__init__.py b/tests/opengl/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/opengl/test_animate_opengl.py b/tests/opengl/test_animate_opengl.py deleted file mode 100644 index 1243fcb212..0000000000 --- a/tests/opengl/test_animate_opengl.py +++ /dev/null @@ -1,122 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pytest - -from manim.animation.creation import Uncreate -from manim.mobject.geometry.arc import Dot -from manim.mobject.geometry.line import Line -from manim.mobject.geometry.polygram import Square -from manim.mobject.mobject import override_animate -from manim.mobject.types.vectorized_mobject import VGroup - - -def test_simple_animate(using_opengl_renderer): - s = Square() - scale_factor = 2 - anim = s.animate.scale(scale_factor).build() - assert anim.mobject.target.width == scale_factor * s.width - - -def test_chained_animate(using_opengl_renderer): - s = Square() - scale_factor = 2 - direction = np.array((1, 1, 0)) - anim = s.animate.scale(scale_factor).shift(direction).build() - assert ( - anim.mobject.target.width == scale_factor * s.width - and (anim.mobject.target.get_center() == direction).all() - ) - - -def test_overridden_animate(using_opengl_renderer): - class DotsWithLine(VGroup): - def __init__(self): - super().__init__() - self.left_dot = Dot().shift((-1, 0, 0)) - self.right_dot = Dot().shift((1, 0, 0)) - self.line = Line(self.left_dot, self.right_dot) - self.add(self.left_dot, self.right_dot, self.line) - - def remove_line(self): - self.remove(self.line) - - @override_animate(remove_line) - def _remove_line_animation(self, anim_args=None): - if anim_args is None: - anim_args = {} - self.remove_line() - return Uncreate(self.line, **anim_args) - - dots_with_line = DotsWithLine() - anim = dots_with_line.animate.remove_line().build() - assert len(dots_with_line.submobjects) == 2 - assert type(anim) is Uncreate - - -def test_chaining_overridden_animate(using_opengl_renderer): - class DotsWithLine(VGroup): - def __init__(self): - super().__init__() - self.left_dot = Dot().shift((-1, 0, 0)) - self.right_dot = Dot().shift((1, 0, 0)) - self.line = Line(self.left_dot, self.right_dot) - self.add(self.left_dot, self.right_dot, self.line) - - def remove_line(self): - self.remove(self.line) - - @override_animate(remove_line) - def _remove_line_animation(self, anim_args=None): - if anim_args is None: - anim_args = {} - self.remove_line() - return Uncreate(self.line, **anim_args) - - with pytest.raises( - NotImplementedError, - match="not supported for overridden animations", - ): - DotsWithLine().animate.shift((1, 0, 0)).remove_line() - - with pytest.raises( - NotImplementedError, - match="not supported for overridden animations", - ): - DotsWithLine().animate.remove_line().shift((1, 0, 0)) - - -def test_animate_with_args(using_opengl_renderer): - s = Square() - scale_factor = 2 - run_time = 2 - - anim = s.animate(run_time=run_time).scale(scale_factor).build() - assert anim.mobject.target.width == scale_factor * s.width - assert anim.run_time == run_time - - -def test_chained_animate_with_args(using_opengl_renderer): - s = Square() - scale_factor = 2 - direction = np.array((1, 1, 0)) - run_time = 2 - - anim = s.animate(run_time=run_time).scale(scale_factor).shift(direction).build() - assert ( - anim.mobject.target.width == scale_factor * s.width - and (anim.mobject.target.get_center() == direction).all() - ) - assert anim.run_time == run_time - - -def test_animate_with_args_misplaced(using_opengl_renderer): - s = Square() - scale_factor = 2 - run_time = 2 - - with pytest.raises(ValueError, match="must be passed before"): - s.animate.scale(scale_factor)(run_time=run_time) - - with pytest.raises(ValueError, match="must be passed before"): - s.animate(run_time=run_time)(run_time=run_time).scale(scale_factor) diff --git a/tests/opengl/test_axes_shift_opengl.py b/tests/opengl/test_axes_shift_opengl.py deleted file mode 100644 index 1ae58afff4..0000000000 --- a/tests/opengl/test_axes_shift_opengl.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim.mobject.graphing.coordinate_systems import Axes - - -def test_axes_origin_shift(using_opengl_renderer): - ax = Axes(x_range=(5, 10, 1), y_range=(40, 45, 0.5)) - np.testing.assert_allclose( - ax.coords_to_point(5.0, 40.0), ax.x_axis.number_to_point(5) - ) - np.testing.assert_allclose( - ax.coords_to_point(5.0, 40.0), ax.y_axis.number_to_point(40) - ) diff --git a/tests/opengl/test_color_opengl.py b/tests/opengl/test_color_opengl.py deleted file mode 100644 index 3aeb2d6021..0000000000 --- a/tests/opengl/test_color_opengl.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import BLACK, BLUE, GREEN, PURE_BLUE, PURE_GREEN, PURE_RED, Scene -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject - - -def test_import_color(using_opengl_renderer): - import manim.utils.color as C - - C.WHITE - - -def test_background_color(using_opengl_renderer): - S = Scene() - S.renderer.background_color = "#FF0000" - S.renderer.update_frame(S) - np.testing.assert_array_equal( - S.renderer.get_frame()[0, 0], np.array([255, 0, 0, 255]) - ) - - S.renderer.background_color = "#436F80" - S.renderer.update_frame(S) - np.testing.assert_array_equal( - S.renderer.get_frame()[0, 0], np.array([67, 111, 128, 255]) - ) - - S.renderer.background_color = "#FFFFFF" - S.renderer.update_frame(S) - np.testing.assert_array_equal( - S.renderer.get_frame()[0, 0], np.array([255, 255, 255, 255]) - ) - - -def test_set_color(using_opengl_renderer): - m = OpenGLMobject() - assert m.color.to_hex() == "#FFFFFF" - np.all(m.rgbas == np.array((0.0, 0.0, 0.0, 1.0))) - - m.set_color(BLACK) - assert m.color.to_hex() == "#000000" - np.all(m.rgbas == np.array((1.0, 1.0, 1.0, 1.0))) - - m.set_color(PURE_GREEN, opacity=0.5) - assert m.color.to_hex() == "#00FF00" - np.all(m.rgbas == np.array((0.0, 1.0, 0.0, 0.5))) - - m = OpenGLVMobject() - assert m.color.to_hex() == "#FFFFFF" - np.all(m.fill_rgba == np.array((0.0, 0.0, 0.0, 1.0))) - np.all(m.stroke_rgba == np.array((0.0, 0.0, 0.0, 1.0))) - - m.set_color(BLACK) - assert m.color.to_hex() == "#000000" - np.all(m.fill_rgba == np.array((1.0, 1.0, 1.0, 1.0))) - np.all(m.stroke_rgba == np.array((1.0, 1.0, 1.0, 1.0))) - - m.set_color(PURE_GREEN, opacity=0.5) - assert m.color.to_hex() == "#00FF00" - np.all(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - np.all(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - -def test_set_fill_color(using_opengl_renderer): - m = OpenGLVMobject() - assert m.fill_color.to_hex() == "#FFFFFF" - np.all(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - m.set_fill(BLACK) - assert m.fill_color.to_hex() == "#000000" - np.all(m.fill_rgba == np.array((1.0, 1.0, 1.0, 1.0))) - - m.set_fill(PURE_GREEN, opacity=0.5) - assert m.fill_color.to_hex() == "#00FF00" - np.all(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - -def test_set_stroke_color(using_opengl_renderer): - m = OpenGLVMobject() - assert m.stroke_color.to_hex() == "#FFFFFF" - np.all(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - m.set_stroke(BLACK) - assert m.stroke_color.to_hex() == "#000000" - np.all(m.stroke_rgba == np.array((1.0, 1.0, 1.0, 1.0))) - - m.set_stroke(PURE_GREEN, opacity=0.5) - assert m.stroke_color.to_hex() == "#00FF00" - np.all(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - -def test_set_fill(using_opengl_renderer): - m = OpenGLMobject() - assert m.color.to_hex() == "#FFFFFF" - m.set_color(BLACK) - assert m.color.to_hex() == "#000000" - - m = OpenGLVMobject() - assert m.color.to_hex() == "#FFFFFF" - m.set_color(BLACK) - assert m.color.to_hex() == "#000000" - - -def test_set_color_handles_lists_of_strs(using_opengl_renderer): - m = OpenGLVMobject() - assert m.color.to_hex() == "#FFFFFF" - m.set_color([BLACK, BLUE, GREEN]) - assert m.get_colors()[0] == BLACK - assert m.get_colors()[1] == BLUE - assert m.get_colors()[2] == GREEN - - assert m.get_fill_colors()[0] == BLACK - assert m.get_fill_colors()[1] == BLUE - assert m.get_fill_colors()[2] == GREEN - - assert m.get_stroke_colors()[0] == BLACK - assert m.get_stroke_colors()[1] == BLUE - assert m.get_stroke_colors()[2] == GREEN - - -def test_set_color_handles_lists_of_color_objects(using_opengl_renderer): - m = OpenGLVMobject() - assert m.color.to_hex() == "#FFFFFF" - m.set_color([PURE_BLUE, PURE_GREEN, PURE_RED]) - assert m.get_colors()[0].to_hex() == "#0000FF" - assert m.get_colors()[1].to_hex() == "#00FF00" - assert m.get_colors()[2].to_hex() == "#FF0000" - - assert m.get_fill_colors()[0].to_hex() == "#0000FF" - assert m.get_fill_colors()[1].to_hex() == "#00FF00" - assert m.get_fill_colors()[2].to_hex() == "#FF0000" - - assert m.get_stroke_colors()[0].to_hex() == "#0000FF" - assert m.get_stroke_colors()[1].to_hex() == "#00FF00" - assert m.get_stroke_colors()[2].to_hex() == "#FF0000" - - -def test_set_fill_handles_lists_of_strs(using_opengl_renderer): - m = OpenGLVMobject() - assert m.fill_color.to_hex() == "#FFFFFF" - m.set_fill([BLACK.to_hex(), BLUE.to_hex(), GREEN.to_hex()]) - assert m.get_fill_colors()[0].to_hex() == BLACK.to_hex() - assert m.get_fill_colors()[1].to_hex() == BLUE.to_hex() - assert m.get_fill_colors()[2].to_hex() == GREEN.to_hex() - - -def test_set_fill_handles_lists_of_color_objects(using_opengl_renderer): - m = OpenGLVMobject() - assert m.fill_color.to_hex() == "#FFFFFF" - m.set_fill([PURE_BLUE, PURE_GREEN, PURE_RED]) - assert m.get_fill_colors()[0].to_hex() == "#0000FF" - assert m.get_fill_colors()[1].to_hex() == "#00FF00" - assert m.get_fill_colors()[2].to_hex() == "#FF0000" - - -def test_set_stroke_handles_lists_of_strs(using_opengl_renderer): - m = OpenGLVMobject() - assert m.stroke_color.to_hex() == "#FFFFFF" - m.set_stroke([BLACK.to_hex(), BLUE.to_hex(), GREEN.to_hex()]) - assert m.get_stroke_colors()[0].to_hex() == BLACK.to_hex() - assert m.get_stroke_colors()[1].to_hex() == BLUE.to_hex() - assert m.get_stroke_colors()[2].to_hex() == GREEN.to_hex() - - -def test_set_stroke_handles_lists_of_color_objects(using_opengl_renderer): - m = OpenGLVMobject() - assert m.stroke_color.to_hex() == "#FFFFFF" - m.set_stroke([PURE_BLUE, PURE_GREEN, PURE_RED]) - assert m.get_stroke_colors()[0].to_hex() == "#0000FF" - assert m.get_stroke_colors()[1].to_hex() == "#00FF00" - assert m.get_stroke_colors()[2].to_hex() == "#FF0000" diff --git a/tests/opengl/test_composition_opengl.py b/tests/opengl/test_composition_opengl.py deleted file mode 100644 index c09cd691a1..0000000000 --- a/tests/opengl/test_composition_opengl.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import annotations - -from unittest.mock import MagicMock - -from manim.animation.animation import Animation, Wait -from manim.animation.composition import AnimationGroup, Succession -from manim.animation.fading import FadeIn, FadeOut -from manim.constants import DOWN, UP -from manim.mobject.geometry.arc import Circle -from manim.mobject.geometry.line import Line -from manim.mobject.geometry.polygram import Square - - -def test_succession_timing(using_opengl_renderer): - """Test timing of animations in a succession.""" - line = Line() - animation_1s = FadeIn(line, shift=UP, run_time=1.0) - animation_4s = FadeOut(line, shift=DOWN, run_time=4.0) - succession = Succession(animation_1s, animation_4s) - assert succession.get_run_time() == 5.0 - succession._setup_scene(MagicMock()) - succession.begin() - assert succession.active_index == 0 - # The first animation takes 20% of the total run time. - succession.interpolate(0.199) - assert succession.active_index == 0 - succession.interpolate(0.2) - assert succession.active_index == 1 - succession.interpolate(0.8) - assert succession.active_index == 1 - # At 100% and more, no animation must be active anymore. - succession.interpolate(1.0) - assert succession.active_index == 2 - assert succession.active_animation is None - succession.interpolate(1.2) - assert succession.active_index == 2 - assert succession.active_animation is None - - -def test_succession_in_succession_timing(using_opengl_renderer): - """Test timing of nested successions.""" - line = Line() - animation_1s = FadeIn(line, shift=UP, run_time=1.0) - animation_4s = FadeOut(line, shift=DOWN, run_time=4.0) - nested_succession = Succession(animation_1s, animation_4s) - succession = Succession( - FadeIn(line, shift=UP, run_time=4.0), - nested_succession, - FadeIn(line, shift=UP, run_time=1.0), - ) - assert nested_succession.get_run_time() == 5.0 - assert succession.get_run_time() == 10.0 - succession._setup_scene(MagicMock()) - succession.begin() - succession.interpolate(0.1) - assert succession.active_index == 0 - # The nested succession must not be active yet, and as a result hasn't set active_animation yet. - assert not hasattr(nested_succession, "active_animation") - succession.interpolate(0.39) - assert succession.active_index == 0 - assert not hasattr(nested_succession, "active_animation") - # The nested succession starts at 40% of total run time - succession.interpolate(0.4) - assert succession.active_index == 1 - assert nested_succession.active_index == 0 - # The nested succession second animation starts at 50% of total run time. - succession.interpolate(0.49) - assert succession.active_index == 1 - assert nested_succession.active_index == 0 - succession.interpolate(0.5) - assert succession.active_index == 1 - assert nested_succession.active_index == 1 - # The last animation starts at 90% of total run time. The nested succession must be finished at that time. - succession.interpolate(0.89) - assert succession.active_index == 1 - assert nested_succession.active_index == 1 - succession.interpolate(0.9) - assert succession.active_index == 2 - assert nested_succession.active_index == 2 - assert nested_succession.active_animation is None - # After 100%, nothing must be playing anymore. - succession.interpolate(1.0) - assert succession.active_index == 3 - assert succession.active_animation is None - assert nested_succession.active_index == 2 - assert nested_succession.active_animation is None - - -def test_animationbuilder_in_group(using_opengl_renderer): - sqr = Square() - circ = Circle() - animation_group = AnimationGroup(sqr.animate.shift(DOWN).scale(2), FadeIn(circ)) - assert all(isinstance(anim, Animation) for anim in animation_group.animations) - succession = Succession(sqr.animate.shift(DOWN).scale(2), FadeIn(circ)) - assert all(isinstance(anim, Animation) for anim in succession.animations) - - -def test_animationgroup_with_wait(using_opengl_renderer): - sqr = Square() - sqr_anim = FadeIn(sqr) - wait = Wait() - animation_group = AnimationGroup(wait, sqr_anim, lag_ratio=1) - - animation_group.begin() - timings = animation_group.anims_with_timings - - assert timings.tolist() == [(wait, 0.0, 1.0), (sqr_anim, 1.0, 2.0)] diff --git a/tests/opengl/test_config_opengl.py b/tests/opengl/test_config_opengl.py deleted file mode 100644 index 378ba33558..0000000000 --- a/tests/opengl/test_config_opengl.py +++ /dev/null @@ -1,143 +0,0 @@ -from __future__ import annotations - -import tempfile -from pathlib import Path - -import numpy as np - -from manim import WHITE, Scene, Square, config, tempconfig - - -def test_tempconfig(using_opengl_renderer): - """Test the tempconfig context manager.""" - original = config.copy() - - with tempconfig({"frame_width": 100, "frame_height": 42}): - # check that config was modified correctly - assert config["frame_width"] == 100 - assert config["frame_height"] == 42 - - # check that no keys are missing and no new keys were added - assert set(original.keys()) == set(config.keys()) - - # check that the keys are still untouched - assert set(original.keys()) == set(config.keys()) - - # check that config is correctly restored - for k, v in original.items(): - if isinstance(v, np.ndarray): - np.testing.assert_allclose(config[k], v) - else: - assert config[k] == v - - -class MyScene(Scene): - def construct(self): - self.add(Square()) - self.wait(1) - - -def test_background_color(using_opengl_renderer): - """Test the 'background_color' config option.""" - with tempconfig({"background_color": WHITE, "verbosity": "ERROR", "dry_run": True}): - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() - np.testing.assert_allclose(frame[0, 0], [255, 255, 255, 255]) - - -def test_digest_file(using_opengl_renderer, tmp_path): - """Test that a config file can be digested programmatically.""" - with tempconfig({}): - tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) - tmp_cfg.write( - """ - [CLI] - media_dir = this_is_my_favorite_path - video_dir = {media_dir}/videos - frame_height = 10 - """, - ) - tmp_cfg.close() - config.digest_file(tmp_cfg.name) - - assert config.get_dir("media_dir") == Path("this_is_my_favorite_path") - assert config.get_dir("video_dir") == Path("this_is_my_favorite_path/videos") - - -def test_frame_size(using_opengl_renderer, tmp_path): - """Test that the frame size can be set via config file.""" - np.testing.assert_allclose( - config.aspect_ratio, config.pixel_width / config.pixel_height - ) - np.testing.assert_allclose(config.frame_height, 8.0) - - with tempconfig({}): - tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) - tmp_cfg.write( - """ - [CLI] - pixel_height = 10 - pixel_width = 10 - """, - ) - tmp_cfg.close() - config.digest_file(tmp_cfg.name) - - # aspect ratio is set using pixel measurements - np.testing.assert_allclose(config.aspect_ratio, 1.0) - # if not specified in the cfg file, frame_width is set using the aspect ratio - np.testing.assert_allclose(config.frame_height, 8.0) - np.testing.assert_allclose(config.frame_width, 8.0) - - with tempconfig({}): - tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) - tmp_cfg.write( - """ - [CLI] - pixel_height = 10 - pixel_width = 10 - frame_height = 10 - frame_width = 10 - """, - ) - tmp_cfg.close() - config.digest_file(tmp_cfg.name) - - np.testing.assert_allclose(config.aspect_ratio, 1.0) - # if both are specified in the cfg file, the aspect ratio is ignored - np.testing.assert_allclose(config.frame_height, 10.0) - np.testing.assert_allclose(config.frame_width, 10.0) - - -def test_temporary_dry_run(using_opengl_renderer): - """Test that tempconfig correctly restores after setting dry_run.""" - assert config["write_to_movie"] - assert not config["save_last_frame"] - - with tempconfig({"dry_run": True}): - assert not config["write_to_movie"] - assert not config["save_last_frame"] - - assert config["write_to_movie"] - assert not config["save_last_frame"] - - -def test_dry_run_with_png_format(using_opengl_renderer): - """Test that there are no exceptions when running a png without output""" - with tempconfig( - {"dry_run": True, "write_to_movie": False, "disable_caching": True} - ): - assert config["dry_run"] is True - scene = MyScene() - scene.render() - - -def test_dry_run_with_png_format_skipped_animations(using_opengl_renderer): - """Test that there are no exceptions when running a png without output and skipped animations""" - with tempconfig( - {"dry_run": True, "write_to_movie": False, "disable_caching": True} - ): - assert config["dry_run"] is True - scene = MyScene(skip_animations=True) - scene.render() diff --git a/tests/opengl/test_coordinate_system_opengl.py b/tests/opengl/test_coordinate_system_opengl.py deleted file mode 100644 index ed596d7e9d..0000000000 --- a/tests/opengl/test_coordinate_system_opengl.py +++ /dev/null @@ -1,140 +0,0 @@ -from __future__ import annotations - -import math - -import numpy as np -import pytest - -from manim import ( - LEFT, - ORIGIN, - PI, - UR, - Axes, - Circle, - ComplexPlane, - NumberPlane, - PolarPlane, - ThreeDAxes, - config, - tempconfig, -) -from manim import CoordinateSystem as CS - - -def test_initial_config(using_opengl_renderer): - """Check that all attributes are defined properly from the config.""" - cs = CS() - assert cs.x_range[0] == round(-config["frame_x_radius"]) - assert cs.x_range[1] == round(config["frame_x_radius"]) - assert cs.x_range[2] == 1.0 - assert cs.y_range[0] == round(-config["frame_y_radius"]) - assert cs.y_range[1] == round(config["frame_y_radius"]) - assert cs.y_range[2] == 1.0 - - ax = Axes() - np.testing.assert_allclose(ax.get_center(), ORIGIN) - np.testing.assert_allclose(ax.y_axis_config["label_direction"], LEFT) - - with tempconfig({"frame_x_radius": 100, "frame_y_radius": 200}): - cs = CS() - assert cs.x_range[0] == -100 - assert cs.x_range[1] == 100 - assert cs.y_range[0] == -200 - assert cs.y_range[1] == 200 - - -def test_dimension(using_opengl_renderer): - """Check that objects have the correct dimension.""" - assert Axes().dimension == 2 - assert NumberPlane().dimension == 2 - assert PolarPlane().dimension == 2 - assert ComplexPlane().dimension == 2 - assert ThreeDAxes().dimension == 3 - - -def test_abstract_base_class(using_opengl_renderer): - """Check that CoordinateSystem has some abstract methods.""" - with pytest.raises(Exception): - CS().get_axes() - - -@pytest.mark.skip( - reason="Causes conflicts with other tests due to axis_config changing default config", -) -def test_NumberPlane(using_opengl_renderer): - """Test that NumberPlane generates the correct number of lines when its ranges do not cross 0.""" - pos_x_range = (0, 7) - neg_x_range = (-7, 0) - - pos_y_range = (2, 6) - neg_y_range = (-6, -2) - - x_vals = [0, 1.5, 2, 2.8, 4, 6.25] - y_vals = [2, 5, 4.25, 6, 4.5, 2.75] - - testing_data = [ - (pos_x_range, pos_y_range, x_vals, y_vals), - (pos_x_range, neg_y_range, x_vals, [-v for v in y_vals]), - (neg_x_range, pos_y_range, [-v for v in x_vals], y_vals), - (neg_x_range, neg_y_range, [-v for v in x_vals], [-v for v in y_vals]), - ] - - for test_data in testing_data: - x_range, y_range, x_vals, y_vals = test_data - - x_start, x_end = x_range - y_start, y_end = y_range - - plane = NumberPlane( - x_range=x_range, - y_range=y_range, - # x_length = 7, - axis_config={"include_numbers": True}, - ) - - # normally these values would be need to be added by one to pass since there's an - # overlapping pair of lines at the origin, but since these planes do not cross 0, - # this is not needed. - num_y_lines = math.ceil(x_end - x_start) - num_x_lines = math.floor(y_end - y_start) - - assert len(plane.y_lines) == num_y_lines - assert len(plane.x_lines) == num_x_lines - - plane = NumberPlane((-5, 5, 0.5), (-8, 8, 2)) # <- test for different step values - assert len(plane.x_lines) == 8 - assert len(plane.y_lines) == 20 - - -def test_point_to_coords(using_opengl_renderer): - ax = Axes(x_range=[0, 10, 2]) - circ = Circle(radius=0.5).shift(UR * 2) - - # get the coordinates of the circle with respect to the axes - coords = np.around(ax.point_to_coords(circ.get_right()), decimals=4) - np.testing.assert_array_equal(coords, (7.0833, 2.6667)) - - -def test_coords_to_point(using_opengl_renderer): - ax = Axes() - - # a point with respect to the axes - c2p_coord = np.around(ax.coords_to_point(2, 2), decimals=4) - np.testing.assert_array_equal(c2p_coord, (1.7143, 1.5, 0)) - - -def test_input_to_graph_point(using_opengl_renderer): - ax = Axes() - curve = ax.plot(lambda x: np.cos(x)) - line_graph = ax.plot_line_graph([1, 3, 5], [-1, 2, -2], add_vertex_dots=False)[ - "line_graph" - ] - - # move a square to PI on the cosine curve. - position = np.around(ax.input_to_graph_point(x=PI, graph=curve), decimals=4) - np.testing.assert_array_equal(position, (2.6928, -0.75, 0)) - - # test the line_graph implementation - position = np.around(ax.input_to_graph_point(x=PI, graph=line_graph), decimals=4) - np.testing.assert_array_equal(position, (2.6928, 1.2876, 0)) diff --git a/tests/opengl/test_copy_opengl.py b/tests/opengl/test_copy_opengl.py deleted file mode 100644 index db2e06dba4..0000000000 --- a/tests/opengl/test_copy_opengl.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from manim import BraceLabel, config -from manim.mobject.opengl.opengl_mobject import OpenGLMobject - - -def test_opengl_mobject_copy(using_opengl_renderer): - """Test that a copy is a deepcopy.""" - orig = OpenGLMobject() - orig.add(*(OpenGLMobject() for _ in range(10))) - copy = orig.copy() - - assert orig is orig - assert orig is not copy - assert orig.submobjects is not copy.submobjects - for i in range(10): - assert orig.submobjects[i] is not copy.submobjects[i] - - -def test_bracelabel_copy(using_opengl_renderer, tmp_path): - """Test that a copy is a deepcopy.""" - # For this test to work, we need to tweak some folders temporarily - original_text_dir = config["text_dir"] - original_tex_dir = config["tex_dir"] - mediadir = Path(tmp_path) / "deepcopy" - config["text_dir"] = str(mediadir.joinpath("Text")) - config["tex_dir"] = str(mediadir.joinpath("Tex")) - for el in ["text_dir", "tex_dir"]: - Path(config[el]).mkdir(parents=True) - - # Before the refactoring of OpenGLMobject.copy(), the class BraceLabel was the - # only one to have a non-trivial definition of copy. Here we test that it - # still works after the refactoring. - orig = BraceLabel(OpenGLMobject(), "label") - copy = orig.copy() - - assert orig is orig - assert orig is not copy - assert orig.brace is not copy.brace - assert orig.label is not copy.label - assert orig.submobjects is not copy.submobjects - assert orig.submobjects[0] is orig.brace - assert copy.submobjects[0] is copy.brace - assert orig.submobjects[0] is not copy.brace - assert copy.submobjects[0] is not orig.brace - - # Restore the original folders - config["text_dir"] = original_text_dir - config["tex_dir"] = original_tex_dir diff --git a/tests/opengl/test_family_opengl.py b/tests/opengl/test_family_opengl.py deleted file mode 100644 index f16d0e4756..0000000000 --- a/tests/opengl/test_family_opengl.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import RIGHT, Circle -from manim.mobject.opengl.opengl_mobject import OpenGLMobject - - -def test_family(using_opengl_renderer): - """Check that the family is gathered correctly.""" - # Check that an empty OpenGLMobject's family only contains itself - mob = OpenGLMobject() - assert mob.get_family() == [mob] - - # Check that all children are in the family - mob = OpenGLMobject() - children = [OpenGLMobject() for _ in range(10)] - mob.add(*children) - family = mob.get_family() - assert len(family) == 1 + 10 - assert mob in family - for c in children: - assert c in family - - # Nested children should be in the family - mob = OpenGLMobject() - grandchildren = {} - for _ in range(10): - child = OpenGLMobject() - grandchildren[child] = [OpenGLMobject() for _ in range(10)] - child.add(*grandchildren[child]) - mob.add(*list(grandchildren.keys())) - family = mob.get_family() - assert len(family) == 1 + 10 + 10 * 10 - assert mob in family - for c in grandchildren: - assert c in family - for gc in grandchildren[c]: - assert gc in family - - -def test_overlapping_family(using_opengl_renderer): - """Check that each member of the family is only gathered once.""" - ( - mob, - child1, - child2, - ) = ( - OpenGLMobject(), - OpenGLMobject(), - OpenGLMobject(), - ) - gchild1, gchild2, gchild_common = OpenGLMobject(), OpenGLMobject(), OpenGLMobject() - child1.add(gchild1, gchild_common) - child2.add(gchild2, gchild_common) - mob.add(child1, child2) - family = mob.get_family() - assert mob in family - assert len(family) == 6 - assert family.count(gchild_common) == 1 - - -def test_shift_family(using_opengl_renderer): - """Check that each member of the family is shifted along with the parent. - - Importantly, here we add a common grandchild to each of the children. So - this test will fail if the grandchild moves twice as much as it should. - - """ - # Note shift() needs the OpenGLMobject to have a non-empty `points` attribute, so - # we cannot use a plain OpenGLMobject or OpenGLVMobject. We use Circle instead. - ( - mob, - child1, - child2, - ) = ( - Circle(), - Circle(), - Circle(), - ) - gchild1, gchild2, gchild_common = Circle(), Circle(), Circle() - - child1.add(gchild1, gchild_common) - child2.add(gchild2, gchild_common) - mob.add(child1, child2) - family = mob.get_family() - - positions_before = {m: m.get_center().copy() for m in family} - mob.shift(RIGHT) - positions_after = {m: m.get_center().copy() for m in family} - - for m in family: - np.testing.assert_allclose(positions_before[m] + RIGHT, positions_after[m]) diff --git a/tests/opengl/test_graph_opengl.py b/tests/opengl/test_graph_opengl.py deleted file mode 100644 index eb56935b9c..0000000000 --- a/tests/opengl/test_graph_opengl.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -from manim import Dot, Graph, Line, Text - - -def test_graph_creation(using_opengl_renderer): - vertices = [1, 2, 3, 4] - edges = [(1, 2), (2, 3), (3, 4), (4, 1)] - layout = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]} - G_manual = Graph(vertices=vertices, edges=edges, layout=layout) - assert len(G_manual.vertices) == 4 - assert len(G_manual.edges) == 4 - G_spring = Graph(vertices=vertices, edges=edges) - assert len(G_spring.vertices) == 4 - assert len(G_spring.edges) == 4 - - -def test_graph_add_vertices(using_opengl_renderer): - G = Graph([1, 2, 3], [(1, 2), (2, 3)]) - G.add_vertices(4) - assert len(G.vertices) == 4 - assert len(G.edges) == 2 - G.add_vertices(5, labels={5: Text("5")}) - assert len(G.vertices) == 5 - assert len(G.edges) == 2 - assert 5 in G._labels - assert 5 in G._vertex_config - G.add_vertices(6, 7, 8) - assert len(G.vertices) == 8 - assert len(G._graph.nodes()) == 8 - - -def test_graph_remove_vertices(using_opengl_renderer): - G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3), (3, 4), (4, 5)]) - removed_mobjects = G.remove_vertices(3) - assert len(removed_mobjects) == 3 - assert len(G.vertices) == 4 - assert len(G.edges) == 2 - assert list(G.vertices.keys()) == [1, 2, 4, 5] - assert list(G.edges.keys()) == [(1, 2), (4, 5)] - removed_mobjects = G.remove_vertices(4, 5) - assert len(removed_mobjects) == 3 - assert len(G.vertices) == 2 - assert len(G.edges) == 1 - assert list(G.vertices.keys()) == [1, 2] - assert list(G.edges.keys()) == [(1, 2)] - - -def test_graph_add_edges(using_opengl_renderer): - G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3)]) - added_mobjects = G.add_edges((1, 3)) - assert isinstance(added_mobjects.submobjects[0], Line) - assert len(G.vertices) == 5 - assert len(G.edges) == 3 - assert set(G.vertices.keys()) == {1, 2, 3, 4, 5} - assert set(G.edges.keys()) == {(1, 2), (2, 3), (1, 3)} - - added_mobjects = G.add_edges((1, 42)) - removed_mobjects = added_mobjects.submobjects - assert isinstance(removed_mobjects[0], Dot) - assert isinstance(removed_mobjects[1], Line) - - assert len(G.vertices) == 6 - assert len(G.edges) == 4 - assert set(G.vertices.keys()) == {1, 2, 3, 4, 5, 42} - assert set(G.edges.keys()) == {(1, 2), (2, 3), (1, 3), (1, 42)} - - added_mobjects = G.add_edges((4, 5), (5, 6), (6, 7)) - assert len(added_mobjects) == 5 - assert len(G.vertices) == 8 - assert len(G.edges) == 7 - assert set(G.vertices.keys()) == {1, 2, 3, 4, 5, 42, 6, 7} - assert set(G._graph.nodes()) == set(G.vertices.keys()) - assert set(G.edges.keys()) == { - (1, 2), - (2, 3), - (1, 3), - (1, 42), - (4, 5), - (5, 6), - (6, 7), - } - assert set(G._graph.edges()) == set(G.edges.keys()) - - -def test_graph_remove_edges(using_opengl_renderer): - G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3), (3, 4), (4, 5), (1, 5)]) - removed_mobjects = G.remove_edges((1, 2)) - assert isinstance(removed_mobjects.submobjects[0], Line) - assert len(G.vertices) == 5 - assert len(G.edges) == 4 - assert set(G.edges.keys()) == {(2, 3), (3, 4), (4, 5), (1, 5)} - assert set(G._graph.edges()) == set(G.edges.keys()) - - removed_mobjects = G.remove_edges((2, 3), (3, 4), (4, 5), (1, 5)) - assert len(removed_mobjects) == 4 - assert len(G.vertices) == 5 - assert len(G.edges) == 0 - assert set(G._graph.edges()) == set() - assert set(G.edges.keys()) == set() diff --git a/tests/opengl/test_ipython_magic_opengl.py b/tests/opengl/test_ipython_magic_opengl.py deleted file mode 100644 index ad1c61a6b5..0000000000 --- a/tests/opengl/test_ipython_magic_opengl.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import re - -from manim import config, tempconfig -from manim.utils.ipython_magic import _generate_file_name - - -def test_jupyter_file_naming(): - """Check the format of file names for jupyter""" - scene_name = "SimpleScene" - expected_pattern = r"[0-9a-zA-Z_]+[@_-]\d\d\d\d-\d\d-\d\d[@_-]\d\d-\d\d-\d\d" - current_renderer = config.renderer - with tempconfig({"scene_names": [scene_name], "renderer": "opengl"}): - file_name = _generate_file_name() - match = re.match(expected_pattern, file_name) - assert scene_name in file_name, ( - "Expected file to contain " + scene_name + " but got " + file_name - ) - assert match, "file name does not match expected pattern " + expected_pattern - # needs manually set back to avoid issues across tests - config.renderer = current_renderer - - -def test_jupyter_file_output(tmp_path): - """Check the jupyter file naming is valid and can be created""" - scene_name = "SimpleScene" - current_renderer = config.renderer - with tempconfig({"scene_names": [scene_name], "renderer": "opengl"}): - file_name = _generate_file_name() - actual_path = tmp_path.with_name(file_name) - with actual_path.open("w") as outfile: - outfile.write("") - assert actual_path.exists() - assert actual_path.is_file() - # needs manually set back to avoid issues across tests - config.renderer = current_renderer diff --git a/tests/opengl/test_markup_opengl.py b/tests/opengl/test_markup_opengl.py deleted file mode 100644 index b96acbeb97..0000000000 --- a/tests/opengl/test_markup_opengl.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations - -from manim import MarkupText - - -def test_good_markup(using_opengl_renderer): - """Test creation of valid :class:`MarkupText` object""" - try: - MarkupText("foo") - MarkupText("foo") - success = True - except ValueError: - success = False - assert success, "'foo' and 'foo' should not fail validation" - - -def test_special_tags_markup(using_opengl_renderer): - """Test creation of valid :class:`MarkupText` object with unofficial tags""" - try: - MarkupText('foo') - MarkupText('foo') - success = True - except ValueError: - success = False - assert success, '\'foo\' and \'foo\' should not fail validation' - - -def test_unbalanced_tag_markup(using_opengl_renderer): - """Test creation of invalid :class:`MarkupText` object (unbalanced tag)""" - try: - MarkupText("foo") - success = False - except ValueError: - success = True - assert success, "'foo' should fail validation" - - -def test_invalid_tag_markup(using_opengl_renderer): - """Test creation of invalid :class:`MarkupText` object (invalid tag)""" - try: - MarkupText("foo") - success = False - except ValueError: - success = True - - assert success, "'foo' should fail validation" diff --git a/tests/opengl/test_number_line_opengl.py b/tests/opengl/test_number_line_opengl.py deleted file mode 100644 index 4092e4bfd1..0000000000 --- a/tests/opengl/test_number_line_opengl.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import NumberLine -from manim.mobject.text.numbers import Integer - - -def test_unit_vector(): - """Check if the magnitude of unit vector along - the NumberLine is equal to its unit_size.""" - axis1 = NumberLine(unit_size=0.4) - axis2 = NumberLine(x_range=[-2, 5], length=12) - for axis in (axis1, axis2): - assert np.linalg.norm(axis.get_unit_vector()) == axis.unit_size - - -def test_decimal_determined_by_step(): - """Checks that step size is considered when determining the number of decimal - places.""" - axis = NumberLine(x_range=[-2, 2, 0.5]) - expected_decimal_places = 1 - actual_decimal_places = axis.decimal_number_config["num_decimal_places"] - assert actual_decimal_places == expected_decimal_places, ( - "Expected 1 decimal place but got " + actual_decimal_places - ) - - axis2 = NumberLine(x_range=[-1, 1, 0.25]) - expected_decimal_places = 2 - actual_decimal_places = axis2.decimal_number_config["num_decimal_places"] - assert actual_decimal_places == expected_decimal_places, ( - "Expected 1 decimal place but got " + actual_decimal_places - ) - - -def test_decimal_config_overrides_defaults(): - """Checks that ``num_decimal_places`` is determined by step size and gets overridden by ``decimal_number_config``.""" - axis = NumberLine( - x_range=[-2, 2, 0.5], - decimal_number_config={"num_decimal_places": 0}, - ) - expected_decimal_places = 0 - actual_decimal_places = axis.decimal_number_config["num_decimal_places"] - assert actual_decimal_places == expected_decimal_places, ( - "Expected 1 decimal place but got " + actual_decimal_places - ) - - -def test_whole_numbers_step_size_default_to_0_decimal_places(): - """Checks that ``num_decimal_places`` defaults to 0 when a whole number step size is passed.""" - axis = NumberLine(x_range=[-2, 2, 1]) - expected_decimal_places = 0 - actual_decimal_places = axis.decimal_number_config["num_decimal_places"] - assert actual_decimal_places == expected_decimal_places, ( - "Expected 1 decimal place but got " + actual_decimal_places - ) - - -def test_add_labels(): - expected_label_length = 6 - num_line = NumberLine(x_range=[-4, 4]) - num_line.add_labels( - dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)])), - ) - actual_label_length = len(num_line.labels) - assert ( - actual_label_length == expected_label_length - ), f"Expected a VGroup with {expected_label_length} integers but got {actual_label_length}." diff --git a/tests/opengl/test_numbers_opengl.py b/tests/opengl/test_numbers_opengl.py deleted file mode 100644 index 9bc2ddd13f..0000000000 --- a/tests/opengl/test_numbers_opengl.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -from manim.mobject.text.numbers import DecimalNumber - - -def test_font_size(): - """Test that DecimalNumber returns the correct font_size value - after being scaled.""" - num = DecimalNumber(0).scale(0.3) - - assert round(num.font_size, 5) == 14.4 - - -def test_font_size_vs_scale(): - """Test that scale produces the same results as .scale()""" - num = DecimalNumber(0, font_size=12) - num_scale = DecimalNumber(0).scale(1 / 4) - - assert num.height == num_scale.height - - -def test_changing_font_size(): - """Test that the font_size property properly scales DecimalNumber.""" - num = DecimalNumber(0, font_size=12) - num.font_size = 48 - - assert num.height == DecimalNumber(0, font_size=48).height - - -def test_set_value_size(): - """Test that the size of DecimalNumber after set_value is correct.""" - num = DecimalNumber(0).scale(0.3) - test_num = num.copy() - num.set_value(0) - - # round because the height is off by 1e-17 - assert round(num.height, 12) == round(test_num.height, 12) diff --git a/tests/opengl/test_opengl_mobject.py b/tests/opengl/test_opengl_mobject.py deleted file mode 100644 index d5bfac97f9..0000000000 --- a/tests/opengl/test_opengl_mobject.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import pytest - -from manim.mobject.opengl.opengl_mobject import OpenGLMobject - - -def test_opengl_mobject_add(using_opengl_renderer): - """Test OpenGLMobject.add().""" - """Call this function with a Container instance to test its add() method.""" - # check that obj.submobjects is updated correctly - obj = OpenGLMobject() - assert len(obj.submobjects) == 0 - obj.add(OpenGLMobject()) - assert len(obj.submobjects) == 1 - obj.add(*(OpenGLMobject() for _ in range(10))) - assert len(obj.submobjects) == 11 - - # check that adding a OpenGLMobject twice does not actually add it twice - repeated = OpenGLMobject() - obj.add(repeated) - assert len(obj.submobjects) == 12 - obj.add(repeated) - assert len(obj.submobjects) == 12 - - # check that OpenGLMobject.add() returns the OpenGLMobject (for chained calls) - assert obj.add(OpenGLMobject()) is obj - assert len(obj.submobjects) == 13 - - obj = OpenGLMobject() - - # an OpenGLMobject cannot contain itself - with pytest.raises(ValueError) as add_self_info: - obj.add(OpenGLMobject(), obj, OpenGLMobject()) - assert str(add_self_info.value) == ( - "Cannot add OpenGLMobject as a submobject of itself (at index 1)." - ) - assert len(obj.submobjects) == 0 - - # can only add Mobjects - with pytest.raises(TypeError) as add_str_info: - obj.add(OpenGLMobject(), OpenGLMobject(), "foo") - assert str(add_str_info.value) == ( - "Only values of type OpenGLMobject can be added as submobjects of " - "OpenGLMobject, but the value foo (at index 2) is of type str." - ) - assert len(obj.submobjects) == 0 - - -def test_opengl_mobject_remove(using_opengl_renderer): - """Test OpenGLMobject.remove().""" - obj = OpenGLMobject() - to_remove = OpenGLMobject() - obj.add(to_remove) - obj.add(*(OpenGLMobject() for _ in range(10))) - assert len(obj.submobjects) == 11 - obj.remove(to_remove) - assert len(obj.submobjects) == 10 - obj.remove(to_remove) - assert len(obj.submobjects) == 10 - - assert obj.remove(OpenGLMobject()) is obj diff --git a/tests/opengl/test_opengl_surface.py b/tests/opengl/test_opengl_surface.py deleted file mode 100644 index d6897691d4..0000000000 --- a/tests/opengl/test_opengl_surface.py +++ /dev/null @@ -1,14 +0,0 @@ -import numpy as np - -from manim.mobject.opengl.opengl_surface import OpenGLSurface -from manim.mobject.opengl.opengl_three_dimensions import OpenGLSurfaceMesh - - -def test_surface_initialization(using_opengl_renderer): - surface = OpenGLSurface( - lambda u, v: (u, v, u * np.sin(v) + v * np.cos(u)), - u_range=(-3, 3), - v_range=(-3, 3), - ) - - mesh = OpenGLSurfaceMesh(surface) diff --git a/tests/opengl/test_opengl_vectorized_mobject.py b/tests/opengl/test_opengl_vectorized_mobject.py deleted file mode 100644 index 6f73ef0265..0000000000 --- a/tests/opengl/test_opengl_vectorized_mobject.py +++ /dev/null @@ -1,310 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pytest - -from manim import Circle, Line, Square, VDict, VGroup -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject - - -def test_opengl_vmobject_add(using_opengl_renderer): - """Test the OpenGLVMobject add method.""" - obj = OpenGLVMobject() - assert len(obj.submobjects) == 0 - - obj.add(OpenGLVMobject()) - assert len(obj.submobjects) == 1 - - # Can't add non-OpenGLVMobject values to a VMobject. - with pytest.raises(TypeError) as add_int_info: - obj.add(3) - assert str(add_int_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "OpenGLVMobject, but the value 3 (at index 0) is of type int." - ) - assert len(obj.submobjects) == 1 - - # Plain OpenGLMobjects can't be added to a OpenGLVMobject if they're not - # OpenGLVMobjects. Suggest adding them into an OpenGLGroup instead. - with pytest.raises(TypeError) as add_mob_info: - obj.add(OpenGLMobject()) - assert str(add_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "OpenGLVMobject, but the value OpenGLMobject (at index 0) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - assert len(obj.submobjects) == 1 - - with pytest.raises(TypeError) as add_vmob_and_mob_info: - # If only one of the added objects is not an instance of VMobject, none of them should be added - obj.add(OpenGLVMobject(), OpenGLMobject()) - assert str(add_vmob_and_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "OpenGLVMobject, but the value OpenGLMobject (at index 1) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - assert len(obj.submobjects) == 1 - - # A VMobject or VGroup cannot contain itself. - with pytest.raises(ValueError) as add_self_info: - obj.add(obj) - assert str(add_self_info.value) == ( - "Cannot add OpenGLVMobject as a submobject of itself (at index 0)." - ) - assert len(obj.submobjects) == 1 - - -def test_opengl_vmobject_point_from_proportion(using_opengl_renderer): - obj = OpenGLVMobject() - - # One long line, one short line - obj.set_points_as_corners( - [ - np.array([0, 0, 0]), - np.array([4, 0, 0]), - np.array([4, 2, 0]), - ], - ) - - # Total length of 6, so halfway along the object - # would be at length 3, which lands in the first, long line. - np.testing.assert_array_equal(obj.point_from_proportion(0.5), np.array([3, 0, 0])) - - with pytest.raises(ValueError, match="between 0 and 1"): - obj.point_from_proportion(2) - - obj.clear_points() - with pytest.raises(Exception, match="with no points"): - obj.point_from_proportion(0) - - -def test_vgroup_init(using_opengl_renderer): - """Test the VGroup instantiation.""" - VGroup() - VGroup(OpenGLVMobject()) - VGroup(OpenGLVMobject(), OpenGLVMobject()) - - # A VGroup cannot contain non-VMobject values. - with pytest.raises(TypeError) as init_with_float_info: - VGroup(3.0) - assert str(init_with_float_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value 3.0 (at index 0) is of type float." - ) - - with pytest.raises(TypeError) as init_with_mob_info: - VGroup(OpenGLMobject()) - assert str(init_with_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value OpenGLMobject (at index 0) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - - with pytest.raises(TypeError) as init_with_vmob_and_mob_info: - VGroup(OpenGLVMobject(), OpenGLMobject()) - assert str(init_with_vmob_and_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value OpenGLMobject (at index 1) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - - -def test_vgroup_add(using_opengl_renderer): - """Test the VGroup add method.""" - obj = VGroup() - assert len(obj.submobjects) == 0 - - obj.add(OpenGLVMobject()) - assert len(obj.submobjects) == 1 - - # Can't add non-OpenGLVMobject values to a VMobject. - with pytest.raises(TypeError) as add_int_info: - obj.add(3) - assert str(add_int_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value 3 (at index 0) is of type int." - ) - assert len(obj.submobjects) == 1 - - # Plain OpenGLMobjects can't be added to a OpenGLVMobject if they're not - # OpenGLVMobjects. Suggest adding them into an OpenGLGroup instead. - with pytest.raises(TypeError) as add_mob_info: - obj.add(OpenGLMobject()) - assert str(add_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value OpenGLMobject (at index 0) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - assert len(obj.submobjects) == 1 - - with pytest.raises(TypeError) as add_vmob_and_mob_info: - # If only one of the added objects is not an instance of VMobject, none of them should be added - obj.add(OpenGLVMobject(), OpenGLMobject()) - assert str(add_vmob_and_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value OpenGLMobject (at index 1) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - assert len(obj.submobjects) == 1 - - # A VMobject or VGroup cannot contain itself. - with pytest.raises(ValueError) as add_self_info: - obj.add(obj) - assert str(add_self_info.value) == ( - "Cannot add VGroup as a submobject of itself (at index 0)." - ) - assert len(obj.submobjects) == 1 - - -def test_vgroup_add_dunder(using_opengl_renderer): - """Test the VGroup __add__ magic method.""" - obj = VGroup() - assert len(obj.submobjects) == 0 - obj + OpenGLVMobject() - assert len(obj.submobjects) == 0 - obj += OpenGLVMobject() - assert len(obj.submobjects) == 1 - with pytest.raises(TypeError): - obj += OpenGLMobject() - assert len(obj.submobjects) == 1 - with pytest.raises(TypeError): - # If only one of the added object is not an instance of OpenGLVMobject, none of them should be added - obj += (OpenGLVMobject(), OpenGLMobject()) - assert len(obj.submobjects) == 1 - with pytest.raises(ValueError): - # a OpenGLMobject cannot contain itself - obj += obj - - -def test_vgroup_remove(using_opengl_renderer): - """Test the VGroup remove method.""" - a = OpenGLVMobject() - c = OpenGLVMobject() - b = VGroup(c) - obj = VGroup(a, b) - assert len(obj.submobjects) == 2 - assert len(b.submobjects) == 1 - obj.remove(a) - b.remove(c) - assert len(obj.submobjects) == 1 - assert len(b.submobjects) == 0 - obj.remove(b) - assert len(obj.submobjects) == 0 - - -def test_vgroup_remove_dunder(using_opengl_renderer): - """Test the VGroup __sub__ magic method.""" - a = OpenGLVMobject() - c = OpenGLVMobject() - b = VGroup(c) - obj = VGroup(a, b) - assert len(obj.submobjects) == 2 - assert len(b.submobjects) == 1 - assert len(obj - a) == 1 - assert len(obj.submobjects) == 2 - obj -= a - b -= c - assert len(obj.submobjects) == 1 - assert len(b.submobjects) == 0 - obj -= b - assert len(obj.submobjects) == 0 - - -def test_vmob_add_to_back(using_opengl_renderer): - """Test the OpenGLMobject add_to_back method.""" - a = OpenGLVMobject() - b = Line() - c = "text" - with pytest.raises(ValueError): - # OpenGLMobject cannot contain self - a.add_to_back(a) - with pytest.raises(TypeError): - # All submobjects must be of type OpenGLMobject - a.add_to_back(c) - - # No submobject gets added twice - a.add_to_back(b) - a.add_to_back(b, b) - assert len(a.submobjects) == 1 - a.submobjects.clear() - a.add_to_back(b, b, b) - a.add_to_back(b, b) - assert len(a.submobjects) == 1 - a.submobjects.clear() - - # Make sure the ordering has not changed - o1, o2, o3 = Square(), Line(), Circle() - a.add_to_back(o1, o2, o3) - assert a.submobjects.pop() == o3 - assert a.submobjects.pop() == o2 - assert a.submobjects.pop() == o1 - - -def test_vdict_init(using_opengl_renderer): - """Test the VDict instantiation.""" - # Test empty VDict - VDict() - # Test VDict made from list of pairs - VDict([("a", OpenGLVMobject()), ("b", OpenGLVMobject()), ("c", OpenGLVMobject())]) - # Test VDict made from a python dict - VDict({"a": OpenGLVMobject(), "b": OpenGLVMobject(), "c": OpenGLVMobject()}) - # Test VDict made using zip - VDict(zip(["a", "b", "c"], [OpenGLVMobject(), OpenGLVMobject(), OpenGLVMobject()])) - # If the value is of type OpenGLMobject, must raise a TypeError - with pytest.raises(TypeError): - VDict({"a": OpenGLMobject()}) - - -def test_vdict_add(using_opengl_renderer): - """Test the VDict add method.""" - obj = VDict() - assert len(obj.submob_dict) == 0 - obj.add([("a", OpenGLVMobject())]) - assert len(obj.submob_dict) == 1 - with pytest.raises(TypeError): - obj.add([("b", OpenGLMobject())]) - - -def test_vdict_remove(using_opengl_renderer): - """Test the VDict remove method.""" - obj = VDict([("a", OpenGLVMobject())]) - assert len(obj.submob_dict) == 1 - obj.remove("a") - assert len(obj.submob_dict) == 0 - with pytest.raises(KeyError): - obj.remove("a") - - -def test_vgroup_supports_item_assigment(using_opengl_renderer): - """Test VGroup supports array-like assignment for OpenGLVMObjects""" - a = OpenGLVMobject() - b = OpenGLVMobject() - vgroup = VGroup(a) - assert vgroup[0] == a - vgroup[0] = b - assert vgroup[0] == b - assert len(vgroup) == 1 - - -def test_vgroup_item_assignment_at_correct_position(using_opengl_renderer): - """Test VGroup item-assignment adds to correct position for OpenGLVMobjects""" - n_items = 10 - vgroup = VGroup() - for _i in range(n_items): - vgroup.add(OpenGLVMobject()) - new_obj = OpenGLVMobject() - vgroup[6] = new_obj - assert vgroup[6] == new_obj - assert len(vgroup) == n_items - - -def test_vgroup_item_assignment_only_allows_vmobjects(using_opengl_renderer): - """Test VGroup item-assignment raises TypeError when invalid type is passed""" - vgroup = VGroup(OpenGLVMobject()) - with pytest.raises(TypeError) as assign_str_info: - vgroup[0] = "invalid object" - assert str(assign_str_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value invalid object (at index 0) is of type str." - ) diff --git a/tests/opengl/test_override_animation_opengl.py b/tests/opengl/test_override_animation_opengl.py deleted file mode 100644 index cb150201c7..0000000000 --- a/tests/opengl/test_override_animation_opengl.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -import pytest - -from manim import Animation, override_animation -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.utils.exceptions import MultiAnimationOverrideException - - -class AnimationA1(Animation): - pass - - -class AnimationA2(Animation): - pass - - -class AnimationA3(Animation): - pass - - -class AnimationB1(AnimationA1): - pass - - -class AnimationC1(AnimationB1): - pass - - -class AnimationX(Animation): - pass - - -class OpenGLMobjectA(OpenGLMobject): - @override_animation(AnimationA1) - def anim_a1(self): - return AnimationA2(self) - - @override_animation(AnimationX) - def anim_x(self, *args, **kwargs): - return args, kwargs - - -class OpenGLMobjectB(OpenGLMobjectA): - pass - - -class OpenGLMobjectC(OpenGLMobjectB): - @override_animation(AnimationA1) - def anim_a1(self): - return AnimationA3(self) - - -class OpenGLMobjectX(OpenGLMobject): - @override_animation(AnimationB1) - def animation(self): - return "Overridden" - - -@pytest.mark.xfail(reason="Needs investigating") -def test_opengl_mobject_inheritance(): - mob = OpenGLMobject() - a = OpenGLMobjectA() - b = OpenGLMobjectB() - c = OpenGLMobjectC() - - assert type(AnimationA1(mob)) is AnimationA1 - assert type(AnimationA1(a)) is AnimationA2 - assert type(AnimationA1(b)) is AnimationA2 - assert type(AnimationA1(c)) is AnimationA3 - - -@pytest.mark.xfail(reason="Needs investigating") -def test_arguments(): - a = OpenGLMobjectA() - args = (1, "two", {"three": 3}, ["f", "o", "u", "r"]) - kwargs = {"test": "manim", "keyword": 42, "arguments": []} - animA = AnimationX(a, *args, **kwargs) - - assert animA[0] == args - assert animA[1] == kwargs - - -@pytest.mark.xfail(reason="Needs investigating") -def test_multi_animation_override_exception(): - with pytest.raises(MultiAnimationOverrideException): - - class OpenGLMobjectB2(OpenGLMobjectA): - @override_animation(AnimationA1) - def anim_a1_different_name(self): - pass - - -@pytest.mark.xfail(reason="Needs investigating") -def test_animation_inheritance(): - x = OpenGLMobjectX() - - assert type(AnimationA1(x)) is AnimationA1 - assert AnimationB1(x) == "Overridden" - assert type(AnimationC1(x)) is AnimationC1 diff --git a/tests/opengl/test_scene_opengl.py b/tests/opengl/test_scene_opengl.py deleted file mode 100644 index e202fa9a7d..0000000000 --- a/tests/opengl/test_scene_opengl.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -from manim import Scene, tempconfig -from manim.mobject.opengl.opengl_mobject import OpenGLMobject - - -def test_scene_add_remove(using_opengl_renderer): - with tempconfig({"dry_run": True}): - scene = Scene() - assert len(scene.mobjects) == 0 - scene.add(OpenGLMobject()) - assert len(scene.mobjects) == 1 - scene.add(*(OpenGLMobject() for _ in range(10))) - assert len(scene.mobjects) == 11 - - # Check that adding a mobject twice does not actually add it twice - repeated = OpenGLMobject() - scene.add(repeated) - assert len(scene.mobjects) == 12 - scene.add(repeated) - assert len(scene.mobjects) == 12 - - # Check that Scene.add() returns the Scene (for chained calls) - assert scene.add(OpenGLMobject()) is scene - to_remove = OpenGLMobject() - scene = Scene() - scene.add(to_remove) - scene.add(*(OpenGLMobject() for _ in range(10))) - assert len(scene.mobjects) == 11 - scene.remove(to_remove) - assert len(scene.mobjects) == 10 - scene.remove(to_remove) - assert len(scene.mobjects) == 10 - - # Check that Scene.remove() returns the instance (for chained calls) - assert scene.add(OpenGLMobject()) is scene diff --git a/tests/opengl/test_sound_opengl.py b/tests/opengl/test_sound_opengl.py deleted file mode 100644 index 40ec914de5..0000000000 --- a/tests/opengl/test_sound_opengl.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import struct -import wave -from pathlib import Path - -import pytest - -from manim import Scene - - -@pytest.mark.xfail(reason="Not currently implemented for opengl") -def test_add_sound(using_opengl_renderer, tmpdir): - # create sound file - sound_loc = Path(tmpdir, "noise.wav") - f = wave.open(str(sound_loc), "w") - f.setparams((2, 2, 44100, 0, "NONE", "not compressed")) - for _ in range(22050): # half a second of sound - packed_value = struct.pack("h", 14242) - f.writeframes(packed_value) - f.writeframes(packed_value) - - f.close() - - scene = Scene() - scene.add_sound(sound_loc) diff --git a/tests/opengl/test_stroke_opengl.py b/tests/opengl/test_stroke_opengl.py deleted file mode 100644 index d5eff1ac71..0000000000 --- a/tests/opengl/test_stroke_opengl.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import manim.utils.color as C -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject - - -def test_stroke_props_in_ctor(using_opengl_renderer): - m = OpenGLVMobject(stroke_color=C.ORANGE, stroke_width=10) - assert m.stroke_color.to_hex() == C.ORANGE.to_hex() - assert m.stroke_width == 10 - - -def test_set_stroke(using_opengl_renderer): - m = OpenGLVMobject() - m.set_stroke(color=C.ORANGE, width=2, opacity=0.8) - assert m.stroke_width == 2 - assert m.stroke_opacity == 0.8 - assert m.stroke_color.to_hex() == C.ORANGE.to_hex() diff --git a/tests/opengl/test_svg_mobject_opengl.py b/tests/opengl/test_svg_mobject_opengl.py deleted file mode 100644 index c1d11f42e5..0000000000 --- a/tests/opengl/test_svg_mobject_opengl.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -from manim import * -from tests.helpers.path_utils import get_svg_resource - - -def test_set_fill_color(using_opengl_renderer): - expected_color = "#FF862F" - svg = SVGMobject(get_svg_resource("heart.svg"), fill_color=expected_color) - assert svg.fill_color.to_hex() == expected_color - - -def test_set_stroke_color(using_opengl_renderer): - expected_color = "#FFFDDD" - svg = SVGMobject(get_svg_resource("heart.svg"), stroke_color=expected_color) - assert svg.stroke_color.to_hex() == expected_color - - -def test_set_color_sets_fill_and_stroke(using_opengl_renderer): - expected_color = "#EEE777" - svg = SVGMobject(get_svg_resource("heart.svg"), color=expected_color) - assert svg.color.to_hex() == expected_color - assert svg.fill_color.to_hex() == expected_color - assert svg.stroke_color.to_hex() == expected_color - - -def test_set_fill_opacity(using_opengl_renderer): - expected_opacity = 0.5 - svg = SVGMobject(get_svg_resource("heart.svg"), fill_opacity=expected_opacity) - assert svg.fill_opacity == expected_opacity - - -def test_stroke_opacity(using_opengl_renderer): - expected_opacity = 0.4 - svg = SVGMobject(get_svg_resource("heart.svg"), stroke_opacity=expected_opacity) - assert svg.stroke_opacity == expected_opacity - - -def test_fill_overrides_color(using_opengl_renderer): - expected_color = "#343434" - svg = SVGMobject( - get_svg_resource("heart.svg"), - color="#123123", - fill_color=expected_color, - ) - assert svg.fill_color.to_hex() == expected_color - - -def test_stroke_overrides_color(using_opengl_renderer): - expected_color = "#767676" - svg = SVGMobject( - get_svg_resource("heart.svg"), - color="#334433", - stroke_color=expected_color, - ) - assert svg.stroke_color.to_hex() == expected_color diff --git a/tests/opengl/test_texmobject_opengl.py b/tests/opengl/test_texmobject_opengl.py deleted file mode 100644 index 4fe5a76f81..0000000000 --- a/tests/opengl/test_texmobject_opengl.py +++ /dev/null @@ -1,109 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from manim import MathTex, SingleStringMathTex, Tex, config - - -def test_MathTex(using_opengl_renderer): - MathTex("a^2 + b^2 = c^2") - assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists() - - -def test_SingleStringMathTex(using_opengl_renderer): - SingleStringMathTex("test") - assert Path(config.media_dir, "Tex", "8ce17c7f5013209f.svg").exists() - - -@pytest.mark.parametrize( # : PT006 - "text_input,length_sub", - [("{{ a }} + {{ b }} = {{ c }}", 5), (r"\frac{1}{a+b\sqrt{2}}", 1)], -) -def test_double_braces_testing(using_opengl_renderer, text_input, length_sub): - t1 = MathTex(text_input) - assert len(t1.submobjects) == length_sub - - -def test_tex(using_opengl_renderer): - Tex("The horse does not eat cucumber salad.") - assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists() - - -def test_tex_whitespace_arg(using_opengl_renderer): - """Check that correct number of submobjects are created per string with whitespace separator""" - separator = "\t" - str_part_1 = "Hello" - str_part_2 = "world" - str_part_3 = "It is" - str_part_4 = "me!" - tex = Tex(str_part_1, str_part_2, str_part_3, str_part_4, arg_separator=separator) - assert len(tex) == 4 - assert len(tex[0]) == len("".join((str_part_1 + separator).split())) - assert len(tex[1]) == len("".join((str_part_2 + separator).split())) - assert len(tex[2]) == len("".join((str_part_3 + separator).split())) - assert len(tex[3]) == len("".join(str_part_4.split())) - - -def test_tex_non_whitespace_arg(using_opengl_renderer): - """Check that correct number of submobjects are created per string with non_whitespace characters""" - separator = "," - str_part_1 = "Hello" - str_part_2 = "world" - str_part_3 = "It is" - str_part_4 = "me!" - tex = Tex(str_part_1, str_part_2, str_part_3, str_part_4, arg_separator=separator) - assert len(tex) == 4 - assert len(tex[0]) == len("".join((str_part_1 + separator).split())) - assert len(tex[1]) == len("".join((str_part_2 + separator).split())) - assert len(tex[2]) == len("".join((str_part_3 + separator).split())) - assert len(tex[3]) == len("".join(str_part_4.split())) - - -def test_tex_white_space_and_non_whitespace_args(using_opengl_renderer): - """Check that correct number of submobjects are created per string when mixing characters with whitespace""" - separator = ", \n . \t\t" - str_part_1 = "Hello" - str_part_2 = "world" - str_part_3 = "It is" - str_part_4 = "me!" - tex = Tex(str_part_1, str_part_2, str_part_3, str_part_4, arg_separator=separator) - assert len(tex) == 4 - assert len(tex[0]) == len("".join((str_part_1 + separator).split())) - assert len(tex[1]) == len("".join((str_part_2 + separator).split())) - assert len(tex[2]) == len("".join((str_part_3 + separator).split())) - assert len(tex[3]) == len("".join(str_part_4.split())) - - -def test_tex_size(using_opengl_renderer): - """Check that the size of a :class:`Tex` string is not changed.""" - text = Tex("what").center() - vertical = text.get_top() - text.get_bottom() - horizontal = text.get_right() - text.get_left() - assert round(vertical[1], 4) == 0.3512 - assert round(horizontal[0], 4) == 1.0420 - - -def test_font_size(using_opengl_renderer): - """Test that tex_mobject classes return - the correct font_size value after being scaled.""" - string = MathTex(0).scale(0.3) - - assert round(string.font_size, 5) == 14.4 - - -def test_font_size_vs_scale(using_opengl_renderer): - """Test that scale produces the same results as .scale()""" - num = MathTex(0, font_size=12) - num_scale = MathTex(0).scale(1 / 4) - - assert num.height == num_scale.height - - -def test_changing_font_size(using_opengl_renderer): - """Test that the font_size property properly scales tex_mobject.py classes.""" - num = Tex("0", font_size=12) - num.font_size = 48 - - assert num.height == Tex("0", font_size=48).height diff --git a/tests/opengl/test_text_mobject_opengl.py b/tests/opengl/test_text_mobject_opengl.py deleted file mode 100644 index 9fa9415314..0000000000 --- a/tests/opengl/test_text_mobject_opengl.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import annotations - -from manim.mobject.text.text_mobject import MarkupText, Text - - -def test_font_size(using_opengl_renderer): - """Test that Text and MarkupText return the - correct font_size value after being scaled.""" - text_string = Text("0").scale(0.3) - markuptext_string = MarkupText("0").scale(0.3) - - assert round(text_string.font_size, 5) == 14.4 - assert round(markuptext_string.font_size, 5) == 14.4 diff --git a/tests/opengl/test_ticks_opengl.py b/tests/opengl/test_ticks_opengl.py deleted file mode 100644 index 3fe4a4f3a0..0000000000 --- a/tests/opengl/test_ticks_opengl.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import PI, Axes, NumberLine - - -def test_duplicate_ticks_removed_for_axes(using_opengl_renderer): - axis = NumberLine( - x_range=[-10, 10], - ) - ticks = axis.get_tick_range() - assert np.unique(ticks).size == ticks.size - - -def test_ticks_not_generated_on_origin_for_axes(using_opengl_renderer): - axes = Axes( - x_range=[-10, 10], - y_range=[-10, 10], - axis_config={"include_ticks": True}, - ) - - x_axis_range = axes.x_axis.get_tick_range() - y_axis_range = axes.y_axis.get_tick_range() - - assert 0 not in x_axis_range - assert 0 not in y_axis_range - - -def test_expected_ticks_generated(using_opengl_renderer): - axes = Axes(x_range=[-2, 2], y_range=[-2, 2], axis_config={"include_ticks": True}) - x_axis_range = axes.x_axis.get_tick_range() - y_axis_range = axes.y_axis.get_tick_range() - - assert 1 in x_axis_range - assert 1 in y_axis_range - assert -1 in x_axis_range - assert -1 in y_axis_range - - -def test_ticks_generated_from_origin_for_axes(using_opengl_renderer): - axes = Axes( - x_range=[-PI, PI], - y_range=[-PI, PI], - axis_config={"include_ticks": True}, - ) - x_axis_range = axes.x_axis.get_tick_range() - y_axis_range = axes.y_axis.get_tick_range() - - assert -2 in x_axis_range - assert -1 in x_axis_range - assert 0 not in x_axis_range - assert 1 in x_axis_range - assert 2 in x_axis_range - - assert -2 in y_axis_range - assert -1 in y_axis_range - assert 0 not in y_axis_range - assert 1 in y_axis_range - assert 2 in y_axis_range diff --git a/tests/opengl/test_unit_geometry_opengl.py b/tests/opengl/test_unit_geometry_opengl.py deleted file mode 100644 index f92bc9ff37..0000000000 --- a/tests/opengl/test_unit_geometry_opengl.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import Sector - - -def test_get_arc_center(using_opengl_renderer): - np.testing.assert_array_equal( - Sector(arc_center=[1, 2, 0]).get_arc_center(), [1, 2, 0] - ) diff --git a/tests/opengl/test_value_tracker_opengl.py b/tests/opengl/test_value_tracker_opengl.py deleted file mode 100644 index ad8ac3acfe..0000000000 --- a/tests/opengl/test_value_tracker_opengl.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -from manim.mobject.value_tracker import ComplexValueTracker, ValueTracker - - -def test_value_tracker_set_value(using_opengl_renderer): - """Test ValueTracker.set_value()""" - tracker = ValueTracker() - tracker.set_value(10.0) - assert tracker.get_value() == 10.0 - - -def test_value_tracker_get_value(using_opengl_renderer): - """Test ValueTracker.get_value()""" - tracker = ValueTracker(10.0) - assert tracker.get_value() == 10.0 - - -def test_value_tracker_interpolate(using_opengl_renderer): - """Test ValueTracker.interpolate()""" - tracker1 = ValueTracker(1.0) - tracker2 = ValueTracker(2.5) - tracker3 = ValueTracker().interpolate(tracker1, tracker2, 0.7) - assert tracker3.get_value() == 2.05 - - -def test_value_tracker_increment_value(using_opengl_renderer): - """Test ValueTracker.increment_value()""" - tracker = ValueTracker(0.0) - tracker.increment_value(10.0) - assert tracker.get_value() == 10.0 - - -def test_value_tracker_bool(using_opengl_renderer): - """Test ValueTracker.__bool__()""" - tracker = ValueTracker(0.0) - assert not tracker - tracker.increment_value(1.0) - assert tracker - - -def test_value_tracker_iadd(using_opengl_renderer): - """Test ValueTracker.__iadd__()""" - tracker = ValueTracker(0.0) - tracker += 10.0 - assert tracker.get_value() == 10.0 - - -def test_value_tracker_ifloordiv(using_opengl_renderer): - """Test ValueTracker.__ifloordiv__()""" - tracker = ValueTracker(5.0) - tracker //= 2.0 - assert tracker.get_value() == 2.0 - - -def test_value_tracker_imod(using_opengl_renderer): - """Test ValueTracker.__imod__()""" - tracker = ValueTracker(20.0) - tracker %= 3.0 - assert tracker.get_value() == 2.0 - - -def test_value_tracker_imul(using_opengl_renderer): - """Test ValueTracker.__imul__()""" - tracker = ValueTracker(3.0) - tracker *= 4.0 - assert tracker.get_value() == 12.0 - - -def test_value_tracker_ipow(using_opengl_renderer): - """Test ValueTracker.__ipow__()""" - tracker = ValueTracker(3.0) - tracker **= 3.0 - assert tracker.get_value() == 27.0 - - -def test_value_tracker_isub(using_opengl_renderer): - """Test ValueTracker.__isub__()""" - tracker = ValueTracker(20.0) - tracker -= 10.0 - assert tracker.get_value() == 10.0 - - -def test_value_tracker_itruediv(using_opengl_renderer): - """Test ValueTracker.__itruediv__()""" - tracker = ValueTracker(5.0) - tracker /= 2.0 - assert tracker.get_value() == 2.5 - - -def test_complex_value_tracker_set_value(using_opengl_renderer): - """Test ComplexValueTracker.set_value()""" - tracker = ComplexValueTracker() - tracker.set_value(1 + 2j) - assert tracker.get_value() == 1 + 2j - - -def test_complex_value_tracker_get_value(using_opengl_renderer): - """Test ComplexValueTracker.get_value()""" - tracker = ComplexValueTracker(2.0 - 3.0j) - assert tracker.get_value() == 2.0 - 3.0j diff --git a/tests/test_config.py b/tests/test_config.py index 0c60d59b10..e174a02e88 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,13 +4,14 @@ from pathlib import Path import numpy as np +import pytest -from manim import WHITE, Scene, Square, Tex, Text, config, tempconfig +from manim import WHITE, Manager, Scene, Square, Tex, Text, tempconfig from manim._config.utils import ManimConfig from tests.assert_utils import assert_dir_exists, assert_dir_filled, assert_file_exists -def test_tempconfig(): +def test_tempconfig(config): """Test the tempconfig context manager.""" original = config.copy() @@ -33,6 +34,20 @@ def test_tempconfig(): assert config[k] == v +@pytest.mark.parametrize( + ("format", "expected_file_extension"), + [ + ("mp4", ".mp4"), + ("webm", ".webm"), + ("mov", ".mov"), + ("gif", ".mp4"), + ], +) +def test_resolve_file_extensions(config, format, expected_file_extension): + config.format = format + assert config.movie_file_extension == expected_file_extension + + class MyScene(Scene): def construct(self): self.add(Square()) @@ -41,144 +56,141 @@ def construct(self): self.wait(1) -def test_transparent(): +def test_transparent(config): """Test the 'transparent' config option.""" - orig_verbosity = config["verbosity"] - config["verbosity"] = "ERROR" - with tempconfig({"dry_run": True}): - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() + config.verbosity = "ERROR" + config.dry_run = True + + manager = Manager(MyScene) + manager.render() + frame = manager.renderer.get_pixels() np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 255]) - with tempconfig({"transparent": True, "dry_run": True}): - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() - np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 0]) + config.transparent = True - config["verbosity"] = orig_verbosity + manager = Manager(MyScene) + manager.render() + frame = manager.renderer.get_pixels() + np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 0]) -def test_background_color(): +def test_background_color(config): """Test the 'background_color' config option.""" - with tempconfig({"background_color": WHITE, "verbosity": "ERROR", "dry_run": True}): - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() - np.testing.assert_allclose(frame[0, 0], [255, 255, 255, 255]) + + config.background_color = WHITE + config.verbosity = "ERROR" + config.dry_run = True + + manager = Manager(MyScene) + manager.render() + frame = manager.renderer.get_pixels() + np.testing.assert_allclose(frame[0, 0], [255, 255, 255, 255]) -def test_digest_file(tmp_path): +def test_digest_file(tmp_path, config): """Test that a config file can be digested programmatically.""" - with tempconfig({}): - tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) - tmp_cfg.write( - """ - [CLI] - media_dir = this_is_my_favorite_path - video_dir = {media_dir}/videos - sections_dir = {media_dir}/{scene_name}/prepare_for_unforeseen_consequences - frame_height = 10 - """, - ) - tmp_cfg.close() - config.digest_file(tmp_cfg.name) - - assert config.get_dir("media_dir") == Path("this_is_my_favorite_path") - assert config.get_dir("video_dir") == Path("this_is_my_favorite_path/videos") - assert config.get_dir("sections_dir", scene_name="test") == Path( - "this_is_my_favorite_path/test/prepare_for_unforeseen_consequences" - ) - - -def test_custom_dirs(tmp_path): - with tempconfig( - { - "media_dir": tmp_path, - "save_sections": True, - "log_to_file": True, - "frame_rate": 15, - "pixel_height": 854, - "pixel_width": 480, - "sections_dir": "{media_dir}/test_sections", - "video_dir": "{media_dir}/test_video", - "partial_movie_dir": "{media_dir}/test_partial_movie_dir", - "images_dir": "{media_dir}/test_images", - "text_dir": "{media_dir}/test_text", - "tex_dir": "{media_dir}/test_tex", - "log_dir": "{media_dir}/test_log", - } - ): - scene = MyScene() - scene.render() - tmp_path = Path(tmp_path) - assert_dir_filled(tmp_path / "test_sections") - assert_file_exists(tmp_path / "test_sections/MyScene.json") - - assert_dir_filled(tmp_path / "test_video") - assert_file_exists(tmp_path / "test_video/MyScene.mp4") - - assert_dir_filled(tmp_path / "test_partial_movie_dir") - assert_file_exists( - tmp_path / "test_partial_movie_dir/partial_movie_file_list.txt" - ) - - # TODO: another example with image output would be nice - assert_dir_exists(tmp_path / "test_images") - - assert_dir_filled(tmp_path / "test_text") - assert_dir_filled(tmp_path / "test_tex") - assert_dir_filled(tmp_path / "test_log") - - -def test_frame_size(tmp_path): + tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) + tmp_cfg.write( + """ + [CLI] + media_dir = this_is_my_favorite_path + video_dir = {media_dir}/videos + sections_dir = {media_dir}/{scene_name}/prepare_for_unforeseen_consequences + frame_height = 10 + """, + ) + tmp_cfg.close() + config.digest_file(tmp_cfg.name) + + assert config.get_dir("media_dir") == Path("this_is_my_favorite_path") + assert config.get_dir("video_dir") == Path("this_is_my_favorite_path/videos") + assert config.get_dir("sections_dir", scene_name="test") == Path( + "this_is_my_favorite_path/test/prepare_for_unforeseen_consequences" + ) + + +def test_custom_dirs(tmp_path, config): + config.media_dir = tmp_path + config.save_sections = True + config.log_to_file = True + config.frame_rate = 15 + config.pixel_height = 854 + config.pixel_width = 480 + config.sections_dir = "{media_dir}/test_sections" + config.video_dir = "{media_dir}/test_video" + config.partial_movie_dir = "{media_dir}/test_partial_movie_dir" + config.images_dir = "{media_dir}/test_images" + config.text_dir = "{media_dir}/test_text" + config.tex_dir = "{media_dir}/test_tex" + config.log_dir = "{media_dir}/test_log" + + manager = Manager(MyScene) + manager.render() + tmp_path = Path(tmp_path) + assert_dir_filled(tmp_path / "test_sections") + assert_file_exists(tmp_path / "test_sections/MyScene.json") + + assert_dir_filled(tmp_path / "test_video") + assert_file_exists(tmp_path / "test_video/MyScene.mp4") + + assert_dir_filled(tmp_path / "test_partial_movie_dir") + assert_file_exists(tmp_path / "test_partial_movie_dir/partial_movie_file_list.txt") + + # TODO: another example with image output would be nice + assert_dir_exists(tmp_path / "test_images") + + assert_dir_filled(tmp_path / "test_text") + assert_dir_filled(tmp_path / "test_tex") + assert_dir_filled(tmp_path / "test_log") + + +def test_pixel_dimensions(tmp_path, config): + tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) + tmp_cfg.write( + """ + [CLI] + pixel_height = 10 + pixel_width = 10 + """, + ) + tmp_cfg.close() + config.digest_file(tmp_cfg.name) + + # aspect ratio is set using pixel measurements + np.testing.assert_allclose(config.aspect_ratio, 1.0) + # if not specified in the cfg file, frame_width is set using the aspect ratio + np.testing.assert_allclose(config.frame_height, 8.0) + np.testing.assert_allclose(config.frame_width, 8.0) + + +def test_frame_size(tmp_path, config): """Test that the frame size can be set via config file.""" np.testing.assert_allclose( config.aspect_ratio, config.pixel_width / config.pixel_height ) np.testing.assert_allclose(config.frame_height, 8.0) - with tempconfig({}): - tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) - tmp_cfg.write( - """ - [CLI] - pixel_height = 10 - pixel_width = 10 - """, - ) - tmp_cfg.close() - config.digest_file(tmp_cfg.name) - - # aspect ratio is set using pixel measurements - np.testing.assert_allclose(config.aspect_ratio, 1.0) - # if not specified in the cfg file, frame_width is set using the aspect ratio - np.testing.assert_allclose(config.frame_height, 8.0) - np.testing.assert_allclose(config.frame_width, 8.0) - - with tempconfig({}): - tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) - tmp_cfg.write( - """ - [CLI] - pixel_height = 10 - pixel_width = 10 - frame_height = 10 - frame_width = 10 - """, - ) - tmp_cfg.close() - config.digest_file(tmp_cfg.name) - - np.testing.assert_allclose(config.aspect_ratio, 1.0) - # if both are specified in the cfg file, the aspect ratio is ignored - np.testing.assert_allclose(config.frame_height, 10.0) - np.testing.assert_allclose(config.frame_width, 10.0) - - -def test_temporary_dry_run(): + tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) + tmp_cfg.write( + """ + [CLI] + pixel_height = 10 + pixel_width = 10 + frame_height = 10 + frame_width = 10 + """, + ) + tmp_cfg.close() + config.digest_file(tmp_cfg.name) + + np.testing.assert_allclose(config.aspect_ratio, 1.0) + # if both are specified in the cfg file, the aspect ratio is ignored + np.testing.assert_allclose(config.frame_height, 10.0) + np.testing.assert_allclose(config.frame_width, 10.0) + + +def test_temporary_dry_run(config): """Test that tempconfig correctly restores after setting dry_run.""" assert config["write_to_movie"] assert not config["save_last_frame"] @@ -191,24 +203,23 @@ def test_temporary_dry_run(): assert not config["save_last_frame"] -def test_dry_run_with_png_format(): +def test_dry_run_with_png_format(config, dry_run): """Test that there are no exceptions when running a png without output""" - with tempconfig( - {"dry_run": True, "write_to_movie": False, "disable_caching": True} - ): - assert config["dry_run"] is True - scene = MyScene() - scene.render() + + config.write_to_movie = False + config.disable_caching = True + assert config.dry_run is True + manager = Manager(MyScene) + manager.render() -def test_dry_run_with_png_format_skipped_animations(): +def test_dry_run_with_png_format_skipped_animations(config, dry_run): """Test that there are no exceptions when running a png without output and skipped animations""" - with tempconfig( - {"dry_run": True, "write_to_movie": False, "disable_caching": True} - ): - assert config["dry_run"] is True - scene = MyScene(skip_animations=True) - scene.render() + config.write_to_movie = False + config.disable_caching = True + assert config["dry_run"] is True + manager = Manager(MyScene) + manager.render() def test_tex_template_file(tmp_path): diff --git a/tests/test_graphical_units/control_data/opengl/Circle.npz b/tests/test_graphical_units/control_data/opengl/Circle.npz deleted file mode 100644 index c68316bc16..0000000000 Binary files a/tests/test_graphical_units/control_data/opengl/Circle.npz and /dev/null differ diff --git a/tests/test_graphical_units/control_data/opengl/FixedMobjects3D.npz b/tests/test_graphical_units/control_data/opengl/FixedMobjects3D.npz deleted file mode 100644 index 204e542a8d..0000000000 Binary files a/tests/test_graphical_units/control_data/opengl/FixedMobjects3D.npz and /dev/null differ diff --git a/tests/test_graphical_units/test_opengl.py b/tests/test_graphical_units/test_opengl.py deleted file mode 100644 index 2c21e692f5..0000000000 --- a/tests/test_graphical_units/test_opengl.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -from manim import * -from manim.camera.camera import OpenGLRenderer -from manim.utils.testing.frames_comparison import frames_comparison - -__module_test__ = "opengl" - - -@frames_comparison(renderer_class=OpenGLRenderer, renderer="opengl") -def test_Circle(scene): - circle = Circle().set_color(RED) - scene.add(circle) - scene.wait() - - -@frames_comparison( - renderer_class=OpenGLRenderer, - renderer="opengl", -) -def test_FixedMobjects3D(scene: Scene): - scene.renderer.camera.set_euler_angles(phi=75 * DEGREES, theta=-45 * DEGREES) - circ = Circle(fill_opacity=1).to_edge(LEFT) - square = Square(fill_opacity=1).to_edge(RIGHT) - triangle = Triangle(fill_opacity=1).to_corner(UR) - [i.fix_orientation() for i in (circ, square)] - triangle.fix_in_frame() diff --git a/tests/test_graphical_units/test_threed.py b/tests/test_graphical_units/test_threed.py index 022201f4c8..b6079e5e4c 100644 --- a/tests/test_graphical_units/test_threed.py +++ b/tests/test_graphical_units/test_threed.py @@ -33,6 +33,18 @@ def test_Cone(scene): scene.add(Cone(resolution=16)) +def test_Cone_get_start_and_get_end(): + cone = Cone().shift(RIGHT).rotate(PI / 4, about_point=ORIGIN, about_edge=OUT) + start = [0.70710678, 0.70710678, -1.0] + end = [0.70710678, 0.70710678, 0.0] + assert np.allclose( + cone.get_start(), start, atol=0.01 + ), "start points of Cone do not match" + assert np.allclose( + cone.get_end(), end, atol=0.01 + ), "end points of Cone do not match" + + @frames_comparison(base_scene=ThreeDScene) def test_Cylinder(scene): scene.add(Cylinder()) @@ -149,3 +161,14 @@ def param_surface(u, v): axes=axes, colorscale=[(RED, -0.4), (YELLOW, 0), (GREEN, 0.4)], axis=1 ) scene.add(axes, surface_plane) + + +def test_get_start_and_end_Arrow3d(): + start, end = ORIGIN, np.array([2, 1, 0]) + arrow = Arrow3D(start, end) + assert np.allclose( + arrow.get_start(), start, atol=0.01 + ), "start points of Arrow3D do not match" + assert np.allclose( + arrow.get_end(), end, atol=0.01 + ), "end points of Arrow3D do not match" diff --git a/tests/test_ipython_magic.py b/tests/test_ipython_magic.py index 91d1fba8c2..6452d58de5 100644 --- a/tests/test_ipython_magic.py +++ b/tests/test_ipython_magic.py @@ -2,30 +2,29 @@ import re -from manim import tempconfig from manim.utils.ipython_magic import _generate_file_name -def test_jupyter_file_naming(): +def test_jupyter_file_naming(config): """Check the format of file names for jupyter""" scene_name = "SimpleScene" expected_pattern = r"[0-9a-zA-Z_]+[@_-]\d\d\d\d-\d\d-\d\d[@_-]\d\d-\d\d-\d\d" - with tempconfig({"scene_names": [scene_name]}): - file_name = _generate_file_name() - match = re.match(expected_pattern, file_name) - assert scene_name in file_name, ( - "Expected file to contain " + scene_name + " but got " + file_name - ) - assert match, "file name does not match expected pattern " + expected_pattern + config.scene_names = [scene_name] + file_name = _generate_file_name() + match = re.match(expected_pattern, file_name) + assert scene_name in file_name, ( + "Expected file to contain " + scene_name + " but got " + file_name + ) + assert match, "file name does not match expected pattern " + expected_pattern -def test_jupyter_file_output(tmp_path): +def test_jupyter_file_output(tmp_path, config): """Check the jupyter file naming is valid and can be created""" scene_name = "SimpleScene" - with tempconfig({"scene_names": [scene_name]}): - file_name = _generate_file_name() - actual_path = tmp_path.with_name(file_name) - with actual_path.open("w") as outfile: - outfile.write("") - assert actual_path.exists() - assert actual_path.is_file() + config.scene_names = [scene_name] + file_name = _generate_file_name() + actual_path = tmp_path.with_name(file_name) + with actual_path.open("w") as outfile: + outfile.write("") + assert actual_path.exists() + assert actual_path.is_file() diff --git a/tests/test_linear_transformation_scene.py b/tests/test_linear_transformation_scene.py index c45e3f6997..7e9830cd5f 100644 --- a/tests/test_linear_transformation_scene.py +++ b/tests/test_linear_transformation_scene.py @@ -1,10 +1,11 @@ -from manim import RIGHT, UP, LinearTransformationScene, Vector, VGroup +from manim import RIGHT, UP, LinearTransformationScene, Manager, Vector, VGroup __module_test__ = "vector_space_scene" def test_ghost_vectors_len_and_types(): - scene = LinearTransformationScene() + manager = Manager(LinearTransformationScene) + scene = manager.scene scene.leave_ghost_vectors = True # prepare vectors (they require a vmobject as their target) diff --git a/tests/test_scene_rendering/conftest.py b/tests/test_scene_rendering/conftest.py index d7ae208524..7263a3f37c 100644 --- a/tests/test_scene_rendering/conftest.py +++ b/tests/test_scene_rendering/conftest.py @@ -4,8 +4,6 @@ import pytest -from manim import config, tempconfig - @pytest.fixture def manim_cfg_file(): @@ -18,50 +16,39 @@ def simple_scenes_path(): @pytest.fixture -def using_temp_config(tmpdir): - """Standard fixture that makes tests use a standard_config.cfg with a temp dir.""" - with tempconfig( - config.digest_file(Path(__file__).parent.parent / "standard_config.cfg"), - ): - config.media_dir = tmpdir - yield +def standard_config(config): + return config.digest_file(Path(__file__).parent.parent / "standard_config.cfg") @pytest.fixture -def using_temp_opengl_config(tmpdir): +def using_temp_config(tmpdir, standard_config): """Standard fixture that makes tests use a standard_config.cfg with a temp dir.""" - with tempconfig( - config.digest_file(Path(__file__).parent.parent / "standard_config.cfg"), - ): - config.media_dir = tmpdir - config.renderer = "opengl" - yield + standard_config.media_dir = tmpdir @pytest.fixture -def disabling_caching(): - with tempconfig({"disable_caching": True}): - yield +def using_temp_opengl_config(tmpdir, standard_config, using_opengl_renderer): + """Standard fixture that makes tests use a standard_config.cfg with a temp dir.""" + standard_config.media_dir = tmpdir @pytest.fixture -def infallible_scenes_path(): - return Path(__file__).parent / "infallible_scenes.py" +def disabling_caching(config): + config.disable_caching = True @pytest.fixture -def use_opengl_renderer(enable_preview): - with tempconfig({"renderer": "opengl", "preview": enable_preview}): - yield +def infallible_scenes_path(): + return Path(__file__).parent / "infallible_scenes.py" @pytest.fixture -def force_window_config_write_to_movie(): - with tempconfig({"force_window": True, "write_to_movie": True}): - yield +def force_window_config_write_to_movie(config): + config.force_window = True + config.write_to_movie = True @pytest.fixture -def force_window_config_pngs(): - with tempconfig({"force_window": True, "format": "png"}): - yield +def force_window_config_pngs(config): + config.force_window = True + config.format = "png" diff --git a/tests/test_scene_rendering/opengl/__init__.py b/tests/test_scene_rendering/opengl/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/test_scene_rendering/opengl/test_caching_related_opengl.py b/tests/test_scene_rendering/opengl/test_caching_related_opengl.py deleted file mode 100644 index c9c82a449b..0000000000 --- a/tests/test_scene_rendering/opengl/test_caching_related_opengl.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -import sys - -import pytest - -from manim import capture - -from ...utils.video_tester import video_comparison - - -@pytest.mark.slow -@video_comparison( - "SceneWithMultipleWaitCallsWithNFlag.json", - "videos/simple_scenes/480p15/SceneWithMultipleWaitCalls.mp4", -) -def test_wait_skip(tmp_path, manim_cfg_file, simple_scenes_path): - # Test for PR #468. Intended to test if wait calls are correctly skipped. - scene_name = "SceneWithMultipleWaitCalls" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "--write_to_movie", - "-ql", - "--media_dir", - str(tmp_path), - "-n", - "3", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - -@pytest.mark.slow -@video_comparison( - "SceneWithMultiplePlayCallsWithNFlag.json", - "videos/simple_scenes/480p15/SceneWithMultipleCalls.mp4", -) -def test_play_skip(tmp_path, manim_cfg_file, simple_scenes_path): - # Intended to test if play calls are correctly skipped. - scene_name = "SceneWithMultipleCalls" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "--write_to_movie", - "-ql", - "--media_dir", - str(tmp_path), - "-n", - "3", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err diff --git a/tests/test_scene_rendering/opengl/test_cli_flags_opengl.py b/tests/test_scene_rendering/opengl/test_cli_flags_opengl.py deleted file mode 100644 index 5b3a299dc5..0000000000 --- a/tests/test_scene_rendering/opengl/test_cli_flags_opengl.py +++ /dev/null @@ -1,692 +0,0 @@ -from __future__ import annotations - -import sys - -import numpy as np -import pytest -from click.testing import CliRunner -from PIL import Image - -from manim import capture, get_video_metadata -from manim.__main__ import __version__, main -from manim.utils.file_ops import add_version_before_extension -from tests.utils.video_tester import video_comparison - - -@pytest.mark.slow -@video_comparison( - "SquareToCircleWithDefaultValues.json", - "videos/simple_scenes/1080p60/SquareToCircle.mp4", -) -def test_basic_scene_with_default_values(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "--write_to_movie", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - -@pytest.mark.slow -def test_resolution_flag(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "NoAnimations" - # test different separators - resolutions = [ - (720, 480, ";"), - (1280, 720, ","), - (1920, 1080, "-"), - (2560, 1440, ";"), - # (3840, 2160, ","), - # (640, 480, "-"), - # (800, 600, ";"), - ] - - for width, height, separator in resolutions: - command = [ - sys.executable, - "-m", - "manim", - "--media_dir", - str(tmp_path), - "--resolution", - f"{width}{separator}{height}", - str(simple_scenes_path), - scene_name, - ] - - _, err, exit_code = capture(command) - assert exit_code == 0, err - - path = ( - tmp_path / "videos" / "simple_scenes" / f"{height}p60" / f"{scene_name}.mp4" - ) - meta = get_video_metadata(path) - assert (width, height) == (meta["width"], meta["height"]) - - -@pytest.mark.slow -@video_comparison( - "SquareToCircleWithlFlag.json", - "videos/simple_scenes/480p15/SquareToCircle.mp4", -) -def test_basic_scene_l_flag(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--write_to_movie", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - -@pytest.mark.slow -@video_comparison( - "SceneWithMultipleCallsWithNFlag.json", - "videos/simple_scenes/480p15/SceneWithMultipleCalls.mp4", -) -def test_n_flag(tmp_path, simple_scenes_path): - scene_name = "SceneWithMultipleCalls" - command = [ - sys.executable, - "-m", - "manim", - "-ql", - "--renderer", - "opengl", - "--write_to_movie", - "-n 3,6", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - _, err, exit_code = capture(command) - assert exit_code == 0, err - - -@pytest.mark.slow -def test_s_flag_no_animations(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "NoAnimations" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "-s", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with -s flag rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim with -s flag did not render an image" - - -@pytest.mark.slow -def test_image_output_for_static_scene(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "StaticScene" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with static scene rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim without animations did not render an image" - - -@pytest.mark.slow -def test_no_image_output_with_interactive_embed( - tmp_path, manim_cfg_file, simple_scenes_path -): - """Check no image is output for a static scene when interactive embed is called""" - scene_name = "InteractiveStaticScene" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with static scene rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert ( - is_empty - ), "running manim static scene with interactive embed rendered an image" - - -@pytest.mark.slow -def test_no_default_image_output_with_non_static_scene( - tmp_path, manim_cfg_file, simple_scenes_path -): - scene_name = "SceneWithNonStaticWait" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with static scene rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert ( - is_empty - ), "running manim static scene with interactive embed rendered an image" - - -@pytest.mark.slow -def test_image_output_for_static_scene_with_write_to_movie( - tmp_path, manim_cfg_file, simple_scenes_path -): - scene_name = "StaticScene" - command = [ - sys.executable, - "-m", - "manim", - "--write_to_movie", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - is_empty = not any((tmp_path / "videos").iterdir()) - assert not is_empty, "running manim with static scene rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim without animations did not render an image" - - -@pytest.mark.slow -def test_s_flag(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "-s", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with -s flag rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim with -s flag did not render an image" - - -@pytest.mark.slow -def test_r_flag(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "-s", - "--media_dir", - str(tmp_path), - "-r", - "200,100", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - is_not_empty = any((tmp_path / "images").iterdir()) - assert is_not_empty, "running manim with -s, -r flag did not render a file" - - filename = add_version_before_extension( - tmp_path / "images" / "simple_scenes" / "SquareToCircle.png", - ) - assert np.asarray(Image.open(filename)).shape == (100, 200, 4) - - -@pytest.mark.slow -def test_a_flag(tmp_path, manim_cfg_file, infallible_scenes_path): - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "--write_to_movie", - "-ql", - "--media_dir", - str(tmp_path), - "-a", - str(infallible_scenes_path), - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - one_is_not_empty = ( - tmp_path / "videos" / "infallible_scenes" / "480p15" / "Wait1.mp4" - ).is_file() - assert one_is_not_empty, "running manim with -a flag did not render the first scene" - - two_is_not_empty = ( - tmp_path / "images" / "infallible_scenes" / f"Wait2_ManimCE_v{__version__}.png" - ).is_file() - assert two_is_not_empty, "running manim with -a flag did not render an image, possible leak of the config dictionary" - - three_is_not_empty = ( - tmp_path / "videos" / "infallible_scenes" / "480p15" / "Wait3.mp4" - ).is_file() - assert ( - three_is_not_empty - ), "running manim with -a flag did not render the second scene" - - -@pytest.mark.slow -def test_custom_folders(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "-s", - "--media_dir", - str(tmp_path), - "--custom_folders", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "--custom_folders produced a 'videos/' dir" - - exists = add_version_before_extension(tmp_path / "SquareToCircle.png").exists() - assert exists, "--custom_folders did not produce the output file" - - -@pytest.mark.slow -def test_dash_as_filename(tmp_path): - code = ( - "class Test(Scene):\n" - " def construct(self):\n" - " self.add(Circle())\n" - " self.wait()" - ) - command = [ - "-ql", - "--renderer", - "opengl", - "-s", - "--media_dir", - str(tmp_path), - "-", - ] - runner = CliRunner() - result = runner.invoke(main, command, input=code) - assert result.exit_code == 0 - exists = add_version_before_extension( - tmp_path / "images" / "-" / "Test.png", - ).exists() - assert exists, result.output - - -@pytest.mark.slow -def test_gif_format_output(tmp_path, manim_cfg_file, simple_scenes_path): - """Test only gif created with manim version in file name when --format gif is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "gif", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_mp4_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mp4" - ) - assert not unexpected_mp4_path.exists(), "unexpected mp4 file found at " + str( - unexpected_mp4_path, - ) - - expected_gif_path = add_version_before_extension( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.gif" - ) - assert expected_gif_path.exists(), "gif file not found at " + str(expected_gif_path) - - -@pytest.mark.slow -def test_mp4_format_output(tmp_path, manim_cfg_file, simple_scenes_path): - """Test only mp4 created without manim version in file name when --format mp4 is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "mp4", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_gif_path = add_version_before_extension( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.gif" - ) - assert not unexpected_gif_path.exists(), "unexpected gif file found at " + str( - unexpected_gif_path, - ) - - expected_mp4_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mp4" - ) - assert expected_mp4_path.exists(), "expected mp4 file not found at " + str( - expected_mp4_path, - ) - - -@pytest.mark.slow -def test_videos_not_created_when_png_format_set( - tmp_path, - manim_cfg_file, - simple_scenes_path, -): - """Test mp4 and gifs are not created when --format png is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "png", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_gif_path = add_version_before_extension( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.gif" - ) - assert not unexpected_gif_path.exists(), "unexpected gif file found at " + str( - unexpected_gif_path, - ) - - unexpected_mp4_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mp4" - ) - assert not unexpected_mp4_path.exists(), "expected mp4 file not found at " + str( - unexpected_mp4_path, - ) - - -@pytest.mark.slow -def test_images_are_created_when_png_format_set( - tmp_path, - manim_cfg_file, - simple_scenes_path, -): - """Test images are created in media directory when --format png is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "png", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - expected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle0000.png" - assert expected_png_path.exists(), "png file not found at " + str(expected_png_path) - - -@pytest.mark.slow -def test_images_are_zero_padded_when_zero_pad_set( - tmp_path, - manim_cfg_file, - simple_scenes_path, -): - """Test images are zero padded when --format png and --zero_pad n are set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "png", - "--zero_pad", - "3", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle0.png" - assert not unexpected_png_path.exists(), "non zero padded png file found at " + str( - unexpected_png_path, - ) - - expected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle000.png" - assert expected_png_path.exists(), "png file not found at " + str(expected_png_path) - - -@pytest.mark.slow -def test_webm_format_output(tmp_path, manim_cfg_file, simple_scenes_path): - """Test only webm created when --format webm is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "webm", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_mp4_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mp4" - ) - assert not unexpected_mp4_path.exists(), "unexpected mp4 file found at " + str( - unexpected_mp4_path, - ) - - expected_webm_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.webm" - ) - assert expected_webm_path.exists(), "expected webm file not found at " + str( - expected_webm_path, - ) - - -@pytest.mark.slow -def test_default_format_output_for_transparent_flag( - tmp_path, - manim_cfg_file, - simple_scenes_path, -): - """Test .mov is created by default when transparent flag is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--write_to_movie", - "--media_dir", - str(tmp_path), - "-t", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_webm_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.webm" - ) - assert not unexpected_webm_path.exists(), "unexpected webm file found at " + str( - unexpected_webm_path, - ) - - expected_mov_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mov" - ) - assert expected_mov_path.exists(), "expected .mov file not found at " + str( - expected_mov_path, - ) - - -@pytest.mark.slow -def test_mov_can_be_set_as_output_format(tmp_path, manim_cfg_file, simple_scenes_path): - """Test .mov is created by when set using --format mov arg""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "mov", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_webm_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.webm" - ) - assert not unexpected_webm_path.exists(), "unexpected webm file found at " + str( - unexpected_webm_path, - ) - - expected_mov_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mov" - ) - assert expected_mov_path.exists(), "expected .mov file not found at " + str( - expected_mov_path, - ) diff --git a/tests/test_scene_rendering/opengl/test_opengl_renderer.py b/tests/test_scene_rendering/opengl/test_opengl_renderer.py deleted file mode 100644 index 7b2c95206d..0000000000 --- a/tests/test_scene_rendering/opengl/test_opengl_renderer.py +++ /dev/null @@ -1,112 +0,0 @@ -from __future__ import annotations - -import platform -from unittest.mock import Mock - -import pytest - -from manim.camera.camera import OpenGLRenderer -from tests.assert_utils import assert_file_exists -from tests.test_scene_rendering.simple_scenes import * - - -def test_write_to_movie_disables_window(using_temp_opengl_config, disabling_caching): - """write_to_movie should disable window by default""" - scene = SquareToCircle() - renderer = scene.renderer - renderer.update_frame = Mock(wraps=renderer.update_frame) - scene.render() - assert renderer.window is None - assert_file_exists(config["output_file"]) - - -@pytest.mark.skip(reason="Temporarily skip due to failing in Windows CI") # type: ignore -def test_force_window_opengl_render_with_movies( - using_temp_opengl_config, - force_window_config_write_to_movie, - disabling_caching, -): - """force_window creates window when write_to_movie is set""" - scene = SquareToCircle() - renderer = scene.renderer - renderer.update_frame = Mock(wraps=renderer.update_frame) - scene.render() - assert renderer.window is not None - assert_file_exists(config["output_file"]) - renderer.window.close() - - -@pytest.mark.skipif( - platform.processor() == "aarch64", reason="Fails on Linux-ARM runners" -) -def test_force_window_opengl_render_with_format( - using_temp_opengl_config, - force_window_config_pngs, - disabling_caching, -): - """force_window creates window when format is set""" - scene = SquareToCircle() - renderer = scene.renderer - renderer.update_frame = Mock(wraps=renderer.update_frame) - scene.render() - assert renderer.window is not None - renderer.window.close() - - -@pytest.mark.parametrize("enable_preview", [False]) -def test_get_frame_with_preview_disabled(use_opengl_renderer): - """Get frame is able to fetch frame with the correct dimensions when preview is disabled""" - scene = SquareToCircle() - assert isinstance(scene.renderer, OpenGLRenderer) - assert not config.preview - - renderer = scene.renderer - renderer.update_frame(scene) - frame = renderer.get_frame() - - # height and width are flipped - assert renderer.get_pixel_shape()[0] == frame.shape[1] - assert renderer.get_pixel_shape()[1] == frame.shape[0] - - -@pytest.mark.slow -@pytest.mark.parametrize("enable_preview", [True]) -def test_get_frame_with_preview_enabled(use_opengl_renderer): - """Get frame is able to fetch frame with the correct dimensions when preview is enabled""" - scene = SquareToCircle() - assert isinstance(scene.renderer, OpenGLRenderer) - assert config.preview is True - - renderer = scene.renderer - renderer.update_frame(scene) - frame = renderer.get_frame() - - # height and width are flipped - assert renderer.get_pixel_shape()[0] == frame.shape[1] - assert renderer.get_pixel_shape()[1] == frame.shape[0] - - -@pytest.mark.parametrize("enable_preview", [True]) -def test_pixel_coords_to_space_coords(use_opengl_renderer): - scene = SquareToCircle() - assert isinstance(scene.renderer, OpenGLRenderer) - - renderer = scene.renderer - renderer.update_frame(scene) - - px, py = 3, 2 - pw, ph = renderer.get_pixel_shape() - _, fh = renderer.camera.get_shape() - fc = renderer.camera.get_center() - - ex = fc[0] + (fh / ph) * (px - pw / 2) - ey = fc[1] + (fh / ph) * (py - ph / 2) - ez = fc[2] - - assert ( - renderer.pixel_coords_to_space_coords(px, py) == np.array([ex, ey, ez]) - ).all() - assert ( - renderer.pixel_coords_to_space_coords(px, py, top_left=True) - == np.array([ex, -ey, ez]) - ).all() diff --git a/tests/test_scene_rendering/opengl/test_play_logic_opengl.py b/tests/test_scene_rendering/opengl/test_play_logic_opengl.py deleted file mode 100644 index cb640c5f80..0000000000 --- a/tests/test_scene_rendering/opengl/test_play_logic_opengl.py +++ /dev/null @@ -1,120 +0,0 @@ -from __future__ import annotations - -import sys -from unittest.mock import Mock - -import pytest - -from manim import * -from manim import config - -from ..simple_scenes import ( - SceneForFrozenFrameTests, - SceneWithMultipleCalls, - SceneWithNonStaticWait, - SceneWithSceneUpdater, - SceneWithStaticWait, - SquareToCircle, -) - - -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Mock object has a different implementation in python 3.7, which makes it broken with this logic.", -) -@pytest.mark.parametrize("frame_rate", argvalues=[15, 30, 60]) -def test_t_values(using_temp_opengl_config, disabling_caching, frame_rate): - """Test that the framerate corresponds to the number of t values generated""" - config.frame_rate = frame_rate - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.render() - assert scene.update_to_time.call_count == config["frame_rate"] - np.testing.assert_allclose( - ([call.args[0] for call in scene.update_to_time.call_args_list]), - np.arange(0, 1, 1 / config["frame_rate"]), - ) - - -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Mock object has a different implementation in python 3.7, which makes it broken with this logic.", -) -def test_t_values_with_skip_animations(using_temp_opengl_config, disabling_caching): - """Test the behaviour of scene.skip_animations""" - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.renderer._original_skipping_status = True - scene.render() - assert scene.update_to_time.call_count == 1 - np.testing.assert_almost_equal( - scene.update_to_time.call_args.args[0], - 1.0, - ) - - -def test_static_wait_detection(using_temp_opengl_config, disabling_caching): - """Test if a static wait (wait that freeze the frame) is correctly detected""" - scene = SceneWithStaticWait() - scene.render() - # Test is is_static_wait of the Wait animation has been set to True by compile_animation_ata - assert scene.animations[0].is_static_wait - assert scene.is_current_animation_frozen_frame() - - -def test_non_static_wait_detection(using_temp_opengl_config, disabling_caching): - scene = SceneWithNonStaticWait() - scene.render() - assert not scene.animations[0].is_static_wait - assert not scene.is_current_animation_frozen_frame() - scene = SceneWithSceneUpdater() - scene.render() - assert not scene.animations[0].is_static_wait - assert not scene.is_current_animation_frozen_frame() - - -def test_frozen_frame(using_temp_opengl_config, disabling_caching): - scene = SceneForFrozenFrameTests() - scene.render() - assert scene.mobject_update_count == 0 - assert scene.scene_update_count == 0 - - -@pytest.mark.xfail(reason="Should be fixed in #2133") -def test_t_values_with_cached_data(using_temp_opengl_config): - """Test the proper generation and use of the t values when an animation is cached.""" - scene = SceneWithMultipleCalls() - # Mocking the file_writer will skip all the writing process. - scene.renderer.file_writer = Mock(scene.renderer.file_writer) - # Simulate that all animations are cached. - scene.renderer.file_writer.is_already_cached.return_value = True - scene.update_to_time = Mock() - - scene.render() - assert scene.update_to_time.call_count == 10 - - -@pytest.mark.xfail(reason="Not currently handled correctly for opengl") -def test_t_values_save_last_frame(using_temp_opengl_config): - """Test that there is only one t value handled when only saving the last frame""" - config.save_last_frame = True - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.render() - scene.update_to_time.assert_called_once_with(1) - - -def test_animate_with_changed_custom_attribute(using_temp_opengl_config): - """Test that animating the change of a custom attribute - using the animate syntax works correctly. - """ - - class CustomAnimateScene(Scene): - def construct(self): - vt = ValueTracker(0) - vt.custom_attribute = "hello" - self.play(vt.animate.set_value(42).set(custom_attribute="world")) - assert vt.get_value() == 42 - assert vt.custom_attribute == "world" - - CustomAnimateScene().render() diff --git a/tests/test_scene_rendering/simple_scenes.py b/tests/test_scene_rendering/simple_scenes.py index 8f4dd9d434..922a93ee6c 100644 --- a/tests/test_scene_rendering/simple_scenes.py +++ b/tests/test_scene_rendering/simple_scenes.py @@ -4,6 +4,21 @@ from manim import * +__all__ = [ + "SquareToCircle", + "SceneWithMultipleCalls", + "SceneWithMultipleWaitCalls", + "NoAnimations", + "SceneWithStaticWait", + "SceneWithSceneUpdater", + "SceneForFrozenFrameTests", + "SceneWithNonStaticWait", + "StaticScene", + "InteractiveStaticScene", + "SceneWithSections", + "ElaborateSceneWithSections", +] + class SquareToCircle(Scene): def construct(self): diff --git a/tests/test_scene_rendering/test_cairo_renderer.py b/tests/test_scene_rendering/test_cairo_renderer.py deleted file mode 100644 index 5134578aa3..0000000000 --- a/tests/test_scene_rendering/test_cairo_renderer.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -from unittest.mock import Mock, patch - -import pytest - -from manim import * - -from ..assert_utils import assert_file_exists -from .simple_scenes import * - - -def test_render(using_temp_config, disabling_caching): - scene = SquareToCircle() - renderer = scene.renderer - renderer.update_frame = Mock(wraps=renderer.update_frame) - renderer.add_frame = Mock(wraps=renderer.add_frame) - scene.render() - assert renderer.add_frame.call_count == config["frame_rate"] - assert renderer.update_frame.call_count == config["frame_rate"] - assert_file_exists(config["output_file"]) - - -def test_skipping_status_with_from_to_and_up_to(using_temp_config, disabling_caching): - """Test if skip_animations is well updated when -n flag is passed""" - config.from_animation_number = 2 - config.upto_animation_number = 6 - - class SceneWithMultipleCalls(Scene): - def construct(self): - number = Integer(0) - self.add(number) - for i in range(10): - self.play(Animation(Square())) - - assert ((i >= 2) and (i <= 6)) or self.renderer.skip_animations - - SceneWithMultipleCalls().render() - - -@pytest.mark.xfail(reason="caching issue") -def test_when_animation_is_cached(using_temp_config): - partial_movie_files = [] - for _ in range(2): - # Render several times to generate a cache. - # In some edgy cases and on some OS, a same scene can produce - # a (finite, generally 2) number of different hash. In this case, the scene wouldn't be detected as cached, making the test fail. - scene = SquareToCircle() - scene.render() - partial_movie_files.append(scene.renderer.file_writer.partial_movie_files) - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.render() - assert scene.renderer.file_writer.is_already_cached( - scene.renderer.animations_hashes[0], - ) - # Check that the same partial movie files has been used (with he same hash). - # As there might have been several hashes, a list is used. - assert scene.renderer.file_writer.partial_movie_files in partial_movie_files - # Check that manim correctly skipped the animation. - scene.update_to_time.assert_called_once_with(1) - # Check that the output video has been generated. - assert_file_exists(config["output_file"]) - - -def test_hash_logic_is_not_called_when_caching_is_disabled( - using_temp_config, - disabling_caching, -): - with patch("manim.renderer.cairo_renderer.get_hash_from_play_call") as mocked: - scene = SquareToCircle() - scene.render() - mocked.assert_not_called() - assert_file_exists(config["output_file"]) - - -def test_hash_logic_is_called_when_caching_is_enabled(using_temp_config): - from manim.renderer.cairo_renderer import get_hash_from_play_call - - with patch( - "manim.renderer.cairo_renderer.get_hash_from_play_call", - wraps=get_hash_from_play_call, - ) as mocked: - scene = SquareToCircle() - scene.render() - mocked.assert_called_once() diff --git a/tests/test_scene_rendering/test_file_writer.py b/tests/test_scene_rendering/test_file_writer.py index 6ccc8b25d0..562fc15644 100644 --- a/tests/test_scene_rendering/test_file_writer.py +++ b/tests/test_scene_rendering/test_file_writer.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from manim import DR, Circle, Create, Scene, Star, tempconfig +from manim import DR, Circle, Create, Manager, Scene, Star from manim.utils.commands import capture, get_video_metadata @@ -32,18 +32,14 @@ def construct(self): "transparent", [False, True], ) -def test_gif_writing(tmp_path, transparent): +def test_gif_writing(tmp_path, config, transparent): output_filename = f"gif_{'transparent' if transparent else 'opaque'}" - with tempconfig( - { - "media_dir": tmp_path, - "quality": "low_quality", - "format": "gif", - "transparent": transparent, - "output_file": output_filename, - } - ): - StarScene().render() + config.media_dir = tmp_path + config.quality = "low_quality" + config.format = "gif" + config.transparent = transparent + config.output_file = output_filename + Manager(StarScene).render() video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.gif" assert video_path.exists() @@ -90,18 +86,14 @@ def test_gif_writing(tmp_path, transparent): ("webm", True, "vp9", "yuv420p"), ], ) -def test_codecs(tmp_path, format, transparent, codec, pixel_format): +def test_codecs(tmp_path, config, format, transparent, codec, pixel_format): output_filename = f"codec_{format}_{'transparent' if transparent else 'opaque'}" - with tempconfig( - { - "media_dir": tmp_path, - "quality": "low_quality", - "format": format, - "transparent": transparent, - "output_file": output_filename, - } - ): - StarScene().render() + config.media_dir = tmp_path + config.quality = "low_quality" + config.format = format + config.transparent = transparent + config.output_file = output_filename + Manager(StarScene).render() video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.{format}" assert video_path.exists() diff --git a/tests/test_scene_rendering/test_play_logic.py b/tests/test_scene_rendering/test_play_logic.py index a0d97920bd..6c0f362474 100644 --- a/tests/test_scene_rendering/test_play_logic.py +++ b/tests/test_scene_rendering/test_play_logic.py @@ -1,12 +1,18 @@ from __future__ import annotations -import sys from unittest.mock import Mock import pytest -from manim import * -from manim import config +from manim import ( + Dot, + Manager, + Mobject, + Scene, + ValueTracker, + Wait, + np, +) from .simple_scenes import ( SceneForFrozenFrameTests, @@ -18,46 +24,43 @@ ) -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Mock object has a different implementation in python 3.7, which makes it broken with this logic.", -) @pytest.mark.parametrize("frame_rate", argvalues=[15, 30, 60]) -def test_t_values(using_temp_config, disabling_caching, frame_rate): - """Test that the framerate corresponds to the number of t values generated""" +def test_t_values(config, using_temp_config, disabling_caching, frame_rate): + """Test that the framerate corresponds to the number of times animations are updated""" + config.frame_rate = frame_rate - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.render() - assert scene.update_to_time.call_count == config["frame_rate"] + manager = Manager(SquareToCircle) + scene = manager.scene + scene._update_animations = Mock() + manager.render() + assert scene._update_animations.call_count == config["frame_rate"] np.testing.assert_allclose( - ([call.args[0] for call in scene.update_to_time.call_args_list]), + ([call.args[1] for call in scene._update_animations.call_args_list]), np.arange(0, 1, 1 / config["frame_rate"]), ) -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Mock object has a different implementation in python 3.7, which makes it broken with this logic.", -) def test_t_values_with_skip_animations(using_temp_config, disabling_caching): """Test the behaviour of scene.skip_animations""" - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.renderer._original_skipping_status = True - scene.render() - assert scene.update_to_time.call_count == 1 + manager = Manager(SquareToCircle) + manager._skip_animations = True + scene = manager.scene + scene._update_animations = Mock() + manager.render() + assert scene._update_animations.call_count == 1 np.testing.assert_almost_equal( - scene.update_to_time.call_args.args[0], + scene._update_animations.call_args.args[1], 1.0, ) +# TODO: Rework Wait animation def test_static_wait_detection(using_temp_config, disabling_caching): """Test if a static wait (wait that freeze the frame) is correctly detected""" - scene = SceneWithStaticWait() - scene.render() + manager = Manager(SceneWithStaticWait) + manager.render() # Test is is_static_wait of the Wait animation has been set to True by compile_animation_ata + scene = manager.scene assert scene.animations[0].is_static_wait assert scene.is_current_animation_frozen_frame() @@ -76,21 +79,21 @@ def test_non_static_wait_detection(using_temp_config, disabling_caching): def test_wait_with_stop_condition(using_temp_config, disabling_caching): class TestScene(Scene): def construct(self): - self.wait_until(lambda: self.renderer.time >= 1) - assert self.renderer.time >= 1 + self.wait_until(lambda: self.time >= 1) + assert self.time >= 1 d = Dot() d.add_updater(lambda mobj, dt: self.add(Mobject())) self.add(d) self.play(Wait(run_time=5, stop_condition=lambda: len(self.mobjects) > 5)) assert len(self.mobjects) > 5 - assert self.renderer.time < 2 + assert self.time < 2 - scene = TestScene() - scene.render() + manager = Manager(TestScene) + manager.render() def test_frozen_frame(using_temp_config, disabling_caching): - scene = SceneForFrozenFrameTests() + scene = Manager(SceneForFrozenFrameTests) scene.render() assert scene.mobject_update_count == 0 assert scene.scene_update_count == 0 @@ -110,7 +113,7 @@ def test_t_values_with_cached_data(using_temp_config): assert scene.update_to_time.call_count == 10 -def test_t_values_save_last_frame(using_temp_config): +def test_t_values_save_last_frame(config, using_temp_config): """Test that there is only one t value handled when only saving the last frame""" config.save_last_frame = True scene = SquareToCircle() @@ -132,4 +135,4 @@ def construct(self): assert vt.get_value() == 42 assert vt.custom_attribute == "world" - CustomAnimateScene().render() + Manager(CustomAnimateScene).render()