Table of contents > More ways to define nodes

More ways to define nodes

Defining a node by importing an existing callable

Since the Python language has been around for so long, longer than Java for instance, it has a vast collection of powerful functions/callables already written, ready to be imported.

Because of that, rather than writing your own callable, you'll often just want to turn an existing one from a module into a node.

To do that just create your node script folder in the desired category (or create a new category folder if you want) and use one of the following templates in your __main__ script.

If you want to use a callable from the standard library, just use the code below. Here, for instance, we turn math.sqrt() into a node.

### turning callable from standard library into a node

### import callable
from math import sqrt

### alias it as the main callable
main_callable = sqrt

### define the standard library import statement
stlib_import_text = 'from math import sqrt'

If you want to use a callable from a third-party library, use the code below instead. Here, for instance, we turn numpy.save() into a node:

### turning callable from third-party library into a node

### import callable
from numpy import save

### alias it as the main callable
main_callable = save

### define the third-party library import statement
third_party_import_text = 'from numpy import save'

Incompatible callables and custom signatures

Even though Nodezator can turn any callable into a node, some require an extra step.

Internally, Nodezator uses the inspect.signature() function from the standard library to gather information about the signature of the callable to be turned into a node.

However, not all callables can have their signature inferred in that way and would instead raise an error, as explained in the inspect.signature() documentation:

...in CPython, some built-in functions defined in C provide no metadata about their arguments.

Fortunately, solving that is trivial and require only a single extra step: that you provide the signature yourself by defining a dummy function. The function doesn't need a body or any code, just the def statement suffices. Here's an example of a __main__.py file where we turn pygame.image.save() into a node, even though it is inherently incompatible with inspect.signature():

### providing a signature for an incompatible callable

### import the callable
from pygame.image import save

### define it as the main callable
main_callable = save

### third-party library import statement
third_party_import_text = 'from pygame.image import save'

### write the dummy function and alias it as the
### signature_callable; this is the only additional step
### to turn an incompatible callable into a node

def _save(surface, filename_or_fileobj, namehint=''):
    pass

signature_callable = _save

Note that the underline character ('_') in the name of the signature callable _save was added only to differentiate it from the original save() function. You can use whichever naming convention you want, though.

You can also use this feature even with compatible callables in order to provide a more useful signature. For instance, to define widgets for the callable, since widgets are defined in the signature.

For example, you know the node we defined for math.sqrt() earlier? Here's how we would make it even more useful by providing a custom signature that is equivalent to its original one but results in the definition of a widget to help the user provide integers.

### providing another (equivalent) signature for math.sqrt()

### import callable
from math import sqrt

### alias it as the main callable
main_callable = sqrt

### define the standard library import statement
stlib_import_text = 'from math import sqrt'

### write the dummy function and alias it as the
### signature_callable; this new signature will be
### used to define the node instead of the main callable

def _sqrt(x:int=4, /) -> float:
    pass

signature_callable = _sqrt

If you want to learn how to define widgets take a look at Basic ways to define widgets, Full syntax and more widgets, Widget presets and more widgets and Preview widgets.

Some callables, besides not being compatible with inspect.signature(), also have more than 01 signature.

For instance, the pygame.Rect class has 03 different signatures:

So, how would we define a node for this class? Simple: you would need to define a node for each signature.

So, suppose you had a generative_art node pack (a folder) and inside it you had a geometry2d category (also a folder), you'd create 03 different node script folders, each with its own __main__.py file representing a node with a distinct signature.

This could be the code for the 1st signature:

### 1st pygame.Rect node
### generative_art/geometry2d/rect_from_values/__main__.py

### import the callable
from pygame import Rect

### define it as the main callable
main_callable = Rect

### third-party library import statement
third_party_import_text = 'from pygame import Rect'

### write the dummy function and alias it as the
### signature_callable;

def _Rect(left, top, width, height):
    pass

signature_callable = _Rect

And this for the 2nd one:

### 2nd pygame.Rect node
### generative_art/geometry2d/rect_from_pos_size/__main__.py

### import the callable
from pygame import Rect

### define it as the main callable
main_callable = Rect

### third-party library import statement
third_party_import_text = 'from pygame import Rect'

