mxutils_localimportpath

Demonstrates importing a package relative to a saved Python script.

mxutils_localimportpath.py

"""Demonstrates importing a package relative to a saved Python script.

Warning:
    This script must be run from a saved file, in a location where Cinema 4D has read and write access.
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2026 MAXON Computer GmbH"
__license__ = "Apache-2.0 License"
__version__ = "2027.0"



# --- Boiler plate code to generate a dummy package next to this script ----------------------------

if not __file__:
    raise RuntimeError("This script must be run from a saved file.")

import os

directory: str = os.path.dirname(os.path.abspath(__file__))
package: str = os.path.join(directory, "dummy_package")
module: str = os.path.join(package, "__init__.py")

if not os.path.exists(package):
    os.makedirs(package)

with open(module, "w") as f:
    f.write("def GetMessage() -> str:\n")
    f.write("    return 'Hello from the dummy package!'\n")

# --- Example Start --------------------------------------------------------------------------------

import mxutils

# Cinema, unlike the native CPython interpreter, does not automatically include a script's directory 
# in the module search paths. This includes both myScript.py and myPoly.pyp files. Which in turn means 
# that sibling packages or modules located next to the script are not automatically discoverable. The
# reason why this is being done, is because unlike vanilla CPython, Cinema 4D is a multi user 
# environment, i.e., there can be multiple scripts and plugins (from different authors) running at 
# the same time. If they would all globally modify the module search paths, it could lead to conflicts 
# and unpredictable behavior.
#
# The class mxutils.LocalImportPath fixes this gap, by gracefully exposing a local directory to the 
# module search path, only for the time it is needed within a specific scope.

# Import from the directory containing this script. Instead of a file path, such as "__file__", you 
# could also provide a directory path directly. When a file path is given, it will be internally 
# converted to its containing directory.
with mxutils.LocalImportPath(__file__):
    # Within this scope, we can import modules from the local directory.
    from dummy_package import GetMessage

# And we can then still use them in the enclosing scope, or the entire script when the call was in
# the global scope.
print(GetMessage())

# LocalImportPath also has an #autoReload argument, which especially comes in handy during plugin 
# development. When you hit 'Reload Python' plugins in Cinema 4D, the module dependencies are reloaded 
# automatically if #autoReload is set to True. This feature can detect dependencies of your dependency
# on its own (you import moduleA, which imports moduleB and moduleB), but will try to reload them in
# order of discovery, not necessarily the order in which they are imported in your code. In case a
# specific order is required, you may need to manually reload certain modules to ensure the correct 
# behavior. You should tie the #autoReload argument to a debug flag or a development setting, so that 
# it is only enabled when needed.

# Import the dummy package again, this will entail a console message that it has been reloaded 
# because the module was already imported previously.
IS_DEBUG: bool = True
with mxutils.LocalImportPath(__file__, autoReload=IS_DEBUG):
    import dummy_package