EasyBPY is amazing. Any of you using it?

I just recently came across this module that simplifies making new addons and python scripts tremendously.

Are any of you using it yet?

1 Like

personally, I find that ‘wrappers’ like this just obfuscate what is happening and introduce unnecessary latency or points of failure in a script.

This is generally true. The way these wrappers can shine though is if the API which is being wrapped is bad while also being cemented in place for the rest of its lifetime. Here, the latter does not apply and the former only applies partially. At least, I’d totally look at the sources of this to use as reference when needed.

For me, an abstraction layer is not a very good idea in this case. A better solution would be more or better documentation of the original API. Let’s say I know by heart half of easyBPY, and find out there it lacks some flexibility at some place. I have to investigate both the wrapper and the Blender API. So I prefer to get familiar with one, which is already quite a task.

I have been thinking, would it be a realistic plan to get a couple of Blender addon developers to band together and start building an unofficial documentation on GitHub? I have learned a lot from asking questions around here which a read of the documentation would never have revealed.

2 Likes

I honestly don’t understand when people say they have issues with the documentation, I’m not really sure what the expectation would be for an improvement? Everything I know is a result of reading that documentation and I can’t even remember a time when I wasn’t able to find the answer (if an answer existed). The only walls I have run into with the API have been when I want it to do something and it’s not even possible.

Lack of code examples would be my biggest gripe on the API. Beyond that, for it being Python code, it is not very Pythonic.

1 Like

There are plenty of examples in the API documentation, especially for the more complex modules such as bmesh, bgl, blf, gpu, etc. Things that get asked frequently (even though they are not very complex) such as bpy.ops are also heavily documented with numerous examples and explanations. Are you wanting a detailed usage example for every property and function in the entire API?

Here is a typical example . . . . https://docs.blender.org/api/current/bpy.types.Panel.html These are extremely simple examples.

Right now, I am trying to update some old code, the code runs now, (it did not even run before I started) but some of the buttons do not show. The simple examples on that page are not going to help me at all. Templates has examples, but they are the same type of example, for the properties window. What I mean by it not being “Pythonic” is, Python is breif, concise, and usually not very difficult to code. Ever tried coding bmesh? It is a laughing stock.

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_info = {
    "name": "Chart Graphics Generator",
    "author": "klibre, PKHG, thanks to JesterKing, Oscurart, Carlos Guerrero",
    "version": (0,0,5),
    "blender": (2, 6, 3),
    "api": 40101,
    "category": "Add Curve",
    "location": "View3d > Tools",
    "description": "Tool to build bar graphics from csv databases,",
    "warning": "",
    "wiki_url": "",
    "tracker_url": "http://www.klibre.net"}


#----------------------------------------------------

import bpy
import csv
from math import pi
import mathutils
import random
import sys

#-------------------globals---------------------------------
pid2 = pi * 0.5
sce = bpy.context.scene
obj = bpy.context.object
bc = bpy.context
boo = bpy.ops.object
boc = bpy.ops.curve
bot = bpy.ops.transform
suma = 0
chartType = '' #not yet used
negativValueSeen = False
sizesCVSfile = None
colnrPKHG = '-1'
factorescala = 5
cvs_colums = 0
allData = None
sce.frame_start = 0
#sce.frame_end = 500
time = 20
origin = bc.scene.cursor.location

#-------------------end globals---------------------------------

def interuptMe(where,debug = True):
    if debug:
        print(where);
        __import__('code').interact(local={k: v for ns in (globals(), locals()) for k, v in ns.items()})

def getCSV(path):
    global sizesCVSfile, cvs_colums  
    result = False
    try:
        csvfile =  open(path, 'r')
        dialect = csv.Sniffer().sniff(csvfile.read(1024))
        csvfile.seek(0)
        reader = csv.reader(csvfile, dialect=dialect)
        csvData = []
        for data in reader:
            csvData.append(data)
        result = csvData
        cvs_rows = len(result)
        cvs_colums = len(result[0])
        sizesCVSfile = [cvs_colums, cvs_rows]
        csvfile.close()
    except:
        print("no good csv file adress given!")
        self.report({'INFO'}, "no good csv file adress given!")
    return result
#
#
#def finalfps():
#   if bpy.ops.screen.keyframe_jump(next=True) == true :
#   
#   bpy.ops.screen.keyframe_jump(next=True)
#   
#   else :
#   
#   return {"FINISHED"}