### write the dummy function and alias it as the
### signature_callable;

def _Rect(topleft, size):
    pass

signature_callable = _Rect

And this, finally, for the 3rd one:

### 3rd pygame.Rect node
### generative_art/geometry2d/rect_from_object/__main__.py

### import the callable
from pygame import Rect

### define it as the main callable
main_callable = Rect

### third-party library import statement
third_party_import_text = 'from pygame import Rect'

### write the dummy function and alias it as the
### signature_callable;

def _Rect(object):
    pass

signature_callable = _Rect

Note that the only difference between the 03 scripts is that each is in its own node script folder (rect_from_values, rect_from_pos_size and rect_from_object) and their signature callable.

Finally, I'd just like to point out that just cause a callable has multiple signatures doesn't mean it is worth to create distinct nodes for all of them. The pygame.image.save() function we met earlier, for instance, actually has 02 different signatures.

The pygame online documentation states that it can be represented by save(surface, filename) or save(surface, fileobj, namehint=''). However I decided to implement only the latter signature, the one containing the namehint parameter, because it already covers all existing use-cases.

Call formatting for improving node definitions and solving issues

There is another useful feature you need to know in other to help you improve your node definitions and even solve specific issues. It is called call formating.

Some function names are too simple for their own good and make name clashes too likely to happen. For instance, both pygame.image.save() and numpy.save() functions have the same name. Instanciating and executing nodes inside Nodezator would not be problem, because they exist in different node script folders, so Nodezator has no problem telling them apart.

Nonetheless you'd still have different nodes with the same title on top ("save") which would be confusing. Even worse, when exporting your node layout as python code, the import statements would cause a name clash, that is, both from numpy import save and from pygame.image import save would be executed and whichever executed last would override the other, creating a bug in the exported code.

Solving this is simple, though: just use a new name for one (or both) of the callables and add a call_format variable specifying how the callable must be called. The variable must contain a string. Here's how you'd do it:

### changing name of numpy.save()

### import callable
from numpy import save as save_array

### alias it as the main callable
main_callable = save_array

### define the third-party library import statement
third_party_import_text = (
  'from numpy import save as save_array'
)

### define a call_format variable specifying how the
### callable will be called from now on
call_format = 'save_array'

Even though you only need to do this to one of the nodes in order to eliminate the name clash, we recommend doing this to both. In fact, everytime you suspect the name of a function is too common/simple, it is a good practice to pick a more expressive name to help differentiate it, even if no name clash exists. Here's how the code for pygame.image.save() would be:

### changing name of pygame.image.save()

### import the callable
from pygame.image import save as save_surface

### define it as the main callable
main_callable = save_surface

### third-party library import statement
third_party_import_text = (
  'from pygame.image import save as save_surface'
)

### dummy function aliased as signature_callable

def _save(surface, filename_or_fileobj, namehint=''):
    pass

signature_callable = _save

### define a call_format variable specifying how the
### callable will be called from now on
call_format = 'save_surface'

There's still another issue call_format helps solving. Some callables cannot be exported directly from a module. For instance, itertools.chain.from_iterable() cannot be exported directly because it is an attribute of the chain() object living within the itertools module. That is, the import statement from itertools.chain import from_iterable would fail.

Fortunately, solving this is also simple using call_format. Here's how it's done:

### turning itertools.chain.from_iterable() into a node

### import statement (cannot import from_iterable())
from itertools import chain

### alias from_iterable() as the main callable;
###
### we don't need to define a signature callable for
### from_iterable(), cause it is compatible with
### inspect.signature()
main_callable = chain.from_iterable

### define the standard library import statement
stlib_import_text = 'from itertools import chain'

### use call_format to define how from_iterable() must
### be called
call_format = 'chain.from_iterable'

Nodezator already includes chain.from_iterable() as an app-defined node, so you don't need to do this yourself, though.

As a curiosity, I actually decided to add a custom signature to chain.from_iterable() in Nodezator using a signature_callable. I did it so that I could annotate the type of the input and output. It was just a cosmetic measure, though, because annotated inputs/outputs are color-coded when displayed as sockets within nodezator.

Previous chapter | Table of contents | Next chapter