UNSOLVED Files and modules organization

Hello there

There is my main module A. It should use a module B (located in the same folder) and Python's standart module YAML (which doesn't exist by default).
Whats the best solution? Should I just download YAML and copy it to module A folder?

Right now module A is placed here

%user%\AppData\Roaming\Maxon\Maxon Cinema 4D R25\library\scripts\

I'm not sure, but maybe I should use another folder, e.g. E:\C4D_scripts and use environment variable C4DPYTHONPATH39?

How do you orginize your files and modules?

Hello @kisaf,

Thank you for reaching out to us. There are multiple search paths being added to our Python interpreter, both predefined and custom. The custom module search paths registered with C4DPYTHONPATHXX are one option but not the only one.

I recently updated the Python libraries manual in the Python docs, the manual has not been reviewed yet, so you will still find typos in there, but it will convey the information you are interested in. The manual will be released in an upcoming version of the SDK, find below the markup of the manual as a 'preview'.

Please excuse the informal approach, but I am quite short on time, and the manual will contain all relevant information for you, so I am taking this route as an exception.

Cheers,
Ferdinand

The manual:

.. _manual-python-libraries:

Python Libraries
================

Learn how to expose third party libraries to the Python environments of Cinema 4D.

.. note::
    MAXON cannot provide support for third party libraries written for CPython and their 
    compatibility with the Python interpreters shipped with Cinema 4D. While our Python interpreters
    are derived from the respective CPython builds, they are not identical to them. Popular Python 
    libraries as for example ``numpy`` might not work at all or only with a reduced feature set. 
    The Cinema 4D Python interpreters are provided *as-is* and not meant to be fully CPython 
    compliant.

The Python interprets shipped with Cinema 4D are pre-configured to allow users to import custom
modules from multiple search paths. The user can also add custom search paths with environment 
variables or on a plugin level by modifying the search paths of a Python instance at runtime.

.. contents:: Table of Contents
   :depth: 3
   :local:
   :backlinks: none

.. _app_module_search_paths:

Installation & App Specific Search Paths
----------------------------------------

There are in principle two predefined search paths into which libraries can be placed for each app
inside an installation of Cinema 4D. One for libraries which are intended for a specific installation
of an app, and one for all apps that use a specific version of Python. The app specific search paths
are all tied to a specific installation of Cinema 4D and also differentiate between the apps that 
have been shipped with that Cinema 4D installation, as for example the *Cinema 4D*, *c4dpy*, or 
*commandline* executable. The search paths can be located with the following patterns:

.. csv-table::
    :header: Cinema 4D Version, Operating System, Path
    :widths: 20, 20, 60

    "R20", "Windows", ``%APPDATA%/MAXON/{prefs}/python27/libs``
    "R20", "MacOS", ``~/Library/Preferences/MAXON/{prefs}/python27/libs``
    "R21", "Windows", ``%APPDATA%/MAXON/{prefs}/python27/libs``
    "R21", "MacOS", ``~/Library/Preferences/MAXON/{prefs}/python27/libs``
    "S22", "Windows", ``%APPDATA%/MAXON/{cinemaveprefsrsion}/python27/libs``
    "S22", "MacOS", ``~/Library/Preferences/MAXON/{prefs}/python27/libs``
    "R23", "Windows",  ``%APPDATA%/MAXON/{prefs}/python37/libs``
    "R23", "MacOS", ``~/Library/Preferences/MAXON/{prefs}/python37/libs``
    "S24 or greater", "Windows",  ``%APPDATA%/MAXON/{prefs}/python39/libs``
    "S24 or greater", "MacOS",  ``~/Library/Preferences/MAXON/{prefs}/python39/libs``

Within these patterns, the symbol ``{prefs}`` stands for the distinct preferences directories of the 
different apps that are being shipped with a Cinema 4D installation. For a singular Cinema 4D 
installation up to six preferences directories can exist, one directory for each app shipped with
Cinema 4D. These six directories will all share a common hash prefix for the installation but are
differentiated by a postfix. Note that these directories only do exist when their respective
app has been run at least once.

