← Back to team overview

kicad-developers team mailing list archive

PATCH : Enhanced Python Shell - Proposed version

 

Hello List,

I believe I have cleaned up my patch and made the enhanced shell do everything it should.

It has a tabbed interface, with simple text editing capabilities.

Its GUI can be used to introspect the full internal state of pcbnew, as exposed by the python interface.

The editor can be used to code python scripts (with syntax highlighting), and it has auto-completion built in. Ie, type word press (dot) and it gives you a list of possibilities which follow. Further, you can use the editor to load up KiCad files and edit them without having to use an external editor, if one desires.

It will allow you to save your interactive history, and restore it in a new session. It has a startup file which is executed when the python shell first starts, and automatically creates a default if it doesn't exist. All of its configuration, history and startup files are stored in the users KiCad configuration directory on each platform.

The default startup file doesn't do anything, but includes a commented out example which will automatically load pcbnew into the shell, and get the current board on screen. Two very common things that need to be done most times when you start the python shell.

The shell is fully written in python, and uses the foundation of PyAlamode included with wxpython. It does not introduce any further dependencies on KiCad, everything it achieves is already built into KiCad, and not currently exposed.

It should be possible to extend the shell in future and add features, without requiring to hack core code inside pcbnew.

The changes to the core of pcbnew, that I did, were to facilitate this, and I discuss why I did them in earlier posts. They haven't changed in this patch. There may be better ways to achieve this and I am open to suggestions.

I have only tested on Linux, but I do not believe any of the OS specific code changes that I made should negatively effect Windows or Mac, nevertheless it needs testing on those platforms and I do not have access to them.

Screenshots of it in action:

http://i.imgur.com/lTFFddR.png
http://i.imgur.com/qbACtPR.png
http://i.imgur.com/Yny4Lnc.png
http://i.imgur.com/q2AjrPi.png

The code is also available on my github:
https://github.com/stevenj/kicad-source-mirror/tree/enahnced-python-shell

Hopefully this is useful.

Steven
diff --git a/pcbnew/CMakeLists.txt b/pcbnew/CMakeLists.txt
index ff99a93..469ddbf 100644
--- a/pcbnew/CMakeLists.txt
+++ b/pcbnew/CMakeLists.txt
@@ -678,6 +678,12 @@ if( KICAD_SCRIPTING )
         DESTINATION ${KICAD_DATA}/scripting/plugins
         FILE_PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ
     )
+
+    # scripting python shell
+    install( DIRECTORY ${PROJECT_SOURCE_DIR}/pcbnew/scripting/kicad_pyshell/
+        DESTINATION ${KICAD_DATA}/scripting/kicad_pyshell
+        FILE_PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ
+    )
 endif()
 
 if( KICAD_SCRIPTING_MODULES )
diff --git a/pcbnew/pcbframe.cpp b/pcbnew/pcbframe.cpp
index b19fca1..185fcb9 100644
--- a/pcbnew/pcbframe.cpp
+++ b/pcbnew/pcbframe.cpp
@@ -70,8 +70,6 @@
 #include <tool/tool_dispatcher.h>
 #include <tools/common_actions.h>
 
-#include <scripting/python_console_frame.h>
-
 #if defined(KICAD_SCRIPTING) || defined(KICAD_SCRIPTING_WXPYTHON)
 #include <python_scripting.h>
 #endif
@@ -980,9 +978,6 @@ void PCB_EDIT_FRAME::UpdateTitle()
 }
 
 
-wxSize PYTHON_CONSOLE_FRAME::m_frameSize;   ///< The size of the PYTHON_CONSOLE_FRAME frame, stored during a session
-wxPoint PYTHON_CONSOLE_FRAME::m_framePos;   ///< The position ofPYTHON_CONSOLE_FRAME  the frame, stored during a session
-
 void PCB_EDIT_FRAME::ScriptingConsoleEnableDisable( wxCommandEvent& aEvent )
 {
 
@@ -990,7 +985,7 @@ void PCB_EDIT_FRAME::ScriptingConsoleEnableDisable( wxCommandEvent& aEvent )
     bool pythonPanelShown = true;
 
     if( pythonPanelFrame == NULL )
-        pythonPanelFrame = new PYTHON_CONSOLE_FRAME( this, pythonConsoleNameId() );
+        pythonPanelFrame = CreatePythonShellWindow( this, pythonConsoleNameId() );
     else
         pythonPanelShown = ! pythonPanelFrame->IsShown();
 
diff --git a/pcbnew/pcbnew.cpp b/pcbnew/pcbnew.cpp
index 0abbd00..2277c65 100644
--- a/pcbnew/pcbnew.cpp
+++ b/pcbnew/pcbnew.cpp
@@ -241,36 +241,26 @@ static bool scriptingSetup()
         }
     }
 
-    // TODO: make this path definable by the user, and set more than one path
-    // (and remove the fixed paths from <src>/scripting/kicadplugins.i)
-
-    // wizard plugins are stored in kicad/bin/plugins.
-    // so add this path to python scripting default search paths
+    // wizard plugins are stored in ../share/kicad/scripting/plugins.
+    // so add the base scripting path to python scripting default search paths
     // which are ( [KICAD_PATH] is an environment variable to define)
+    // [KICAD_PATH]/scripting
     // [KICAD_PATH]/scripting/plugins
     // Add this default search path:
-    path_frag = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting/plugins" );
+    path_frag = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting" );
 
 #elif defined( __WXMAC__ )
-    // TODO:
-    // For scripting currently only the bundle scripting path and the path
-    // defined by $(KICAD_PATH)/scripting/plugins is defined.
-    // These paths are defined here and in kicadplugins.i
-    // In future, probably more paths are of interest:
-    // * User folder (~/Library/Application Support/kicad/scripting/plugins)
-    //   => GetOSXKicadUserDataDir() + wxT( "/scripting/plugins" );
-    // * Machine folder (/Library/Application Support/kicad/scripting/plugins)
-    //   => GetOSXKicadMachineDataDir() + wxT( "/scripting/plugins" );
 
     // This path is given to LoadPlugins() from kicadplugins.i, which
-    // only supports one path. Only use bundle scripting path for now.
-    path_frag = GetOSXKicadDataDir() + wxT( "/scripting/plugins" );
+    // only supports one path, the bundle scripting path for now.
+    // All other paths are determined by the pcbnew.py initialisation code
+    path_frag = GetOSXKicadDataDir() + wxT( "/scripting" );
 
     // Add default paths to PYTHONPATH
     wxString pypath;
 
-    // Bundle scripting folder (<kicad.app>/Contents/SharedSupport/scripting/plugins)
-    pypath += GetOSXKicadDataDir() + wxT( "/scripting/plugins" );
+    // Bundle scripting folder (<kicad.app>/Contents/SharedSupport/scripting)
+    pypath += GetOSXKicadDataDir() + wxT( "/scripting" );
 
     // $(KICAD_PATH)/scripting/plugins is always added in kicadplugins.i
     if( wxGetenv("KICAD_PATH") != NULL )
@@ -303,9 +293,11 @@ static bool scriptingSetup()
     wxSetEnv( wxT( "PYTHONPATH" ), pypath );
 
     // Add this default search path:
-    path_frag = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting/plugins" );
+    path_frag = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting" );
 #endif
 
+    // path_frag is the path to the bundled scripts and plugins, all other paths are
+    // determined by the python pcbnew.py initialisation code.
     if( !pcbnewInitPythonScripting( TO_UTF8( path_frag ) ) )
     {
         wxLogError( wxT( "pcbnewInitPythonScripting() failed." ) );
diff --git a/pcbnew/scripting/kicad_pyshell/__init__.py b/pcbnew/scripting/kicad_pyshell/__init__.py
new file mode 100644
index 0000000..c0114d2
--- /dev/null
+++ b/pcbnew/scripting/kicad_pyshell/__init__.py
@@ -0,0 +1,188 @@
+
+import wx, sys, os
+
+from wx.py import crust, editor, frame, version, dispatcher
+from wx.py.editor import EditorNotebook
+
+import pcbnew
+
+intro = "KiCAD:PCBNEW - Python Shell - PyAlaMode %s" % version.VERSION
+
+class PcbnewPyShell(editor.EditorNotebookFrame): #, frame.ShellFrameMixin):
+    """ The Pythonshell of PCBNEW. """
+
+    def _setup_startup(self):
+        """ Initialise the startup script """
+        # Create filename for startup script.
+        self.startup_file = os.path.join(self.config_dir,"PyShell_pcbnew_startup.py")
+        self.execStartupScript = True
+
+        # Check if startup script exists
+        if not os.path.isfile(self.startup_file):
+            # Not, so create a default.
+            default_startup = open (self.startup_file, 'w') ## a will append, w will over-write
+            # provide the content for the default startup file.
+            default_startup.write(
+                "### DEFAULT STARTUP FILE FOR KiCad:PCBNEW Python Shell\n" +
+                "# Enter any Python code you would like to execute when the PCBNEW python shell first runs.\n" +
+                "\n" +
+                "# Eg:\n" +
+                "\n" +
+                "# import pcbnew\n" +
+                "# board = pcbnew.GetBoard()\n" )
+            default_startup.close()
+
+    def _setup(self):
+        """Setup prior to first buffer creation.
+
+        Called automatically by base class during init."""
+
+        self.notebook = EditorNotebook(parent=self)
+        intro = 'Py %s' % version.VERSION
+        import imp
+        module = imp.new_module('__main__')
+        import __builtin__
+        module.__dict__['__builtins__'] = __builtin__
+        namespace = module.__dict__.copy()
+
+        self.config_dir = pcbnew.GetKicadConfigPath()
+        self.dataDir = self.config_dir
+
+        self._setup_startup()
+        self.history_file = os.path.join(self.config_dir,"PyShell_pcbnew.history")
+
+        self.config_file  = os.path.join(self.config_dir,"PyShell_pcbnew.cfg")
+        self.config = wx.FileConfig(localFilename=self.config_file)
+        self.config.SetRecordDefaults(True)
+        self.autoSaveSettings = False
+        self.autoSaveHistory = False
+        self.LoadSettings()
+
+        self.crust = crust.Crust(parent=self.notebook,
+                                 intro=intro, locals=namespace,
+                                 rootLabel="locals()",
+                                 startupScript=self.startup_file,
+                                 execStartupScript=self.execStartupScript)
+
+        self.shell = self.crust.shell
+        # Override the filling so that status messages go to the status bar.
+        self.crust.filling.tree.setStatusText = self.SetStatusText
+        # Override the shell so that status messages go to the status bar.
+        self.shell.setStatusText = self.SetStatusText
+        # Fix a problem with the sash shrinking to nothing.
+        self.crust.filling.SetSashPosition(200)
+        self.notebook.AddPage(page=self.crust, text='*Shell*', select=True)
+        self.setEditor(self.crust.editor)
+        self.crust.editor.SetFocus()
+
+        self.LoadHistory()
+
+
+    def OnAbout(self, event):
+        '''Display an About window.'''
+        title = 'About : KiCad:PCBNEW - Python Shell'
+        text = "Enahnced Python Shell for KiCad:PCBNEW\n\n" + \
+               "This KiCad Python Shell is based on wxPython PyAlaMode.\n\n" + \
+               "see: http://wiki.wxpython.org/PyAlaMode\n\n"; + \
+               "KiCad Revision: %s\n" % "??.??" + \
+               "PyAlaMode Revision : %s\n" % version.VERSION + \
+               "Platform: %s\n" % sys.platform + \
+               "Python Version: %s\n" % sys.version.split()[0] + \
+               "wxPython Version: %s\n" % wx.VERSION_STRING + \
+               ("\t(%s)\n" % ", ".join(wx.PlatformInfo[1:]))
+
+        dialog = wx.MessageDialog(self, text, title,
+                                    wx.OK | wx.ICON_INFORMATION)
+        dialog.ShowModal()
+        dialog.Destroy()
+
+    def EditStartupScript(self):
+        self.bufferCreate(filename=self.startup_file)
+
+    def LoadSettings(self):
+        if self.config is not None:
+            editor.EditorNotebookFrame.LoadSettings(self,self.config)
+            self.autoSaveSettings = \
+                 self.config.ReadBool('Options/AutoSaveSettings', False)
+            self.execStartupScript = \
+                 self.config.ReadBool('Options/ExecStartupScript', True)
+            self.autoSaveHistory = \
+                 self.config.ReadBool('Options/AutoSaveHistory', False)
+            self.hideFoldingMargin = \
+                 self.config.ReadBool('Options/HideFoldingMargin', True)
+
+    def SaveSettings(self, force=False):
+        if self.config is not None:
+            # always save these
+            self.config.WriteBool('Options/AutoSaveSettings',
+                                  self.autoSaveSettings)
+            if self.autoSaveSettings or force:
+                editor.EditorNotebookFrame.SaveSettings(self,self.config)
+
+                self.config.WriteBool('Options/AutoSaveHistory',
+                                      self.autoSaveHistory)
+                self.config.WriteBool('Options/ExecStartupScript',
+                                      self.execStartupScript)
+                self.config.WriteBool('Options/HideFoldingMargin',
+                                      self.hideFoldingMargin)
+            if self.autoSaveHistory:
+                self.SaveHistory()
+
+    def DoSaveSettings(self):
+        if self.config is not None:
+            self.SaveSettings(force=True)
+            self.config.Flush()
+
+
+    def SaveHistory(self):
+        if self.dataDir:
+            try:
+                name = self.history_file
+                f = file(name, 'w')
+                hist = []
+                enc = wx.GetDefaultPyEncoding()
+                for h in self.shell.history:
+                    if isinstance(h, unicode):
+                        h = h.encode(enc)
+                    hist.append(h)
+                hist = '\x00\n'.join(hist)
+                f.write(hist)
+                f.close()
+            except:
+                d = wx.MessageDialog(self, "Error saving history file.",
+                                     "Error", wx.ICON_EXCLAMATION|wx.OK)
+                d.ShowModal()
+                d.Destroy()
+                raise
+
+    def LoadHistory(self):
+        ec = 0
+        if self.dataDir:
+            ec = 1
+            name = self.history_file
+            ec = 2
+            if os.path.exists(name):
+                ec = 3
+                try:
+                    ec = 4
+                    f = file(name, 'U')
+                    ec = 5
+                    hist = f.read()
+                    ec = 6
+                    f.close()
+                    ec = 7
+                    self.shell.history = hist.split('\x00\n')
+                    ec = 8
+                    dispatcher.send(signal="Shell.loadHistory",
+                                    history=self.shell.history)
+                    ec = 9
+                except:
+                    d = wx.MessageDialog(self, "Error loading history file. (%d)" % ec,
+                                         "Error", wx.ICON_EXCLAMATION|wx.OK)
+                    d.ShowModal()
+                    d.Destroy()
+
+def makePcbnewShellWindow(parent):
+    pyshell = PcbnewPyShell(parent, id=-1, title=intro)
+    pyshell.Show()
+    return pyshell
diff --git a/scripting/kicadplugins.i b/scripting/kicadplugins.i
index 9a310fa..609fb59 100644
--- a/scripting/kicadplugins.i
+++ b/scripting/kicadplugins.i
@@ -78,21 +78,59 @@ def ReloadPlugins():
             ReloadPlugin(k)
 
 
-def LoadPlugins(plugpath):
+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.
+    """
     import os
     import sys
+    import pcbnew
 
     kicad_path = os.environ.get('KICAD_PATH')
+    config_path = pcbnew.GetKicadConfigPath()
     plugin_directories=[]
 
-    if plugpath:
-        plugin_directories.append(plugpath)
+    # These are all of the possible "default" search paths for kicad python scripts.
+    #
+    # The Scripts bundled with Kicad:
+    #   <bundlepath>
+    #   <bundlepath>/plugins
+    #
+    # The Scripts relative to the Kicad search path:
+    #   <kicad_path>/scripting
+    #   <kicad_path>/scripting/plugins
+    #
+    # The Scripts relative to the Kicad Users configuration:
+    #   <config_path>/scripting
+    #   <config_path>/scripting/plugins
+    #
+    # And on Linux, extra paths relative to the home directory:
+    #   ~/.kicad_plugins
+    #   ~/.kicad/scripting
+    #   ~/.kicad/scripting/plugins
+    #
+    #  NOTE: These paths will ONLY be added to the python search path IF they exist.
+
+    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.environ['HOME']+'/.kicad_plugins/')
+        plugin_directories.append(os.environ['HOME']+'/.kicad/scripting/')
         plugin_directories.append(os.environ['HOME']+'/.kicad/scripting/plugins/')
 
     for plugins_dir in plugin_directories:
diff --git a/scripting/python_scripting.cpp b/scripting/python_scripting.cpp
index c74abf5..773237a 100644
--- a/scripting/python_scripting.cpp
+++ b/scripting/python_scripting.cpp
@@ -136,7 +136,7 @@ static void swigSwitchPythonBuiltin()
 
 PyThreadState* g_PythonMainTState;
 
-bool pcbnewInitPythonScripting( const char * aUserPluginsPath )
+bool pcbnewInitPythonScripting( const char * aUserScriptingPath )
 {
     swigAddBuiltin();           // add builtin functions
     swigAddModules();           // add our own modules
@@ -191,7 +191,7 @@ bool pcbnewInitPythonScripting( const char * aUserPluginsPath )
         snprintf( cmd, sizeof(cmd), "import sys, traceback\n"
                       "sys.path.append(\".\")\n"
                       "import pcbnew\n"
-                      "pcbnew.LoadPlugins(\"%s\")", aUserPluginsPath );
+                      "pcbnew.LoadPlugins(\"%s\")", aUserScriptingPath );
         PyRun_SimpleString( cmd );
     }
 
@@ -227,20 +227,15 @@ void RedirectStdio()
 }
 
 
-wxWindow* CreatePythonShellWindow( wxWindow* parent )
+wxWindow* CreatePythonShellWindow( wxWindow* parent, const wxString& aFramenameId )
 {
-    const char* pycrust_panel =
-        "import wx\n"
-        "from wx.py import shell, version\n"
-        "\n"
-        "intro = \"PyCrust %s - KiCAD Python Shell\" % version.VERSION\n"
+    const char* pcbnew_pyshell =
+        "import kicad_pyshell\n"
         "\n"
         "def makeWindow(parent):\n"
-        "    pycrust = shell.Shell(parent, -1, introText=intro)\n"
-        "    return pycrust\n"
+        "    return kicad_pyshell.makePcbnewShellWindow(parent)\n"
         "\n";
 
-
     wxWindow*   window = NULL;
     PyObject*   result;
 
@@ -257,7 +252,7 @@ wxWindow* CreatePythonShellWindow( wxWindow* parent )
     Py_DECREF( builtins );
 
     // Execute the code to make the makeWindow function we defined above
-    result = PyRun_String( pycrust_panel, Py_file_input, globals, globals );
+    result = PyRun_String( pcbnew_pyshell, Py_file_input, globals, globals );
 
     // Was there an exception?
     if( !result )
@@ -297,6 +292,8 @@ wxWindow* CreatePythonShellWindow( wxWindow* parent )
 
         wxASSERT_MSG( success, _T( "Returned object was not a wxWindow!" ) );
         Py_DECREF( result );
+
+        window->SetName(aFramenameId);
     }
 
     // Release the python objects we still have
diff --git a/scripting/python_scripting.h b/scripting/python_scripting.h
index 8d0ae17..ca25de4 100644
--- a/scripting/python_scripting.h
+++ b/scripting/python_scripting.h
@@ -24,14 +24,14 @@
  * Initializes the Python engine inside pcbnew
  */
 
-bool        pcbnewInitPythonScripting( const char * aUserPluginsPath );
+bool        pcbnewInitPythonScripting( const char * aUserScriptingPath );
 void        pcbnewFinishPythonScripting();
 
 
 #ifdef KICAD_SCRIPTING_WXPYTHON
 
 void        RedirectStdio();
-wxWindow*   CreatePythonShellWindow( wxWindow* parent );
+wxWindow*   CreatePythonShellWindow( wxWindow* parent, const wxString& aFramenameId );
 
 class PyLOCK
 {

Follow ups