mxutils

Contains helper functions and types that are written in Python and exclusive to the Python API.

Overview

Classes

mxutils.IdProvider(idType)

Generates int or maxon.Id identifiers when defining them manually is not desirable.

mxutils.LocalImportPath(path, autoReload)

Provides a context manger to import from packages relative to a file which do not lie in a module search path.

Functions

mxutils.CheckIterable(item[, tElement, …])

Checks item for being an iterable of a specific type, being composed of specific elements, or being of specific length.

mxutils.CheckType(item[, t, label])

Checks item for being of type t.

mxutils.GetTreeString(node[, childrenAttr, …])

Returns a string representation of a tree structure.

mxutils.ImportSymbols(path[, output])

Parses elements of all resource files contained in the given path into the global scope of the caller or into a dictionary.

Function Listing

mxutils.CheckIterable(item: Any, tElement: Optional[Union[Type, tuple]] = None, tIterable: Optional[Union[Type, tuple]] = None, label: str | None = None, minCount: int | None = None, maxCount: int | None = None) → Any
Checks item for being an iterable of a specific type, being composed of specific elements, or being of specific length.

When any of the checks fail, an error is being raised. If not, item is being passed through. The function can automatically discover the symbol of item when it is defined in the local or global scope of the function call. While all types of iterables are supported, it is only dict and c4d.BaseContainer which are supported in their specific item, i.e., pairs, decomposition. All other iterables are treated as iterables of singular elements as for example lists or tuples.

When possible, it is advantageous to replace iterable type checks with type conversions due to the computationally expensive nature of checking every item in an iterable. The expensive part of this call is passing not None for tElement, i.e., checking the type of every element in item (when item is very large), all other checks are negligible in cost.
Parameters
  • item (typing.Any) – The item to be type checked.

  • tElement (typing.Type | tuple[typing.Type] | None (, optional)) – The type or types of elements the iterable item is allowed to have. Passing None will will accept any kind of element type. Defaults to None.

  • tIterable (typing.Type | tuple[typing.Type] | None (, optional)) – The type or types the iterable item is allowed to be. Passing None will accept any kind of iterable. Defaults to None.

  • label (str | None (, optional)) – The label with which to refer to item in exceptions. Passing None will result in an attempt to infer the symbol. Symbol discovery only works for values in the local or global scope. Defaults to None.

  • minCount (int | None (, optional)) – The minimum length for the iterable item. Passing None will accept any iterable that is smaller than or equal to maxCount. Value must be smaller than or equal to maxCount Defaults to None.

  • maxCount (int | None (, optional)) – The maximum length for the iterable item. Passing None will accept any iterable that is greater than or equal to minCount. Value must be larger or equal to minCount Defaults to None.

Raises
  • TypeError – When item is failing the type tests.

  • ValueError – When minCount is larger than maxCount.

  • RuntimeError – When any of the length checks fail.

  • RuntimeError – When accessing a stack trace or reading an executed file failed on a symbol discovery attempt.

Returns

The value of item when the checks succeeded.

Return type

typing.Any

Example

import c4d

from mxutils import CheckIterable

# Checks if `data` is composed of strings only; this will fail as there is an `int` in it.
data: list[str|int] = ["1", "2", "3", 4, "5"]
data = CheckIterable(data, str)

# But the prior case is also an example for where element type assertions are unnecessary. 
# As we can enforce the type of items in `data` with `str()`.
item: str = str(data[3])

# The common use case is checking type composition assumptions of function arguments passed 
# by unsafe sources, in order to avoid carrying out expensive computations which fail in a 
# very late stage due to encountering a malformed item in the input data.
class Foo:
    def __init__(self, nodes: list[c4d.BaseObject | c4d.BaseTag]) -> None:
        # Assure that _nodes will be a list of tags and objects.
        self._nodes = CheckIterable(nodes, (c4d.BaseObject, c4d.BaseTag), list)

# Tested can also be the length of the iterable.

# Assures that `data` has at least the length of one. No specific type checks are performed 
# in this case, but `data` is still being tested for being an iterable. This is a very low
# cost call, no matter how large data is, as the first argument `tElement` has is `None` and
# `data` is therefore never traversed as elements of any type are accepted.
data = CheckIterable(data, minCount=1)

# Assures that `data` is not longer than five elements.
data = CheckIterable(data, maxCount=5)