# testpath 'C:\A000\example'

class PANEL_PT_ImportadorUI(bpy.types.Panel):
    bl_label = "Generador de Graficas"
    bl_idname = "PANEL_PT_ImportadorUI"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    #
    def draw(self, context):
        global colnrPKHG, allData, cvs_colums, time
        layout = self.layout		
        col = layout.column(align=1)
        row = layout.row()
        split=row.split()
        col.label("Path of the CSV file:")
        row.operator("lee.ruta",icon="FILE_SCRIPT")
        col = split.column()				
        col.prop(bc.scene,"importPydataPath")
        row = layout.row()
        row.prop(bc.scene,"useColumnNr")
        colnrPKHG = bc.scene.useColumnNr
        path = bc.scene.importPydataPath
        check = getCSV(path)
        #
        if colnrPKHG >= cvs_colums:
            print("column ", colnrPKHG," not possible")
        elif check:
            allData = check
            row=layout.row()
            split=row.split()
            colL0 = split.column()
            colL0.label("Graphic Type:")
            row = layout.row()
            row.prop(bc.scene,"scaleFactor")
            factorescala = bc.scene.scaleFactor
            row=layout.row()
            split=row.split()
            colL1 = split.column()  
            colL1.operator("importador.cubos", icon="MESH_CUBE")
            colR1 = split.column()
            colR1.operator("importador.cilindros", icon="MESH_CYLINDER")
            colL3 = split.column()
            colL3.operator("importador.trazos", icon="CURVE_DATA")
            colL6 = split.column()
            colL6.operator("fabricar.quesito", icon="MESH_CIRCLE")
            row=layout.row()
            split=row.split()
            colL5 = split.column()  
        #    
        if suma <= 1:
            print("build graphic before")		   
        elif check:
            allData = check
            colL5.label("Configuration:")
            row=layout.row()
            split=row.split()
            colR2 = split.column()
            colR2.operator("calculo.porcentajes", icon="LINENUMBERS_ON")
            split=row.split()
            colL4 = split.column()
            colL4.operator("align.data", icon="FONTPREVIEW")
            colL5 = split.column()
            colL5.operator("colorear.grafica", icon="MATERIAL")
            row=layout.row()



class leeruta (bpy.types.Operator):
    bl_idname = "lee.ruta"
    bl_label = "Leer" 
    bl_options = {"REGISTER", "UNDO"}  # %% ADDED
    #
    def execute(self,context):
        global path
        path = bc.scene.importPydataPath
        return {"FINISHED"}


# format data for values
class formatcurves (bpy.types.Operator):
    bl_idname = "format.curves"
    bl_label = "format curves" 
    #
    def execute(self,context):  
        # boo.convert(target='CURVE', keep_original=False)
        bc.selected_objects[0].data.extrude = 0.01
        bc.selected_objects[0].data.bevel_depth = 0.005
        return {"FINISHED"}


# importa data, fabica  cubos con los datos
class importadorcubos(bpy.types.Operator):
    bl_idname = "importador.cubos"
    bl_label = "Cubes"
    bl_options = {"REGISTER", "UNDO"}  # %% ADDED
    #
    def execute(self,context):
        global colnrPKHG, allData, chartType, origin
        factorescala = bc.scene.scaleFactor
        chartType = 'cubes'
        distancia= 0
        iescalado = 0
        bpy.ops.ver.nombres()
        bpy.ops.calculo.total()
        offset = bpy.context.scene.cursor_location.copy() #scris de mine dupa printscreenuri
        for data in allData:
            distancia += 3
            escalando = float(data[colnrPKHG])/factorescala
            bpy.context.scene.cursor_location = (offset[0],offset[1]+2,offset[2])#scris de mine dupa printscreenuri
            if escalando >= 0:
                boo.text_add(location=((origin[0] + distancia),(origin[1]+1.5),(origin[2]+((escalando)*2)+0.2)),rotation=(pid2, 0, 0))
                boo.editmode_toggle()
                bpy.ops.font.delete()
                bpy.ops.font.text_insert(text=data[colnrPKHG], accent=False)
                boo.editmode_toggle()
                bpy.ops.mesh.primitive_cube_add(location=((origin[0]+distancia),(origin[1]+1.5),(origin[2]+escalando)))
                bot.resize(value=(1,1,escalando))
                xmat = random.uniform(0.05,0.95)
                ymat = random.uniform(0.05,0.95)
                zmat = random.uniform(0.05,0.95)
                colordifuso = bpy.data.materials.new('RandomColor')
                colordifuso.diffuse_color = (xmat,ymat,zmat)
                colordifuso.specular_color = ((xmat+0.4),(ymat+0.4),(zmat+0.4))
                colordifuso.specular_intensity = (0.7)
                bc.object.data.materials.append(colordifuso)
                boo.select_all(action='DESELECT')
            else:
                negativValueSeen = True
                self.report({'WARNING'}, "negative value occured")
                print("error: not meaningfull with negativ values")
                return {"CANCELLED"}	
        #    
        return {"FINISHED"}