Preferences Directory Patterns
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. csv-table::
    :header: Pattern, Example, Application
    :widths: 20, 60, 20

    "name", ``\AppData\Roaming\MAXON\R26_F0AA9E5D``, "Cinema 4D"
    "name_p", ``\AppData\Roaming\MAXON\R26_F0AA9E5D_p``, "c4dpy"
    "name_x", ``\AppData\Roaming\MAXON\R26_F0AA9E5D_x``, "Commandline"
    "name_c", ``\AppData\Roaming\MAXON\R26_F0AA9E5D_c``, "Teamrender Client"
    "name_s", ``\AppData\Roaming\MAXON\R26_F0AA9E5D_s``, "Teamrender Server"
    "name_w", ``\AppData\Roaming\MAXON\R26_F0AA9E5D_w``, "Cineware"


.. _interpreter_module_search_paths:

Python Interpreter Bound Search Paths
-------------------------------------

For cases where the complexity of app specific search paths is not required, the search paths for 
specific Python versions can be used instead. All apps that use the targeted Python interpreter will
be able import modules from these paths.

.. csv-table::
    :header: Python Core, Operating System, Path
    :widths: 20, 20, 60

    "Python 2.7 (``R20 <= C4D_VERSION < R23``)", "Windows", ``%APPDATA%/MAXON/python27/libs``
    "Python 2.7 (``R20 <= C4D_VERSION < R23``)", "MacOS", ``~/Library/Preferences/MAXON/python27/libs``
    "Python 3.7 (``C4D_VERSION == R23``)", "Windows", ``%APPDATA%/MAXON/python37/libs``
    "Python 3.7 (``C4D_VERSION == R23``)", "MacOS", ``~/Library/Preferences/MAXON/python37/libs``
    "Python 3.9 (``C4D_VERSION >= S24``)", "Windows", ``%APPDATA%/MAXON/python39/libs``
    "Python 3.9 (``C4D_VERSION >= S24``)", "MacOS", ``~/Library/Preferences/MAXON/python39/libs``

.. _custom_module_search_paths:

Custom Interpreter Bound Search Paths
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Cinema 4D also offers the ability to register custom interpreter bound search path, similar to the
``PYTHONPATH`` environment variable of the standard CPython interpreter. The paths must be setup as 
an environment variable in the OS, as shown at the example of Windows below.

.. image:: /_imgs/manuals/introduction/python_environment_variable.gif

Setting up a module search path in ``E:\C4DModules39`` for all Cinema 4D apps the make use of a
CPython 3.9 core.

The environment variable of the Pyton interpreters are both on Windows and MacOS as follows:

.. csv-table::
    :header: Python Core, Variable
    :widths: 20, 20, 60

    "Python 2.7 (``C4D_VERSION < R23``)", ``C4DPYTHONPATH``
    "Python 3.7 (``C4D_VERSION == R23``)", ``C4DPYTHONPATH37``
    "Python 3.9 (``C4D_VERSION >= S24``)", ``C4DPYTHONPATH39``

.. note::
    To register multiple search paths, the elements must be separated by a semicolon on Windows
    and by a colon on MacOS in the value of the variable. This feature is only available for
    ``C4DPYTHONPATH37`` and ``C4DPYTHONPATH39``.

.. _manual-python-init:

Executing Code with ``python_init.py``
--------------------------------------

For each app in a Cinema 4D installation also two files named ``python_init.py`` are being executed
before the plugin registrations of a booting app are being carried out.

.. note::
    See :ref:`Plugin Messages` for a per plugin approach of executing code at the different stages
    of the Cinema 4D boot sequence.

The ``python_init.py`` files do not exist by default and must be created manually. One file
can be placed at the root of the interpreter bound search path, and one at the root of the search
path in the preferences directory of the booting app. The code contained in these *python_init* 
modules can already make use of the `c4d` and `maxon` packages. The module located in the interpreter
bound search path is being executed first, the one in the search path in the preferences directory
of the booting app after that. For booting the *Cinema 4D* executable of an S26 installation, the
files and order could be as follows:

 * Windows:
    #. ``%APPDATA%/MAXON/python39/python_init.py``.
    #. ``%APPDATA%/MAXON/R26_F0AA9E5D/python39/python_init.py``.

 * MacOS:
    #. ``~/Library/Preferences/MAXON/python39/python_init.py``.
    #. ``~/Library/Preferences/MAXON/R26_F0AA9E5D/python39/python_init.py``.

The exact location of the *python_init* module in the app specific search paths depends on the 
information lined out under :ref:`Installation & App Specific Search Paths`.