# Assures that the length of `data` lies in the interval [1, 5].
data = CheckIterable(data, minCount=1, maxCount=5)

# The function will always test if ``item`` is an iterable, even when both type tests are
# set to None. So, this will raise an error as `int` does not implement `__iter__`
CheckIterable(1)

# but this will not, as `c4d.BaseContainer` does implement `__iter__`:
data: c4d.BaseContainer = c4d.BaseContainer()
CheckIterable(data)

# All forms of iterable are supported, but only `c4d.BaseContainer` and `dict` are handled
# manually to type test values in key, value pairs instead of testing the type of the pair.

data[0] = "Hello world"
data[1] = True
# Will test data for being of type BaseContainer, holding only values of type str or bool.
CheckIterable(data, (str, bool), c4d.BaseContainer)

# The same thing for dict, key type testing is not supported.
data = {1: "Hello World", "my_key": True}
CheckIterable(data, (str, bool), dict)

# Just as for CheckType(), automatic symbol discovery is only possible for values in the
# local or global scope. So, when testing `sys.path`, one should pass a label, as errors
# will otherwise refer to an 'unknown symbol'.

import sys
# Assures that sys.path is a list of strings with at least one element.
paths = CheckIterable(sys.path, str, list, "sys.path", 1)
mxutils.CheckType(item: Any, t: Optional[Union[Type, tuple]] = None, label: str | None = None) → Any
Checks item for being of type t.

When the check fails, an error is being raised. If not, item is being passed through. The function can automatically discover the symbol of item when it is defined in the local or global scope of the function call.
Parameters
  • item (typing.Any) – The item to be type checked.

  • t (typing.Type | tuple[typing.Type] | None (, optional)) – The type or types item is allowed to be. Passing None will only assert that item is not None. Defaults to None.

  • label (str | None (, optional)) – The label with which to refer to item in exceptions. Passing None will result in an attempt to infer the symbol. Symbol discovery only works for values in the local or global scope. Defaults to None.

Raises
  • TypeError – When item is failing the type test.

  • RuntimeError – When accessing a stack trace or reading an executed file failed on a symbol discovery attempt.

Returns

The value of item when the check succeeded.

Return type

typing.Any

Example

import c4d
import math

from mxutils import CheckType

MY_INT: int = 42

class Foo:
    SOME_INT: int = 42

    def __init__(self, x: int) -> None:
        # Asserts that `x` is not `None`.
        CheckType(x)

        # Asserts that `x` is not `None` and assigns its values to `self._x`.
        self._x: object = CheckType(x)

        # Asserts that `MY_INT` is of type `int` or `float` and assigns its int value to `self._y`.
        self._y: int = int(CheckType(MY_INT, (int, float)))


        # Symbols which are neither in the global nor local scope are not automatically
        # discoverable and error messages for such values will refer to the value with
        # 'unknown symbol'.

        # `Foo.SOME_INT` and `math.pi` are not part of the local or global scope. When error
        # messages should not refer to a generic 'Unknown Symbol', one must pass in a label.

        # CheckType(Foo.SOME_INT, bool)
        # TypeError: Expected value of type <class 'bool'> for 'Unknown Symbol'. Received: <class 'int'>

        CheckType(math.pi, int, 'math.pi')
        # TypeError: Expected value of type <class 'int'> for 'math.pi'. Received: <class 'float'>

        # `x`, `y`, and `MY_INT` are in the global/local scope and automatically discovered.
        y: float = 1.0

        CheckType(x, float) 
        # TypeError: Expected value of type <class 'float'> for 'x'. Received: <class 'int'>

        CheckType(y, int)
        # TypeError: Expected value of type <class 'int'> for 'y'. Received: <class 'float'>

        CheckType(MY_INT, bool)
        # TypeError: Expected value of type <class 'bool'> for 'MY_INT'. Received: <class 'int'>
mxutils.GetTreeString(node: Any, childrenAttr: str | None = None, getDownFunc: str | None = 'GetDown', getNextFunc: str | None = 'GetNext', indentWidth: int = 3, callback: Optional[Callable, None] = None) → str
Returns a string representation of a tree structure.

The tree of #node is traversed using either a list of child nodes attached to each node under the attribute #childrenAttr, or by calling the functions #getDownFunc and #getNextFunc on each node to get the first child and the next sibling, respectively.

Examples:

# A node type to print trees for, this is a node type which stores its children as a list.
class Node:
    def __init__(self, value: int) -> None:
        self.value = value
        self.children = []

    def __str__(self) -> str:
        return str(self.value)

    def AddChild(self, child: 'Node') -> None:
        self.children.append(child)

# Create a tree structure.
tree: Node = Node(1)
tree.AddChild(Node(2))
tree.AddChild(Node(3))
tree.children[1].AddChild(Node(4))

# Print the tree structure by telling the function to use the #children attribute.
print(GetTreeString(tree, "children"))
1
├─ 2
└─ 3
   └─ 4
# Example for printing a node tree for a node type which can be navigated using a function which
# yields the first child and a function which yields the next sibling, here at the example of a
# Cinema 4D BaseObject tree.

# Get the first object in the active document.
doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()
op: c4d.BaseObject = doc.GetFirstObject()
if not op:
    raise ValueError("No object found in the document.")

# Print the object tree of #using its navigation functions 'GetDown' and 'GetNext'. We could
# just invoke #GetTreeString(op) here since 'GetDown' and 'GetNext' are the default values.
print(GetTreeString(op, getDownFunc="GetDown", getNextFunc="GetNext"))

print("")

def PrintObject(node: c4d.BaseObject) -> str:
    '''Returns the name of an object and the names of its tags as a string.
    '''
    return f"{node.GetName()} {' '.join([f"[{n.GetName()}]" for n in node.GetTags()])}"

# We can also customize the string representation of each node by passing a callback function.
print(GetTreeString(op, callback=PrintObject))
<c4d.BaseObject object called Cone/Cone with ID 5162 at 140511902113152>
├── <c4d.BaseObject object called Cube/Cube with ID 5159 at 140511902142592>
└── <c4d.BaseObject object called Sphere/Sphere with ID 5160 at 140511902160768>
    └── <c4d.BaseObject object called Torus/Torus with ID 5163 at 140511902065024>

Cone [Display] [Phong] [Material]
├── Cube [Phong]
└── Sphere [Phong] [Material]
    └── Torus [Phong]
Parameters
  • node (typing.Any) – The root node of the tree.

  • childrenAttr (str, optional) – The attribute name to access the children of a node, defaults to None.

  • getDownFunc (str, optional) – The function name to get the first child of a node, defaults to “GetDown”.

  • getNextFunc (str, optional) – The function name to get the next sibling of a node, defaults to “GetNext”.

  • indentWidth (int, optional) – The width of the tree indentation for one level, defaults to 3.

  • callback (typing.Callable, optional) – A callback function to customize the string representation of each node, defaults to None.

Returns

The string representation of the tree structure.

Return type

str

Raises
  • ValueError – If the callback is provided but not callable.

  • ValueError – If the childrenAttr is not found on the node.

  • ValueError – If the getDownFunc is not found on the node.

  • ValueError – If the getNextFunc is not found on the node.

  • ValueError – If neither childrenAttr nor getDownFunc and getNextFunc are provided.

mxutils.ImportSymbols(path: str, output: Type = None) → dict[str, float | int | str] | None

Parses elements of all resource files contained in the given path into the global scope of the caller or into a dictionary.

See also

Symbols Manual

Parameters
  • path (str) – The path within which header files should be discovered and parsed.

  • output (typing.Type) – If the parsed symbols should be placed in the global scope or be returned as a dictionary. Pass None to let them be placed in the global scope and dict to have the function return them as a dictionary. Defaults to None.

Returns

The parsed symbol value pairs as or nothing when the output was None

Return type

dict[str, float | int | str] | None

Example

The following header file c:\data\c4d_symbols.h,

enum
{
    ID_FOO = 1000,
    ID_BAR = 1001,
    _DUMMY_ELEMENT_
};

can be imported as follows:

import os
import mxutils

# Recursively parse header files in "c:\data" into this scope.
mxutils.ImportSymbols("c:\data")

# This would also work when only a singular file should be parsed.
mxutils.ImportSymbols("c:\data\c4d_symbols.h")

# The parsed symbols are now exposed in the global scope.
print(ID_FOO) # Will print 1000
print(ID_BAR) # Will print 1001

# Alternatively, symbols can also be parsed into a dictionary.
data: dict[str, int] = mxutils.ImportSymbols("c:\data\c4d_symbols.h", output=dict)

print(data["ID_FOO"]) # Will print 1000
print(data["ID_BAR"]) # Will print 1001