Merge branch 'experimental' of https://github.com/ManimCommunity/manim into exp_replace_openglmob_imports

This commit is contained in:
Francisco Manríquez Novoa 2026-02-09 14:41:57 -03:00
commit 41f40657c9
38 changed files with 1452 additions and 630 deletions

View file

@ -1,10 +1,6 @@
<!-- Thank you for contributing to Manim! Learn more about the process in our contributing guidelines: https://docs.manim.community/en/latest/contributing.html -->
## Overview: What does this pull request change?
<!-- If there is more information than the PR title that should be added to our release changelog, add it in the following changelog section. This is optional, but recommended for larger pull requests. -->
<!--changelog-start-->
<!--changelog-end-->
## Motivation and Explanation: Why and how do your changes improve the library?
<!-- Optional for bugfixes, small enhancements, and documentation-related PRs. Otherwise, please give a short reasoning for your changes. -->

View file

@ -2,15 +2,17 @@
Changelog
#########
This page contains a list of changes made between releases. Changes
from versions that are not listed below (in particular patch-level
releases since v0.18.0) are documented on our
`GitHub release page <https://github.com/ManimCommunity/manim/releases/>`__.
This page contains a list of changes made between releases.
.. toctree::
:maxdepth: 1
changelog/experimental
changelog/0.19.2-changelog
changelog/0.19.1-changelog
changelog/0.19.0-changelog
changelog/0.18.1-changelog
changelog/0.18.0.post0-changelog
changelog/0.18.0-changelog
changelog/0.17.3-changelog
changelog/0.17.2-changelog

View file

@ -0,0 +1,9 @@
*************
v0.18.0.post0
*************
:Date: April 08, 2024
This release is a post-release fixing `#3676
<https://github.com/ManimCommunity/manim/issues/3676>`_, a bug caused by a recent
change introduced to the way how SVG files of text are generated by Pango.

View file

