Skip to content

Add new method of specifying seed and using warnings.warn #3908

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions manim/_config/logger_utils.py
Original file line number Diff line number Diff line change
@@ -95,6 +95,11 @@ def make_logger(
keywords=HIGHLIGHTED_KEYWORDS,
)

# redirect warnings.warn to logging.warn
logging.captureWarnings(True)
py_warning = logging.getLogger("py.warnings")
py_warning.addHandler(rich_handler)

# finally, the logger
logger = logging.getLogger("manim")
logger.addHandler(rich_handler)
35 changes: 29 additions & 6 deletions manim/scene/scene.py
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
import threading
import time
import types
import warnings
from queue import Queue

import srt
@@ -96,19 +97,41 @@ class MyScene(Scene):
def construct(self):
self.play(Write(Text("Hello World!")))

You can specify the seed for random functions inside Manim via :attr:`Scene.random_seed`.

.. code-block:: python

class MyScene(Scene):
random_seed = 3

def construct(self): ...

"""

random_seed: int | None = None

def __init__(
self,
renderer=None,
camera_class=Camera,
always_update_mobjects=False,
random_seed=None,
skip_animations=False,
renderer: OpenGLRenderer | CairoRenderer | None = None,
camera_class: type[Camera] = Camera,
always_update_mobjects: bool = False,
random_seed: int | None = None,
skip_animations: bool = False,
):
self.camera_class = camera_class
self.always_update_mobjects = always_update_mobjects
self.random_seed = random_seed

if random_seed is not None:
warnings.warn(
"Setting the random seed in the Scene constructor is deprecated. "
"Please set the class attribute random_seed instead.",
category=FutureWarning,
# assuming no scene subclassing, this should refer
# to instantiation of the Scene
stacklevel=4,
)
self.random_seed = random_seed

self.skip_animations = skip_animations

self.animations = None
64 changes: 47 additions & 17 deletions manim/utils/deprecation.py
Original file line number Diff line number Diff line change
@@ -8,13 +8,18 @@
import inspect
import logging
import re
from collections.abc import Iterable
from typing import Any, Callable
import warnings
from collections.abc import Callable, Iterable
from typing import Any, TypeVar, cast, overload

from decorator import decorate, decorator
from typing_extensions import ParamSpec

logger = logging.getLogger("manim")

T = TypeVar("T")
P = ParamSpec("P")


def _get_callable_info(callable_: Callable, /) -> tuple[str, str]:
"""Returns type and name of a callable.
@@ -70,13 +75,33 @@ def _deprecation_text_component(
return f"deprecated {since}and {until}.{msg}"


@overload
def deprecated(
func: None = None,
since: str | None = None,
until: str | None = None,
replacement: str | None = None,
message: str = "",
) -> Callable[[Callable[P, T]], Callable[P, T]]: ...


@overload
def deprecated(
func: Callable = None,
func: Callable[P, T],
since: str | None = None,
until: str | None = None,
replacement: str | None = None,
message: str | None = "",
) -> Callable:
message: str = "",
) -> Callable[P, T]: ...


def deprecated(
func: Callable[P, T] | None = None,
since: str | None = None,
until: str | None = None,
replacement: str | None = None,
message: str = "",
) -> Callable[P, T] | Callable[[Callable[P, T]], Callable[P, T]]:
"""Decorator to mark a callable as deprecated.

The decorated callable will cause a warning when used. The docstring of the
@@ -160,7 +185,11 @@ def foo():
"""
# If used as factory:
if func is None:
return lambda func: deprecated(func, since, until, replacement, message)

def wrapper(f: Callable[P, T]) -> Callable[P, T]:
return deprecated(f, since, until, replacement, message)

return wrapper

what, name = _get_callable_info(func)

@@ -187,7 +216,7 @@ def warning_msg(for_docs: bool = False) -> str:
deprecated = _deprecation_text_component(since, until, msg)
return f"The {what} {name} has been {deprecated}"

def deprecate_docs(func: Callable):
def deprecate_docs(func: Callable[..., object]) -> None:
"""Adjust docstring to indicate the deprecation.

Parameters
@@ -199,7 +228,7 @@ def deprecate_docs(func: Callable):
doc_string = func.__doc__ or ""
func.__doc__ = f"{doc_string}\n\n.. attention:: Deprecated\n {warning}"

def deprecate(func: Callable, *args, **kwargs):
def deprecate(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
"""The actual decorator used to extend the callables behavior.