# importa data, fabica  cilindros y los textcurves con la data
class importadorcilindros (bpy.types.Operator):
    bl_idname = "importador.cilindros"
    bl_label = "Cylinders"
    bl_options = {"REGISTER", "UNDO"}  # %% ADDED
    #
    def execute(self,context):
        global colnrPKHG, allData, chartType, origin
        factorescala = bc.scene.scaleFactor
        chartType = 'cylinders'
        distancia= 0
        iescalado = 0
        bpy.ops.ver.nombres()
        bpy.ops.calculo.total()
        offset = bpy.context.scene.cursor_location.copy() #scris de mine dupa printscreenuri
        #
        for data in allData:
            distancia += 3
            escalando = float(data[colnrPKHG])/factorescala
            bpy.context.scene.cursor_location = (offset[0],offset[1]+2,offset[2])#scris de mine dupa printscreenuri
            if escalando >= 0:
                boo.text_add(location=((origin[0] + distancia),(origin[1]+1.5),(origin[2]+((escalando)*2)+0.2)),rotation=(pid2, 0, 0))
                boo.editmode_toggle()
                bpy.ops.font.delete()
                bpy.ops.font.text_insert(text=data[colnrPKHG], accent=False)
                boo.editmode_toggle()
                bpy.ops.mesh.primitive_cylinder_add(location=((origin[0]+distancia+1),(origin[1]+1.5),(origin[2]+escalando)))
                bot.resize(value=(1,1,escalando))
                xmat = random.uniform(0.05,0.95)
                ymat = random.uniform(0.05,0.95)
                zmat = random.uniform(0.05,0.95)
                colordifuso = bpy.data.materials.new('RandomColor')
                colordifuso.diffuse_color = (xmat,ymat,zmat)
                colordifuso.specular_color = ((xmat+0.4),(ymat+0.4),(zmat+0.4))
                colordifuso.specular_intensity = (0.7)
                bc.object.data.materials.append(colordifuso)
                boo.select_all(action='DESELECT')
            else:
                negativValueSeen = True
                self.report({'WARNING'}, "negative value occured")
                print("error: not meaningfull with negativ values")
                return {"CANCELLED"}	
        #    
        return {"FINISHED"}
    
# muestra la suma total de los elementos
class calculototal (bpy.types.Operator):
    bl_idname = "calculo.total"
    bl_label = "Total"
    bl_options = {"REGISTER", "UNDO"}  # %% ADDED
    #
    def execute(self,context):
        global colnrPKHG, allData, suma , negativValueSeen, factorescala, origin
        negativValueSeen = False #assume OK at start
        suma = 0
        offset = bpy.context.scene.cursor_location.copy()
        for data in allData:
            bpy.context.scene.cursor_location.copy()
            tmp = float(data[colnrPKHG])
            if tmp < 0:
                negativValueSeen = True
                self.report({'WARNING'}, "negative value occured")
                print("error: not meaningfull with negativ values")
                return {"CANCELLED"}
            suma += tmp
            sumados ="total:" + str(suma)
        boo.text_add(location=((origin[0]-2.5),origin[1]+2,(origin[2] + 3)),rotation=(pid2, 0, 0))
        boo.editmode_toggle()
        bpy.ops.font.delete()
        bpy.ops.font.text_insert(text=(sumados), accent=False)
        boo.editmode_toggle()
        bpy.ops.format.curves()
        sce.frame_end = suma + 30
        boo.select_all(action='DESELECT')
        return {"FINISHED"}
    