.. note:: In R20 SP1 the *python_init* modules are not being loaded.

Module Attributes of ``python_init.py``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Before execution, the following module attributes will be injected into each module. This means
these symbols can be used in code although they are not defined in the module.

- `doc`: :class:`BaseDocument <c4d.documents.BaseDocument>` : The active document.
- `op`: :class:`c4d.BaseObject` : The selected object. Can be ``None``.
- `tp`: :class:`c4d.modules.thinkingparticles.TP_MasterSystem` : The Thinking Particles Master 
   system of the document. Can be ``None``.

.. note:: In R20 SP2 the module attributes are not available.


.. _local_module_search_paths:

Local Plugin Search Paths
-------------------------

It is important to understand that the directory of plugin does *not* lie in the default search 
paths of a Cinema 4D Python environment. In order to import a module or package which is located in 
relation to a ``pyp`` file, the path must be added to ``sys.path`` of the Python instance which has 
loaded the plugin.

This approach can cause multiple problems of which some are being lined out in the code example
below.

 .. code-block:: python

    """Code example for loading local modules for a plugin with the following structure:

        MyPlugin
        +-- res
            +-- ...
        +-- modules               <-- Contains the local modules/packages
        |   +-- __init__.py
        |   +-- myModule.py       <-- A module to import
        +-- myPlugin.pyp          <-- The plugin that is meant to import from MyPlugin/modules
    """

    import os
    import sys

    # Get the directory the plugin is located in and add the directory called "modules" in this root 
    # directory to the search paths of the Python instance executing this file.
    root = os.path.dirname(__file__)
    sys.path.insert(0, os.path.join(root, "modules"))

    # One must check if there is already a module present before importing a local module and if 
    # this is the case, refrain from importing the local module. There are two scenarios how a local
    # module called "myModule" could already be present:
    #
    #   1. One's plugin has run before and already imported the module before. Invoking 
    #      "import myModule" again will do no harm in this case (and also have no effect).
    #   2. There is either another plugin installed on the machine which happens to also have a 
    #      local module called "myModule" or there is a globally installed module "myModule. 
    #      Invoking "import myModule" might replace the module of that other plugin with the local 
    #      module "myModule", rendering the other plugin non-functional or even causing Cinema 4D 
    #      to crash.

    # Attempt to import a local module called "myModule"
    if "myModule" not in sys.modules.keys():
        import myModule
    # This else statement is optional, but useful for debugging. The condition should be removed in 
    # release versions of the plugin as it is not necessary there. This condition will ensure that 
    # the command "Reload Python Plugins" of Cinema 4D not only reloads/executes the pyp files again, 
    # but that also their local modules are being reloaded. When not done, Cinema 4D must be restarted 
    # to reflect changes made in "myModule.py" while the app was already running. Note that this is 
    # Python 3.x specific code, in Python 2.x one must use  the builtin function ``reload`` instead, 
    # e.g.: ``reload(myModule)``.
    else:
        import importlib
        importlib.reload(myModule)

    # After the local module has been imported, the module search paths must be cleaned up, so that
    # other code does not accidentally import a module from it.
    sys.path.pop(0)

    # A few notes:
    #
    #   1. The module search paths in ``sys.path`` are inspected by ``import`` in a lazy sequential 
    #      order. I.e., if there is a ``math`` module to be found in ``sys.path[0]`` and one in 
    #      ``sys.path[1]``, the ``math`` module in ``sys.path[1]`` will be shadowed.
    #   2. There are however exceptions to that rule, and a Python interpreter might reorder 
    #      ``sys.path`` to move a builtin path to the start of the list, or outright ignore 
    #      ``sys.path`` and search a builtin search path before searching any path in ``sys.path`` 
    #      at all.
    #   3. It is therefore recommended to avoid name collisions as best as possible:
    #       a. Do not name a module ``math``, name it ``myplugin_math``
    #       b. When shipping popular public packages with your plugin, rename the packages when 
    #          possible. When you for example want to ship ``urllib3`` with your plugin, it is best 
    #          to rename it to ``myplugin_urllib3`` to avoid version collisions.


    # The normal plugin code starts here ...

    import c4d

    class MyPluginData(c4d.plugins.ObjectData):
        """My Plugin.
        """
        ...