@ -0,0 +1,160 @@
---
short-title: v0.18.1
description: Changelog for Manim v0.18.1
---
# v0.18.1
Date
: April 28, 2024
## What's Changed
### Breaking Changes and Deprecations
* Removed deprecated `manim new` command by {user}`chopan050` in {pr}`3512`
* Removed support for dynamic plugin imports by {user}`Viicos` in {pr}`3524`
* Remove meth:``.Mobject.wag`` by {user}`JasonGrace2282` in {pr}`3539`
* Remove deprecated parameters and animations by {user}`JasonGrace2282` in {pr}`3688`
### New Features
* Added `cap_style` feature to `VMobject` by {user}`MathItYT` in {pr}`3516`
* Allow hiding version splash by {user}`jeertmans` in {pr}`3329`
* Added the ability to pass lists and generators to `Scene.play()` by {user}`MrDiver` in {pr}`3365`
* Added ``--preview_command`` cli flag by {user}`JasonGrace2282` in {pr}`3615`
### Fixed Bugs and Enhancements
* Allow accessing ghost vectors in :class:`.LinearTransformationScene` by {user}`JasonGrace2282` in {pr}`3435`
* Optimized `get_unit_normal()` and replaced `np.cross()` with custom `cross()` in `manim.utils.space_ops` by {user}`chopan050` in {pr}`3494`
* Implement caching of fonts list to improve runtime performance by {user}`MrDiver` in {pr}`3316`
* Reformatting the `--save_sections` output to have the format `<Scene>_<SecNum>_<SecName><extension>` by {user}`doaamuham` in {pr}`3499`
* Account for dtype in the pixel array so the maximum value stays correct in the invert function by {user}`jeertmans` in {pr}`3493`
* Added `grid_lines` attribute to `Rectangle` to add individual styling to the grid lines by {user}`RobinPH` in {pr}`3428`
* Fixed rectangle grid properties (#3082) by {user}`pauluhlenbruck` in {pr}`3513`
* Fixed animations with zero runtime length to give a useful error instead of a broken pipe by {user}`MrDiver` in {pr}`3491`
* Fixed stroke width being ignored by `StreamLines` with a single color by {user}`yashm277` in {pr}`3436`
* Fixed formatting of ``MoveAlongPath`` docs by {user}`JasonGrace2282` in {pr}`3541`
* Added helpful hints to `VGroup.add()` error message by {user}`vvolhejn` in {pr}`3561`
* Made `earclip_triangulation` more robust by {user}`hydromelvictor` in {pr}`3574`
* Refactored `TexTemplate` by {user}`Viicos` in {pr}`3520`
* Fixed `write_subcaption_file` error when using OpenGL renderer by {user}`yuan-xy` in {pr}`3546`
* Fixed `get_arc_center()` returning reference of point by {user}`sparshg` in {pr}`3599`
* Improved handling of specified font name by {user}`staghado` in {pr}`3429`
* Fixing the behavior of `.become` to not modify target mobject via side effects fix color linking by {user}`MrDiver` in {pr}`3508`
* Fixed bug in :class:`.VMobjectFromSVGPath` by {user}`abul4fia` in {pr}`3677`
* Fix for windows cp1252 encoding failure (fix test pipeline) by {user}`JasonGrace2282` in {pr}`3687`
* Fix NameError in try... except by {user}`JasonGrace2282` in {pr}`3694`
* Fix successive calls of :meth:`.LinearTransformationScene.apply_matrix` by {user}`SirJamesClarkMaxwell` in {pr}`3675`
* Fixed `Mobject.put_start_and_end_on` with same start and end point by {user}`MontroyJosh` in {pr}`3718`
* Fixed issue where `SpiralIn` doesn't show elements by {user}`Gixtox` in {pr}`3589`
* Cleaned `Graph` layouts and increase flexibility by {user}`Nikhil-42` in {pr}`3434`
* `AnimationGroup`: optimized `interpolate()` and fixed alpha bug on `finish()` by {user}`chopan050` in {pr}`3542`
* Fixed warning about missing plugin `""` by {user}`behackl` in {pr}`3734`
### Documentation
* Typo in `indication` documentation by {user}`jcep` in {pr}`3477`
* Fixed typo: 360° to 180° in quickstart tutorial by {user}`szchixy` in {pr}`3498`
* Fixed typo in mobject docstring: `line` -> `square` by {user}`yuan-xy` in {pr}`3509`
* Explain ``.Transform`` vs ``.ReplacementTransform`` in quickstart examples by {user}`JasonGrace2282` in {pr}`3500`
* Fixed formatting in building blocks tutorial by {user}`MrDiver` in {pr}`3515`
* Fixed `Indicate` docstring typo by {user}`Lawqup` in {pr}`3461`
* Added Documentation to `.to_edge` and `to_corner` by {user}`TheMathematicFanatic` in {pr}`3408`
* Added some words about Cairo 1.18 by {user}`jeertmans` in {pr}`3530`
* Fixed typo of `get_y_axis_label` parameter documentation by {user}`yuan-xy` in {pr}`3547`
* Added note in docstring of `ManimColor` about class constructors by {user}`JasonGrace2282` in {pr}`3554`
* Improve documentation section about contributing to docs by {user}`chopan050` in {pr}`3555`
* Removed duplicated documentation for -s / --save_last_frame CLI flag by {user}`Gixtox` in {pr}`3528`
* Updated Docker instructions to use bash from the PATH by {user}`NotWearingPants` in {pr}`3582`
* Fixed typo in `value_tracker.py` by {user}`yuan-xy` in {pr}`3594`
* Added `ref_class` for `BooleanOperations` in Example Gallery by {user}`JasonGrace2282` in {pr}`3598`
* Changed `Vector3` to `Vector3D` in contributing docs by {user}`JasonGrace2282` in {pr}`3639`
* Added some examples for `Mobject`/`VMobject` methods by {user}`JasonGrace2282` in {pr}`3641`
* Fixed broken link to Poetry's installation guide in the documentation by {user}`biinnnggggg` in {pr}`3692`
* Fixed minor grammatical errors found in the index page of the documentation by {user}`biinnnggggg` in {pr}`3690`
* Fixed typo on page about translations by {user}`biinnnggggg` in {pr}`3696`
* Fixed outdated description of CLI option in Manim's Output Settings by {user}`HairlessVillager` in {pr}`3674`
* Mention pixi in installation guide by {user}`pavelzw` in {pr}`3678`
* Updated typing guidelines by {user}`JasonGrace2282` in {pr}`3704`
* Updated documentation and typings for `ParametricFunction` by {user}`danielzsh` in {pr}`3703`
* Fixed docstring markup in `Rotate` by {user}`TheCrowned` in {pr}`3721`
* Improve consistency in axis label example by {user}`amrear` in {pr}`3730`
### Maintenance and Testing
* Fixed wrong path in action building downloadable docs by {user}`behackl` in {pr}`3450`
* Add type hints to `_config` by {user}`Viicos` in {pr}`3440`
* Update dependency constraints, fix deprecation warnings by {user}`Viicos` in {pr}`3376`
* Update Docker base image to python3.12-slim (#3458) by {user}`PikaBlue107` in {pr}`3459`
* Fixed `line_join` to `joint_type` in example_scenes/basic.py by {user}`szchixy` in {pr}`3510`
* Fixed :attr:`.Mobject.animate` type-hint to allow LSP autocomplete by {user}`JasonGrace2282` in {pr}`3543`
* Finish TODO's in ``contributing/typings.rst`` by {user}`JasonGrace2282` in {pr}`3545`
* Fixed use of `Mobject`'s deprecated `get_*()` and `set_*()` methods in Cairo tests by {user}`JasonGrace2282` in {pr}`3549`
* Added support for Manim type aliases in Sphinx docs and added new TypeAliases by {user}`chopan050` in {pr}`3484`
* Fixed typing of `Animation` by {user}`dandavison` in {pr}`3568`
* Added some TODOs for future use of `ManimFrame` by {user}`chopan050` in {pr}`3553`
* Fixed typehint of :attr:`InternalPoint2D_Array` by {user}`JasonGrace2282` in {pr}`3592`
* Fixed error in Windows CI pipeline by {user}`behackl` in {pr}`3611`
* Fixed type hint of indication.py by {user}`yuan-xy` in {pr}`3613`
* Revert vector type aliases to NumPy ndarrays by {user}`chopan050` in {pr}`3595`
* Run `poetry lock --no-update` by {user}`JasonGrace2282` in {pr}`3621`
* Code Cleanup: removing unused imports and global variables by {user}`JasonGrace2282` in {pr}`3620`
* Fixed type hint of `Vector` direction parameter by {user}`JasonGrace2282` in {pr}`3640`
* Flake8 rule C901 is about McCabe code complexity by {user}`cclauss` in {pr}`3673`
* Updated year in license by {user}`JasonGrace2282` in {pr}`3689`
* Automated copyright updating for docs by {user}`JasonGrace2282` in {pr}`3708`
* Fixed some typehints in `mobject.py` by {user}`JasonGrace2282` in {pr}`3668`
* Search for type aliases if TYPE_CHECKING by {user}`JasonGrace2282` in {pr}`3671`
* Follow-up to graph layout cleanup: improvements for tests and typing by {user}`behackl` in {pr}`3728`
* GH Actions: Changed from macos-latest to macos-13 by {user}`JasonGrace2282` in {pr}`3729`
* Fixed return type inconsistency for `get_anchors()` by {user}`JinchuLi2002` in {pr}`3214`
* Prepared new release: `v0.18.1` by {user}`behackl` in {pr}`3719`
#### Dependency Version Changes
* Bump jupyter-server from 2.9.1 to 2.11.2 by {user}`dependabot` in {pr}`3497`
* Bump github/codeql-action from 2 to 3 by {user}`dependabot` in {pr}`3567`
* Bump actions/upload-artifact from 3 to 4 by {user}`dependabot` in {pr}`3566`
* Bump actions/setup-python from 4 to 5 by {user}`dependabot` in {pr}`3565`
* updated several packages (pillow, jupyterlab, notebook, jupyterlab-lsp, jinja2, gitpython) by {user}`behackl` in {pr}`3593`
* Update jupyter.rst by {user}`abul4fia` in {pr}`3630`
* Bump black from 23.12.1 to 24.3.0 by {user}`dependabot` in {pr}`3649`
* Bump cryptography from 42.0.0 to 42.0.4 by {user}`dependabot` in {pr}`3629`
* Bump actions/cache from 3 to 4 by {user}`dependabot` in {pr}`3607`
* Bump FedericoCarboni/setup-ffmpeg from 2 to 3 by {user}`dependabot` in {pr}`3608`
* Bump ssciwr/setup-mesa-dist-win from 1 to 2 by {user}`dependabot` in {pr}`3609`
* Bump idna from 3.6 to 3.7 by {user}`dependabot` in {pr}`3693`
* Bump pillow from 10.2.0 to 10.3.0 by {user}`dependabot` in {pr}`3672`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci` in {pr}`3332`
* Updated sphinx deps by {user}`JasonGrace2282` in {pr}`3720`
## New Contributors
* {user}`Lawqup` made their first contribution in {pr}`3461`
* {user}`jcep` made their first contribution in {pr}`3477`
* {user}`szchixy` made their first contribution in {pr}`3498`
* {user}`PikaBlue107` made their first contribution in {pr}`3459`
* {user}`yuan-xy` made their first contribution in {pr}`3509`
* {user}`MathItYT` made their first contribution in {pr}`3516`
* {user}`doaamuham` made their first contribution in {pr}`3499`
* {user}`RobinPH` made their first contribution in {pr}`3428`
* {user}`pauluhlenbruck` made their first contribution in {pr}`3513`
* {user}`yashm277` made their first contribution in {pr}`3436`
* {user}`TheMathematicFanatic` made their first contribution in {pr}`3408`
* {user}`vvolhejn` made their first contribution in {pr}`3561`
* {user}`hydromelvictor` made their first contribution in {pr}`3574`
* {user}`dandavison` made their first contribution in {pr}`3568`
* {user}`Gixtox` made their first contribution in {pr}`3528`
* {user}`staghado` made their first contribution in {pr}`3429`
* {user}`biinnnggggg` made their first contribution in {pr}`3692`
* {user}`HairlessVillager` made their first contribution in {pr}`3674`
* {user}`SirJamesClarkMaxwell` made their first contribution in {pr}`3675`
* {user}`danielzsh` made their first contribution in {pr}`3703`
* {user}`TheCrowned` made their first contribution in {pr}`3721`
* {user}`MontroyJosh` made their first contribution in {pr}`3718`
* {user}`amrear` made their first contribution in {pr}`3730`
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.18.0.post0...v0.18.1

View file

@ -0,0 +1,197 @@
---
short-title: v0.19.1
description: Changelog for Manim v0.19.1
---
# v0.19.1
Date
: December 01, 2025
## What's Changed
### New Features
* Introduce seed in `random_color` method to produce colors deterministically by {user}`ishu9bansal` in {pr}`4265`
* Add support for arithmetic operators `//`, `%`, `*`, `**` and `/` on `ValueTracker` by {user}`fmuenkel` in {pr}`4351`
* Add `TangentialArc` mobject by {user}`Brainsucker92` in {pr}`4469`
### Fixed Bugs and Enhancements
* Fix environment formatting for Tex() mobject by {user}`fmuenkel` in {pr}`4159`
* Improved consistency of rate_function implementations by {user}`BenKirkels` in {pr}`4144`
* Make new `Code` mobject compatible with OpenGL renderer by {user}`behackl` in {pr}`4164`
* Fix HSL color ordering in ManimColor by {user}`thehugwizard` in {pr}`4202`
* Fix return type of `Polygram.get_vertex_groups()` and rename variables in `.round_corners()` by {user}`chopan050` in {pr}`4063`
* Improve `Mobject.align_data` docstring by {user}`irvanalhaq9` in {pr}`4152`
* Fix :meth:`VMobject.pointwise_become_partial` failing when `vmobject` is `self` by {user}`irvanalhaq9` in {pr}`4193`
* Fix `add_points_as_corners` not connecting single point to existing path by {user}`irvanalhaq9` in {pr}`4219`
* Complete typing for logger_utils.py by {user}`fmuenkel` in {pr}`4134`
* Fix(graph): Allow any Line subclass as edge_type in Graph/DiGraph by {user}`Akshat-Mishra-py` in {pr}`4251`
* Replace exceptions, remove unused parameters, and fix type hints in `Animation`, `ShowPartial`, `Create`, `ShowPassingFlash`, and `DrawBorderThenFill` by {user}`irvanalhaq9` in {pr}`4214`
* Fix: `Axes` submobject colors are not being set properly by {user}`ishu9bansal` in {pr}`4291`
* Refactor `Rotating` and add docstrings to `Mobject.rotate()` and `Rotating` by {user}`irvanalhaq9` in {pr}`4147`
* Fix default config of `manim init project` to use correct `pixel_height` and `pixel_width` by {user}`StevenH34` in {pr}`4213`
* Handle opacity and transparent images by {user}`henrikmidtiby` in {pr}`4313`
* Gracefully fall back when version metadata is missing by {user}`mohiuddin-khan-shiam` in {pr}`4324`
* Fix for issue 4255 - Do not clear points when the number of curves is zero by {user}`henrikmidtiby` in {pr}`4320`
* Use utf-8 encoding to read generated .tex files. by {user}`OliverStrait` in {pr}`4334`
* Add zero to vmobject points to remove negative zeros in `get_mobject_key` by {user}`elshorbagyx` in {pr}`4332`
* Ensure `stroke_width` attribute of `SVGMobject` is not set to `None` by {user}`henrikmidtiby` in {pr}`4319`
* Fix `Prism` incorrectly rendering with `dimensions=[2, 2, 2]` in OpenGL by {user}`ra1u` in {pr}`4003`
* Fix `BraceLabel.change_label()` and document `BraceText` by {user}`henrikmidtiby` in {pr}`4347`
* Include `Text.gradient` in hash to properly regenerate `Text` when its gradient changes by {user}`AbhilashaTandon` in {pr}`4099`
* Fixed surface animations in OpenGL by {user}`nubDotDev` in {pr}`4286`
* Add type hints and support for arithmetic operators `+` and `-` on `ValueTracker` by {user}`fmuenkel` in {pr}`4129`
* Fix duplicate references in `Scene.mobjects` after `ReplacementTransform` with existing target mobject by {user}`irvanalhaq9` in {pr}`4242`
* Optimize `always_redraw()` by reducing `Mobject` copying in `Mobject.become()` by {user}`chopan050` in {pr}`4357`
* Enhance `manim cfg show` output and add info-level logging for config files read by {user}`xnov18` in {pr}`4375`
* Let `Cube` use Bevel type line joints by {user}`nubDotDev` in {pr}`4361`
* Properly define `init_points` methods for use in OpenGL instead of defining `init_points = generate_points` by {user}`chopan050` in {pr}`4360`
* Allow passing a tuple to `buff` in `SurroundingRectangle` to specify buffer in x and y direction independently by {user}`nubDotDev` in {pr}`4390`
* Rewrite `color_gradient` to always return a list of ManimColors by {user}`henrikmidtiby` in {pr}`4380`
* Ensure leading whitespace does not change line height for lines in CodeMobject by {user}`behackl` in {pr}`4392`
* Simplify the function `remove_invisible_chars` in `text_mobject.py` by {user}`henrikmidtiby` in {pr}`4394`
* Fix some config options specified via `--config_file` not being respected properly by {user}`behackl` in {pr}`4401`
* Fix: Correct resolution tuple order to (height, width) by {user}`Nikhil172913832` in {pr}`4440`
* Ensure that start and end points are stored as float values in Line3D by {user}`SirJamesClarkMaxwell` in {pr}`4080`
* OpenGL: Fix iterated nesting in `DecimalNumber.set_value` by {user}`henrikmidtiby` in {pr}`4373`
* Update default resolution in CLI to match Manims 1920x1080 default settings by {user}`SASHAKT1290` in {pr}`4452`
* Better parsing of color styles in CodeMobject by {user}`SirJamesClarkMaxwell` in {pr}`4454`
* Allow selection of all scenes to render using '*' by {user}`NightyStudios` in {pr}`4470`
* Prevent mutation of `about_point` in `apply_points_function_about_point` by {user}`Morkunas` in {pr}`4478`
* Fix behavior of `Mobject.suspend_updating`: when only suspending parent mobject, let children continue updating by {user}`behackl` in {pr}`4402`
* Allow passing a `buff` to `LabeledDot` by {user}`nubDotDev` in {pr}`4403`
* Pass ndarrays to `mapbox_earcut.triangulate_float32()` to fix `TypeError` in `mapbox_earcut==2.0.0` by {user}`GuiCT` in {pr}`4479`
* Fix duplicated arrow tips in DashedVMobject (issue #3220) by {user}`jakekinchen` in {pr}`4484`
### Documentation
* Add docstring to :meth:`.Mobject.get_family` by {user}`irvanalhaq9` in {pr}`4127`
* Fix link formatting and clarify the distinction between Manim versions in index.rst by {user}`irvanalhaq9` in {pr}`4131`
* Add instructions for installing system utilities `cairo` and `pkg-config` via Homebrew on MacOS by {user}`behackl` in {pr}`4146`
* Add missing line break in Code of Conduct's conflict of interest policy by {user}`Hasan-Mesbaul-Ali-Taher` in {pr}`4185`
* Fix links to Pango website by {user}`ragibson` in {pr}`4217`
* Replace poetry with uv in the README by {user}`xinoehp512` in {pr}`4226`
* Improve docstring for `interpolate` method in `Mobject` class by {user}`irvanalhaq9` in {pr}`4149`
* Add docstrings to `Line` and remove `None` handling for `path_arc` parameter by {user}`irvanalhaq9` in {pr}`4223`
* Add docstring to :meth:`Mobject.family_members_with_points` by {user}`irvanalhaq9` in {pr}`4128`
* Update incorrect docstring for :attr:`ManimConfig.gui_location` property by {user}`SAYAN02-DEV` in {pr}`4254`
* Fix formatting of color space documentation by {user}`behackl` in {pr}`4274`
* Enhance and Paraphrase Description of ManimCE in README.md by {user}`irvanalhaq9` in {pr}`4141`
* docs: add explanation about the rate_func in the custom animation by {user}`pedropxoto` in {pr}`4278`
* Fixed artifact in docstring of Animation by {user}`barollet` in {pr}`4283`
* Rename update function `dot_position` to `update_label` in `.add_updater` example by {user}`irvanalhaq9` in {pr}`4196`
* Fix Microsoft typo in `TexFontTemplateLibrary` scene in `example_scenes/advanced_tex_fonts.py` by {user}`alterdim` in {pr}`4305`
* Improved readability, grammar, as well as added docstrings for consistency by {user}`NASAnerd05` in {pr}`4267`
* Add docstrings for `ChangingDecimal` and `ChangeDecimalToValue` by {user}`haveheartt` in {pr}`4346`
* Fix Sphinx exceptions when trying to build documentation via latex / as pdf by {user}`behackl` in {pr}`4370`
* Added license information to documentation landing page by {user}`Nikil-D-Gr8` in {pr}`3986`
* Set the default Python version to 3.13 in the uv installation guide by {user}`henrikmidtiby` in {pr}`4480`
### Maintenance and Testing
* Change project management tool from poetry to uv by {user}`behackl` in {pr}`4138`
* Re-add ffmpeg as dependency within Docker image by {user}`behackl` in {pr}`4150`
* Add tests for Matrix, DecimalMatrix, IntegerMatrix by {user}`pdrzan` in {pr}`4279`
* Add tests for polylabel utility by {user}`giolucasd` in {pr}`4269`
* Add support for `pycodestyle W` rule in Ruff by {user}`KaiqueDultra` in {pr}`4276`
* Fix files with few MyPy typing errors by {user}`henrikmidtiby` in {pr}`4263`
* Explicitly mention all files that mypy should ignore in the `mypy.ini` configuration file by {user}`henrikmidtiby` in {pr}`4306`
* Remove dead code from `scene.py` and `vector_space_scene.py` by {user}`henrikmidtiby` in {pr}`4310`
* Add type annotations to `scene.py` and `vector_space_scene.py` by {user}`henrikmidtiby` in {pr}`4260`
* Replace setup-texlive-action in CI workflow by {user}`behackl` in {pr}`4326`
* Adding type annotations to polyhedra.py and matrix.py by {user}`henrikmidtiby` in {pr}`4322`
* Handling typing errors in text/numbers.py by {user}`henrikmidtiby` in {pr}`4317`
* Move `configure_pygui` into a `Scene` method and remove `manim.gui` by {user}`chopan050` in {pr}`4314`
* Add typing annotations to svg_mobject.py by {user}`henrikmidtiby` in {pr}`4318`
* Add type annotations to `mobject/svg/brace.py` and default to `label_constructor=Text` in `BraceText` by {user}`henrikmidtiby` in {pr}`4309`
* Add classes `MethodWithArgs`, `SceneInteractContinue` and `SceneInteractRerun` inside new module `manim.data_structures` by {user}`chopan050` in {pr}`4315`
* Fix typo in import of OpenGLCamera in `utils/hashing.py` by {user}`fmuenkel` in {pr}`4352`
* Add type annotations to `manim/renderer/shader.py` by {user}`henrikmidtiby` in {pr}`4350`
* Add type annotations to `tex_mobject.py` by {user}`henrikmidtiby` in {pr}`4355`
* Add type annotations to `three_d_camera.py` by {user}`henrikmidtiby` in {pr}`4356`
* Revert change of default value for tex_environment by {user}`henrikmidtiby` in {pr}`4358`
* Add type hints to `scene_file_writer.py`, `section.py`, and `zoomed_scene.py` by {user}`fmuenkel` in {pr}`4133`
* Add type annotations for most of `camera` and `mobject.graphing` by {user}`henrikmidtiby` in {pr}`4125`
* Add `VectorNDLike` type aliases by {user}`chopan050` in {pr}`4068`
* Add type annotations to `dot_cloud.py`, `vectorized_mobject_rendering.py` and `opengl_three_dimensions.py` by {user}`henrikmidtiby` in {pr}`4359`
* Add type annotations to `indication.py` by {user}`henrikmidtiby` in {pr}`4367`
* Add type annotations to `composition.py` by {user}`henrikmidtiby` in {pr}`4366`
* Add type annotations to `growing.py` by {user}`henrikmidtiby` in {pr}`4368`
* Add type annotations to `movement.py` by {user}`henrikmidtiby` in {pr}`4371`
* Exclude check for cyclic imports by CodeQL by {user}`behackl` in {pr}`4384`
* Refactor imports from `collections.abc`, `typing` and `typing_extensions` for Python 3.9 by {user}`chopan050` in {pr}`4353`
* Add type annotations to `opengl_renderer_window.py` by {user}`fmuenkel` in {pr}`4363`
* Rename `SceneFileWriter.save_final_image()` to `save_image()` by {user}`fmuenkel` in {pr}`4378`
* Add type annotations to `text_mobject.py` by {user}`henrikmidtiby` in {pr}`4381`
* Rename types like `RGBA_Array_Float` to `FloatRGBA` and add types like `FloatRGBA_Array` by {user}`chopan050` in {pr}`4386`
* Add type annotations to `opengl_geometry.py` by {user}`henrikmidtiby` in {pr}`4396`
* Add type annotations to `moving_camera.py` by {user}`henrikmidtiby` in {pr}`4397`
* Add type annotations to `opengl_mobject.py` by {user}`RBerga06` in {pr}`4398`
* Fix failing pre-commit tests by {user}`cclauss` in {pr}`4434`
* Add type annotations to `cairo_renderer.py` by {user}`fmuenkel` in {pr}`4393`
* Fix type errors and add typings for `Mobject.apply_function()`, its derivatives, and other utility functions by {user}`godalming123` in {pr}`4228`
* Bump macOS image from deprecated macos-13 to macos-15-intel by {user}`chopan050` in {pr}`4481`
* Prepare new release `v0.19.1` and bump minimum required Python version to 3.10 by {user}`behackl` in {pr}`4490`
### Dependency Version Changes
* Bump typing extensions minimum version by {user}`JasonGrace2282` in {pr}`4121`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4122`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4140`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4148`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4181`
* Bump astral-sh/setup-uv from 5 to 6 by {user}`dependabot`[bot] in {pr}`4234`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4204`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4391`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4405`
* Bump actions/setup-python from 5 to 6 by {user}`dependabot`[bot] in {pr}`4433`
* Bump actions/checkout from 4 to 5 by {user}`dependabot`[bot] in {pr}`4418`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4409`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4460`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4467`
* Bump github/codeql-action from 3 to 4 by {user}`dependabot`[bot] in {pr}`4466`
* Bump astral-sh/setup-uv from 6 to 7 by {user}`dependabot`[bot] in {pr}`4465`
* Bump actions/upload-artifact from 4 to 5 by {user}`dependabot`[bot] in {pr}`4464`
## New Contributors
* {user}`BenKirkels` made their first contribution in {pr}`4144`
* {user}`Hasan-Mesbaul-Ali-Taher` made their first contribution in {pr}`4185`
* {user}`ragibson` made their first contribution in {pr}`4217`
* {user}`thehugwizard` made their first contribution in {pr}`4202`
* {user}`xinoehp512` made their first contribution in {pr}`4226`
* {user}`SAYAN02-DEV` made their first contribution in {pr}`4254`
* {user}`Akshat-Mishra-py` made their first contribution in {pr}`4251`
* {user}`pdrzan` made their first contribution in {pr}`4279`
* {user}`pedropxoto` made their first contribution in {pr}`4278`
* {user}`giolucasd` made their first contribution in {pr}`4269`
* {user}`KaiqueDultra` made their first contribution in {pr}`4276`
* {user}`ishu9bansal` made their first contribution in {pr}`4291`
* {user}`StevenH34` made their first contribution in {pr}`4213`
* {user}`alterdim` made their first contribution in {pr}`4305`
* {user}`mohiuddin-khan-shiam` made their first contribution in {pr}`4324`
* {user}`elshorbagyx` made their first contribution in {pr}`4332`
* {user}`NASAnerd05` made their first contribution in {pr}`4267`
* {user}`ra1u` made their first contribution in {pr}`4003`
* {user}`AbhilashaTandon` made their first contribution in {pr}`4099`
* {user}`nubDotDev` made their first contribution in {pr}`4286`
* {user}`haveheartt` made their first contribution in {pr}`4346`
* {user}`xnov18` made their first contribution in {pr}`4375`
* {user}`Nikil-D-Gr8` made their first contribution in {pr}`3986`
* {user}`RBerga06` made their first contribution in {pr}`4398`
* {user}`Nikhil172913832` made their first contribution in {pr}`4440`
* {user}`SASHAKT1290` made their first contribution in {pr}`4452`
* {user}`Brainsucker92` made their first contribution in {pr}`4469`
* {user}`NightyStudios` made their first contribution in {pr}`4470`
* {user}`Morkunas` made their first contribution in {pr}`4478`
* {user}`GuiCT` made their first contribution in {pr}`4479`
* {user}`godalming123` made their first contribution in {pr}`4228`
* {user}`jakekinchen` made their first contribution in {pr}`4484`
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.19.0...v0.19.1

View file

@ -0,0 +1,41 @@
---
short-title: v0.19.2
description: Changelog for Manim v0.19.2
---
# v0.19.2
Date
: January 17, 2026
## What's Changed
### Highlights 🌟
* Add support for Python 3.14, bump minimum Python to 3.11 and av to 14.0.1 by {user}`behackl` in {pr}`4385`
### Bug Fixes 🐛
* Fix argument passed to `get_hash_from_play_call` in hashing by {user}`judenimo` in {pr}`4524`
* Fix incorrect `Circle.point_at_angle` calculation by {user}`Swarnlataaa` in {pr}`4438`
### Testing 🧪
* Test on Apple Silicon ARM64 by {user}`cclauss` in {pr}`4496`
### Code Quality & Refactoring 🧹
* Add ruff rules PERF for performance by {user}`cclauss` in {pr}`4492`
* Remove deprecation warning from pytest "np.trapz" -> "np.trapezoid" by {user}`henrikmidtiby` in {pr}`4513`
* Bump Python target versions of both mypy and ruff by {user}`behackl` in {pr}`4520`
* Replace legacy numpy usage -- ruff rule NPY002 by {user}`cclauss` in {pr}`4516`
* Add `.github/release.yml` for improved classifications in automatically generated changelogs by {user}`behackl` in {pr}`4526`
* Check and bump lower version requirements for dependencies by {user}`henrikmidtiby` in {pr}`4529`
### Type Hints 📝
* Add type annotations to `three_dimensions.py` by {user}`henrikmidtiby` in {pr}`4497`
### Other Changes
* Prepare new release `v0.19.2` by {user}`behackl` in {pr}`4528`
## New Contributors
* {user}`judenimo` made their first contribution in {pr}`4524`
* {user}`Swarnlataaa` made their first contribution in {pr}`4438`
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.19.1...v0.19.2

View file

@ -58,7 +58,7 @@ extensions = [
# Automatically generate stub pages when using the .. autosummary directive
autosummary_generate = True
myst_enable_extensions = ["colon_fence", "amsmath"]
myst_enable_extensions = ["colon_fence", "amsmath", "deflist"]
# redirects (for moved / deleted pages)
redirects = {
@ -161,7 +161,8 @@ latex_engine = "lualatex"
# external links
extlinks = {
"issue": ("https://github.com/ManimCommunity/manim/issues/%s", "#%s"),
"pr": ("https://github.com/ManimCommunity/manim/pull/%s", "#%s"),
"pr": ("https://github.com/ManimCommunity/manim/pull/%s", "PR #%s"),
"user": ("https://github.com/%s", "@%s"),
}
# opengraph settings

View file

@ -8,7 +8,7 @@ or specific OpenGL classes like `OpenGLSurface`, but documentation for some of
them is available in form of docstrings
[in the source code](https://github.com/ManimCommunity/manim/tree/main/manim/mobject/opengl).
Furthermore, [this user guide by *aquabeam*](https://www.aquabeam.me/manim/opengl_guide/)
Furthermore, [this user guide by *aquabeam*](https://web.archive.org/web/20250708135737/https://www.aquabeam.me/manim/opengl_guide/)
can be helpful to get started using the OpenGL renderer.
---

View file

@ -298,6 +298,7 @@ class ManimConfig(MutableMapping):
"save_sections",
"save_last_frame",
"scene_names",
"seed",
"show_in_file_browser",
"tex_dir",
"tex_template",
@ -615,6 +616,7 @@ class ManimConfig(MutableMapping):
# the next two must be set BEFORE digesting frame_width and frame_height
"pixel_height",
"pixel_width",
"seed",
"window_monitor",
"zero_pad",
]:
@ -779,6 +781,7 @@ class ManimConfig(MutableMapping):
"dry_run",
"no_latex_cleanup",
"preview_command",
"seed",
]:
if hasattr(args, key):
attr = getattr(args, key)
@ -1789,6 +1792,17 @@ class ManimConfig(MutableMapping):
def plugins(self, value: list[str]):
self._d["plugins"] = value
@property
def seed(self) -> int | None:
"""Random seed for reproducibility. None means no seed is set."""
return self._d["seed"]
@seed.setter
def seed(self, value: int | None) -> None:
if value is None:
return
self._set_pos_number("seed", value, False)
# TODO: to be used in the future - see PR #620
# https://github.com/ManimCommunity/manim/pull/620

View file

@ -157,7 +157,17 @@ def turn_animation_into_updater(
nonlocal total_time
if total_time >= 0:
run_time = animation.get_run_time()
time_ratio = total_time / run_time
# handle zero/negative runtime safely
if run_time <= 0:
# instantly snap to final state once, then remove updater
animation.interpolate(1)
animation.update_mobjects(dt)
animation.finish()
m.remove_updater(update)
return
time_ratio = animation.total_time / run_time
if cycle:
alpha = time_ratio % 1
else:

View file

@ -145,4 +145,10 @@ global_options = option_group(
help="The command used to preview the output file (for example vlc for video files)",
default=None,
),
option(
"--seed",
type=int,
help="Set the random seed to allow reproducibility.",
default=None,
),
)

View file

@ -146,12 +146,6 @@ class BackgroundRectangle(SurroundingRectangle):
)
return self
def get_fill_color(self) -> ManimColor:
# The type of the color property is set to Any using the property decorator
# vectorized_mobject.py#L571
temp_color: ManimColor = self.color
return temp_color
class Cross(VGroup):
"""Creates a cross.

View file

@ -591,9 +591,7 @@ class GenericGraph(VMobject):
self._labels = labels
elif isinstance(labels, bool):
if labels:
self._labels = {
v: MathTex(v, fill_color=label_fill_color) for v in vertices
}
self._labels = {v: MathTex(v, color=label_fill_color) for v in vertices}
else:
self._labels = {}
@ -700,7 +698,7 @@ class GenericGraph(VMobject):
)
if label is True:
label = MathTex(vertex, fill_color=label_fill_color)
label = MathTex(vertex, color=label_fill_color)
elif vertex in self._labels:
label = self._labels[vertex]
elif not isinstance(label, Mobject):

View file

@ -3240,6 +3240,7 @@ class PolarPlane(Axes):
}
for i in a_values
]
a_tex = []
if self.azimuth_units == "PI radians" or self.azimuth_units == "TAU radians":
a_tex = [
self.get_radian_label(

View file

@ -1305,23 +1305,23 @@ class OpenGLMobject:
alignments[i] = mapping[alignments[i]]
return alignments
row_alignments = init_alignments(
row_alignments_seq: Sequence[Vector3D] = init_alignments(
row_alignments,
rows,
{"u": UP, "c": ORIGIN, "d": DOWN},
"row",
RIGHT,
)
col_alignments = init_alignments(
col_alignments_seq: Sequence[Vector3D] = init_alignments(
col_alignments,
cols,
{"l": LEFT, "c": ORIGIN, "r": RIGHT},
"col",
UP,
)
# Now row_alignment[r] + col_alignment[c] is the alignment in cell [r][c]
# Now row_alignments_seq[r] + col_alignment_seq[c] is the alignment in cell [r][c]
mapper = {
mapper: dict[str, Callable[[int, int], int]] = {
"dr": lambda r, c: (rows - r - 1) + c * rows,
"dl": lambda r, c: (rows - r - 1) + (cols - c - 1) * rows,
"ur": lambda r, c: r + c * rows,
@ -1332,10 +1332,11 @@ class OpenGLMobject:
"lu": lambda r, c: r * cols + (cols - c - 1),
}
if flow_order not in mapper:
valid_flow_orders = ",".join([f'"{key}"' for key in mapper])
raise ValueError(
'flow_order must be one of the following values: "dr", "rd", "ld" "dl", "ru", "ur", "lu", "ul".',
f"flow_order must be one of the following values: {valid_flow_orders}.",
)
flow_order = mapper[flow_order]
flow_order_func = mapper[flow_order]
# Reverse row_alignments and row_heights. Necessary since the
# grid filling is handled bottom up for simplicity reasons.
@ -1345,7 +1346,7 @@ class OpenGLMobject:
maybe_list.reverse()
return maybe_list
row_alignments = reverse(row_alignments)
row_alignments_seq = reverse(row_alignments_seq)
row_heights = reverse(row_heights)
placeholder = OpenGLMobject()
@ -1354,7 +1355,7 @@ class OpenGLMobject:
# properties of 0.
mobs.extend([placeholder] * (rows * cols - len(mobs)))
grid = [[mobs[flow_order(r, c)] for c in range(cols)] for r in range(rows)]
grid = [[mobs[flow_order_func(r, c)] for c in range(cols)] for r in range(rows)]
measured_heigths = [
max(grid[r][c].height for c in range(cols)) for r in range(rows)
@ -1370,18 +1371,19 @@ class OpenGLMobject:
if len(sizes) != num:
raise ValueError(f"{name} has a mismatching size.")
return [
sizes[i] if sizes[i] is not None else measures[i] for i in range(num)
size if (size := sizes[i]) is not None else measures[i]
for i in range(num)
]
heights = init_sizes(row_heights, rows, measured_heigths, "row_heights")
widths = init_sizes(col_widths, cols, measured_widths, "col_widths")
x, y = 0, 0
x, y = 0.0, 0.0
for r in range(rows):
x = 0
x = 0.0
for c in range(cols):
if grid[r][c] is not placeholder:
alignment = row_alignments[r] + col_alignments[c]
alignment = row_alignments_seq[r] + col_alignments_seq[c]
line = Line(
x * RIGHT + y * UP,
(x + widths[c]) * RIGHT + (y + heights[r]) * UP,

View file

@ -149,10 +149,10 @@ class Surface(VGroup):
self.resolution = resolution
self.surface_piece_config = surface_piece_config
self.checkerboard_colors: list[ManimColor] | Literal[False]
if checkerboard_colors:
self.checkerboard_colors = [ManimColor(x) for x in checkerboard_colors]
else:
if checkerboard_colors is False:
self.checkerboard_colors = checkerboard_colors
else:
self.checkerboard_colors = [ManimColor(i) for i in checkerboard_colors]
self.should_make_jagged = should_make_jagged
self.pre_function_handle_to_anchor_scale_factor = (
pre_function_handle_to_anchor_scale_factor

View file

@ -111,9 +111,10 @@ class Scene:
self.quit_interaction = False
# Much nicer to work with deterministic scenes
if self.random_seed is not None:
random.seed(self.random_seed)
np.random.default_rng(self.random_seed)
if self.random_seed is None:
self.random_seed = config.seed
random.seed(self.random_seed)
np.random.seed(self.random_seed) # noqa: NPY002 (only way to set seed globally)
def __str__(self) -> str:
return self.__class__.__name__

214
manim/scene/zoomed_scene.py Normal file
View file

@ -0,0 +1,214 @@
"""A scene supporting zooming in on a specified section.
Examples
--------
.. manim:: UseZoomedScene
class UseZoomedScene(ZoomedScene):
def construct(self):
dot = Dot().set_color(GREEN)
self.add(dot)
self.wait(1)
self.activate_zooming(animate=False)
self.wait(1)
self.play(dot.animate.shift(LEFT))
.. manim:: ChangingZoomScale
class ChangingZoomScale(ZoomedScene):
def __init__(self, **kwargs):
ZoomedScene.__init__(
self,
zoom_factor=0.3,
zoomed_display_height=1,
zoomed_display_width=3,
image_frame_stroke_width=20,
zoomed_camera_config={
"default_frame_stroke_width": 3,
},
**kwargs
)
def construct(self):
dot = Dot().set_color(GREEN)
sq = Circle(fill_opacity=1, radius=0.2).next_to(dot, RIGHT)
self.add(dot, sq)
self.wait(1)
self.activate_zooming(animate=False)
self.wait(1)
self.play(dot.animate.shift(LEFT * 0.3))
self.play(self.zoomed_camera.frame.animate.scale(4))
self.play(self.zoomed_camera.frame.animate.shift(0.5 * DOWN))
"""
from __future__ import annotations
__all__ = ["ZoomedScene"]
from typing import TYPE_CHECKING, Any
from ..animation.transform import ApplyMethod
from ..camera.camera import Camera
from ..camera.moving_camera import MovingCamera
from ..camera.multi_camera import MultiCamera
from ..constants import *
from ..mobject.types.image_mobject import ImageMobjectFromCamera
from ..renderer.opengl_renderer import OpenGLCamera
from ..scene.moving_camera_scene import MovingCameraScene
if TYPE_CHECKING:
from manim.typing import Point3DLike, Vector3D
# Note, any scenes from old videos using ZoomedScene will almost certainly
# break, as it was restructured.
class ZoomedScene(MovingCameraScene):
"""This is a Scene with special configurations made for when
a particular part of the scene must be zoomed in on and displayed
separately.
"""
def __init__(
self,
camera_class: type[Camera] = MultiCamera,
zoomed_display_height: float = 3,
zoomed_display_width: float = 3,
zoomed_display_center: Point3DLike | None = None,
zoomed_display_corner: Vector3D = UP + RIGHT,
zoomed_display_corner_buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER,
zoomed_camera_config: dict[str, Any] = {
"default_frame_stroke_width": 2,
"background_opacity": 1,
},
zoomed_camera_image_mobject_config: dict[str, Any] = {},
zoomed_camera_frame_starting_position: Point3DLike = ORIGIN,
zoom_factor: float = 0.15,
image_frame_stroke_width: float = 3,
zoom_activated: bool = False,
**kwargs: Any,
) -> None:
self.zoomed_display_height = zoomed_display_height
self.zoomed_display_width = zoomed_display_width
self.zoomed_display_center = zoomed_display_center
self.zoomed_display_corner = zoomed_display_corner
self.zoomed_display_corner_buff = zoomed_display_corner_buff
self.zoomed_camera_config = zoomed_camera_config
self.zoomed_camera_image_mobject_config = zoomed_camera_image_mobject_config
self.zoomed_camera_frame_starting_position = (
zoomed_camera_frame_starting_position
)
self.zoom_factor = zoom_factor
self.image_frame_stroke_width = image_frame_stroke_width
self.zoom_activated = zoom_activated
super().__init__(camera_class=camera_class, **kwargs)
def setup(self) -> None:
"""This method is used internally by Manim to
setup the scene for proper use.
"""
super().setup()
# Initialize camera and display
zoomed_camera = MovingCamera(**self.zoomed_camera_config)
zoomed_display = ImageMobjectFromCamera(
zoomed_camera, **self.zoomed_camera_image_mobject_config
)
zoomed_display.add_display_frame()
for mob in zoomed_camera.frame, zoomed_display:
mob.stretch_to_fit_height(self.zoomed_display_height)
mob.stretch_to_fit_width(self.zoomed_display_width)
zoomed_camera.frame.scale(self.zoom_factor)
# Position camera and display
zoomed_camera.frame.move_to(self.zoomed_camera_frame_starting_position)
if self.zoomed_display_center is not None:
zoomed_display.move_to(self.zoomed_display_center)
else:
zoomed_display.to_corner(
self.zoomed_display_corner,
buff=self.zoomed_display_corner_buff,
)
self.zoomed_camera = zoomed_camera
self.zoomed_display = zoomed_display
def activate_zooming(self, animate: bool = False) -> None:
"""This method is used to activate the zooming for the zoomed_camera.
Parameters
----------
animate
Whether or not to animate the activation
of the zoomed camera.
"""
self.zoom_activated = True
self.renderer.camera.add_image_mobject_from_camera(self.zoomed_display) # type: ignore[union-attr]
if animate:
self.play(self.get_zoom_in_animation())
self.play(self.get_zoomed_display_pop_out_animation())
self.add_foreground_mobjects(
self.zoomed_camera.frame,
self.zoomed_display,
)
def get_zoom_in_animation(self, run_time: float = 2, **kwargs: Any) -> ApplyMethod:
"""Returns the animation of camera zooming in.
Parameters
----------
run_time
The run_time of the animation of the camera zooming in.
**kwargs
Any valid keyword arguments of ApplyMethod()
Returns
-------
ApplyMethod
The animation of the camera zooming in.
"""
frame = self.zoomed_camera.frame
if isinstance(self.camera, OpenGLCamera):
full_frame_width, full_frame_height = self.camera.frame_shape
else:
full_frame_height = self.camera.frame_height
full_frame_width = self.camera.frame_width
frame.save_state()
frame.stretch_to_fit_width(full_frame_width)
frame.stretch_to_fit_height(full_frame_height)
frame.center()
frame.set_stroke(width=0)
return ApplyMethod(frame.restore, run_time=run_time, **kwargs)
def get_zoomed_display_pop_out_animation(self, **kwargs: Any) -> ApplyMethod:
"""This is the animation of the popping out of the mini-display that
shows the content of the zoomed camera.
Returns
-------
ApplyMethod
The Animation of the Zoomed Display popping out.
"""
display = self.zoomed_display
display.save_state()
display.replace(self.zoomed_camera.frame, stretch=True)
return ApplyMethod(display.restore)
def get_zoom_factor(self) -> float:
"""Returns the Zoom factor of the Zoomed camera.
Defined as the ratio between the height of the zoomed camera and
the height of the zoomed mini display.
Returns
-------
float
The zoom factor.
"""
zoom_factor: float = (
self.zoomed_camera.frame.height / self.zoomed_display.height
)
return zoom_factor

View file

@ -1570,10 +1570,9 @@ class RandomColorGenerator:
seed: int | None = None,
sample_colors: list[ManimColor] | None = None,
) -> None:
self.choice = random.choice if seed is None else random.Random(seed).choice
from manim.utils.color.manim_colors import _all_manim_colors
self.choice = random.choice if seed is None else random.Random(seed).choice
self.colors = _all_manim_colors if sample_colors is None else sample_colors
def next(self) -> ManimColor:

View file

@ -18,7 +18,9 @@ __all__ = [
def capture(
command: str, cwd: StrOrBytesPath | None = None, command_input: str | None = None
command: str | list[str],
cwd: StrOrBytesPath | None = None,
command_input: str | None = None,
) -> tuple[str, str, int]:
p = run(
command,

View file

@ -42,7 +42,7 @@ def restructure_list_to_exclude_certain_family_members(
to_remove = extract_mobject_family_members(to_remove)
def add_safe_mobjects_from_list(
list_to_examine: list[Mobject], set_to_remove: set[Mobject]
list_to_examine: Iterable[Mobject], set_to_remove: set[Mobject]
) -> None:
for mob in list_to_examine:
if mob in set_to_remove:

View file

@ -6,16 +6,19 @@ import re
import sys
import types
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, overload
from manim import config, console, constants, logger
from manim.constants import CHOOSE_NUMBER_MESSAGE, INVALID_NUMBER_MESSAGE
from manim._config import config, console, logger
from manim.constants import (
CHOOSE_NUMBER_MESSAGE,
INVALID_NUMBER_MESSAGE,
NO_SCENE_MESSAGE,
SCENE_NOT_FOUND_MESSAGE,
)
from manim.file_writer import FileWriter
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from pathlib import Path
from manim.scene.scene import Scene
__all__ = ["scene_classes_from_file"]
@ -83,9 +86,9 @@ def get_scene_classes_from_module(module: types.ModuleType) -> list[type[Scene]]
]
def get_scenes_to_render(scene_classes: Sequence[type[Scene]]) -> Sequence[type[Scene]]:
def get_scenes_to_render(scene_classes: list[type[Scene]]) -> list[type[Scene]]:
if not scene_classes:
logger.error(constants.NO_SCENE_MESSAGE)
logger.error(NO_SCENE_MESSAGE)
return []
if config.write_all:
return scene_classes
@ -98,7 +101,7 @@ def get_scenes_to_render(scene_classes: Sequence[type[Scene]]) -> Sequence[type[
result.append(scene_class)
break
else:
logger.error(constants.SCENE_NOT_FOUND_MESSAGE.format(scene_name))
logger.error(SCENE_NOT_FOUND_MESSAGE.format(scene_name))
if result:
return result
if len(scene_classes) == 1:
@ -107,9 +110,7 @@ def get_scenes_to_render(scene_classes: Sequence[type[Scene]]) -> Sequence[type[
return prompt_user_for_choice(scene_classes)
def prompt_user_for_choice(
scene_classes: Iterable[type[Scene]],
) -> Sequence[type[Scene]]:
def prompt_user_for_choice(scene_classes: list[type[Scene]]) -> list[type[Scene]]:
num_to_class = {}
FileWriter.use_output_as_scene_name()
for count, scene_class in enumerate(scene_classes, 1):
@ -122,7 +123,7 @@ def prompt_user_for_choice(
)
if user_input == "*":
selected_scenes_classes = list(scene_classes)
selected_scenes_classes = scene_classes
else:
selected_scenes_classes = [
num_to_class[int(num_str)]

View file

@ -373,9 +373,9 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
def normalize(
vect: npt.NDArray[float], fall_back: npt.NDArray[float] | None = None
) -> npt.NDArray[float]:
norm = get_norm(vect)
vect: np.ndarray | tuple[float], fall_back: np.ndarray | None = None
) -> np.ndarray:
norm = np.linalg.norm(vect)
if norm > 0:
return np.array(vect) / norm
elif fall_back is not None:

View file

@ -64,30 +64,12 @@ ignore_errors = True
[mypy-manim.animation.animation]
ignore_errors = True
[mypy-manim.animation.changing]
ignore_errors = True
[mypy-manim.animation.composition]
ignore_errors = True
[mypy-manim.animation.creation]
ignore_errors = True
[mypy-manim.animation.fading]
ignore_errors = True
[mypy-manim.animation.growing]
ignore_errors = True
[mypy-manim.animation.indication]
ignore_errors = True
[mypy-manim.animation.movement]
ignore_errors = True
[mypy-manim.animation.numbers]
ignore_errors = True
[mypy-manim.animation.specialized]
ignore_errors = True
@ -100,9 +82,6 @@ ignore_errors = True
[mypy-manim.animation.transform]
ignore_errors = True
[mypy-manim.animation.updaters.update]
ignore_errors = True
[mypy-manim.animation.updaters.mobject_update_utils]
ignore_errors = True
@ -112,33 +91,15 @@ ignore_errors = True
[mypy-manim.camera.mapping_camera]
ignore_errors = True
[mypy-manim.cli.checkhealth.commands]
ignore_errors = True
[mypy-manim.cli.default_group]
ignore_errors = True
[mypy-manim.mobject.frame]
ignore_errors = True
[mypy-manim.mobject.geometry.arc]
ignore_errors = True
[mypy-manim.mobject.geometry.boolean_ops]
ignore_errors = True
[mypy-manim.mobject.geometry.labeled]
ignore_errors = True
[mypy-manim.mobject.geometry.line]
ignore_errors = True
[mypy-manim.mobject.geometry.polygram]
ignore_errors = True
[mypy-manim.mobject.geometry.shape_matchers]
ignore_errors = True
[mypy-manim.mobject.graphing.coordinate_systems]
ignore_errors = True
@ -151,18 +112,6 @@ ignore_errors = True
[mypy-manim.mobject.mobject]
ignore_errors = True
[mypy-manim.mobject.text.tex_mobject]
ignore_errors = True
[mypy-manim.mobject.text.text_mobject]
ignore_errors = True
[mypy-manim.mobject.opengl.dot_cloud]
ignore_errors = True
[mypy-manim.mobject.opengl.shader]
ignore_errors = True
[mypy-manim.mobject.opengl.opengl_compatibility]
ignore_errors = True
@ -193,39 +142,15 @@ ignore_errors = True
[mypy-manim.mobject.vector_field]
ignore_errors = True
[mypy-manim.renderer.buffers.buffer]
ignore_errors = True
[mypy-manim.renderer.cairo_renderer]
ignore_errors = True
[mypy-manim.renderer.opengl_renderer]
ignore_errors = True
[mypy-manim.renderer.opengl_shader_program]
ignore_errors = True
[mypy-manim.renderer.shader_wrapper]
ignore_errors = True
[mypy-manim.scene.three_d_scene]
ignore_errors = True
[mypy-manim.utils.caching]
ignore_errors = True
[mypy-manim.utils.directories]
ignore_errors = True
[mypy-manim.utils.family_ops]
ignore_errors = True
[mypy-manim.utils.hashing]
ignore_errors = True
[mypy-manim.utils.testing.frames_comparison]
ignore_errors = True
# Added temporarily due to current mypy failures
[mypy-manim.camera.three_d_camera]
ignore_errors = True
@ -293,27 +218,18 @@ ignore_errors = True
[mypy-manim.utils.commands]
ignore_errors = True
[mypy-manim.utils.debug]
ignore_errors = True
[mypy-manim.utils.images]
ignore_errors = True
[mypy-manim.utils.iterables]
ignore_errors = True
[mypy-manim.utils.module_ops]
ignore_errors = True
[mypy-manim.utils.opengl]
ignore_errors = True
[mypy-manim.utils.paths]
ignore_errors = True
[mypy-manim.utils.progressbar]
ignore_errors = True
[mypy-manim.utils.space_ops]
ignore_errors = True

View file

@ -78,11 +78,9 @@ jupyterlab = [
[dependency-groups]
dev = [
"furo>=2024.8.6",
"gitpython>=3.1.44",
"matplotlib>=3.9.4",
"myst-parser>=3.0.1",
"pre-commit>=4.1.0",
"pygithub>=2.5.0",
"pytest>=8.3.4",
"pytest-cov>=6.0.0",
"pytest-xdist>=2.2,<3.0",
@ -213,9 +211,6 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
"F403",
]
[tool.ruff.lint.isort]
required-imports = ["from __future__ import annotations"]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false

View file

@ -1,316 +0,0 @@
#!/usr/bin/env python
"""Script to generate contributor and pull request lists.
This script generates contributor and pull request lists for release
changelogs using Github v3 protocol. Use requires an authentication token in
order to have sufficient bandwidth, you can get one following the directions at
`<https://help.github.com/articles/creating-an-access-token-for-command-line-use/>_
Don't add any scope, as the default is read access to public information. The
token may be stored in an environment variable as you only get one chance to
see it.
Usage::
$ ./scripts/dev_changelog.py [OPTIONS] TOKEN PRIOR TAG [ADDITIONAL]...
The output is utf8 rst.
Dependencies
------------
- gitpython
- pygithub
Examples
--------
From a bash command line with $GITHUB environment variable as the GitHub token::
$ ./scripts/dev_changelog.py $GITHUB v0.3.0 v0.4.0
This would generate 0.4.0-changelog.rst file and place it automatically under
docs/source/changelog/.
As another example, you may also run include PRs that have been excluded by
providing a space separated list of ticket numbers after TAG::
$ ./scripts/dev_changelog.py $GITHUB v0.3.0 v0.4.0 1911 1234 1492 ...
Note
----
This script was taken from Numpy under the terms of BSD-3-Clause license.
"""
from __future__ import annotations
import concurrent.futures
import datetime
import re
from collections import defaultdict
from pathlib import Path
from textwrap import dedent, indent
import cloup
from git import Repo
from github import Github
from tqdm import tqdm
from manim.constants import CONTEXT_SETTINGS, EPILOG
this_repo = Repo(str(Path(__file__).resolve().parent.parent))
PR_LABELS = {
"breaking changes": "Breaking changes",
"highlight": "Highlights",
"pr:deprecation": "Deprecated classes and functions",
"new feature": "New features",
"enhancement": "Enhancements",
"pr:bugfix": "Fixed bugs",
"documentation": "Documentation-related changes",
"testing": "Changes concerning the testing system",
"infrastructure": "Changes to our development infrastructure",
"maintenance": "Code quality improvements and similar refactors",
"revert": "Changes that needed to be reverted again",
"release": "New releases",
"unlabeled": "Unclassified changes",
}
SILENT_CONTRIBUTORS = [
"dependabot[bot]",
]
def update_citation(version, date):
current_directory = Path(__file__).parent
parent_directory = current_directory.parent
contents = (current_directory / "TEMPLATE.cff").read_text()
contents = contents.replace("<version>", version)
contents = contents.replace("<date_released>", date)
with (parent_directory / "CITATION.cff").open("w", newline="\n") as f:
f.write(contents)
def process_pullrequests(lst, cur, github_repo, pr_nums):
lst_commit = github_repo.get_commit(sha=this_repo.git.rev_list("-1", lst))
lst_date = lst_commit.commit.author.date
authors = set()
reviewers = set()
pr_by_labels = defaultdict(list)
with concurrent.futures.ThreadPoolExecutor() as executor:
future_to_num = {
executor.submit(github_repo.get_pull, num): num for num in pr_nums
}
for future in tqdm(
concurrent.futures.as_completed(future_to_num), "Processing PRs"
):
pr = future.result()
authors.add(pr.user)
reviewers = reviewers.union(rev.user for rev in pr.get_reviews())
pr_labels = [label.name for label in pr.labels]
for label in PR_LABELS:
if label in pr_labels:
pr_by_labels[label].append(pr)
break # ensure that PR is only added in one category
else:
pr_by_labels["unlabeled"].append(pr)
# identify first-time contributors:
author_names = []
for author in authors:
name = author.name if author.name is not None else author.login
if name in SILENT_CONTRIBUTORS:
continue
if github_repo.get_commits(author=author, until=lst_date).totalCount == 0:
name += " +"
author_names.append(name)
reviewer_names = []
for reviewer in reviewers:
name = reviewer.name if reviewer.name is not None else reviewer.login
if name in SILENT_CONTRIBUTORS:
continue
reviewer_names.append(name)
# Sort items in pr_by_labels
for i in pr_by_labels:
pr_by_labels[i] = sorted(pr_by_labels[i], key=lambda pr: pr.number)
return {
"authors": sorted(author_names),
"reviewers": sorted(reviewer_names),
"PRs": pr_by_labels,
}
def get_pr_nums(lst, cur):
print("Getting PR Numbers:")
prnums = []
# From regular merges
merges = this_repo.git.log("--oneline", "--merges", f"{lst}..{cur}")
issues = re.findall(r".*\(\#(\d+)\)", merges)
prnums.extend(int(s) for s in issues)
# From fast forward squash-merges
commits = this_repo.git.log(
"--oneline",
"--no-merges",
"--first-parent",
f"{lst}..{cur}",
)
split_commits = list(
filter(
lambda x: not any(
["pre-commit autoupdate" in x, "New Crowdin updates" in x]
),
commits.split("\n"),
),
)
commits = "\n".join(split_commits)
issues = re.findall(r"^.*\(\#(\d+)\)$", commits, re.M)
prnums.extend(int(s) for s in issues)
print(prnums)
return prnums
def get_summary(body):
pattern = '<!--changelog-start-->([^"]*)<!--changelog-end-->'
try:
has_changelog_pattern = re.search(pattern, body)
if has_changelog_pattern:
return has_changelog_pattern.group()[22:-21].strip()
except Exception:
print(f"Error parsing body for changelog: {body}")
@cloup.command(
context_settings=CONTEXT_SETTINGS,
epilog=EPILOG,
)
@cloup.argument("token")
@cloup.argument("prior")
@cloup.argument("tag")
@cloup.argument(
"additional",
nargs=-1,
required=False,
type=int,
)
@cloup.option(
"-o",
"--outfile",
type=str,
help="Path and file name of the changelog output.",
)
def main(token, prior, tag, additional, outfile):
"""Generate Changelog/List of contributors/PRs for release.
TOKEN is your GitHub Personal Access Token.
PRIOR is the tag/commit SHA of the previous release.
TAG is the tag of the new release.
ADDITIONAL includes additional PR(s) that have not been recognized automatically.
"""
lst_release, cur_release = prior, tag
github = Github(token)
github_repo = github.get_repo("ManimCommunity/manim")
pr_nums = get_pr_nums(lst_release, cur_release)
if additional:
print(f"Adding {additional} to the mix!")
pr_nums = pr_nums + list(additional)
# document authors
contributions = process_pullrequests(lst_release, cur_release, github_repo, pr_nums)
authors = contributions["authors"]
reviewers = contributions["reviewers"]
# update citation file
today = datetime.date.today()
update_citation(tag, str(today))
if not outfile:
outfile = (
Path(__file__).resolve().parent.parent / "docs" / "source" / "changelog"
)
outfile = outfile / f"{tag[1:] if tag.startswith('v') else tag}-changelog.rst"
else:
outfile = Path(outfile).resolve()
with outfile.open("w", encoding="utf8", newline="\n") as f:
f.write("*" * len(tag) + "\n")
f.write(f"{tag}\n")
f.write("*" * len(tag) + "\n\n")
f.write(f":Date: {today.strftime('%B %d, %Y')}\n\n")
heading = "Contributors"
f.write(f"{heading}\n")
f.write("=" * len(heading) + "\n\n")
f.write(
dedent(
f"""\
A total of {len(set(authors).union(set(reviewers)))} people contributed to this
release. People with a '+' by their names authored a patch for the first
time.\n
""",
),
)
for author in authors:
f.write(f"* {author}\n")
f.write("\n")
f.write(
dedent(
"""
The patches included in this release have been reviewed by
the following contributors.\n
""",
),
)
for reviewer in reviewers:
f.write(f"* {reviewer}\n")
# document pull requests
heading = "Pull requests merged"
f.write("\n")
f.write(heading + "\n")
f.write("=" * len(heading) + "\n\n")
f.write(
f"A total of {len(pr_nums)} pull requests were merged for this release.\n\n",
)
pr_by_labels = contributions["PRs"]
for label in PR_LABELS:
pr_of_label = pr_by_labels[label]
if pr_of_label:
heading = PR_LABELS[label]
f.write(f"{heading}\n")
f.write("-" * len(heading) + "\n\n")
for PR in pr_by_labels[label]:
num = PR.number
title = PR.title
label = PR.labels
f.write(f"* :pr:`{num}`: {title}\n")
overview = get_summary(PR.body)
if overview:
f.write(indent(f"{overview}\n\n", " "))
else:
f.write("\n\n")
print(f"Wrote changelog to: {outfile}")
if __name__ == "__main__":
main()

540
scripts/release.py Normal file
View file

@ -0,0 +1,540 @@
#!/usr/bin/env python3
"""
Release management tools for Manim.
This script provides commands for preparing and managing Manim releases:
- Generate changelogs from GitHub's release notes API
- Update CITATION.cff with new version information
- Fetch existing release notes for documentation
Usage:
# Generate changelog for an upcoming release
uv run python scripts/release.py changelog --base v0.19.0 --version 0.20.0
# Also update CITATION.cff at the same time
uv run python scripts/release.py changelog --base v0.19.0 --version 0.20.0 --update-citation
# Update only CITATION.cff
uv run python scripts/release.py citation --version 0.20.0
# Fetch existing release changelogs for documentation
uv run python scripts/release.py fetch-releases
Requirements:
- gh CLI installed and authenticated
- Python 3.11+
"""
from __future__ import annotations
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
from collections.abc import Sequence
# =============================================================================
# Constants
# =============================================================================
REPO = "manimcommunity/manim"
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent
CHANGELOG_DIR = REPO_ROOT / "docs" / "source" / "changelog"
CITATION_TEMPLATE = SCRIPT_DIR / "TEMPLATE.cff"
CITATION_FILE = REPO_ROOT / "CITATION.cff"
# Minimum version for fetching historical releases
DEFAULT_MIN_VERSION = "0.18.0"
# =============================================================================
# GitHub CLI Helpers
# =============================================================================
def run_gh(
args: Sequence[str],
*,
check: bool = True,
suppress_errors: bool = False,
) -> subprocess.CompletedProcess[str]:
"""
Run a gh CLI command.
Args:
args: Arguments to pass to gh
check: If True, raise on non-zero exit
suppress_errors: If True, don't print errors to stderr
Returns:
CompletedProcess with stdout/stderr
"""
result = subprocess.run(
["gh", *args],
capture_output=True,
text=True,
)
if (
result.returncode != 0
and not suppress_errors
and "not found" not in result.stderr.lower()
):
click.echo(f"gh error: {result.stderr}", err=True)
if check and result.returncode != 0:
raise click.ClickException(f"gh command failed: gh {' '.join(args)}")
return result
def get_release_tags() -> list[str]:
"""Get all published release tags from GitHub, newest first."""
result = run_gh(
["release", "list", "--repo", REPO, "--limit", "100", "--json", "tagName"],
check=False,
)
if result.returncode != 0 or not result.stdout.strip():
return []
import json
try:
data = json.loads(result.stdout)
return [item["tagName"] for item in data]
except (json.JSONDecodeError, KeyError):
return []
def get_release_body(tag: str) -> str | None:
"""Get the release body for a published release."""
result = run_gh(
["release", "view", tag, "--repo", REPO, "--json", "body", "--jq", ".body"],
check=False,
suppress_errors=True,
)
if result.returncode != 0:
return None
return result.stdout.strip() or None
def get_release_date(tag: str) -> str | None:
"""Get the release date formatted as 'Month DD, YYYY'."""
result = run_gh(
[
"release",
"view",
tag,
"--repo",
REPO,
"--json",
"createdAt",
"--jq",
".createdAt",
],
check=False,
)
if result.returncode != 0:
return None
date_str = result.stdout.strip().strip('"')
try:
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
return dt.strftime("%B %d, %Y")
except ValueError:
return None
def generate_release_notes(head_tag: str, base_tag: str) -> str:
"""
Generate release notes using GitHub's API.
This respects .github/release.yml for PR categorization.
"""
result = run_gh(
[
"api",
f"repos/{REPO}/releases/generate-notes",
"--field",
f"tag_name={head_tag}",
"--field",
f"previous_tag_name={base_tag}",
"--jq",
".body",
]
)
body = result.stdout.strip()
if not body:
raise click.ClickException("GitHub API returned empty release notes")
return body
# =============================================================================
# Version Utilities
# =============================================================================
def normalize_tag(tag: str) -> str:
"""Ensure tag has 'v' prefix."""
return tag if tag.startswith("v") else f"v{tag}"
def version_from_tag(tag: str) -> str:
"""Extract version from tag (e.g., 'v0.18.0' -> '0.18.0')."""
return tag[1:] if tag.startswith("v") else tag
def parse_version(version: str) -> tuple[int, ...]:
"""Parse version string into comparable tuple."""
# Handle post-releases like '0.18.0.post0'
version = version.replace(".post", "-post")
parts = []
for part in version.replace("-", ".").split("."):
try:
parts.append(int(part))
except ValueError:
continue
# Pad to at least 3 components
while len(parts) < 3:
parts.append(0)
return tuple(parts)
def version_gte(version: str, min_version: str) -> bool:
"""Check if version >= min_version."""
return parse_version(version) >= parse_version(min_version)
# =============================================================================
# Markdown Conversion
# =============================================================================
def convert_to_myst(body: str) -> str:
"""
Convert GitHub markdown to MyST format.
- PR URLs -> {pr}`NUMBER`
- Issue URLs -> {issue}`NUMBER`
- @mentions -> {user}`USERNAME`
- Strips HTML comments
- Ensures proper list spacing
"""
lines = body.split("\n")
result = []
prev_bullet = False
for line in lines:
# Skip HTML comments
if line.strip().startswith("<!--") and line.strip().endswith("-->"):
continue
# Convert URLs to extlinks
line = re.sub(
r"https://github\.com/ManimCommunity/manim/pull/(\d+)",
r"{pr}`\1`",
line,
)
line = re.sub(
r"https://github\.com/ManimCommunity/manim/issues/(\d+)",
r"{issue}`\1`",
line,
)
line = re.sub(r"@([a-zA-Z0-9_-]+)", r"{user}`\1`", line)
if line.startswith("**Full Changelog**:"):
_, url = line.split(":", 1)
url = url.strip().replace("vmain", "main")
line = f"**Full Changelog**: [Compare view]({url})"
# Handle list spacing
is_bullet = line.strip().startswith("*") and not line.strip().startswith("**")
if prev_bullet and not is_bullet and line.strip():
result.append("")
result.append(line)
prev_bullet = is_bullet
return "\n".join(result)
def format_changelog(
version: str,
body: str,
date: str | None = None,
title: str | None = None,
) -> str:
"""Create changelog file content with MyST frontmatter."""
title = title or f"v{version}"
body = convert_to_myst(body)
date_section = f"Date\n: {date}\n" if date else ""
return f"""---
short-title: {title}
description: Changelog for {title}
---
# {title}
{date_section}
{body}
"""
# =============================================================================
# File Operations
# =============================================================================
def get_existing_versions() -> set[str]:
"""Get versions that already have changelog files."""
if not CHANGELOG_DIR.exists():
return set()
return {
f.stem.replace("-changelog", "") for f in CHANGELOG_DIR.glob("*-changelog.*")
}
def save_changelog(version: str, content: str) -> Path:
"""Save changelog and return filepath."""
filepath = CHANGELOG_DIR / f"{version}-changelog.md"
filepath.write_text(content)
return filepath
def update_citation(version: str, date: str | None = None) -> Path:
"""Update CITATION.cff from template."""
if not CITATION_TEMPLATE.exists():
raise click.ClickException(f"Citation template not found: {CITATION_TEMPLATE}")
date = date or datetime.now().strftime("%Y-%m-%d")
version_tag = normalize_tag(version)
content = CITATION_TEMPLATE.read_text()
content = content.replace("<version>", version_tag)
content = content.replace("<date_released>", date)
CITATION_FILE.write_text(content)
return CITATION_FILE
# =============================================================================
# CLI Commands
# =============================================================================
@click.group()
@click.option(
"--dry-run", is_flag=True, help="Show what would be done without making changes"
)
@click.pass_context
def cli(ctx: click.Context, dry_run: bool) -> None:
"""Release management tools for Manim."""
ctx.ensure_object(dict)
ctx.obj["dry_run"] = dry_run
@cli.command()
@click.option("--base", required=True, help="Base tag for comparison (e.g., v0.19.0)")
@click.option(
"--version", "version", required=True, help="New version number (e.g., 0.20.0)"
)
@click.option("--head", default="main", help="Head ref for comparison (default: main)")
@click.option("--title", help="Custom changelog title (default: vX.Y.Z)")
@click.option("--update-citation", is_flag=True, help="Also update CITATION.cff")
@click.pass_context
def changelog(
ctx: click.Context,
base: str,
version: str,
head: str,
title: str | None,
update_citation: bool,
) -> None:
"""Generate changelog for an upcoming release.
Uses GitHub's release notes API with .github/release.yml categorization.
"""
dry_run = ctx.obj["dry_run"]
base_tag = normalize_tag(base)
head_tag = normalize_tag(head) if head != "main" else normalize_tag(version)
click.echo(f"Generating changelog for v{version}...")
click.echo(f" Comparing: {base_tag}{head}")
body = generate_release_notes(head_tag, base_tag)
date = datetime.now().strftime("%B %d, %Y")
content = format_changelog(version, body, date=date, title=title)
if dry_run:
click.echo()
click.secho("[DRY RUN]", fg="yellow", bold=True)
click.echo(f" Would save: {CHANGELOG_DIR / f'{version}-changelog.md'}")
if update_citation:
click.echo(f" Would update: {CITATION_FILE}")
click.echo()
click.echo("--- Generated changelog ---")
click.echo(content)
return
filepath = save_changelog(version, content)
click.secho(f" ✓ Saved: {filepath}", fg="green")
if update_citation:
citation_path = update_citation(version)
click.secho(f" ✓ Updated: {citation_path}", fg="green")
click.echo()
click.echo("Next steps:")
click.echo(" • Review and edit the changelog as needed")
click.echo(" • Update docs/source/changelog.rst to include the new file")
@cli.command()
@click.option(
"--version", "version", required=True, help="Version number (e.g., 0.20.0)"
)
@click.option("--date", help="Release date as YYYY-MM-DD (default: today)")
@click.pass_context
def citation(ctx: click.Context, version: str, date: str | None) -> None:
"""Update CITATION.cff for a release."""
dry_run = ctx.obj["dry_run"]
display_date = date or datetime.now().strftime("%Y-%m-%d")
if dry_run:
click.secho("[DRY RUN]", fg="yellow", bold=True)
click.echo(f" Would update: {CITATION_FILE}")
click.echo(f" Version: v{version}")
click.echo(f" Date: {display_date}")
return
filepath = update_citation(version, date)
click.secho(f"✓ Updated: {filepath}", fg="green")
click.echo(f" Version: v{version}")
click.echo(f" Date: {display_date}")
@cli.command("fetch-releases")
@click.option("--tag", help="Fetch a specific release tag")
@click.option(
"--min-version",
default=DEFAULT_MIN_VERSION,
help=f"Minimum version to fetch (default: {DEFAULT_MIN_VERSION})",
)
@click.option("--force", is_flag=True, help="Overwrite existing changelog files")
@click.pass_context
def fetch_releases(
ctx: click.Context,
tag: str | None,
min_version: str,
force: bool,
) -> None:
"""Fetch existing release changelogs from GitHub.
Converts GitHub release notes to documentation-ready MyST markdown.
"""
dry_run = ctx.obj["dry_run"]
existing = get_existing_versions()
# Single tag mode
if tag:
tag = normalize_tag(tag)
version = version_from_tag(tag)
if version in existing and not force:
click.echo(
f"Changelog for {version} already exists. Use --force to overwrite."
)
return
if dry_run:
click.secho("[DRY RUN]", fg="yellow", bold=True)
click.echo(f" Would fetch: {version}")
return
_fetch_single_release(tag, version)
return
# Batch mode
click.echo(f"Existing versions: {', '.join(sorted(existing)) or '(none)'}")
click.echo("Fetching release list...")
tags = get_release_tags()
click.echo(f"Found {len(tags)} releases")
fetched = 0
prev_tag = None
for tag in reversed(tags):
version = version_from_tag(tag)
if not version_gte(version, min_version):
prev_tag = tag
continue
if version in existing and not force:
click.echo(f" Skipping {version} (exists)")
prev_tag = tag
continue
if dry_run:
click.echo(f" [DRY RUN] Would fetch {version}")
fetched += 1
else:
if _fetch_single_release(tag, version, prev_tag):
existing.add(version)
fetched += 1
prev_tag = tag
click.echo()
click.echo(f"Processed {fetched} changelog(s)")
if fetched > 0 and not dry_run:
click.echo()
click.echo("Next steps:")
click.echo(" • Update docs/source/changelog.rst to include new files")
def _fetch_single_release(tag: str, version: str, prev_tag: str | None = None) -> bool:
"""Fetch and save a single release changelog."""
click.echo(f" Fetching {version}...")
body = get_release_body(tag)
if not body and prev_tag:
click.echo(f" No body, generating from {prev_tag}...")
try:
body = generate_release_notes(tag, prev_tag)
except click.ClickException:
body = None
if not body:
click.secho(f" ✗ Could not get release notes for {tag}", fg="red", err=True)
return False
date = get_release_date(tag)
content = format_changelog(version, body, date=date)
filepath = save_changelog(version, content)
click.secho(f" ✓ Saved: {filepath}", fg="green")
return True
# =============================================================================
# Entry Point
# =============================================================================
def main() -> None:
"""Entry point."""
cli()
if __name__ == "__main__":
sys.exit(main() or 0)

View file

@ -0,0 +1,56 @@
from __future__ import annotations
from manim import Circle, FadeIn
from manim.animation.updaters.mobject_update_utils import turn_animation_into_updater
def test_turn_animation_into_updater_zero_run_time():
"""Test that turn_animation_into_updater handles zero run_time correctly."""
# Create a simple mobject and animation
mobject = Circle()
animation = FadeIn(mobject, run_time=0)
# Track updater calls
update_calls = []
original_updaters = mobject.updaters.copy()
# Call turn_animation_into_updater
result = turn_animation_into_updater(animation)
# Verify mobject is returned
assert result is mobject
# Get the updater that was added
assert len(mobject.updaters) == len(original_updaters) + 1
updater = mobject.updaters[-1]
# Simulate calling the updater
updater(mobject, dt=0.1)
# The updater should have finished and removed itself
assert len(mobject.updaters) == len(original_updaters)
assert updater not in mobject.updaters
# Animation should be in finished state
assert animation.total_time >= 0
def test_turn_animation_into_updater_positive_run_time_persists():
"""Test that updater persists with positive run_time."""
mobject = Circle()
animation = FadeIn(mobject, run_time=1.0)
original_updaters = mobject.updaters.copy()
# Call turn_animation_into_updater
result = turn_animation_into_updater(animation)
# Get the updater that was added
updater = mobject.updaters[-1]
# Simulate calling the updater (partial progress)
updater(mobject, dt=0.1)
# The updater should still be present (not finished)
assert len(mobject.updaters) == len(original_updaters) + 1
assert updater in mobject.updaters

View file

@ -7,6 +7,7 @@ import numpy as np
from manim import (
DEGREES,
DOWN,
GREEN,
LEFT,
ORIGIN,
RIGHT,
@ -152,6 +153,18 @@ def test_BackgroundRectangle(manim_caplog):
)
def test_BackgroundRectangle_color_access():
"""Test that BackgroundRectangle color access works correctly.
Regression test for https://github.com/ManimCommunity/manim/issues/4419
"""
square = Square()
bg_rect = BackgroundRectangle(square, color=GREEN)
# Should not cause infinite recursion
assert bg_rect.color == GREEN
def test_Square_side_length_reflets_correct_width_and_height():
sq = Square(side_length=1).scale(3)
assert sq.side_length == 3

View file

@ -105,6 +105,7 @@ def is_close(x, y):
return abs(x - y) < 0.00001
@pytest.mark.slow
def test_mobject_dimensions_nested_mobjects():
vg = VGroup()

View file

@ -0,0 +1,19 @@
"""Tests for Table and related mobjects."""
from __future__ import annotations
from manim import Table
from manim.utils.color import GREEN
def test_highlighted_cell_color_access():
"""Test that accessing the color of a highlighted cell doesn't cause infinite recursion.
Regression test for https://github.com/ManimCommunity/manim/issues/4419
"""
table = Table([["This", "is a"], ["simple", "table"]])
rect = table.get_highlighted_cell((1, 1), color=GREEN)
# Should not raise RecursionError
color = rect.color
assert color == GREEN

View file

@ -4,7 +4,7 @@ import datetime
import pytest
from manim import Circle, FadeIn, Group, Manager, Scene, Square
from manim import Circle, Dot, FadeIn, Group, Manager, Scene, Square
from manim.animation.animation import Wait
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
@ -108,3 +108,48 @@ def test_replace(dry_run):
scene.replace(second, beta)
assert_names(scene.mobjects, ["alpha", "group", "fourth"])
assert_names(scene.mobjects[1], ["beta", "third"])
def test_reproducible_scene(dry_run):
import numpy as np
scene = Scene(random_seed=42)
dots1 = []
for _ in range(10):
dot = Dot(np.random.uniform(-3, 3, size=3)) # noqa: NPY002
dots1.append(dot)
scene.add(*dots1)
scene2 = Scene(random_seed=42)
dots2 = []
for _ in range(5):
dot = Dot(np.random.uniform(-3, 3, size=3)) # noqa: NPY002
dots2.append(dot)
scene2.add(*dots2)
for d1, d2 in zip(dots1, dots2, strict=False):
np.testing.assert_allclose(d1.get_center(), d2.get_center())
def test_random_color_reproducibility_with_seed(dry_run):
from manim import random_color, tempconfig
with tempconfig({"seed": 123}):
# First run: create scene (which seeds global random state) and generate colors
scene1 = Scene()
colors_first_run = [random_color() for _ in range(5)]
# Interrupt with a scene that has an explicit different seed
scene_explicit = Scene(random_seed=999)
colors_interrupted = [random_color() for _ in range(3)]
# Second run: create a new scene without explicit seed (should use config.seed)
scene2 = Scene()
colors_second_run = [random_color() for _ in range(5)]
# The colors from the first and second run should match
# because both scenes were seeded with config.seed=123
assert colors_first_run == colors_second_run
# The interrupted colors should be different (seeded with 999)
assert colors_interrupted != colors_first_run[:3]

View file

@ -1,5 +1,7 @@
from __future__ import annotations
import pytest
from manim import *
from manim.utils.testing.frames_comparison import frames_comparison
@ -37,6 +39,7 @@ def test_line_graph(scene):
scene.add(plane, first_line, second_line)
@pytest.mark.slow
@frames_comparison
def test_plot_surface(scene):
axes = ThreeDAxes(x_range=(-5, 5, 1), y_range=(-5, 5, 1), z_range=(-5, 5, 1))
@ -57,6 +60,7 @@ def test_plot_surface(scene):
scene.add(axes, trig_plane)
@pytest.mark.slow
@frames_comparison
def test_plot_surface_colorscale(scene):
axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-5, 5, 1))
@ -143,6 +147,7 @@ def test_number_plane_log(scene):
scene.add(VGroup(plane1, plane2).arrange())
@pytest.mark.slow
@frames_comparison
def test_gradient_line_graph_x_axis(scene):
"""Test that using `colorscale` generates a line whose gradient matches the y-axis"""
@ -158,6 +163,7 @@ def test_gradient_line_graph_x_axis(scene):
scene.add(axes, curve)
@pytest.mark.slow
@frames_comparison
def test_gradient_line_graph_y_axis(scene):
"""Test that using `colorscale` generates a line whose gradient matches the y-axis"""

View file

@ -1,3 +1,5 @@
import pytest
from manim.constants import LEFT
from manim.mobject.graphing.probability import BarChart
from manim.mobject.text.tex_mobject import MathTex
@ -7,6 +9,7 @@ from manim.utils.testing.frames_comparison import frames_comparison
__module_test__ = "probability"
@pytest.mark.slow
@frames_comparison
def test_default_chart(scene):
pull_req = [54, 23, 47, 48, 40, 64, 112, 87]

View file

@ -1,5 +1,7 @@
from __future__ import annotations
import pytest
from manim import *
from manim.utils.testing.frames_comparison import frames_comparison
@ -18,6 +20,7 @@ def test_Cube(scene: Scene) -> None:
scene.add(Cube())
@pytest.mark.slow
@frames_comparison
def test_Sphere(scene: Scene) -> None:
scene.add(Sphere())
@ -64,6 +67,7 @@ def test_Arrow3D(scene: Scene) -> None:
scene.add(Arrow3D(resolution=16))
@pytest.mark.slow
@frames_comparison
def test_Torus(scene: Scene) -> None:
scene.add(Torus())

View file

@ -165,3 +165,15 @@ class ElaborateSceneWithSections(Scene):
self.next_section("fade out")
self.play(FadeOut(square))
self.wait()
class SceneWithRandomness(Scene):
def construct(self):
dots = VGroup()
for _ in range(10):
dot = Dot(
point=np.random.uniform(-3, 3, size=3), # noqa: NPY002
)
dots.add(dot)
self.add(dots)
self.wait()

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pytest
@ -713,6 +714,49 @@ def test_mov_can_be_set_as_output_format(
)
@pytest.mark.slow
def test_reproducible_animation(tmp_path: Path, manim_cfg_file, simple_scenes_path):
scene_name = "SceneWithRandomness"
command = [
sys.executable,
"-m",
"manim",
"-ql",
"--media_dir",
str(tmp_path),
"--seed",
"42",
str(simple_scenes_path),
scene_name,
]
out, err, exit_code = capture(command)
assert exit_code == 0, err
first_path = tmp_path / "videos" / "simple_scenes" / "480p15" / f"{scene_name}.mp4"
command = [
sys.executable,
"-m",
"manim",
"-ql",
"--media_dir",
str(tmp_path),
"--seed",
"42",
str(simple_scenes_path),
scene_name,
]
out, err, exit_code = capture(command)
assert exit_code == 0, err
second_path = first_path.with_name(scene_name).with_suffix(".mp4")
with open(first_path, "rb") as f1, open(second_path, "rb") as f2:
first_data = f1.read()
second_data = f2.read()
assert first_data == second_data, "Videos with the same seed differ."
@pytest.mark.slow
@video_comparison(
"InputFileViaCfg.json",

166
uv.lock generated
View file

@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.14'",
@ -655,68 +655,6 @@ toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "cryptography"
version = "46.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
{ url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
{ url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
{ url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
{ url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
{ url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
]
[[package]]
name = "cycler"
version = "0.12.1"
@ -934,30 +872,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" },
]
[[package]]
name = "gitdb"
version = "4.0.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "smmap" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
]
[[package]]
name = "gitpython"
version = "3.1.46"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
]
[[package]]
name = "glcontext"
version = "3.0.0"
@ -1525,12 +1439,10 @@ jupyterlab = [
[package.dev-dependencies]
dev = [
{ name = "furo" },
{ name = "gitpython" },
{ name = "matplotlib" },
{ name = "myst-parser" },
{ name = "pre-commit" },
{ name = "psutil" },
{ name = "pygithub" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-xdist" },
@ -1585,12 +1497,10 @@ provides-extras = ["gui", "jupyterlab"]
[package.metadata.requires-dev]
dev = [
{ name = "furo", specifier = ">=2024.8.6" },
{ name = "gitpython", specifier = ">=3.1.44" },
{ name = "matplotlib", specifier = ">=3.9.4" },
{ name = "myst-parser", specifier = ">=3.0.1" },
{ name = "pre-commit", specifier = ">=4.1.0" },
{ name = "psutil", specifier = ">=6.1.1" },
{ name = "pygithub", specifier = ">=2.5.0" },
{ name = "pytest", specifier = ">=8.3.4" },
{ name = "pytest-cov", specifier = ">=6.0.0" },
{ name = "pytest-xdist", specifier = ">=2.2,<3.0" },
@ -2420,22 +2330,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
]
[[package]]
name = "pygithub"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyjwt", extra = ["crypto"] },
{ name = "pynacl" },
{ name = "requests" },
{ name = "typing-extensions" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/74/e560bdeffea72ecb26cff27f0fad548bbff5ecc51d6a155311ea7f9e4c4c/pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9", size = 2246994, upload-time = "2025-09-02T17:41:54.674Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/ba/7049ce39f653f6140aac4beb53a5aaf08b4407b6a3019aae394c1c5244ff/pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0", size = 432709, upload-time = "2025-09-02T17:41:52.947Z" },
]
[[package]]
name = "pyglet"
version = "2.1.12"
@ -2512,55 +2406,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyjwt"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
]
[package.optional-dependencies]
crypto = [
{ name = "cryptography" },
]
[[package]]
name = "pynacl"
version = "1.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" },
{ url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" },
{ url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" },
{ url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" },
{ url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" },
{ url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" },
{ url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" },
{ url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" },
{ url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" },
{ url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" },
{ url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" },
{ url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" },
{ url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" },
{ url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" },
{ url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" },
{ url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" },
{ url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" },
{ url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" },
{ url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" },
{ url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" },
{ url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" },
{ url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" },
]
[[package]]
name = "pyobjc-core"
version = "12.1"
@ -3167,15 +3012,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/be/7daf7bf5ec6e4f245804842364222b1e857b42b2ca13192791e2b8cafc14/skia_pathops-0.9.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c9ccc68d316371be3817eb20eaae4a7810d85f329276a7d7ca5a21f47fa6522", size = 1779252, upload-time = "2025-12-08T11:44:47.15Z" },
]
[[package]]
name = "smmap"
version = "5.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" },
]
[[package]]
name = "snowballstemmer"
version = "3.0.1"