Logs a warning message.
@@ -219,12 +248,12 @@ def deprecate(func: Callable, *args, **kwargs):
The return value of the given callable when being passed the given
arguments.
"""
logger.warning(warning_msg())
warnings.warn(warning_msg(), category=FutureWarning, stacklevel=1)
return func(*args, **kwargs)

if type(func).__name__ != "function":
deprecate_docs(func)
func.__init__ = decorate(func.__init__, deprecate)
func.__init__ = decorate(func.__init__, deprecate) # type: ignore[misc]
return func

func = decorate(func, deprecate)
@@ -236,10 +265,10 @@ def deprecated_params(
params: str | Iterable[str] | None = None,
since: str | None = None,
until: str | None = None,
message: str | None = "",
message: str = "",
redirections: None
| (Iterable[tuple[str, str] | Callable[..., dict[str, Any]]]) = None,
) -> Callable:
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Decorator to mark parameters of a callable as deprecated.

It can also be used to automatically redirect deprecated parameter values to their
@@ -426,7 +455,7 @@ def foo(**kwargs):

redirections = list(redirections)

def warning_msg(func: Callable, used: list[str]):
def warning_msg(func: Callable, used: list[str]) -> str:
"""Generate the deprecation warning message.

Parameters
@@ -449,7 +478,7 @@ def warning_msg(func: Callable, used: list[str]):
deprecated = _deprecation_text_component(since, until, message)
return f"The parameter{parameter_s} {used_} of {what} {name} {has_have_been} {deprecated}"

def redirect_params(kwargs: dict, used: list[str]):
def redirect_params(kwargs: dict[str, Any], used: list[str]) -> None:
"""Adjust the keyword arguments as defined by the redirections.

Parameters
@@ -473,7 +502,7 @@ def redirect_params(kwargs: dict, used: list[str]):
if len(redirector_args) > 0:
kwargs.update(redirector(**redirector_args))

def deprecate_params(func, *args, **kwargs):
def deprecate_params(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
"""The actual decorator function used to extend the callables behavior.

Logs a warning message when a deprecated parameter is used and redirects it if
@@ -501,8 +530,9 @@ def deprecate_params(func, *args, **kwargs):
used.append(param)

if len(used) > 0:
logger.warning(warning_msg(func, used))
warnings.warn(warning_msg(func, used), category=FutureWarning, stacklevel=1)
redirect_params(kwargs, used)
return func(*args, **kwargs)

return decorator(deprecate_params)
# unfortunately, decorator has really bad stubs that involve Any
return cast(Callable, decorator(deprecate_params))
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -82,6 +82,9 @@ ignore_errors = True
[mypy-manim.utils.*]
ignore_errors = True

[mypy-manim.utils.deprecation]
ignore_errors = False

[mypy-manim.utils.iterables]
ignore_errors = False
warn_return_any = False
24 changes: 24 additions & 0 deletions tests/module/scene/test_scene.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import datetime
import random

import numpy as np
import pytest

from manim import Circle, FadeIn, Group, Mobject, Scene, Square
@@ -101,3 +103,25 @@ def assert_names(mobjs, names):
scene.replace(second, beta)
assert_names(scene.mobjects, ["alpha", "group", "fourth"])
assert_names(scene.mobjects[1], ["beta", "third"])


def test_random_seed():
class GoodScene(Scene):
random_seed = 3

class BadScene(Scene):
def __init__(self):
super().__init__(random_seed=3)

good = GoodScene()
assert good.random_seed == 3
random_random = random.random()
np_random = np.random.random()

with pytest.warns(FutureWarning):
bad = BadScene()
assert bad.random_seed == 3

# check that they both actually set the seed
assert random.random() == random_random
assert np.random.random() == np_random
236 changes: 78 additions & 158 deletions tests/module/utils/test_deprecation.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
from __future__ import annotations

from manim.utils.deprecation import deprecated, deprecated_params

import pytest

def _get_caplog_record_msg(manim_caplog):
logger_name, level, message = manim_caplog.record_tuples[0]
return message
from manim.utils.deprecation import deprecated, deprecated_params


@deprecated
@@ -67,88 +64,61 @@ def __init__(self):
doc_admonition = "\n\n.. attention:: Deprecated\n "


def test_deprecate_class_no_args(manim_caplog):
def test_deprecate_class_no_args():
"""Test the deprecation of a class (decorator with no arguments)."""
f = Foo()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Foo has been deprecated and may be removed in a later version."
)

msg = "The class Foo has been deprecated and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
f = Foo()
assert f.__doc__ == f"{doc_admonition}{msg}"


def test_deprecate_class_since(manim_caplog):
def test_deprecate_class_since():
"""Test the deprecation of a class (decorator with since argument)."""
b = Bar()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Bar has been deprecated since v0.6.0 and may be removed in a later version."
)
msg = "The class Bar has been deprecated since v0.6.0 and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
b = Bar()
assert b.__doc__ == f"The Bar class.{doc_admonition}{msg}"


