644 lines
22 KiB
OpenEdge ABL
644 lines
22 KiB
OpenEdge ABL
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2012 NBEE Embedded Systems, Miguel Angel Ajo <miguelangel@nbee.es>
|
|
* Copyright (C) 1992-2017 KiCad Developers, see AUTHORS.txt for contributors.
|
|
*
|
|
* 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, you may find one here:
|
|
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
* or you may search the http://www.gnu.org website for the version 2 license,
|
|
* or you may write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
/**
|
|
* This file builds the base classes for all kind of python plugins that
|
|
* can be included into kicad.
|
|
* they provide generic code to all the classes:
|
|
*
|
|
* KiCadPlugin
|
|
* /|\
|
|
* |
|
|
* |\-FilePlugin
|
|
* |\-FootprintWizardPlugin
|
|
* |\-ActionPlugin
|
|
*
|
|
* It defines the LoadPlugins() function that loads all the plugins
|
|
* available in the system
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Remark:
|
|
* Avoid using the print function in python wizards
|
|
*
|
|
* Be aware print messages create IO exceptions, because the wizard
|
|
* is run from Pcbnew. And if pcbnew is not run from a console, there is
|
|
* no io channel to read the output of print function.
|
|
* When the io buffer is full, a IO exception is thrown.
|
|
*/
|
|
|
|
%pythoncode
|
|
{
|
|
|
|
KICAD_PLUGINS={} # the list of loaded footprint wizards
|
|
|
|
""" the list of not loaded python scripts
|
|
(usually because there is a syntax error in python script)
|
|
this is the python script full filenames list.
|
|
filenames are separated by '\n'
|
|
"""
|
|
NOT_LOADED_WIZARDS=""
|
|
|
|
""" the list of paths used to search python scripts.
|
|
Stored here to be displayed on request in Pcbnew
|
|
paths are separated by '\n'
|
|
"""
|
|
PLUGIN_DIRECTORIES_SEARCH=""
|
|
|
|
""" the trace of errors during execution of footprint wizards scripts
|
|
"""
|
|
FULL_BACK_TRACE=""
|
|
|
|
def GetUnLoadableWizards():
|
|
global NOT_LOADED_WIZARDS
|
|
return NOT_LOADED_WIZARDS
|
|
|
|
def GetWizardsSearchPaths():
|
|
global PLUGIN_DIRECTORIES_SEARCH
|
|
return PLUGIN_DIRECTORIES_SEARCH
|
|
|
|
def GetWizardsBackTrace():
|
|
global FULL_BACK_TRACE
|
|
return FULL_BACK_TRACE
|
|
|
|
|
|
def LoadOnePlugin(Dirname, ModuleName):
|
|
"""
|
|
Load the plugin file ModuleName found in folder Dirname.
|
|
If this file cannot be loaded, its name is stored in failed_wizards_list
|
|
and the error trace is stored in FULL_BACK_TRACE
|
|
"""
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
global NOT_LOADED_WIZARDS
|
|
global FULL_BACK_TRACE
|
|
|
|
module_filename = os.path.join(Dirname, ModuleName)
|
|
|
|
try: # If there is an error loading the script, skip it
|
|
mtime = os.path.getmtime(module_filename)
|
|
|
|
if KICAD_PLUGINS.has_key(ModuleName):
|
|
plugin = KICAD_PLUGINS[ModuleName]
|
|
|
|
if not plugin["modification_time"] == mtime:
|
|
mod = reload(plugin["ModuleName"])
|
|
plugin["modification_time"] = mtime
|
|
else:
|
|
mod = plugin["ModuleName"]
|
|
else:
|
|
mod = __import__(ModuleName[:-3], locals(), globals() )
|
|
|
|
KICAD_PLUGINS[ModuleName]={ "filename":module_filename,
|
|
"modification_time":mtime,
|
|
"ModuleName":mod }
|
|
|
|
except:
|
|
if NOT_LOADED_WIZARDS != "" :
|
|
NOT_LOADED_WIZARDS += "\n"
|
|
NOT_LOADED_WIZARDS += module_filename
|
|
FULL_BACK_TRACE += traceback.format_exc(sys.exc_info())
|
|
pass
|
|
|
|
|
|
|
|
def LoadOneSubdirPlugin(Dirname, SubDirname):
|
|
"""
|
|
Load the plugins found in folder Dirname/SubDirname, by loading __ini__.py file.
|
|
If files cannot be loaded, its name is stored in failed_wizards_list
|
|
and the error trace is stored in FULL_BACK_TRACE
|
|
"""
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
global NOT_LOADED_WIZARDS
|
|
global FULL_BACK_TRACE
|
|
|
|
fullPath = os.path.join(Dirname,SubDirname)
|
|
|
|
if os.path.isdir(fullPath):
|
|
"""
|
|
Skip subdir which does not contain __init__.py, becuase if can be
|
|
a non python subdir (can be a subdir for .xsl plugins for instance)
|
|
"""
|
|
if os.path.exists( os.path.join(fullPath, '__init__.py') ):
|
|
try: # If there is an error loading the script, skip it
|
|
__import__(SubDirname, locals(), globals())
|
|
|
|
except:
|
|
if NOT_LOADED_WIZARDS != "" :
|
|
NOT_LOADED_WIZARDS += "\n"
|
|
NOT_LOADED_WIZARDS += fullPath
|
|
FULL_BACK_TRACE += traceback.format_exc(sys.exc_info())
|
|
pass
|
|
|
|
else:
|
|
if NOT_LOADED_WIZARDS != "" :
|
|
NOT_LOADED_WIZARDS += "\n"
|
|
NOT_LOADED_WIZARDS += 'Skip subdir ' + fullPath
|
|
|
|
|
|
def LoadPlugins(bundlepath=None):
|
|
"""
|
|
Initialise Scripting/Plugin python environment and load plugins.
|
|
|
|
Arguments:
|
|
scriptpath -- The path to the bundled scripts.
|
|
The bunbled Plugins are relative to this path, in the
|
|
"plugins" subdirectory.
|
|
|
|
NOTE: These are all of the possible "default" search paths for kicad
|
|
python scripts. These paths will ONLY be added to the python
|
|
search path ONLY IF they already exist.
|
|
|
|
The Scripts bundled with the KiCad installation:
|
|
<bundlepath>/
|
|
<bundlepath>/plugins/
|
|
|
|
The Scripts relative to the KiCad search path environment variable:
|
|
[KICAD_PATH]/scripting/
|
|
[KICAD_PATH]/scripting/plugins/
|
|
|
|
The Scripts relative to the KiCad Users configuration:
|
|
<kicad_config_path>/scripting/
|
|
<kicad_config_path>/scripting/plugins/
|
|
|
|
And on Linux ONLY, extra paths relative to the users home directory:
|
|
~/.kicad_plugins/
|
|
~/.kicad/scripting/
|
|
~/.kicad/scripting/plugins/
|
|
"""
|
|
import os
|
|
import sys
|
|
import traceback
|
|
import pcbnew
|
|
|
|
kicad_path = os.environ.get('KICAD_PATH')
|
|
config_path = pcbnew.GetKicadConfigPath()
|
|
plugin_directories=[]
|
|
|
|
if bundlepath:
|
|
plugin_directories.append(bundlepath)
|
|
plugin_directories.append(os.path.join(bundlepath, 'plugins'))
|
|
|
|
if kicad_path:
|
|
plugin_directories.append(os.path.join(kicad_path, 'scripting'))
|
|
plugin_directories.append(os.path.join(kicad_path, 'scripting', 'plugins'))
|
|
|
|
if config_path:
|
|
plugin_directories.append(os.path.join(config_path, 'scripting'))
|
|
plugin_directories.append(os.path.join(config_path, 'scripting', 'plugins'))
|
|
|
|
if sys.platform.startswith('linux'):
|
|
plugin_directories.append(os.path.join(os.environ['HOME'],'.kicad_plugins'))
|
|
plugin_directories.append(os.path.join(os.environ['HOME'],'.kicad','scripting'))
|
|
plugin_directories.append(os.path.join(os.environ['HOME'],'.kicad','scripting','plugins'))
|
|
|
|
global PLUGIN_DIRECTORIES_SEARCH
|
|
PLUGIN_DIRECTORIES_SEARCH=""
|
|
for plugins_dir in plugin_directories: # save search path list for later use
|
|
if PLUGIN_DIRECTORIES_SEARCH != "" :
|
|
PLUGIN_DIRECTORIES_SEARCH += "\n"
|
|
PLUGIN_DIRECTORIES_SEARCH += plugins_dir
|
|
|
|
global FULL_BACK_TRACE
|
|
FULL_BACK_TRACE="" # clear any existing trace
|
|
|
|
global NOT_LOADED_WIZARDS
|
|
NOT_LOADED_WIZARDS = "" # save not loaded wizards names list for later use
|
|
|
|
global KICAD_PLUGINS
|
|
|
|
for plugins_dir in plugin_directories:
|
|
if not os.path.isdir(plugins_dir):
|
|
continue
|
|
|
|
sys.path.append(plugins_dir)
|
|
|
|
for module in os.listdir(plugins_dir):
|
|
if os.path.isdir(os.path.join(plugins_dir,module)):
|
|
LoadOneSubdirPlugin(plugins_dir, module)
|
|
continue
|
|
|
|
if module == '__init__.py' or module[-3:] != '.py':
|
|
continue
|
|
|
|
LoadOnePlugin(plugins_dir, module);
|
|
|
|
|
|
class KiCadPlugin:
|
|
def __init__(self):
|
|
pass
|
|
|
|
def register(self):
|
|
if isinstance(self,FilePlugin):
|
|
pass # register to file plugins in C++
|
|
|
|
if isinstance(self,FootprintWizardPlugin):
|
|
PYTHON_FOOTPRINT_WIZARD_LIST.register_wizard(self)
|
|
return
|
|
|
|
if isinstance(self,ActionPlugin):
|
|
PYTHON_ACTION_PLUGINS.register_action(self)
|
|
return
|
|
|
|
return
|
|
|
|
def deregister(self):
|
|
if isinstance(self,FilePlugin):
|
|
pass # deregister to file plugins in C++
|
|
|
|
if isinstance(self,FootprintWizardPlugin):
|
|
PYTHON_FOOTPRINT_WIZARD_LIST.deregister_wizard(self)
|
|
return
|
|
|
|
if isinstance(self,ActionPlugin):
|
|
PYTHON_ACTION_PLUGINS.deregister_action(self)
|
|
return
|
|
|
|
return
|
|
|
|
|
|
class FilePlugin(KiCadPlugin):
|
|
def __init__(self):
|
|
KiCadPlugin.__init__(self)
|
|
|
|
|
|
from math import ceil, floor, sqrt
|
|
|
|
uMM = "mm" # Millimetres
|
|
uMils = "mils" # Mils
|
|
uFloat = "float" # Natural number units (dimensionless)
|
|
uInteger = "integer" # Integer (no decimals, numeric, dimensionless)
|
|
uBool = "bool" # Boolean value
|
|
uRadians = "radians" # Angular units (radians)
|
|
uDegrees = "degrees" # Angular units (degrees)
|
|
uPercent = "%" # Percent (0% -> 100%)
|
|
uString = "string" # Raw string
|
|
|
|
uNumeric = [uMM, uMils, uFloat, uInteger, uDegrees, uRadians, uPercent] # List of numeric types
|
|
uUnits = [uMM, uMils, uFloat, uInteger, uBool, uDegrees, uRadians, uPercent, uString] # List of allowable types
|
|
|
|
class FootprintWizardParameter(object):
|
|
_true = ['true','t','y','yes','on','1',1,]
|
|
_false = ['false','f','n','no','off','0',0,'',None]
|
|
|
|
_bools = _true + _false
|
|
|
|
def __init__(self, page, name, units, default, **kwarg):
|
|
self.page = page
|
|
self.name = name
|
|
self.hint = kwarg.get('hint','') # Parameter hint (shown as mouse-over text)
|
|
self.designator = kwarg.get('designator',' ') # Parameter designator such as "e, D, p" (etc)
|
|
|
|
if units.lower() in uUnits:
|
|
self.units = units.lower()
|
|
elif units.lower() == 'percent':
|
|
self.units = uPercent
|
|
elif type(units) in [list, tuple]: # Convert a list of options into a single string
|
|
self.units = ",".join([str(el).strip() for el in units])
|
|
else:
|
|
self.units = units
|
|
|
|
self.multiple = int(kwarg.get('multiple',1)) # Check integer values are multiples of this number
|
|
self.min_value = kwarg.get('min_value',None) # Check numeric values are above or equal to this number
|
|
self.max_value = kwarg.get('max_value',None) # Check numeric values are below or equal to this number
|
|
|
|
self.SetValue(default)
|
|
self.default = self.raw_value # Save value as default
|
|
|
|
def ClearErrors(self):
|
|
self.error_list = []
|
|
|
|
def AddError(self, err, info=None):
|
|
|
|
if err in self.error_list: # prevent duplicate error messages
|
|
return
|
|
if info is not None:
|
|
err = err + " (" + str(info) + ")"
|
|
|
|
self.error_list.append(err)
|
|
|
|
def Check(self, min_value=None, max_value=None, multiple=None, info=None):
|
|
|
|
if min_value is None:
|
|
min_value = self.min_value
|
|
if max_value is None:
|
|
max_value = self.max_value
|
|
if multiple is None:
|
|
multiple = self.multiple
|
|
|
|
if self.units not in uUnits and ',' not in self.units: # Allow either valid units or a list of strings
|
|
self.AddError("type '{t}' unknown".format(t=self.units),info)
|
|
self.AddError("Allowable types: " + str(self.units),info)
|
|
|
|
if self.units in uNumeric:
|
|
try:
|
|
to_num = float(self.raw_value)
|
|
|
|
if min_value is not None: # Check minimum value if it is present
|
|
if to_num < min_value:
|
|
self.AddError("value '{v}' is below minimum ({m})".format(v=self.raw_value,m=min_value),info)
|
|
|
|
if max_value is not None: # Check maximum value if it is present
|
|
if to_num > max_value:
|
|
self.AddError("value '{v}' is above maximum ({m})".format(v=self.raw_value,m=max_value),info)
|
|
|
|
except:
|
|
self.AddError("value '{v}' is not of type '{t}'".format(v = self.raw_value, t=self.units),info)
|
|
|
|
if self.units == uInteger: # Perform integer specific checks
|
|
try:
|
|
to_int = int(self.raw_value)
|
|
|
|
if multiple is not None and multiple > 1:
|
|
if (to_int % multiple) > 0:
|
|
self.AddError("value '{v}' is not a multiple of {m}".format(v=self.raw_value,m=multiple),info)
|
|
except:
|
|
self.AddError("value {'v}' is not an integer".format(v=self.raw_value),info)
|
|
|
|
if self.units == uBool: # Check that the value is of a correct boolean format
|
|
if self.raw_value in [True,False] or str(self.raw_value).lower() in self._bools:
|
|
pass
|
|
else:
|
|
self.AddError("value '{v}' is not a boolean value".format(v = self.raw_value),info)
|
|
|
|
@property
|
|
def value(self): # Return the current value, converted to appropriate units (from string representation) if required
|
|
v = str(self.raw_value) # Enforce string type for known starting point
|
|
|
|
if self.units == uInteger: # Integer values
|
|
return int(v)
|
|
elif self.units in uNumeric: # Any values that use floating points
|
|
v = v.replace(",",".") # Replace "," separators with "."
|
|
v = float(v)
|
|
|
|
if self.units == uMM: # Convert from millimetres to nanometres
|
|
return FromMM(v)
|
|
|
|
elif self.units == uMils: # Convert from mils to nanometres
|
|
return FromMils(v)
|
|
|
|
else: # Any other floating-point values
|
|
return v
|
|
|
|
elif self.units == uBool:
|
|
if v.lower() in self._true:
|
|
return True
|
|
else:
|
|
return False
|
|
else:
|
|
return v
|
|
|
|
def DefaultValue(self): # Reset the value of the parameter to its default
|
|
self.raw_value = str(self.default)
|
|
|
|
def SetValue(self, new_value): # Update the value
|
|
new_value = str(new_value)
|
|
|
|
if len(new_value.strip()) == 0:
|
|
if not self.units in [uString, uBool]:
|
|
return # Ignore empty values unless for strings or bools
|
|
|
|
if self.units == uBool: # Enforce the same boolean representation as is used in KiCad
|
|
new_value = "1" if new_value.lower() in self._true else "0"
|
|
elif self.units in uNumeric:
|
|
new_value = new_value.replace(",", ".") # Enforce decimal point separators
|
|
elif ',' in self.units: # Select from a list of values
|
|
if new_value not in self.units.split(','):
|
|
new_value = self.units.split(',')[0]
|
|
|
|
self.raw_value = new_value
|
|
|
|
def __str__(self): # pretty-print the parameter
|
|
|
|
s = self.name + ": " + str(self.raw_value)
|
|
|
|
if self.units in [uMM, uMils, uPercent, uRadians, uDegrees]:
|
|
s += self.units
|
|
elif self.units == uBool: # Special case for Boolean values
|
|
s = self.name + ": {b}".format(b = "True" if self.value else "False")
|
|
elif self.units == uString:
|
|
s = self.name + ": '" + self.raw_value + "'"
|
|
|
|
return s
|
|
|
|
|
|
class FootprintWizardPlugin(KiCadPlugin, object):
|
|
def __init__(self):
|
|
KiCadPlugin.__init__(self)
|
|
self.defaults()
|
|
|
|
def defaults(self):
|
|
self.module = None
|
|
self.params = [] # List of added parameters that observes addition order
|
|
|
|
self.name = "KiCad FP Wizard"
|
|
self.description = "Undefined Footprint Wizard plugin"
|
|
self.image = ""
|
|
self.buildmessages = ""
|
|
|
|
def AddParam(self, page, name, unit, default, **kwarg):
|
|
|
|
if self.GetParam(page,name) is not None: # Param already exists!
|
|
return
|
|
|
|
param = FootprintWizardParameter(page, name, unit, default, **kwarg) # Create a new parameter
|
|
self.params.append(param)
|
|
|
|
@property
|
|
def parameters(self): # This is a helper function that returns a nested (unordered) dict of the VALUES of parameters
|
|
pages = {} # Page dict
|
|
for p in self.params:
|
|
if p.page not in pages:
|
|
pages[p.page] = {}
|
|
|
|
pages[p.page][p.name] = p.value # Return the 'converted' value (convert from string to actual useful units)
|
|
|
|
return pages
|
|
|
|
@property
|
|
def values(self): # Same as above
|
|
return self.parameters
|
|
|
|
def ResetWizard(self): # Reset all parameters to default values
|
|
for p in self.params:
|
|
p.DefaultValue()
|
|
|
|
def GetName(self): # Return the name of this wizard
|
|
return self.name
|
|
|
|
def GetImage(self): # Return the filename of the preview image associated with this wizard
|
|
return self.image
|
|
|
|
def GetDescription(self): # Return the description text
|
|
return self.description
|
|
|
|
def GetValue(self):
|
|
raise NotImplementedError
|
|
|
|
def GetReferencePrefix(self):
|
|
return "REF" # Default reference prefix for any footprint
|
|
|
|
def GetParam(self, page, name): # Grab a parameter
|
|
for p in self.params:
|
|
if p.page == page and p.name == name:
|
|
return p
|
|
|
|
return None
|
|
|
|
def CheckParam(self, page, name, **kwarg):
|
|
self.GetParam(page,name).Check(**kwarg)
|
|
|
|
def AnyErrors(self):
|
|
return any([len(p.error_list) > 0 for p in self.params])
|
|
|
|
@property
|
|
def pages(self): # Return an (ordered) list of the available page names
|
|
page_list = []
|
|
for p in self.params:
|
|
if p.page not in page_list:
|
|
page_list.append(p.page)
|
|
|
|
return page_list
|
|
|
|
def GetNumParameterPages(self): # Return the number of parameter pages
|
|
return len(self.pages)
|
|
|
|
def GetParameterPageName(self,page_n): # Return the name of a page at a given index
|
|
return self.pages[page_n]
|
|
|
|
def GetParametersByPageName(self, page_name): # Return a list of parameters on a given page
|
|
params = []
|
|
|
|
for p in self.params:
|
|
if p.page == page_name:
|
|
params.append(p)
|
|
|
|
return params
|
|
|
|
def GetParametersByPageIndex(self, page_index): # Return an ordered list of parameters on a given page
|
|
return self.GetParametersByPageName(self.GetParameterPageName(page_index))
|
|
|
|
def GetParameterDesignators(self, page_index): # Return a list of designators associated with a given page
|
|
params = self.GetParametersByPageIndex(page_index)
|
|
return [p.designator for p in params]
|
|
|
|
def GetParameterNames(self,page_index): # Return the list of names associated with a given page
|
|
params = self.GetParametersByPageIndex(page_index)
|
|
return [p.name for p in params]
|
|
|
|
def GetParameterValues(self,page_index): # Return the list of values associated with a given page
|
|
params = self.GetParametersByPageIndex(page_index)
|
|
return [str(p.raw_value) for p in params]
|
|
|
|
def GetParameterErrors(self,page_index): # Return list of errors associated with a given page
|
|
params = self.GetParametersByPageIndex(page_index)
|
|
return [str("\n".join(p.error_list)) for p in params]
|
|
|
|
def GetParameterTypes(self, page_index): # Return list of units associated with a given page
|
|
params = self.GetParametersByPageIndex(page_index)
|
|
return [str(p.units) for p in params]
|
|
|
|
def GetParameterHints(self, page_index): # Return a list of units associated with a given page
|
|
params = self.GetParametersByPageIndex(page_index)
|
|
return [str(p.hint) for p in params]
|
|
|
|
def GetParameterDesignators(self, page_index): # Return a list of designators associated with a given page
|
|
params = self.GetParametersByPageIndex(page_index)
|
|
return [str(p.designator) for p in params]
|
|
|
|
def SetParameterValues(self, page_index, list_of_values): # Update values on a given page
|
|
|
|
params = self.GetParametersByPageIndex(page_index)
|
|
|
|
for i, param in enumerate(params):
|
|
if i >= len(list_of_values):
|
|
break
|
|
param.SetValue(list_of_values[i])
|
|
|
|
def GetFootprint( self ):
|
|
self.BuildFootprint()
|
|
return self.module
|
|
|
|
def BuildFootprint(self):
|
|
return
|
|
|
|
def GetBuildMessages( self ):
|
|
return self.buildmessages
|
|
|
|
def Show(self):
|
|
text = "Footprint Wizard Name: {name}\n".format(name=self.GetName())
|
|
text += "Footprint Wizard Description: {desc}\n".format(desc=self.GetDescription())
|
|
|
|
n_pages = self.GetNumParameterPages()
|
|
|
|
text += "Pages: {n}\n".format(n=n_pages)
|
|
|
|
for i in range(n_pages):
|
|
name = self.GetParameterPageName(i)
|
|
|
|
params = self.GetParametersByPageName(name)
|
|
|
|
text += "{name}\n".format(name=name)
|
|
|
|
for j in range(len(params)):
|
|
text += ("\t{param}{err}\n".format(
|
|
param = str(params[j]),
|
|
err = ' *' if len(params[j].error_list) > 0 else ''
|
|
))
|
|
|
|
if self.AnyErrors():
|
|
text += " * Errors exist for these parameters"
|
|
|
|
return text
|
|
|
|
class ActionPlugin(KiCadPlugin, object):
|
|
def __init__( self ):
|
|
KiCadPlugin.__init__( self )
|
|
self.defaults()
|
|
|
|
def defaults( self ):
|
|
self.name = "Undefined Action plugin"
|
|
self.category = "Undefined"
|
|
self.description = ""
|
|
|
|
def GetName( self ):
|
|
return self.name
|
|
|
|
def GetCategoryName( self ):
|
|
return self.category
|
|
|
|
def GetDescription( self ):
|
|
return self.description
|
|
|
|
def Run(self):
|
|
return
|
|
|
|
}
|