Develop Add-ons like a pro

Everything* about

add-on development

workflow

by dr. Sybren A. Stüvel

* Not necessarily valid for every meaning of the word "Everything"

Ph.D. on crowd simulation

& developer

Employed by the Blender Institute since March 2016

Blender commit rights since 2014

First bug report in 2004

Two things

Interrupt me

&

Ask stupid questions

Outline

  1. Work environment & project structure
  2. Best behaviour
  3. Common pitfalls

Work Environment

Development environments

  • Blender Text Editor
  • PyCharm & PyDev
  • Atom & Sublime
  • VIM & Emacs

My desktop

  • Kubuntu Linux 16.04
  • Shell: ZSH
  • Atom, PyCharm (Community edition), Qt Creator
  • Blender Text Editor

Code completion & Highlighting

Or: how do I get rid of all those red squigglies?

Rebuild Blender as module

In CMakeCache.txt set WITH_PYTHON_MODULE:BOOL=ON

make && make install

Virtualenv & Virtualenvwrapper

Install them, well documented how


mkvirtualenv -p /usr/bin/python3.5 bladdons
cdvirtualenv
cd lib/python3.5/site-packages
ln -s ~/workspace/blender-git/build_bpy/bin/bpy.so
ln -s ~/workspace/blender-git/blender/release
        

Virtualenv & Virtualenvwrapper


% python
Python 3.5.2 (default, Sep 10 2016, 08:21:44)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import bpy
>>> bpy.ops.wm.quit_blender()

Blender quit
        

Project structure

Can be a single Python file

Better: proper Python project

  • README.md
  • setup.py
  • package_name / __init__.py
  • / submodule.py
  • tests/test_xxx.py
  • setup.cfg

Show of hands

Who uses version control software?

Where are my files?

Where you want them to be

Making Blender find them

Linux, BSDs, etc.:
Symlink package into ~/.config/blender/2.78/scripts/addons
Windows:
Configure Blender to load scripts from C:\workspace\my_addons
MacOS:
No idea, probably either of the above options work

Getting started

Blender Text Editor has many examples


import bpy

def main(context):
    for ob in context.scene.objects:
        print(ob)

class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        main(context)
        return {'FINISHED'}

def register():
    bpy.utils.register_class(SimpleOperator)

def unregister():
    bpy.utils.unregister_class(SimpleOperator)
        

Turning your package into an add-on


bl_info = {
    'name': 'Blender Cloud',
    'author': 'Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis',
    'version': (1, 4, 99),
    'blender': (2, 77, 0),
    'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
    'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '
                   'and Blender 2.77a or newer.',
    'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.6/Py/'
                'Scripts/System/BlenderCloud',
    'category': 'System',
    'warning': 'This is a beta version; the first to support Attract.'
}
        

Parsed, not executed

Best Behaviour

Naming your classes

Naming your classes

Operator bpy.ops.bcloud.browse
class BCLOUD_OT_browse
Panel bl_idname='mesh.scramble'
class MESH_PT_scramble

... and similar for menus, headers, etc.

Reloading with F8

What does F8 do?

  1. Call unregister()
  2. Reload Blender-loaded modules from disk
  3. Execute those modules' code
  4. Call register()

Code on disk != module in memory


# Your code
x = 5
        

# Your new code
x = 8
        

Making your code reloadable with F8


if "bpy" in locals():
    import importlib
    importlib.reload(Boltfactory)
else:
    from add_mesh_BoltFactory import Boltfactory

import bpy

...
        

What's happening?

  • self.report()
  • print() statements
  • remote debugging
  • logging module

print() statements

+
Simple to use, well-known
-
Must be removed to hide output
No metadata (timestamps, severity level, location in code)
No user control

Remote debugging

+
Detailed info of code flow & state
Does not require extra code
(depending on IDE) Smart breakpoints
-
Little metadata (timestamps, severity level)
Requires IDE support & configuration
Only for developers

Remote debugging with PyDev

screenshot
screenshot
screenshot
screenshot
screenshot
screenshot
screenshot

Logging module

+
Can be left in the code
Metadata available (timestamps, severity level, location in code)
Detailed user control
-
Slightly more complex than print()
Requires some configuration for optimal use

Logging module example


class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    log = logging.getLogger('bpy.ops.%s' % bl_idname)

    def execute(self, context):
        self.log.info('It is happening now in scene %s', context.scene)

        if not context.object:
            self.log.warning('No active object, not doing anything')
        self.log.debug('Not doing anything here either')

        return {'FINISHED'}
    

Default behaviour

Shown on stdout, only WARNING or higher level.


No active object, not doing anything
        

Configured as I like it

Still shown on stdout, INFO or higher level.

DEBUG level for selected loggers.

Metadata shown partially.