class calculoporcentajes (bpy.types.Operator): # calcul procentaj
    bl_idname = "calculo.porcentajes"
    bl_label = "percentages"
    bl_options = {"REGISTER", "UNDO"}  # %% ADDED
    #
    def execute(self,context):
        global colnrPKHG, allData, suma, negativValueSeen, factorescala, origin
        distancia= 0
        bpy.ops.calculo.total()
        offset = bpy.context.scene.cursor_location.copy()
        print("\n?????? negativValueSeen", negativValueSeen)
        if negativValueSeen:
            self.report({'WARNING'}, "negative value occured")
            return {'CANCELLED'}
        for data in allData:
            bpy.context.scene.cursor_location = (offset[0],offset[1]+2,offset[2])
            distancia += 3 
            sumando = float(data[colnrPKHG])
            if sumando >= 0:
                porciento = '{:.2%}.'.format(sumando/suma)
                escalado = float(data[colnrPKHG])/factorescala
                boo.text_add(location=((origin[0]+0.5 + distancia),(origin[1]-3),(origin[2] + (escalado*2)+0.5)),rotation=(pid2, 0, 0))  
                boo.editmode_toggle()
                bpy.ops.font.delete()
                bpy.ops.font.text_insert(text=(porciento), accent=False)
                boo.editmode_toggle()
                bot.resize(value=(0.6,0.6,0.6))
                bpy.ops.format.curves()
                boo.select_all(action='DESELECT')
            else:
                print("negativ value for percentage not meaningful+ ")
        return {"FINISHED"}

        
# importa data, fabica  cilindros y los textcurves con la data
class vernombres (bpy.types.Operator):
    bl_idname = "ver.nombres"
    bl_label = "Names"
    bl_options = {"REGISTER", "UNDO"}  # %% ADDED
    #
    def execute(self,context):
        global factorescala, origin 
        path = bc.scene.importPydataPath
        reader = csv.reader(open(path, 'r'))
        distancia= 0	
        offset = bpy.context.scene.cursor_location.copy()
        #separacion de las cajitas
        for data in reader:
            bpy.context.scene.cursor_location = (offset[0],offset[1]+2,offset[2])
            distancia += 3  
            data[0]=data[0].replace(' ', '\n')
            boo.text_add(location=((origin[0]+0.2 + distancia),origin[1]+4,(origin[2]+0)),rotation=(pid2, 0, 0))
            boo.editmode_toggle()
            bpy.ops.font.delete()
            bpy.ops.font.text_insert(text=data[0], accent=False)
            boo.editmode_toggle()
            bot.resize(value=(0.6,0.6,0.6))
            bpy.ops.format.curves()
            boo.select_all(action='DESELECT')
        return {"FINISHED"}
            
# asigna un material a la grafica azar
#   Material by http://wiki.blender.org/index.php/Dev:2.5/Py/Scripts/Cookbook/Code_snippets/Materials_and_textures
class coloreargrafica (bpy.types.Operator):
    bl_idname = "colorear.grafica"
    bl_label = "Paint it!"
    bl_options = {"REGISTER", "UNDO"}  # %% ADDED
    #
    def execute(self,context):
        
        for ob in bpy.data.objects:
            if ob.type == 'CURVE' or ob.type == 'MESH':
                bpy.ops.object.material_slot_copy()
            else:
                self.report({'INFO'}, "Please. select any object before")   
        return{'FINISHED'}  

# Align data to camera        
class aligncurvestocamera (bpy.types.Operator):
    bl_idname = "align.data"
    bl_label = "Align data" 
    bl_options = {"REGISTER", "UNDO"}  # %% ADDED
    #
    def execute(self,context):
        boo.select_by_type(type='FONT')
        if  bc.selected_objects[0].type == 'FONT':
            boo.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
            boo.visual_transform_apply()
            bot.select_orientation(orientation='VIEW')
            bot.transform(mode='ALIGN', constraint_orientation='VIEW')
            boo.select_all(action='DESELECT')
#   
#	   else:
#		   message = "You have to build data before"
#		   self.report({'WARNING'}, message)
#	   
        return{'FINISHED'}


