|
1 | 1 | Spring Python's plugin system |
2 | | -============================= |
| 2 | +============================= |
| 3 | + |
| 4 | +Spring Python's plugin system is designed to help you rapidly develop applications. |
| 5 | +Plugin-based solutions have been proven to enhance developer efficiency, with |
| 6 | +examples such as `Grails <http://grails.org/>`_ and `Eclipse <http://eclipse.org/>`_ |
| 7 | +being market leaders in usage and productivity. |
| 8 | + |
| 9 | +This plugin solution was mainly inspired by the Grails demo presented by |
| 10 | +Graeme Rocher at the SpringOne Americas 2008 conference, in which he created |
| 11 | +a Twitter application in 40 minutes. Who wouldn't want to have something similar |
| 12 | +to support Spring Python development? |
| 13 | + |
| 14 | +Introduction |
| 15 | +------------ |
| 16 | + |
| 17 | +Spring Python will manage an approved set of plugins. These are plugins written |
| 18 | +by the committers of Spring Python and are verified to work with an associated |
| 19 | +version of the library. These plugins are also hosted by the same services used |
| 20 | +to host Spring Python downloads, meaning they have the same level of support |
| 21 | +as Spring Python. |
| 22 | + |
| 23 | +However, being an open source framework, developers have every right to code |
| 24 | +their own plugins. We fully support the concept of 3rd party plugins. We want |
| 25 | +to provide as much support in the way of documentation and extension points |
| 26 | +for you to develop your own plugins as well. |
| 27 | + |
| 28 | +.. note:: |
| 29 | + |
| 30 | + Have you considered submitting your plugin as a Spring Extension? |
| 31 | + |
| 32 | + `Spring Extensions <http://www.springsource.org/extensions>`_ is the official |
| 33 | + incubator process for SpringSource. You can |
| 34 | + always maintain your own plugin separately, using whatever means you wish. But |
| 35 | + if want to get a larger adoption of your plugin, name association with |
| 36 | + SpringSource, and perhaps one day becoming an official part of the software |
| 37 | + suite of SpringSource, you may want to consider looking into the Spring |
| 38 | + Extensions process. |
| 39 | + |
| 40 | + |
| 41 | +Coily - Spring Python's command-line tool |
| 42 | +----------------------------------------- |
| 43 | + |
| 44 | +Coily is the command-line tool that utilizes the plugin system. It is similar |
| 45 | +to grails command-line tool, in that through a series of installed plugins, |
| 46 | +you are able to do many tasks, including build skeleton apps that you can later |
| 47 | +flesh out. If you look at the details of this app, you will find a sophisticated, |
| 48 | +command driven tool to built to manage plugins. The real power is in the |
| 49 | +plugins themselves. |
| 50 | + |
| 51 | +Commands |
| 52 | +++++++++ |
| 53 | + |
| 54 | +.. highlight:: bash |
| 55 | + |
| 56 | +To get started, all you need is a copy of coily installed in some directory located |
| 57 | +on your path:: |
| 58 | + |
| 59 | + % coily --help |
| 60 | + |
| 61 | +The results should list available commands:: |
| 62 | + |
| 63 | + Coily - the command-line management tool for Spring Python |
| 64 | + ========================================================== |
| 65 | + Copyright 2006-2008 SpringSource (http://springsource.com), All Rights Reserved |
| 66 | + Licensed under the Apache License, Version 2.0 |
| 67 | + |
| 68 | + |
| 69 | + Usage: coily [command] |
| 70 | + |
| 71 | + --help print this help message |
| 72 | + --list-installed-plugins list currently installed plugins |
| 73 | + --list-available-plugins list plugins available for download |
| 74 | + --install-plugin [name] install coily plugin |
| 75 | + --uninstall-plugin [name] uninstall coily plugin |
| 76 | + --reinstall-plugin [name] reinstall coily plugin |
| 77 | + |
| 78 | + |
| 79 | +* --help - Print out the help menu being displayed |
| 80 | + |
| 81 | +* --list-installed-plugins - list the plugins currently installed in this |
| 82 | + account. It is important to know that each plugin creates a directly |
| 83 | + underneath the user's home directory in a hidden directory *~/.springpython*. |
| 84 | + If you delete this entire directory, you have effectively uninstalled all plugins. |
| 85 | + |
| 86 | +* --list-available-plugins - list the plugins available for installation. |
| 87 | + Coily will check certain network locations, such as the S3 site used to host |
| 88 | + Spring Python downloads. It will also look on the local file system. This is |
| 89 | + in case you have a checked out copy of the plugins source code, and want to |
| 90 | + test things out without uploading to the network. |
| 91 | + |
| 92 | +* --install-plugin - install the named plugin. In this case, you don't have to |
| 93 | + specify a version number. Coily will figure out which version of the plugin |
| 94 | + you need, download it if necessary, and finally copy it into *~/.springpython*. |
| 95 | + |
| 96 | +* --uninstall-plugin - uninstall the named plugin by deleting its entry from *~/.springpython* |
| 97 | + |
| 98 | +* --reinstall-plugin - uninstall then install the plugin. This is particulary |
| 99 | + useful if you are working on a plugin, and need a shortcut step to deploy. |
| 100 | + |
| 101 | +In this case, no plugins have been installed yet. Every installed plugin will |
| 102 | +list itself as another available command to run. If you have already installed |
| 103 | +the *gen-cherrypy-app* plugin, you will see it listed:: |
| 104 | + |
| 105 | + Coily - the command-line management tool for Spring Python |
| 106 | + ========================================================== |
| 107 | + Copyright 2006-2008 SpringSource (http://springsource.com), All Rights Reserved |
| 108 | + Licensed under the Apache License, Version 2.0 |
| 109 | + |
| 110 | + |
| 111 | + Usage: coily [command] |
| 112 | + |
| 113 | + --help print this help message |
| 114 | + --list-installed-plugins list currently installed plugins |
| 115 | + --list-available-plugins list plugins available for download |
| 116 | + --install-plugin [name] install coily plugin |
| 117 | + --uninstall-plugin [name] uninstall coily plugin |
| 118 | + --reinstall-plugin [name] reinstall coily plugin |
| 119 | + --gen-cherrypy-app [name] plugin to create skeleton CherryPy applications |
| 120 | + |
| 121 | +You should notice an extra option listed at the bottom: *gen-cherrypy-app* |
| 122 | +is listed as another command with one argument. Later on, you can read |
| 123 | +official documentation on the existing plugins, and also how to write your own. |
| 124 | + |
| 125 | + |
| 126 | +Officially Supported Plugins |
| 127 | +---------------------------- |
| 128 | + |
| 129 | +This section documents plugins that are developed by the Spring Python team. |
| 130 | + |
| 131 | +External dependencies |
| 132 | ++++++++++++++++++++++ |
| 133 | + |
| 134 | +*gen-cherrypy-app* plugin requires the installation of `CherryPy 3 <http://cherrypy.org/>`_. |
| 135 | + |
| 136 | +gen-cherrypy-app |
| 137 | +++++++++++++++++ |
| 138 | + |
| 139 | +This plugin is used to generate a skeleton `CherryPy <http://cherrypy.org/>`_ |
| 140 | +application based on feeding it a command-line argument:: |
| 141 | + |
| 142 | + % coily --gen-cherrypy-app twitterclone |
| 143 | + |
| 144 | +This will generate a subdirectory *twitterclone* in the user's current directory. |
| 145 | +Inside twitterclone are several files, including *twitterclone.py*. If you run |
| 146 | +the app, you will see a working CherryPy application, with Spring Python |
| 147 | +security in place:: |
| 148 | + |
| 149 | + % cd twitterclone |
| 150 | + % python twitterclone.py |
| 151 | + |
| 152 | +You can immediately start modifying it to put in your features. |
| 153 | + |
| 154 | +Writing your own plugin |
| 155 | +----------------------- |
| 156 | + |
| 157 | +Architecture of a plugin |
| 158 | +++++++++++++++++++++++++ |
| 159 | + |
| 160 | +.. highlight:: python |
| 161 | + |
| 162 | +A plugin is pretty simple in structure. It is basically a Python package with |
| 163 | +some special things added on. *gen-cherrypy-app* plugin demonstrates this. |
| 164 | + |
| 165 | +.. image:: gfx/gen-cherrypy-app-folder-struct.png |
| 166 | + :align: center |
| 167 | + |
| 168 | +The special things needed to define a plugin are as follows: |
| 169 | + |
| 170 | +* A root folder with the same name as your plugin and a *__init__.py*, making |
| 171 | + the plugin a Python package. |
| 172 | + |
| 173 | +* A package-level variable named *__description__* |
| 174 | + This attribute should be assigned the string value description you want |
| 175 | + shown for your plugin when coily --help is run. |
| 176 | + |
| 177 | +* A package-level function named either *create* or *apply* |
| 178 | + |
| 179 | + * If your plugin needs one command line argument, define a *create* method with the following signature:: |
| 180 | + |
| 181 | + def create(plugin_path, name) |
| 182 | + |
| 183 | + * If your plugin doesn't need any arguments, define an *apply* method with the following signature:: |
| 184 | + |
| 185 | + def apply(plugin_path) |
| 186 | + |
| 187 | + In either case, your plugin gets passed an extra argument, plugin_path, |
| 188 | + which contains the directory the plugin is actually installed in. This is |
| 189 | + typically so you can reference other files your plugin needs access to. |
| 190 | + |
| 191 | + .. note:: |
| 192 | + |
| 193 | + What does "package-level" mean? |
| 194 | + |
| 195 | + The code needs to be in the __init__.py file. This file makes the enclosing |
| 196 | + directory a Python package. |
| 197 | + |
| 198 | +Case Study - gen-cherrypy-app plugin |
| 199 | +++++++++++++++++++++++++++++++++++++ |
| 200 | + |
| 201 | +*gen-cherrypy-app* is a plugin used to build a `CherryPy <http://cherrypy.org/>`_ web application using |
| 202 | +Spring Python's feature set. It saves the developer from having to re-configure |
| 203 | +Spring Python's security module, coding CherryPy's engine, and so forth. This |
| 204 | +allows the developer to immediately start writing business code against a |
| 205 | +working application. |
| 206 | + |
| 207 | +Using this plugin, we will de-construct this simple, template-based plugin. |
| 208 | +This will involve looking line-by-line at *gen-cherrypy-app/__init__.py*. |
| 209 | + |
| 210 | +Source Code |
| 211 | +>>>>>>>>>>> |
| 212 | + |
| 213 | +:: |
| 214 | + |
| 215 | + """ |
| 216 | + Copyright 2006-2008 SpringSource (http://springsource.com), All Rights Reserved |
| 217 | + |
| 218 | + Licensed under the Apache License, Version 2.0 (the "License"); |
| 219 | + you may not use this file except in compliance with the License. |
| 220 | + You may obtain a copy of the License at |
| 221 | + |
| 222 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 223 | + |
| 224 | + Unless required by applicable law or agreed to in writing, software |
| 225 | + distributed under the License is distributed on an "AS IS" BASIS, |
| 226 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 227 | + See the License for the specific language governing permissions and |
| 228 | + limitations under the License. |
| 229 | + """ |
| 230 | + import re |
| 231 | + import os |
| 232 | + import shutil |
| 233 | + |
| 234 | + __description__ = "plugin to create skeleton CherryPy applications" |
| 235 | + |
| 236 | + def create(plugin_path, name): |
| 237 | + if not os.path.exists(name): |
| 238 | + print "Creating CherryPy skeleton app %s" % name |
| 239 | + os.makedirs(name) |
| 240 | + |
| 241 | + # Copy/transform the template files |
| 242 | + for file_name in ["cherrypy-app.py", "controller.py", "view.py", "app_context.py"]: |
| 243 | + input_file = open(plugin_path + "/" + file_name).read() |
| 244 | + |
| 245 | + # Iterate over a list of patterns, performing string substitution on the input file |
| 246 | + patterns_to_replace = [("name", name), ("properName", name[0].upper() + name[1:])] |
| 247 | + for pattern, replacement in patterns_to_replace: |
| 248 | + input_file = re.compile(r"\$\{%s}" % pattern).sub(replacement, input_file) |
| 249 | + |
| 250 | + output_filename = name + "/" + file_name |
| 251 | + if file_name == "cherrypy-app.py": |
| 252 | + output_filename = name + "/" + name + ".py" |
| 253 | + |
| 254 | + app = open(output_filename, "w") |
| 255 | + app.write(input_file) |
| 256 | + app.close() |
| 257 | + |
| 258 | + # Recursively copy other parts |
| 259 | + shutil.copytree(plugin_path + "/images", name + "/" + "images") |
| 260 | + else: |
| 261 | + print "There is already something called %s. ABORT!" % name |
| 262 | + |
| 263 | + |
| 264 | +Deconstructing the factory |
| 265 | +>>>>>>>>>>>>>>>>>>>>>>>>>> |
| 266 | + |
| 267 | +* The opening section shows the copyright statement, which should tip you off |
| 268 | + that this is an official plugin. |
| 269 | + |
| 270 | +* __description__ is a required variable:: |
| 271 | + |
| 272 | + __description__ = "plugin to create skeleton CherryPy applications" |
| 273 | + |
| 274 | + It contains the description displayed when a user runs:: |
| 275 | + |
| 276 | + % coily --help |
| 277 | + |
| 278 | + :: |
| 279 | + |
| 280 | + Usage: coily [command] |
| 281 | + ... |
| 282 | + --gen-cherrypy-app [name] plugin to create skeleton CherryPy applications |
| 283 | + |
| 284 | +* Opening line defines create with two arguments:: |
| 285 | + |
| 286 | + def create(plugin_path, name): |
| 287 | + |
| 288 | + The arguments allow both the plugin path to be fed along with the command-line |
| 289 | + argument that is filled in when the user runs the command:: |
| 290 | + |
| 291 | + % coily --gen-cherrypy-app [name] |
| 292 | + |
| 293 | + It is important to realize that *plugin_path* is needed in case the plugin |
| 294 | + needs to refer to any files inside its installed directory. This is because |
| 295 | + plugins are not installed anywhere on the *PYTHONPATH*, but instead, in the |
| 296 | + user's home directory underneath *~/.springpython*. |
| 297 | + |
| 298 | + This mechanism was chosen because it gives users an easy ability to pick |
| 299 | + which plugins they wish to use, without requiring system admin power. It also |
| 300 | + eliminates the need to deal with multiple versions of plugins being installed |
| 301 | + on your *PYTHONPATH*. This provides maximum flexibility which is needed in a |
| 302 | + development environment. |
| 303 | + |
| 304 | +* This plugin works by creating a directory in the user's current working directory, |
| 305 | + and putting all relevant files into it. The argument passed into the command-line |
| 306 | + is used as the name of an application, and the directory created has the same name:: |
| 307 | + |
| 308 | + if not os.path.exists(name): |
| 309 | + print "Creating CherryPy skeleton app %s" % name |
| 310 | + os.makedirs(name) |
| 311 | + |
| 312 | + However, if the directory already exists, it won't proceed:: |
| 313 | + |
| 314 | + else: |
| 315 | + print "There is already something called %s. ABORT!" % name |
| 316 | + |
| 317 | +* This plugin then iterates over a list of filenames, which happen to match the |
| 318 | + names of files found in the plugin's directory. These are essentially template |
| 319 | + files, intended to be copied into the target directory. However, the files |
| 320 | + are not copied directly. Instead they are opened and read into memory:: |
| 321 | + |
| 322 | + # Copy/transform the template files |
| 323 | + for file_name in ["cherrypy-app.py", "controller.py", "view.py", "app_context.py"]: |
| 324 | + input_file = open(plugin_path + "/" + file_name).read() |
| 325 | + |
| 326 | + Then, the contents are scanned for key phrases, and substituted. In this case, |
| 327 | + the substitution is a variant of the name of the application being generated:: |
| 328 | + |
| 329 | + # Iterate over a list of patterns, performing string substitution on the input file |
| 330 | + patterns_to_replace = [("name", name), ("properName", name[0].upper() + name[1:])] |
| 331 | + for pattern, replacement in patterns_to_replace: |
| 332 | + input_file = re.compile(r"\$\{%s}" % pattern).sub(replacement, input_file) |
| 333 | + |
| 334 | + The substituted content is written to a new output file. In most cases, |
| 335 | + the original filename is also the target filename. However, the key file, |
| 336 | + *cherrypy-app.py* is renamed to the application's name:: |
| 337 | + |
| 338 | + output_filename = name + "/" + file_name |
| 339 | + if file_name == "cherrypy-app.py": |
| 340 | + output_filename = name + "/" + name + ".py" |
| 341 | + |
| 342 | + app = open(output_filename, "w") |
| 343 | + app.write(input_file) |
| 344 | + app.close() |
| 345 | + |
| 346 | +* Finally, the images directory is recursively copied into the target directory:: |
| 347 | + |
| 348 | + # Recursively copy other parts |
| 349 | + shutil.copytree(plugin_path + "/images", name + "/" + "images") |
| 350 | + |
| 351 | +Summary |
| 352 | +>>>>>>> |
| 353 | + |
| 354 | +All these steps effectively copy a set of files used to template an application. |
| 355 | +With this template approach, the major effort of developing this plugin is spent |
| 356 | +working on the templates themselves, not on this template factory. While this is |
| 357 | +mostly working with python code for a python solution, the fact that this is a |
| 358 | +template requires reinstalling the plugin everytime a change is made in order |
| 359 | +to test them. |
| 360 | + |
| 361 | +Users are welcome to use *gen-cherypy-app*'s *__init__.py* file to generate their |
| 362 | +own template solutions, and work on other skeleton tools or solutions. |
0 commit comments