def test_deprecate_class_until(manim_caplog):
def test_deprecate_class_until():
"""Test the deprecation of a class (decorator with until argument)."""
bz = Baz()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Baz has been deprecated and is expected to be removed after 06/01/2021."
)
msg = "The class Baz has been deprecated and is expected to be removed after 06/01/2021."
with pytest.warns(FutureWarning, match=msg):
bz = Baz()
assert bz.__doc__ == f"The Baz class.{doc_admonition}{msg}"


def test_deprecate_class_since_and_until(manim_caplog):
def test_deprecate_class_since_and_until():
"""Test the deprecation of a class (decorator with since and until arguments)."""
qx = Qux()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Qux has been deprecated since 0.7.0 and is expected to be removed after 0.9.0-rc2."
)
msg = "The class Qux has been deprecated since 0.7.0 and is expected to be removed after 0.9.0-rc2."
with pytest.warns(FutureWarning, match=msg):
qx = Qux()
assert qx.__doc__ == f"{doc_admonition}{msg}"


def test_deprecate_class_msg(manim_caplog):
def test_deprecate_class_msg():
"""Test the deprecation of a class (decorator with msg argument)."""
qu = Quux()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Quux has been deprecated and may be removed in a later version. Use something else."
)
msg = "The class Quux has been deprecated and may be removed in a later version. Use something else."
with pytest.warns(FutureWarning, match=msg):
qu = Quux()
assert qu.__doc__ == f"{doc_admonition}{msg}"


def test_deprecate_class_replacement(manim_caplog):
def test_deprecate_class_replacement():
"""Test the deprecation of a class (decorator with replacement argument)."""
qz = Quuz()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Quuz has been deprecated and may be removed in a later version. Use ReplaceQuuz instead."
)
msg = "The class Quuz has been deprecated and may be removed in a later version. Use ReplaceQuuz instead."
with pytest.warns(FutureWarning, match=msg):
qz = Quuz()
doc_msg = "The class Quuz has been deprecated and may be removed in a later version. Use :class:`~.ReplaceQuuz` instead."
assert qz.__doc__ == f"{doc_admonition}{doc_msg}"


def test_deprecate_class_all(manim_caplog):
def test_deprecate_class_all():
"""Test the deprecation of a class (decorator with all arguments)."""
qza = QuuzAll()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class QuuzAll has been deprecated since 0.7.0 and is expected to be removed after 1.2.1. Use ReplaceQuuz instead. Don't use this please."
)
msg = "The class QuuzAll has been deprecated since 0.7.0 and is expected to be removed after 1.2.1. Use ReplaceQuuz instead. Don't use this please."
with pytest.warns(FutureWarning, match=msg):
qza = QuuzAll()
doc_msg = "The class QuuzAll has been deprecated since 0.7.0 and is expected to be removed after 1.2.1. Use :class:`~.ReplaceQuuz` instead. Don't use this please."
assert qza.__doc__ == f"{doc_admonition}{doc_msg}"

@@ -233,156 +203,106 @@ def quuz(self, **kwargs):
return kwargs


def test_deprecate_func_no_args(manim_caplog):
def test_deprecate_func_no_args():
"""Test the deprecation of a method (decorator with no arguments)."""
useless()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The function useless has been deprecated and may be removed in a later version."
)
msg = "The function useless has been deprecated and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
useless()
assert useless.__doc__ == f"{doc_admonition}{msg}"


def test_deprecate_func_in_class_since_and_message(manim_caplog):
def test_deprecate_func_in_class_since_and_message():
"""Test the deprecation of a method within a class (decorator with since and message arguments)."""
t = Top()
t.mid_func()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The method Top.mid_func has been deprecated since 0.8.0 and may be removed in a later version. This method is useless."
)
msg = "The method Top.mid_func has been deprecated since 0.8.0 and may be removed in a later version. This method is useless."
with pytest.warns(FutureWarning, match=msg):
t.mid_func()
assert t.mid_func.__doc__ == f"Middle function in Top.{doc_admonition}{msg}"


def test_deprecate_nested_class_until_and_replacement(manim_caplog):
def test_deprecate_nested_class_until_and_replacement():
"""Test the deprecation of a nested class (decorator with until and replacement arguments)."""
n = Top().Nested()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Top.Nested has been deprecated and is expected to be removed after 1.4.0. Use Top.NewNested instead."
)
msg = "The class Top.Nested has been deprecated and is expected to be removed after 1.4.0. Use Top.NewNested instead."
with pytest.warns(FutureWarning, match=msg):
n = Top().Nested()
doc_msg = "The class Top.Nested has been deprecated and is expected to be removed after 1.4.0. Use :class:`~.Top.NewNested` instead."
assert n.__doc__ == f"{doc_admonition}{doc_msg}"


def test_deprecate_nested_class_func_since_and_until(manim_caplog):
def test_deprecate_nested_class_func_since_and_until():
"""Test the deprecation of a method within a nested class (decorator with since and until arguments)."""
n = Top().NewNested()
n.nested_func()
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The method Top.NewNested.nested_func has been deprecated since 1.0.0 and is expected to be removed after 12/25/2025."
)
msg = "The method Top.NewNested.nested_func has been deprecated since 1.0.0 and is expected to be removed after 12/25/2025."
with pytest.warns(FutureWarning, match=msg):
n.nested_func()
assert (
n.nested_func.__doc__
== f"Nested function in Top.NewNested.{doc_admonition}{msg}"
)


def test_deprecate_nested_func(manim_caplog):
def test_deprecate_nested_func():
"""Test the deprecation of a nested method (decorator with no arguments)."""
b = Top().Bottom()
answer = b.normal_func()
answer(1)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The method Top.Bottom.normal_func.<locals>.nested_func has been deprecated and may be removed in a later version."
)
msg = "The method Top.Bottom.normal_func.<locals>.nested_func has been deprecated and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
answer(1)
assert answer.__doc__ == f"{doc_admonition}{msg}"


def test_deprecate_func_params(manim_caplog):
def test_deprecate_func_params():
"""Test the deprecation of method parameters (decorator with params argument)."""
t = Top()
t.foo(a=2, b=3, z=4)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameters a and b of method Top.foo have been deprecated and may be removed in a later version. Use something else."
)
msg = "The parameters a and b of method Top.foo have been deprecated and may be removed in a later version. Use something else."
with pytest.warns(FutureWarning, match=msg):
t.foo(a=2, b=3, z=4)


def test_deprecate_func_single_param_since_and_until(manim_caplog):
def test_deprecate_func_single_param_since_and_until():
"""Test the deprecation of a single method parameter (decorator with since and until arguments)."""
t = Top()
t.bar(a=1, b=2)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter a of method Top.bar has been deprecated since v0.2 and is expected to be removed after v0.4."
)
msg = "The parameter a of method Top.bar has been deprecated since v0.2 and is expected to be removed after v0.4."
with pytest.warns(FutureWarning, match=msg):
t.bar(a=1, b=2)


def test_deprecate_func_param_redirect_tuple(manim_caplog):
def test_deprecate_func_param_redirect_tuple():
"""Test the deprecation of a method parameter and redirecting it to a new one using tuple."""
t = Top()
obj = t.baz(x=1, old_param=2)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter old_param of method Top.baz has been deprecated and may be removed in a later version."
)
msg = "The parameter old_param of method Top.baz has been deprecated and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
obj = t.baz(x=1, old_param=2)
assert obj == {"x": 1, "new_param": 2}


def test_deprecate_func_param_redirect_lambda(manim_caplog):
def test_deprecate_func_param_redirect_lambda():
"""Test the deprecation of a method parameter and redirecting it to a new one using lambda function."""
t = Top()
obj = t.qux(runtime_in_ms=500)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter runtime_in_ms of method Top.qux has been deprecated and may be removed in a later version."
)
msg = "The parameter runtime_in_ms of method Top.qux has been deprecated and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
obj = t.qux(runtime_in_ms=500)
assert obj == {"run_time": 0.5}


def test_deprecate_func_param_redirect_many_to_one(manim_caplog):
def test_deprecate_func_param_redirect_many_to_one():
"""Test the deprecation of multiple method parameters and redirecting them to one."""
t = Top()
obj = t.quux(point2D_x=3, point2D_y=5)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameters point2D_x and point2D_y of method Top.quux have been deprecated and may be removed in a later version."
)
msg = "The parameters point2D_x and point2D_y of method Top.quux have been deprecated and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
obj = t.quux(point2D_x=3, point2D_y=5)
assert obj == {"point2D": (3, 5)}


def test_deprecate_func_param_redirect_one_to_many(manim_caplog):
def test_deprecate_func_param_redirect_one_to_many():
"""Test the deprecation of one method parameter and redirecting it to many."""
t = Top()
obj1 = t.quuz(point2D=0)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter point2D of method Top.quuz has been deprecated and may be removed in a later version."
)
msg = "The parameter point2D of method Top.quuz has been deprecated and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
obj1 = t.quuz(point2D=0)
assert obj1 == {"x": 0, "y": 0}

manim_caplog.clear()

obj2 = t.quuz(point2D=(2, 3))
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter point2D of method Top.quuz has been deprecated and may be removed in a later version."
)
msg = "The parameter point2D of method Top.quuz has been deprecated and may be removed in a later version."
with pytest.warns(FutureWarning, match=msg):
obj2 = t.quuz(point2D=(2, 3))
assert obj2 == {"x": 2, "y": 3}