16:00:03.03 bpy.ops.object.simple_operator  WARNING No active object, not doing anything
16:00:03.13 bpy.ops.object.simple_operator  DEBUG   Not doing anything here either
        

Configuration

Loaded at Blender startup.

.../scripts/startup/config_logging.py

Linux
~/.config/blender/2.78/...
Windows
%APPDATA%\Blender Foundation\Blender\2.78\...
MacOS
/Users/$USER/Library/Application Support/Blender/2.78/...

"Configuring Directories" in Blender Manual


import logging.config
logging.config.dictConfig({
    'version': 1,
    'formatters': {'default': {'format': '%(asctime)-15s %(levelname)8s %(name)s %(message)s'}},
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'default',
            'stream': 'ext://sys.stderr',
        }
    },
    'loggers': {
        'bpy.ops.': {'level': 'DEBUG'},
        'blenderid': {'level': 'DEBUG'},
        'bid_api': {'level': 'DEBUG'},
        'bid_addon_support': {'level': 'DEBUG'},
    },
    'root': {
        'level': 'WARNING',
        'handlers': ['console'],
    }
})
        

Common pitfalls

Comments

Document why

The what & how should be clear from code.

Docstring can describe the what, and is the first thing to write.

EnumProperty items callbacks

Warning: There is a known bug with using a callback, Python must keep a reference to the strings returned or Blender will misbehave or even crash.

EnumProperty items callbacks (SEE NEXT SLIDE)


def pyside_cache(wrapped):
    """Stores the result of the callable in Python-managed memory.

    This is to work around the warning at
    https://www.blender.org/api/blender_python_api_master/bpy.props.html#bpy.props.EnumProperty
    """

    import functools

    @functools.wraps(wrapped)
    # We can't use (*args, **kwargs), because EnumProperty explicitly checks
    # for the number of fixed positional arguments.
    def wrapper(self, context):
        result = None
        try:
            result = wrapped(self, context)
            return result
        finally:
            wrapped._cached_result = result
    return wrapper
            

EnumProperty items callbacks revised version

This stores the cached results in the bl_rna dict for the property, rather than on the callback function. A downside is that the decorator now needs the name of the property it's a callback for.


def pyside_cache(propname):

    if callable(propname):
        raise TypeError('Usage: pyside_cache("property_name")')

    def decorator(wrapped):
        """Stores the result of the callable in Python-managed memory.

        This is to work around the warning at
        https://www.blender.org/api/blender_python_api_master/bpy.props.html#bpy.props.EnumProperty
        """

        import functools

        @functools.wraps(wrapped)
        # We can't use (*args, **kwargs), because EnumProperty explicitly checks
        # for the number of fixed positional arguments.
        def wrapper(self, context):
            result = None
            try:
                result = wrapped(self, context)
                return result
            finally:
                rna_type, rna_info = getattr(self.bl_rna, propname)
                rna_info['_cached_result'] = result
        return wrapper
    return decorator
        

"A function should have only one point of return"

BUT WHY!?

Return early

... and use break, continue etc.

common approach: If good → do it

dr. Sybren says: If bad → abort

NEE


def some_func(param1: int, param2: float) -> float:
    """Performs our special snowflake multiplication.

    :param param1: non-zero integer
    :param param2: positive float
    :returns: our magic number
    """

    if param1 != 0:
        if param2 > 0:
            return param1 * param2
        else:
            raise ValueError('param2 must be positive')
    else:
        raise ValueError('param1 may not be zero')
        

Oooooh JA!


def some_func(param1: int, param2: float) -> float:
    """Performs our special snowflake multiplication.

    :param param1: non-zero integer
    :param param2: positive float
    :returns: our magic number
    """

    if param1 == 0:
        raise ValueError('param1 may not be zero')

    if param2 <= 0:
        raise ValueError('param2 must be positive')

    return param1 * param2
        

NOOOO


def somefunc(...):
    ...
    if jemoeder.weight > 3:
        return True
    else:
        return False
        

Yes!


def somefunc(...):
    ...
    return jemoeder.weight > 3



        
http://docs.quantifiedcode.com/python-anti-patterns/

Must read!

The little book of Python anti-patterns

http://docs.quantifiedcode.com/python-anti-patterns/

Putting

1 GB of assets

in your add-on

Instead of putting 1 GB of assets in your add-on...

bpy.utils.user_resource('CACHE') (once T47684 is done)

https://github.com/ActiveState/appdirs/blob/master/appdirs.py

Just one more thing

x += y

is not the same as

x = x + y

We looked at

  • Setting up a work environment
  • Creating an add-on & naming classes
  • F8 to reload
  • Using logging
  • Returning early & often
  • … and other silly things

Thank you!

Questions? Remarks?