#-----REGISTROS------------------------
def register():
    bpy.utils.register_class(PANEL_PT_ImportadorUI)
    bpy.utils.register_class(leeruta)
    bpy.utils.register_class(formatcurves)
    bpy.utils.register_class(importadorcubos)
    bpy.utils.register_class(importadorcilindros)
    bpy.utils.register_class(calculototal)
    bpy.utils.register_class(calculoporcentajes)
    bpy.utils.register_class(vernombres)
    bpy.utils.register_class(coloreargrafica)
    bpy.utils.register_class(aligncurvestocamera)
    bpy.types.Scene.importPydataPath=bpy.props.StringProperty(default="Insert absolute path")
    bpy.types.Scene.useColumnNr=bpy.props.IntProperty(name = "Nr. of column to use",min = 1, max = 100, soft_max = 100, default = 1) #soft_max numar de grafice ce pot aparea
    bpy.types.Scene.scaleFactor=bpy.props.IntProperty(name = "Scale factor",min = 1, max = 1000, soft_max = 100, default = 10)
    #
    ## REGISTRA CLASSES
    burc = bpy.utils.register_class


def unregister():
    bpy.utils.unregister_class(PANEL_PT_ImportadorUI)
    bpy.utils.unregister_class(leeruta)
    bpy.utils.unregister_class(formatcurves)
    bpy.utils.unregister_class(importadorcubos)
    bpy.utils.unregister_class(importadorcilindros)
    bpy.utils.unregister_class(calculototal)
    bpy.utils.unregister_class(calculoporcentajes)
    bpy.utils.unregister_class(vernombres)
    bpy.utils.unregister_class(coloreargrafica)
    bpy.utils.unregister_class(aligncurvestocamera)
    burc = bpy.utils.unregister_class

if __name__ == "__main__":
    register()

OK, then going back to @Zyl’s question

I don’t know what examples I could write that would help someone who only recently started using Blender port an 8 year old addon to the most recent version. You’re in “specific question” territory with that one.

I do, daily. Whom is laughing about bmesh? is it the people wondering how it’s being used to crank out groundbreaking blender addons that are still not entirely replicated in other DCC packages? Hardops, Box Cutter, Meshmachin3, Decalmachine, Kit Ops, the list goes on- these all use bmesh. Is it hard to learn? That’s debatable but I’ll give it to you. A laughing stock though? Come on now…

I found the issue in the code I posted.

useColumnNr
more specifically:
bpy.context.scene.useColumnNr

Did a Google search on “useColumnNr”, which should bring up anything in the API back to even some early API’s . . . . It IS in blender, you can look it up in the console. This is what I got back:

By the way, I have been using Blender for years, before version 2.64, and before that, I was scripting using MEL in MAYA. Have been writing code since the early 90’s, so if that is “someone who only recently started using Blender” I guess that makes you more of a dinosaur than I am . . . .

The reason you can’t find ‘useColumnNr’ is because it doesn’t exist- in this version of Blender or any other. it’s not part of Blender’s API, it is defined and used only in that script you linked.

Try it yourself:

bpy.types.Scene.myvar = bpy.props.IntProperty('test', default=42)
print(bpy.context.scene.myvar)

Incidentally, this behavior of adding attributes to existing classes is thoroughly explained in the documentation.

In the context of this conversation, I was referring to your having just started using python in Blender. Porting an 8 year old addon when you are not already experienced with the API is going to be a difficult task no matter what your background is.

I did NOT just start writing Python code in Blender, have been doing it for years. And that DOES exist in Blender 2.91, which I am currently running, and, as I said, try looking it up in the console, and you will find it there. Open up the console, at the command line, put in this, with the period at the end as shown, then press your TAB button . . .

bpy.context.scene.useColumnNr.

You can also use:
dir(bpy.context.scene.useColumnNr)

As shown below, and this IS Blender 2.91 I am running.

image

I just explained that it doesn’t exist, and more importantly WHY it doesn’t exist. It exists on your machine because you’re running an addon that has created it. It does not exist in the Blender API, and therefore cannot be found in the official documentation. Not sure how else to spell this one out.

This is not a plugin de facto, I have not installed it as such, and I wouldn’t anyway, because it is so old. I am strictly running it in the text editor, and the console. If that creates it, when it should not exist, and there is no code history that tells me that it is no longer in use, that kind of makes my point right there anyway.

I see how it was created. I get it now. I have created my own before, but was busy for a long time studying for the CCNA, so I did forget a lot. I am no newby, and you should not be making such assumptions. It makes it all the more difficult to communicate when that happens, because it adds to the feeling that the person you are communicating with is NOT listening, and then it snowballs from there.

moot point… dropping

Good idea. If you ask about something and want to know what can improve it, then do not want to listen, there is not much more I can say about it.

I was thinking more along the lines of- why are we bothering discussing documentation when it’s crystal clear that it’s not being read? So you’re right, not much more to be said about it :slightly_smiling_face: