Skip to content
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

Automatically import Plugins #967

Merged
merged 15 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from 13 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
75 changes: 59 additions & 16 deletions docs/source/installation/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ install, use, and create your own plugins.
for the best practices on installing, using, and creating plugins; as
well as new subcommands/flags for ``manim plugins``

.. tip::

See https://plugins.manim.community/ for the list of plugins available.

Installing Plugins
******************
Plugins can be easily installed via the ``pip``
Expand Down Expand Up @@ -55,27 +59,37 @@ You can list plugins as such:

Using Plugins in Projects
*************************
Plugins specified in ``plugins/__init__.py`` are imported automatically by
manim's ``__init__.py``. As such, writing:
For enabling a plugin ``manim.cfg`` or command line parameters should be used.

.. important::

The plugins should be module name of the plugin and not PyPi name.

.. code-block:: python
Enabling plugins through ``manim.cfg``

from manim import *
.. code-block:: ini

in your projects will import any of the plugins imported in
``plugins/__init__.py``.
[CLI]
plugins = manim_rubikscube

By default, ``plugins/__init__.py`` is not provided; although, there are
plans to support subcommands that would manage this file. It is especially
useful to create this file for projects that involve usage of the same
plugins. Alternatively, you may manually specify the plugins in your project
scripts.
For specifing multiple plugins, command separated values must be used.

.. code-block:: python
.. code-block:: ini

import manim_cool_plugin
# or
from manim_cool_plugin import feature_x, feature_y, ...
[CLI]
plugins = manim_rubikscube, manim_plugintemplate

Enabling Plugins through CLI

.. code-block:: bash

manim basic.py --plugins=manim_plugintemplate

For multiple plugins

.. code-block:: bash

manim basic.py --plugins=manim_rubikscube,manim_plugintemplate

Creating Plugins
****************
Expand All @@ -101,4 +115,33 @@ specified in poetry as:
.. code-block:: toml

[tool.poetry.plugins."manim.plugins"]
"name" = "object_reference"
"name" = "object_reference"

Here ``name`` is the name of the module of the plugin.

Here ``object_reference`` can point to either a function in a module or a module
itself. For example,

.. code-block:: toml

[tool.poetry.plugins."manim.plugins"]
"manim_plugintemplate" = "manim_plugintemplate"

Here a module is used as ``object_reference``, and when this plugin is enabled,
Manim will look for ``__all__`` keyword defined in ``manim_plugintemplate`` and
everything as a global variable one by one.

If ``object_reference`` is a function, Manim calls the function and expects the
function returns a list of modules or functions that needs to defined globally and
it defined it.

For example,

.. code-block:: toml

[tool.poetry.plugins."manim.plugins"]
"manim_plugintemplate" = "manim_awesomeplugin.imports:setup_things"

Here, Manim will call the function ``setup_things`` defined in
``manim_awesomeplugin.imports`` and calls that. It returns a list of function or
modules which will be imported globally.
8 changes: 4 additions & 4 deletions docs/source/tutorials/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,12 @@ A list of all config options
'frame_y_radius', 'from_animation_number', 'images_dir', 'input_file',
'js_renderer_path', 'leave_progress_bars', 'left_side', 'log_dir', 'log_to_file',
'max_files_cached', 'media_dir', 'movie_file_extension', 'output_file',
'partial_movie_dir', 'pixel_height', 'pixel_width', 'png_mode', 'preview',
'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame',
'partial_movie_dir', 'pixel_height', 'pixel_width', 'plugins', 'png_mode',
'preview', 'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame',
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent',
'upto_animation_number', 'use_js_renderer', 'verbosity', 'video_dir', 'write_all',
'write_to_movie']
'upto_animation_number', 'use_js_renderer', 'verbosity', 'video_dir',
'write_all', 'write_to_movie']


A list of all CLI flags
Expand Down
3 changes: 3 additions & 0 deletions manim/_config/default.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ disable_caching = False
# --tex_template
tex_template =

# specify the plugins as comma seperated values
# manim will load that plugin if it specified here.
plugins =

# Overrides the default output folders, NOT the output file names. Note that
# if the custom_folders flag is present, the Tex and text files will not be put
Expand Down
18 changes: 17 additions & 1 deletion manim/_config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ class MyScene(Scene):
"partial_movie_dir",
"pixel_height",
"pixel_width",
"plugins",
"png_mode",
"preview",
"progress_bar",
Expand Down Expand Up @@ -551,7 +552,12 @@ def digest_parser(self, parser: configparser.ConfigParser) -> "ManimConfig":
# "frame_height",
]:
setattr(self, key, parser["CLI"].getfloat(key))

# plugins
setattr(
self,
"plugins",
parser["CLI"].get("plugins", fallback="", raw=True).split(","),
)
naveen521kk marked this conversation as resolved.
Show resolved Hide resolved
# the next two must be set AFTER digesting pixel_width and pixel_height
self["frame_height"] = parser["CLI"].getfloat("frame_height", 8.0)
width = parser["CLI"].getfloat("frame_width", None)
Expand Down Expand Up @@ -1304,6 +1310,16 @@ def tex_template_file(self, val: str) -> None:
self._d["tex_template_file"] = val # actually set the falsy value
self._tex_template = TexTemplate() # but don't use it

@property
def plugins(self):
"""List of plugins to enable."""

naveen521kk marked this conversation as resolved.
Show resolved Hide resolved
return self._d["plugins"]

@plugins.setter
def plugins(self, value):
self._d["plugins"] = value


class ManimFrame(Mapping):
_OPTS: typing.Set[str] = {
Expand Down
1 change: 0 additions & 1 deletion manim/plugins/.gitignore

This file was deleted.

1 change: 1 addition & 0 deletions manim/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .import_plugins import *
42 changes: 42 additions & 0 deletions manim/plugins/import_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import types
from importlib import import_module

import pkg_resources

from .. import config, logger

__all__ = []


plugins_requested: list = config["plugins"]
if "" in plugins_requested:
plugins_requested.remove("")
for plugin in pkg_resources.iter_entry_points("manim.plugins"):
if plugin.name not in plugins_requested:
continue
loaded_plugin = plugin.load()
if isinstance(loaded_plugin, types.ModuleType):
# it is a module so it can't be called
# see if __all__ is defined
# if it is defined use that to load all the modules necessary
# essentially this would be similar to `from plugin import *``
# if not just import the module with the plugin name
if hasattr(loaded_plugin, "__all__"):
for thing in loaded_plugin.__all__:
exec(f"{thing}=loaded_plugin.{thing}")
__all__.append(thing)
else:
exec(f"{plugin.name}=loaded_plugin")
__all__.append(plugin.name)
elif isinstance(loaded_plugin, types.FunctionType):
# call the function first
# it will return a list of modules to add globally
# finally add it
lists = loaded_plugin()
for l in lists:
exec(f"{l.__name__}=l")
__all__.append(l.__name__)
plugins_requested.remove(plugin.name)
else:
if plugins_requested != []:
logger.warning("Missing Plugins: %s", plugins_requested)
54 changes: 27 additions & 27 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ pygments = "*"
rich = "^6.0"
pycairo = "^1.19"
manimpango = "^0.1.7"
networkx = "^2.5"
setuptools = "*"
importlib-metadata = {version = "^1.0", python = "<3.8"}
grpcio = { version = "1.33.*", optional = true }
grpcio-tools = { version = "1.33.*", optional = true }
watchdog = { version = "*", optional = true }
networkx = "^2.5"
importlib-metadata = {version = "^1.0", python = "<3.8"}

[tool.poetry.extras]
js_renderer = ["grpcio","grpcio-tools","watchdog"]
Expand Down
5 changes: 4 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ def pytest_collection_modifyitems(config, items):

@pytest.fixture(scope="session")
def python_version():
return "python3" if sys.platform == "darwin" else "python"
# use the same python executable as it is running currently
# rather than randomly calling using python or python3, which
# may create problems.
return sys.executable


@pytest.fixture
Expand Down
Empty file added tests/test_plugins/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions tests/test_plugins/simple_scenes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from manim import *


class SquareToCircle(Scene):
def construct(self):
square = Square()
circle = Circle()
self.play(Transform(square, circle))


class FunctionLikeTest(Scene):
def contruct(self):
assert "FunctionLike" in globals()
a = FunctionLike()
self.play(FadeIn(a))


class NoAllTest(Scene):
def construct(self):
assert "test_plugin" in globals()
a = test_plugin.NoAll()
self.play(FadeIn(a))


class WithAllTest(Scene):
def construct(self):
assert "WithAll" in globals()
leotrs marked this conversation as resolved.
Show resolved Hide resolved
a = WithAll()
self.play(FadeIn(a))
Loading