From 556abfaf0becbeafa5cad50b2b2866a76e587156 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 18 Jul 2008 17:51:34 +0000 Subject: 1: Started roughing out ipalib package --- ipalib/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 ipalib/__init__.py (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py new file mode 100644 index 00000000..8eb39d3c --- /dev/null +++ b/ipalib/__init__.py @@ -0,0 +1,18 @@ +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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; version 2 only +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -- cgit From 00f4da79a900ae2af1db82e4e697bd2552cdabc5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 18 Jul 2008 20:31:12 +0000 Subject: 2: Got basics of NameSpace working, added corresponding unit tests --- ipalib/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 8eb39d3c..ddce3ac9 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -16,3 +16,7 @@ # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +IPA library. +""" -- cgit From ef7594ffe1bad349dc539f69ee90708460999a71 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 19 Jul 2008 04:28:03 +0000 Subject: 4: Got basics of API.register_command() working; added corresponding unit tests --- ipalib/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index ddce3ac9..1337d812 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -20,3 +20,7 @@ """ IPA library. """ + +import base + +api = base.API() -- cgit From 7acf12e988f45d503d7d93f03f706618f7696504 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 01:29:59 +0000 Subject: 10: Updated base.API to reflect the fact that base.Object is now the new unit of plugin functionality; updated corresponding unit tests --- ipalib/__init__.py | 4 ---- 1 file changed, 4 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 1337d812..ddce3ac9 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -20,7 +20,3 @@ """ IPA library. """ - -import base - -api = base.API() -- cgit From a3dc04ade4c8b640a881519144f009b70c6e4cfd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 09:01:02 +0000 Subject: 157: More docstring cleanup; fixed remaining epydoc warnings --- ipalib/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index ddce3ac9..309bd2e2 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -18,5 +18,14 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -IPA library. +The IPA Library. + +To learn about the library, you should probably read the code in this order: + + 1. Start with the `ipalib.plugable` module + + 2. Then read the `ipalib.public` module + +Some of the plugin architecture was inspired by ``bzr``, so you might also +read http://bazaar-vcs.org/WritingPlugins """ -- cgit From 43c04f1cd356a46aab6720c64e8d15900b46bfdf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 19:36:54 +0000 Subject: 163: Docstring improvement for ipalib/__init__.py and plugable.py --- ipalib/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 309bd2e2..84b529e1 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -20,11 +20,13 @@ """ The IPA Library. -To learn about the library, you should probably read the code in this order: +To learn about the ``ipalib`` library, you should read the code in this order: - 1. Start with the `ipalib.plugable` module + 1. Learn about the plugin framework in `ipalib.plugable` - 2. Then read the `ipalib.public` module + 2. Learn about the public api in `ipalib.public` + + 3. Look at some example plugins in `ipalib.plugins.example` Some of the plugin architecture was inspired by ``bzr``, so you might also read http://bazaar-vcs.org/WritingPlugins -- cgit From ec0596b429d10b8659ea21a051fd98e047aece46 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 03:24:37 +0000 Subject: 174: Fleshed out docstrings for SetProxy, DictProxy, and MagicDict --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 84b529e1..4d96c2d6 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -24,7 +24,7 @@ To learn about the ``ipalib`` library, you should read the code in this order: 1. Learn about the plugin framework in `ipalib.plugable` - 2. Learn about the public api in `ipalib.public` + 2. Learn about the public API in `ipalib.public` 3. Look at some example plugins in `ipalib.plugins.example` -- cgit From 2fc3819beca86c3d19d85e2f5777af3566305175 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 25 Aug 2008 23:35:29 +0000 Subject: 191: Removed ipalib/api.py module; standard plugable.API instance is now in ipalib.__init__.py --- ipalib/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 4d96c2d6..2436d9b1 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -31,3 +31,13 @@ To learn about the ``ipalib`` library, you should read the code in this order: Some of the plugin architecture was inspired by ``bzr``, so you might also read http://bazaar-vcs.org/WritingPlugins """ + +import plugable +import public + +api = plugable.API( + public.Command, + public.Object, + public.Method, + public.Property, +) -- cgit From 0d35c96f1afd8b058a6431b944130e5d7bfe41bb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 00:04:15 +0000 Subject: 192: Added a quick console example to docstring in ipalib/__init__.py --- ipalib/__init__.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 2436d9b1..7f2249ef 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -30,6 +30,30 @@ To learn about the ``ipalib`` library, you should read the code in this order: Some of the plugin architecture was inspired by ``bzr``, so you might also read http://bazaar-vcs.org/WritingPlugins + +Here is a short console example on using the plugable API: + +>>> from ipalib import api +>>> list(api.register) # Plugins must subclass from one of these base classes: +['Command', 'Method', 'Object', 'Property'] +>>> 'user_add' in api.register.Command # Has 'user_add' been registered? +False +>>> import ipalib.load_plugins # This causes all plugins to be loaded +>>> 'user_add' in api.register.Command # Yes, 'user_add' has been registered: +True +>>> list(api) # API is empty till finalize() is called: +[] +>>> api.finalize() # Instantiates plugins, builds API namespaces: +>>> list(api) # Lists the namespaces in the API: +['Command', 'Method', 'Object', 'Property'] +>>> 'user_add' in api.Command # Yes, the 'user_add' command exists: +True +>>> api['Command'] is api.Command # Access as dict item or as attribute: +True +>>> list(api.Command) # List available commands: +['discover', 'group_add', 'group_del', 'group_find', 'group_mod', 'krbtest', 'service_add', 'service_del', 'service_find', 'service_mod', 'user_add', 'user_del', 'user_find', 'user_mod'] +>>> list(api.Command.user_add) # List public methods for user_add: +['__call__', 'default', 'execute', 'get_doc', 'normalize', 'options', 'validate'] """ import plugable -- cgit From 5157d8fc50bab1bd5ac1ebe2c006faaf910fef31 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 00:28:43 +0000 Subject: 194: Removed like to Bazaar Plugin doc as it's not very relevant --- ipalib/__init__.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 7f2249ef..a5fc3f11 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -28,9 +28,6 @@ To learn about the ``ipalib`` library, you should read the code in this order: 3. Look at some example plugins in `ipalib.plugins.example` -Some of the plugin architecture was inspired by ``bzr``, so you might also -read http://bazaar-vcs.org/WritingPlugins - Here is a short console example on using the plugable API: >>> from ipalib import api -- cgit From ab81ca56fd336af4b83ef19a6f97dffe0b1a0923 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 04:39:01 +0000 Subject: 255: CLI help, console commands now subclass from public.Application; other tweeking to make CLI utilize Application --- ipalib/__init__.py | 1 + 1 file changed, 1 insertion(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index a5fc3f11..b0f0a1fc 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -61,4 +61,5 @@ api = plugable.API( public.Object, public.Method, public.Property, + public.Application, ) -- cgit From 4e8ff5c65675fe7534afa02ce06d6ff73fd024c9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 00:01:29 +0000 Subject: 318: Renamed all references to 'public' module to 'frontend' --- ipalib/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index b0f0a1fc..e6548ff8 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -54,12 +54,12 @@ True """ import plugable -import public +import frontend api = plugable.API( - public.Command, - public.Object, - public.Method, - public.Property, - public.Application, + frontend.Command, + frontend.Object, + frontend.Method, + frontend.Property, + frontend.Application, ) -- cgit From c38b90d5d4de1c6d351ce1d9fd94555376d6dda7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 01:36:54 +0000 Subject: 321: Standard ipalib.api attribute now accepts plugins from the Backend base class --- ipalib/__init__.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index e6548ff8..216d654f 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -55,6 +55,7 @@ True import plugable import frontend +import backend api = plugable.API( frontend.Command, @@ -62,4 +63,5 @@ api = plugable.API( frontend.Method, frontend.Property, frontend.Application, + backend.Backend, ) -- cgit From 4dbbf5656d4db96068ca6c936120827e52ba5ad8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 01:43:51 +0000 Subject: 322: Updated ipalib package docstring, replacing cross reference to 'public' with 'frontend' --- ipalib/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 216d654f..6c129a41 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -24,9 +24,11 @@ To learn about the ``ipalib`` library, you should read the code in this order: 1. Learn about the plugin framework in `ipalib.plugable` - 2. Learn about the public API in `ipalib.public` + 2. Learn about the frontend plugins in `ipalib.frontend` - 3. Look at some example plugins in `ipalib.plugins.example` + 3. Learn about the backend plugins in `ipalib.backend` + + 4. Look at some example plugins in `ipalib.plugins.example` Here is a short console example on using the plugable API: -- cgit From afdc72103847fc27efd00f8cc97a7320909ff6a0 Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Mon, 29 Sep 2008 17:41:30 +0200 Subject: Add support for environment variables, change tests accordingly --- ipalib/__init__.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 6c129a41..f0d43aad 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -58,8 +58,10 @@ True import plugable import frontend import backend +import config api = plugable.API( + config.default_environment(), frontend.Command, frontend.Object, frontend.Method, -- cgit From 149429f3057e3ae934e660e3276c9e8d3c935d17 Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Thu, 2 Oct 2008 20:24:05 +0200 Subject: Environment is now subclassed from object, rather then dict. Added tests for Environment and config.py --- ipalib/__init__.py | 1 - 1 file changed, 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index f0d43aad..956e4610 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -61,7 +61,6 @@ import backend import config api = plugable.API( - config.default_environment(), frontend.Command, frontend.Object, frontend.Method, -- cgit From b2b5b904bcc1ab96d5efb992d5630505022d0ecb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 7 Oct 2008 20:07:16 -0600 Subject: Made package-level docstrings more consistent so they read better in generated documentation --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 956e4610..46edb822 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -The IPA Library. +Package containing core library. To learn about the ``ipalib`` library, you should read the code in this order: -- cgit From f7b7fa5553fa216caea67ba8a952ce71f29863db Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 19:11:26 -0600 Subject: Cleaned up ipalib package-level docstring, removed broken cross-referce to --- ipalib/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 46edb822..92544e10 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -22,13 +22,11 @@ Package containing core library. To learn about the ``ipalib`` library, you should read the code in this order: - 1. Learn about the plugin framework in `ipalib.plugable` + 1. Get the big picture from some actual plugins, like `plugins.f_user`. - 2. Learn about the frontend plugins in `ipalib.frontend` + 2. Learn about the base classes for frontend plugins in `frontend`. - 3. Learn about the backend plugins in `ipalib.backend` - - 4. Look at some example plugins in `ipalib.plugins.example` + 3. Learn about the core plugin framework in `plugable`. Here is a short console example on using the plugable API: -- cgit From f1eb74e22cadf3a9f4ac991e0f8b922f6fb56d1e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 20:50:34 -0600 Subject: make-test now runs doctests also; fixed several broken doctests --- ipalib/__init__.py | 24 ------------------------ 1 file changed, 24 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 92544e10..4593e581 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -27,30 +27,6 @@ To learn about the ``ipalib`` library, you should read the code in this order: 2. Learn about the base classes for frontend plugins in `frontend`. 3. Learn about the core plugin framework in `plugable`. - -Here is a short console example on using the plugable API: - ->>> from ipalib import api ->>> list(api.register) # Plugins must subclass from one of these base classes: -['Command', 'Method', 'Object', 'Property'] ->>> 'user_add' in api.register.Command # Has 'user_add' been registered? -False ->>> import ipalib.load_plugins # This causes all plugins to be loaded ->>> 'user_add' in api.register.Command # Yes, 'user_add' has been registered: -True ->>> list(api) # API is empty till finalize() is called: -[] ->>> api.finalize() # Instantiates plugins, builds API namespaces: ->>> list(api) # Lists the namespaces in the API: -['Command', 'Method', 'Object', 'Property'] ->>> 'user_add' in api.Command # Yes, the 'user_add' command exists: -True ->>> api['Command'] is api.Command # Access as dict item or as attribute: -True ->>> list(api.Command) # List available commands: -['discover', 'group_add', 'group_del', 'group_find', 'group_mod', 'krbtest', 'service_add', 'service_del', 'service_find', 'service_mod', 'user_add', 'user_del', 'user_find', 'user_mod'] ->>> list(api.Command.user_add) # List public methods for user_add: -['__call__', 'default', 'execute', 'get_doc', 'normalize', 'options', 'validate'] """ import plugable -- cgit From ddb5449c7faabbd4c1b71adfe84c386b943a163f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 30 Oct 2008 01:11:33 -0600 Subject: Did some initial work for Context plugins --- ipalib/__init__.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 4593e581..a6664f73 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -30,15 +30,18 @@ To learn about the ``ipalib`` library, you should read the code in this order: """ import plugable -import frontend -import backend -import config - -api = plugable.API( - frontend.Command, - frontend.Object, - frontend.Method, - frontend.Property, - frontend.Application, - backend.Backend, -) +from backend import Backend, Context +from frontend import Command, Object, Method, Property, Application +from ipa_types import Bool, Int, Unicode, Enum +from frontend import Param, DefaultFrom + +def get_standard_api(unit_test=False): + api = plugable.API( + Command, Object, Method, Property, Application, + Backend, Context, + ) + if unit_test is True: + api.env.mode = 'unit_test' + return api + +api = get_standard_api() -- cgit From 2fee6a3e20169f12b837647f0f71d6f28937490f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 30 Oct 2008 01:34:46 -0600 Subject: Added tests.util.get_api() function to create a standard (api, home) tuple for unit testing --- ipalib/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index a6664f73..5cc4c121 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -35,13 +35,11 @@ from frontend import Command, Object, Method, Property, Application from ipa_types import Bool, Int, Unicode, Enum from frontend import Param, DefaultFrom -def get_standard_api(unit_test=False): - api = plugable.API( +def get_standard_api(): + return plugable.API( Command, Object, Method, Property, Application, Backend, Context, ) - if unit_test is True: - api.env.mode = 'unit_test' - return api + api = get_standard_api() -- cgit From c26a3c8542472a2d3931c7dc82edfd684354af6b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 7 Nov 2008 02:26:38 -0700 Subject: Finished fist draft of plugin tutorial in ipalib/__init__.py docstring --- ipalib/__init__.py | 646 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 637 insertions(+), 9 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 5cc4c121..4db6a04f 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -17,17 +17,622 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -""" + +''' Package containing core library. -To learn about the ``ipalib`` library, you should read the code in this order: +============================= + Tutorial for Plugin Authors +============================= + +This tutorial gives a broad learn-by-doing introduction to writing plugins +for freeIPA v2. As not to overwhelm the reader, it does not cover every +detail, but it does provides enough to get one started and is heavily +cross-referenced with further documentation that (hopefully) fills in the +missing details. + +Where the documentation has left the reader confused, the many built-in +plugins in `ipalib.plugins` and `ipa_server.plugins` provide real-life +examples of how to write good plugins. + +*Note:* + + This tutorial, along with all the Python docstrings in freeIPA v2, + uses the *reStructuredText* markup language. For documentation on + reStructuredText, see: + + http://docutils.sourceforge.net/rst.html + + For documentation on using reStructuredText markup with epydoc, see: + + http://epydoc.sourceforge.net/manual-othermarkup.html + + +------------------------------------ +First steps: A simple command plugin +------------------------------------ + +Our first example will create the most basic command plugin possible. A +command plugin simultaneously adds a new command that can be called through +the command-line ``ipa`` script *and* adds a new XML-RPC method... the two are +one in the same, simply invoked in different ways. + +All plugins must subclass from `plugable.Plugin`, and furthermore, must +subclass from one of the base classes allowed by the `plugable.API` instance +returned by the `get_standard_api()` function. + +To be a command plugin, your plugin must subclass from `frontend.Command`. +Creating a basic plugin involves two steps, defining the class and then +registering the class: + +>>> from ipalib import Command, get_standard_api +>>> api = get_standard_api() +>>> class my_command(Command): # Step 1, define class +... """My example plugin.""" +... +>>> api.register(my_command) # Step 2, register class + +Notice that we are registering the ``my_command`` class itself and not an +instance thereof. + +Until `plugable.API.finalize()` is called, your plugin class has not been +instantiated nor the does the ``Command`` namespace yet exist. For example: + +>>> hasattr(api, 'Command') +False +>>> api.finalize() +>>> hasattr(api.Command, 'my_command') +True +>>> api.Command.my_command.doc +'My example plugin.' + +Notice that your plugin instance in accessed through an attribute named +'my_command', the same name as your plugin class name. + + +------------------------------ +Make your command do something +------------------------------ + +This simplest way to make your example command plugin do something is to +implement a ``run()`` method, like this: + +>>> class my_command(Command): +... """My example plugin with run().""" +... +... def run(self): +... return 'My run() method was called!' +... +>>> api = get_standard_api() +>>> api.register(my_command) +>>> api.finalize() +>>> api.Command.my_command() # Call your plugin +'My run() method was called!' + +When `frontend.Command.__call__()` is called, it first validates any arguments +and options your command plugin takes (if any) and then calls its ``run()`` +method. + + +------------------------ +Forwarding vs. execution +------------------------ + +However, unlike the example above, a typical command plugin will implement an +``execute()`` method instead of a ``run()`` method. Your command plugin can +be loaded in two distinct contexts: + + 1. In a *client* context - Your command plugin is only used to validate + any arguments and options it takes, and then ``self.forward()`` is + called, which forwards the call over XML-RPC to an IPA server where + the actual work is done. + + 2. In a *server* context - Your same command plugin validates any + arguments and options it takes, and then ``self.execute()`` is called, + which you should implement to perform whatever work your plugin does. + +The base `frontend.Command.run()` method simply dispatches the call to +``self.execute()`` if ``self.env.in_server`` is True, or otherwise +dispatches the call to ``self.forward()``. + +For example, say you have a command plugin like this: + +>>> class my_command(Command): +... """Forwarding vs. execution.""" +... +... def forward(self): +... return 'in_server=%r; forward() was called.' % self.env.in_server +... +... def execute(self): +... return 'in_server=%r; execute() was called.' % self.env.in_server +... + +If ``my_command`` is loaded in a *client* context, ``forward()`` will be +called: + +>>> api = get_standard_api() +>>> api.env.in_server = False # run() will dispatch to forward() +>>> api.register(my_command) +>>> api.finalize() +>>> api.Command.my_command() # Call your command plugin +'in_server=False; forward() was called.' + +On the other hand, if ``my_command`` is loaded in a *server* context, +``execute()`` will be called: + +>>> api = get_standard_api() +>>> api.env.in_server = True # run() will dispatch to execute() +>>> api.register(my_command) +>>> api.finalize() +>>> api.Command.my_command() # Call your command plugin +'in_server=True; execute() was called.' + +Normally there should be no reason to override `frontend.Command.forward()`, +but, as above, it can be done for demonstration purposes. In contrast, there +*is* a reason you might want to override `frontend.Command.run()`: if it only +makes sense to execute your command locally, if it should never be forwarded +to the server. In this case, you should implement your *do-stuff* in the +``run()`` method instead of in the ``execute()`` method. + +For example, the ``ipa`` command line script has a ``help`` command +(`ipalib.cli.help`) that is specific to the command-line-interface and should +never be forwarded to the server. + + +--------------- +Backend plugins +--------------- + +There are two types of plugins: + + 1. *Frontend plugins* - These are loaded in both the *client* and *server* + contexts. These need to be installed with any application built atop + the `ipalib` library. The built-in frontend plugins can be found in + `ipalib.plugins`. The ``my_command`` example above is a frontend + plugin. + + 2. *Backend plugins* - These are only loaded in a *server* context and + only need to be installed on the IPA server. The built-in backend + plugins can be found in `ipa_server.plugins`. + +Backend plugins should provide a set of methods that standardize how IPA +interacts with some external system or library. For example, all interaction +with LDAP is done through the ``ldap`` backend plugin defined in +`ipa_server.plugins.b_ldap`. As a good rule of thumb, anytime you need to +import some package that is not part of the Python standard library, you +should probably interact with that package via a corresponding backend +plugin you implement. + +Backend plugins are much more free-form than command plugins. Aside from a +few reserved attribute names, you can define arbitrary public methods on your +backend plugin (in contrast, frontend plugins get wrapped in a +`plugable.PluginProxy`, which allow access to only specific attributes on the +frontend plugin). + +Here is a simple example: + +>>> from ipalib import Backend +>>> class my_backend(Backend): +... """My example backend plugin.""" +... +... def do_stuff(self): +... """Part of your API.""" +... return 'Stuff got done.' +... +>>> api = get_standard_api() +>>> api.register(my_backend) +>>> api.finalize() +>>> api.Backend.my_backend.do_stuff() +'Stuff got done.' + + +------------------------------- +How your command should do work +------------------------------- + +We now return to our ``my_command`` plugin example. + +Plugins are separated into frontend and backend plugins so that there are not +unnecessary dependencies required by an application that only uses `ipalib` and +its built-in frontend plugins (and then forwards over XML-RPC for execution). + +But how do we avoid introducing additional dependencies? For example, the +``user_add`` command needs to talk to LDAP to add the user, yet we want to +somehow load the ``user_add`` plugin on client machines without requiring the +``python-ldap`` package (Python bindings to openldap) to be installed. To +answer that, we consult our golden rule: + + **The golden rule:** A command plugin should implement its ``execute()`` + method strictly via calls to methods on one or more backend plugins. + +So the module containing the ``user_add`` command does not itself import the +Python LDAP bindings, only the module containing the ``ldap`` backend plugin +does that, and the backend plugins are only installed on the server. The +``user_add.execute()`` method, which is only called when in a server context, +is implemented as a series of calls to methods on the ``ldap`` backend plugin. + +When `plugable.Plugin.set_api()` is called, each plugin stores a reference to +the `plugable.API` instance it has been loaded into. So your plugin can +access the ``my_backend`` plugin as ``self.api.Backend.my_backend``. + +Additionally, convenience attributes are set for each namespace, so your +plugin can also access the ``my_backend`` plugin as simply +``self.Backend.my_backend``. + +This next example will tie everything together. First we create our backend +plugin: + +>>> api = get_standard_api() +>>> api.env.in_server = True # We want to execute, not forward +>>> class my_backend(Backend): +... """My example backend plugin.""" +... +... def do_stuff(self): +... """my_command.execute() calls this.""" +... return 'my_backend.do_stuff() indeed did do stuff!' +... +>>> api.register(my_backend) + +Second, we have our frontend plugin, the command: + +>>> class my_command(Command): +... """My example command plugin.""" +... +... def execute(self): +... """Implemented against Backend.my_backend""" +... return self.Backend.my_backend.do_stuff() +... +>>> api.register(my_command) + +Lastly, we call ``api.finalize()`` and see what happens when we call +``my_command()``: + +>>> api.finalize() +>>> api.Command.my_command() +'my_backend.do_stuff() indeed did do stuff!' + +When not in a server context, ``my_command.execute()`` never gets called, so +it never tries to access the non-existent backend plugin at +``self.Backend.my_backend.`` To emphasize this point, here is one last +example: + +>>> api = get_standard_api() +>>> api.env.in_server = False # We want to forward, not execute +>>> class my_command(Command): +... """My example command plugin.""" +... +... def execute(self): +... """Same as above.""" +... return self.Backend.my_backend.do_stuff() +... +... def forward(self): +... return 'Just my_command.forward() getting called here.' +... +>>> api.register(my_command) +>>> api.finalize() + +Notice that the ``my_backend`` plugin has certainly not be registered: + +>>> hasattr(api.Backend, 'my_backend') +False + +And yet we can call ``my_command()``: + +>>> api.Command.my_command() +'Just my_command.forward() getting called here.' + + +---------------------------------------- +Calling other commands from your command +---------------------------------------- + +It can be useful to have your ``execute()`` method call other command plugins. +Among other things, this allows for meta-commands that conveniently call +several other commands in a single operation. For example: + +>>> api = get_standard_api() +>>> api.env.in_server = True # We want to execute, not forward +>>> class meta_command(Command): +... """My meta-command plugin.""" +... +... def execute(self): +... """Calls command_1(), command_2()""" +... return '%s; %s.' % ( +... self.Command.command_1(), +... self.Command.command_2() +... ) +>>> class command_1(Command): +... def execute(self): +... return 'command_1.execute() called' +... +>>> class command_2(Command): +... def execute(self): +... return 'command_2.execute() called' +... +>>> api.register(meta_command) +>>> api.register(command_1) +>>> api.register(command_2) +>>> api.finalize() +>>> api.Command.meta_command() +'command_1.execute() called; command_2.execute() called.' + +Because this is quite useful, we are going to revise our golden rule somewhat: - 1. Get the big picture from some actual plugins, like `plugins.f_user`. + **The revised golden rule:** A command plugin should implement its + ``execute()`` method strictly via what it can access through ``self.api``, + most likely via the backend plugins in ``self.api.Backend`` (which can also + be conveniently accessed as ``self.Backend``). - 2. Learn about the base classes for frontend plugins in `frontend`. - 3. Learn about the core plugin framework in `plugable`. -""" +----------------------------------------------- +Defining arguments and options for your command +----------------------------------------------- + +You can define a command can accept arbitrary arguments and options. +For example: + +>>> from ipalib import Param +>>> class nudge(Command): +... """Takes one argument, one option""" +... +... takes_args = ['programmer'] +... +... takes_options = [Param('stuff', default=u'documentation')] +... +... def execute(self, programmer, **kw): +... return '%s, go write more %s!' % (programmer, kw['stuff']) +... +>>> api = get_standard_api() +>>> api.env.in_server = True +>>> api.register(nudge) +>>> api.finalize() +>>> api.Command.nudge('Jason') +u'Jason, go write more documentation!' +>>> api.Command.nudge('Jason', stuff='unit tests') +u'Jason, go write more unit tests!' + +The ``args`` and ``options`` attributes are `plugable.NameSpace` instances +containing a command's arguments and options, respectively, as you can see: + +>>> list(api.Command.nudge.args) # Iterates through argument names +['programmer'] +>>> api.Command.nudge.args.programmer +Param('programmer', Unicode()) +>>> list(api.Command.nudge.options) # Iterates through option names +['stuff'] +>>> api.Command.nudge.options.stuff +Param('stuff', Unicode()) +>>> api.Command.nudge.options.stuff.default +u'documentation' + +The arguments and options must not contain colliding names. They are both +merged together into the ``params`` attribute, another `plugable.NameSpace` +instance, as you can see: + +>>> api.Command.nudge.params +NameSpace(<2 members>, sort=False) +>>> list(api.Command.nudge.params) # Iterates through the param names +['programmer', 'stuff'] + +When calling a command, its positional arguments can also be provided as +keyword arguments, and in any order. For example: + +>>> api.Command.nudge(stuff='lines of code', programmer='Jason') +u'Jason, go write more lines of code!' + +When a command plugin is called, the values supplied for its parameters are +put through a sophisticated processing pipeline that includes steps for +normalization, type conversion, validation, and dynamically constructing +the defaults for missing values. The details wont be covered here; however, +here is a quick teaser: + +>>> from ipalib import Int +>>> class create_player(Command): +... takes_options = [ +... 'first', +... 'last', +... Param('nick', +... normalize=lambda value: value.lower(), +... default_from=lambda first, last: first[0] + last, +... ), +... Param('points', type=Int(), default=0), +... ] +... +>>> cp = create_player() +>>> cp.finalize() +>>> cp.convert(points=" 1000 ") +{'points': 1000} +>>> cp.normalize(nick=u'NickName') +{'nick': u'nickname'} +>>> cp.get_default(first='Jason', last='DeRose') +{'nick': u'jderose', 'points': 0} + +For the full details on the parameter system, see the +`frontend.parse_param_spec()` function, and the `frontend.Param` and +`frontend.Command` classes. + + +------------------------ +Logging from your plugin +------------------------ + +After `plugable.Plugin.set_api()` is called, your plugin will have a +``self.log`` attribute. Plugins should only log through this attribute. +For example: + +>>> class paint_house(Command): +... +... takes_args = ['color'] +... +... def execute(self, color): +... """Uses self.log.error()""" +... if color not in ('red', 'blue', 'green'): +... self.log.error("I don't have %s paint!", color) # Log error +... return +... return 'I painted the house %s.' % color +... + +Some basic knowledge of the Python ``logging`` module might be helpful. See: + + http://www.python.org/doc/2.5.2/lib/module-logging.html + +The important thing to remember is that your plugin should not configure +logging itself, but should instead simply use the ``self.log`` logger. + +Also see the `plugable.API.bootstrap()` method for details on how the logging +is configured. + + +--------------------- +Environment variables +--------------------- + +Plugins access various environment variables and run-time information through +``self.api.env`` (for convenience, ``self.env`` is equivalent). + +When you create a fresh `plugable.API` instance, its ``env`` attribute is +likewise a freshly created `config.Env` instance, which will already be +populated with certain run-time information. For example: + +>>> api = get_standard_api() +>>> list(api.env) +['bin', 'dot_ipa', 'home', 'ipalib', 'mode', 'script', 'site_packages'] + +Here is a quick overview of the run-time information: + +============= ================================ ======================= +Key Source or example value Description +============= ================================ ======================= +bin /usr/bin Dir. containing script +dot_ipa ~/.ipa User config directory +home os.environ['HOME'] User home dir. +ipalib .../site-packages/ipalib Dir. of ipalib package +mode 'production' or 'unit_test' The mode ipalib is in +script sys.argv[0] Path of script +site_packages /usr/lib/python2.5/site-packages Dir. containing ipalib/ +============= ================================ ======================= + +After `plugable.API.bootstrap()` has been called, the env instance will be +populated with all the environment information used by the built-in plugins. +This will typically be called before any plugins are registered. For example: + +>>> len(api.env) +7 +>>> api.bootstrap(in_server=True) # We want to execute, not forward +>>> len(api.env) +33 + +If your plugin requires new environment variables *and* will be included in +the freeIPA built-in plugins, you should add the defaults for your variables +in `ipalib.constants.DEFAULT_CONFIG`. Also, you should consider whether your +new environment variables should have any auto-magic logic to determine their +values if they haven't already been set by the time `config.Env._bootstrap()`, +`config.Env._finalize_core()`, or `config.Env._finalize()` is called. + +On the other hand, if your plugin requires new environment variables and will +be installed in a 3rd-party package, your plugin should set these variables +in the module it is defined in. + +`config.Env` values work on a first-one-wins basis... after a value has been +set, it can not be overridden with a new value. As any variables can be set +using the command-line ``-e`` global option or set in a configuration file, +your module must check whether a variable has already been set before +setting its default value. For example: + +>>> if 'message_of_the_day' not in api.env: +... api.env.message_of_the_day = 'Hello, world!' +... + +Your plugin can access any environment variables via ``self.env``. +For example: + +>>> class motd(Command): +... """Print message of the day.""" +... +... def execute(self): +... return self.env.message_of_the_day +... +>>> api.register(motd) +>>> api.finalize() +>>> api.Command.motd() +'Hello, world!' + +Also see the `plugable.API.bootstrap_with_global_options()` method. + + +--------------------------------------------- +Indispensable ipa script commands and options +--------------------------------------------- + +The ``console`` command will launch a custom interactive Python interpreter +session. The global environment will have an ``api`` variable, which is the +standard `plugable.API` instance found at ``ipalib.api``. All plugins will +have been loaded (well, except the backend plugins if ``in_server`` is False) +and ``api`` will be fully initialized. To launch the console from within the +top-level directory in the the source tree, just run ``ipa console`` from a +terminal, like this: + + :: + + $ ./ipa console + +By default, ``in_server`` is False. If you want to start the console in a +server context (so that all the backend plugins are loaded), you can use the +``-e`` option to set the ``in_server`` environment variable, like this: + + :: + + $ ./ipa -e in_server=True console + +You can specify multiple environment variables by including the ``-e`` option +multiple times, like this: + + :: + + $ ./ipa -e in_server=True -e mode=dummy console + +The space after the ``-e`` is optional. This is equivalent to the above command: + + :: + + $ ./ipa -ein_server=True -emode=dummy console + +The ``env`` command will print out the full environment in key=value pairs, +like this: + + :: + + $ ./ipa env + +If you use the ``--server`` option, it will forward the call to the server +over XML-RPC and print out what the environment is on the server, like this: + + :: + + $ ./ipa env --server + +The ``plugins`` command will show details of all the plugin that are loaded, +like this: + + :: + + $ ./ipa plugins + + +---------------- +Learning more... +---------------- + +To learn more about writing plugins, you should: + + 1. Look at some of the built-in plugins, like the frontend plugins in + `ipalib.plugins.f_user` and the backend plugins in + `ipa_server.plugins.b_ldap`. + + 2. Learn about the base classes for frontend plugins in `ipalib.frontend`. + + 3. Learn about the core plugin framework in `ipalib.plugable`. +''' import plugable from backend import Backend, Context @@ -35,11 +640,34 @@ from frontend import Command, Object, Method, Property, Application from ipa_types import Bool, Int, Unicode, Enum from frontend import Param, DefaultFrom -def get_standard_api(): - return plugable.API( +def get_standard_api(mode='dummy'): + """ + Return standard `plugable.API` instance. + + This standard instance allows plugins that subclass from the following + base classes: + + - `frontend.Command` + + - `frontend.Object` + + - `frontend.Method` + + - `frontend.Property` + + - `frontend.Application` + + - `backend.Backend` + + - `backend.Context` + """ + api = plugable.API( Command, Object, Method, Property, Application, Backend, Context, ) + if mode is not None: + api.env.mode = mode + return api -api = get_standard_api() +api = get_standard_api(mode=None) -- cgit From 174af50f6d895572792d5601f19b499c677b4fdf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 7 Nov 2008 02:30:19 -0700 Subject: Fixed typo and made sentance clearer in tutorial --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 4db6a04f..90a76a31 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -368,7 +368,7 @@ Because this is quite useful, we are going to revise our golden rule somewhat: Defining arguments and options for your command ----------------------------------------------- -You can define a command can accept arbitrary arguments and options. +You can define a command that will accept specific arguments and options. For example: >>> from ipalib import Param -- cgit From 9aa14333a46d3a57c1fc9fad6068090eb029070f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 10 Nov 2008 15:53:10 -0700 Subject: Added 'conf_dir' env variable, which is directory containing config files --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 90a76a31..d943a7c7 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -520,7 +520,7 @@ This will typically be called before any plugins are registered. For example: 7 >>> api.bootstrap(in_server=True) # We want to execute, not forward >>> len(api.env) -33 +34 If your plugin requires new environment variables *and* will be included in the freeIPA built-in plugins, you should add the defaults for your variables -- cgit From 2be9f2bba81f844088d0699910698e187ca9d957 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 10 Nov 2008 20:08:22 -0700 Subject: More tutorial work: made introduction more concise; moved note on markup to end; added note about Bazaar --- ipalib/__init__.py | 102 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 38 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index d943a7c7..ecb2d633 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -25,45 +25,32 @@ Package containing core library. Tutorial for Plugin Authors ============================= -This tutorial gives a broad learn-by-doing introduction to writing plugins -for freeIPA v2. As not to overwhelm the reader, it does not cover every -detail, but it does provides enough to get one started and is heavily -cross-referenced with further documentation that (hopefully) fills in the -missing details. +This tutorial will introduce you to writing plugins for freeIPA v2. It does +not cover every detail, but it provides enough to get you started and is +heavily cross-referenced with further documentation that (hopefully) fills +in the missing details. -Where the documentation has left the reader confused, the many built-in -plugins in `ipalib.plugins` and `ipa_server.plugins` provide real-life -examples of how to write good plugins. - -*Note:* - - This tutorial, along with all the Python docstrings in freeIPA v2, - uses the *reStructuredText* markup language. For documentation on - reStructuredText, see: - - http://docutils.sourceforge.net/rst.html - - For documentation on using reStructuredText markup with epydoc, see: - - http://epydoc.sourceforge.net/manual-othermarkup.html +In addition to this tutorial, the many built-in plugins in `ipalib.plugins` +and `ipa_server.plugins` provide real-life examples of how to write good +plugins. ------------------------------------ First steps: A simple command plugin ------------------------------------ -Our first example will create the most basic command plugin possible. A -command plugin simultaneously adds a new command that can be called through +Our first example will create the most basic command plugin possible. This +command will be seen in the list of command plugins, but it wont be capable +of actually doing anything yet. + +A command plugin simultaneously adds a new command that can be called through the command-line ``ipa`` script *and* adds a new XML-RPC method... the two are one in the same, simply invoked in different ways. -All plugins must subclass from `plugable.Plugin`, and furthermore, must -subclass from one of the base classes allowed by the `plugable.API` instance -returned by the `get_standard_api()` function. - -To be a command plugin, your plugin must subclass from `frontend.Command`. -Creating a basic plugin involves two steps, defining the class and then -registering the class: +A freeIPA plugin is a Python class, and when you create a plugin, you register +this class itself (instead of an instance of the class). To be a command +plugin, your plugin must subclass from `frontend.Command` (or from a subclass +thereof). Here is our first example: >>> from ipalib import Command, get_standard_api >>> api = get_standard_api() @@ -72,8 +59,8 @@ registering the class: ... >>> api.register(my_command) # Step 2, register class -Notice that we are registering the ``my_command`` class itself and not an -instance thereof. +Notice that we are registering the ``my_command`` class itself, not an +instance of ``my_command``. Until `plugable.API.finalize()` is called, your plugin class has not been instantiated nor the does the ``Command`` namespace yet exist. For example: @@ -86,8 +73,8 @@ True >>> api.Command.my_command.doc 'My example plugin.' -Notice that your plugin instance in accessed through an attribute named -'my_command', the same name as your plugin class name. +Notice that your plugin instance is accessed through an attribute named +``my_command``, the same name as your plugin class name. ------------------------------ @@ -106,7 +93,7 @@ implement a ``run()`` method, like this: >>> api = get_standard_api() >>> api.register(my_command) >>> api.finalize() ->>> api.Command.my_command() # Call your plugin +>>> api.Command.my_command() # Call your command 'My run() method was called!' When `frontend.Command.__call__()` is called, it first validates any arguments @@ -619,11 +606,11 @@ like this: $ ./ipa plugins ----------------- -Learning more... ----------------- +----------------------------------- +Learning more about freeIPA plugins +----------------------------------- -To learn more about writing plugins, you should: +To learn more about writing freeIPA plugins, you should: 1. Look at some of the built-in plugins, like the frontend plugins in `ipalib.plugins.f_user` and the backend plugins in @@ -632,6 +619,45 @@ To learn more about writing plugins, you should: 2. Learn about the base classes for frontend plugins in `ipalib.frontend`. 3. Learn about the core plugin framework in `ipalib.plugable`. + +Furthermore, the freeIPA plugin architecture was inspired by the Bazaar plugin +architecture. Although the two are different enough that learning how to +write plugins for Bazaar will not particularly help you write plugins for +freeIPA, some might be interested in the documentation on writing plugins for +Bazaar, available here: + + http://bazaar-vcs.org/WritingPlugins + +If nothing else, we just want to give credit where credit is deserved! +However, freeIPA does not use any *code* from Bazaar... it merely borrows a +little inspiration. + + +-------------------------- +A note on docstring markup +-------------------------- + +Lastly, a quick note on markup: All the Python docstrings in freeIPA v2 +(including this tutorial) use the *reStructuredText* markup language. For +information on reStructuredText, see: + + http://docutils.sourceforge.net/rst.html + +For information on using reStructuredText markup with epydoc, see: + + http://epydoc.sourceforge.net/manual-othermarkup.html + + +-------------------------------------------------- +Next steps: get involved with freeIPA development! +-------------------------------------------------- + +The freeIPA team is always interested in feedback and contribution from the +community. To get involved with freeIPA, see the *Contribute* page on +freeIPA.org: + + http://freeipa.org/page/Contribute + ''' import plugable -- cgit From 16b86d559a9c80db3634987226cf8766a3a86cd9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 10 Nov 2008 21:33:15 -0700 Subject: Tutorial: added intro section about Python interactive intepreter --- ipalib/__init__.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 9 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index ecb2d633..86a0c277 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -19,7 +19,7 @@ ''' -Package containing core library. +Package containing the core library. ============================= Tutorial for Plugin Authors @@ -35,6 +35,54 @@ and `ipa_server.plugins` provide real-life examples of how to write good plugins. +---------------------------- +How this tutorial is written +---------------------------- + +The code examples in this tutorial are presented as if entered into a Python +interactive interpreter session. As such, when you create a real plugin in +a source file, a few details will be different (in addition to the fact that +you will never include the ``>>>`` nor ``...`` at the beginning of each line +of code). + +The tutorial examples all have this pattern: + + :: + + >>> from ipalib import Command, get_standard_api + >>> api = get_standard_api() + >>> class my_command(Command): + ... pass + ... + >>> api.register(my_command) + >>> api.finalize() + +We call `get_standard_api()` to get an *example* instance of `plugable.API` +to work with. But a real plugin will use the standard *run-time* instance +of `plugable.API`, which is available at ``ipalib.api``. + +A real plugin will have this pattern: + + :: + + from ipalib import Command, api + + class my_command(Command): + pass + api.register(my_command) + +The differences are that in a real plugin you will use the standard +``ipalib.api`` instance of `plugable.API` and that you will *not* call +`plugable.API.finalize()`. When in doubt, look at some of the built-in +plugins for guidance, like those in `ipalib.plugins`. + +If don't know what the Python *interactive interpreter* is, or are confused +about what this *Python* is in the first place, then you probably should start +with the Python tutorial: + + http://docs.python.org/tutorial/index.html + + ------------------------------------ First steps: A simple command plugin ------------------------------------ @@ -67,7 +115,7 @@ instantiated nor the does the ``Command`` namespace yet exist. For example: >>> hasattr(api, 'Command') False ->>> api.finalize() +>>> api.finalize() # plugable.API.finalize() >>> hasattr(api.Command, 'my_command') True >>> api.Command.my_command.doc @@ -561,7 +609,7 @@ terminal, like this: :: - $ ./ipa console + $ ./ipa console By default, ``in_server`` is False. If you want to start the console in a server context (so that all the backend plugins are loaded), you can use the @@ -569,41 +617,41 @@ server context (so that all the backend plugins are loaded), you can use the :: - $ ./ipa -e in_server=True console + $ ./ipa -e in_server=True console You can specify multiple environment variables by including the ``-e`` option multiple times, like this: :: - $ ./ipa -e in_server=True -e mode=dummy console + $ ./ipa -e in_server=True -e mode=dummy console The space after the ``-e`` is optional. This is equivalent to the above command: :: - $ ./ipa -ein_server=True -emode=dummy console + $ ./ipa -ein_server=True -emode=dummy console The ``env`` command will print out the full environment in key=value pairs, like this: :: - $ ./ipa env + $ ./ipa env If you use the ``--server`` option, it will forward the call to the server over XML-RPC and print out what the environment is on the server, like this: :: - $ ./ipa env --server + $ ./ipa env --server The ``plugins`` command will show details of all the plugin that are loaded, like this: :: - $ ./ipa plugins + $ ./ipa plugins ----------------------------------- -- cgit From 786c965c12dccbf04e1bb2e8e9786decc8163e80 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 10 Nov 2008 21:36:56 -0700 Subject: Tutorial: fixed typo --- ipalib/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 86a0c277..da849e7d 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -76,9 +76,9 @@ The differences are that in a real plugin you will use the standard `plugable.API.finalize()`. When in doubt, look at some of the built-in plugins for guidance, like those in `ipalib.plugins`. -If don't know what the Python *interactive interpreter* is, or are confused -about what this *Python* is in the first place, then you probably should start -with the Python tutorial: +If you don't know what the Python *interactive interpreter* is, or are +confused about what this *Python* is in the first place, then you probably +should start with the Python tutorial: http://docs.python.org/tutorial/index.html -- cgit From 18945135747b15a98b64ddcf92d0847099469208 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 11 Nov 2008 10:24:30 -0700 Subject: Tutorial: improved clarity of 'How this tutorial is written' section --- ipalib/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index da849e7d..71d78dc4 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -42,8 +42,8 @@ How this tutorial is written The code examples in this tutorial are presented as if entered into a Python interactive interpreter session. As such, when you create a real plugin in a source file, a few details will be different (in addition to the fact that -you will never include the ``>>>`` nor ``...`` at the beginning of each line -of code). +you will never include the ``>>>`` nor ``...`` that the interpreter places at +the beginning of each line of code). The tutorial examples all have this pattern: @@ -57,9 +57,9 @@ The tutorial examples all have this pattern: >>> api.register(my_command) >>> api.finalize() -We call `get_standard_api()` to get an *example* instance of `plugable.API` -to work with. But a real plugin will use the standard *run-time* instance -of `plugable.API`, which is available at ``ipalib.api``. +In the tutorial we call `get_standard_api()` to create an *example* instance +of `plugable.API` to work with. But a real plugin will simply use +``ipalib.api``, the standard run-time instance of `plugable.API`. A real plugin will have this pattern: @@ -71,8 +71,7 @@ A real plugin will have this pattern: pass api.register(my_command) -The differences are that in a real plugin you will use the standard -``ipalib.api`` instance of `plugable.API` and that you will *not* call +As seen above, also note that in a real plugin you will *not* call `plugable.API.finalize()`. When in doubt, look at some of the built-in plugins for guidance, like those in `ipalib.plugins`. -- cgit From f3869d7b24f65ca04494ff756e092d7aedd67a5c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 11 Nov 2008 15:24:18 -0700 Subject: Renamed ipalib.get_standard_api() to create_api() --- ipalib/__init__.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 71d78dc4..4ebb7a1e 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -49,15 +49,15 @@ The tutorial examples all have this pattern: :: - >>> from ipalib import Command, get_standard_api - >>> api = get_standard_api() + >>> from ipalib import Command, create_api + >>> api = create_api() >>> class my_command(Command): ... pass ... >>> api.register(my_command) >>> api.finalize() -In the tutorial we call `get_standard_api()` to create an *example* instance +In the tutorial we call `create_api()` to create an *example* instance of `plugable.API` to work with. But a real plugin will simply use ``ipalib.api``, the standard run-time instance of `plugable.API`. @@ -99,8 +99,8 @@ this class itself (instead of an instance of the class). To be a command plugin, your plugin must subclass from `frontend.Command` (or from a subclass thereof). Here is our first example: ->>> from ipalib import Command, get_standard_api ->>> api = get_standard_api() +>>> from ipalib import Command, create_api +>>> api = create_api() >>> class my_command(Command): # Step 1, define class ... """My example plugin.""" ... @@ -137,7 +137,7 @@ implement a ``run()`` method, like this: ... def run(self): ... return 'My run() method was called!' ... ->>> api = get_standard_api() +>>> api = create_api() >>> api.register(my_command) >>> api.finalize() >>> api.Command.my_command() # Call your command @@ -184,7 +184,7 @@ For example, say you have a command plugin like this: If ``my_command`` is loaded in a *client* context, ``forward()`` will be called: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = False # run() will dispatch to forward() >>> api.register(my_command) >>> api.finalize() @@ -194,7 +194,7 @@ called: On the other hand, if ``my_command`` is loaded in a *server* context, ``execute()`` will be called: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = True # run() will dispatch to execute() >>> api.register(my_command) >>> api.finalize() @@ -253,7 +253,7 @@ Here is a simple example: ... """Part of your API.""" ... return 'Stuff got done.' ... ->>> api = get_standard_api() +>>> api = create_api() >>> api.register(my_backend) >>> api.finalize() >>> api.Backend.my_backend.do_stuff() @@ -296,7 +296,7 @@ plugin can also access the ``my_backend`` plugin as simply This next example will tie everything together. First we create our backend plugin: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = True # We want to execute, not forward >>> class my_backend(Backend): ... """My example backend plugin.""" @@ -330,7 +330,7 @@ it never tries to access the non-existent backend plugin at ``self.Backend.my_backend.`` To emphasize this point, here is one last example: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = False # We want to forward, not execute >>> class my_command(Command): ... """My example command plugin.""" @@ -364,7 +364,7 @@ It can be useful to have your ``execute()`` method call other command plugins. Among other things, this allows for meta-commands that conveniently call several other commands in a single operation. For example: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = True # We want to execute, not forward >>> class meta_command(Command): ... """My meta-command plugin.""" @@ -416,7 +416,7 @@ For example: ... def execute(self, programmer, **kw): ... return '%s, go write more %s!' % (programmer, kw['stuff']) ... ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = True >>> api.register(nudge) >>> api.finalize() @@ -528,7 +528,7 @@ When you create a fresh `plugable.API` instance, its ``env`` attribute is likewise a freshly created `config.Env` instance, which will already be populated with certain run-time information. For example: ->>> api = get_standard_api() +>>> api = create_api() >>> list(api.env) ['bin', 'dot_ipa', 'home', 'ipalib', 'mode', 'script', 'site_packages'] @@ -713,7 +713,7 @@ from frontend import Command, Object, Method, Property, Application from ipa_types import Bool, Int, Unicode, Enum from frontend import Param, DefaultFrom -def get_standard_api(mode='dummy'): +def create_api(mode='dummy'): """ Return standard `plugable.API` instance. @@ -743,4 +743,4 @@ def get_standard_api(mode='dummy'): return api -api = get_standard_api(mode=None) +api = create_api(mode=None) -- cgit From 860d391f3e905e20ba3f409c92d98e68450f3137 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 22:16:04 -0700 Subject: Change Param.__repr__() so it returns the exact expression that could create it; added unit test for Param.__repre__() --- ipalib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 4ebb7a1e..59d725f5 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -431,11 +431,11 @@ containing a command's arguments and options, respectively, as you can see: >>> list(api.Command.nudge.args) # Iterates through argument names ['programmer'] >>> api.Command.nudge.args.programmer -Param('programmer', Unicode()) +Param('programmer') >>> list(api.Command.nudge.options) # Iterates through option names ['stuff'] >>> api.Command.nudge.options.stuff -Param('stuff', Unicode()) +Param('stuff', default=u'documentation') >>> api.Command.nudge.options.stuff.default u'documentation' -- cgit From 44171a0bad44a16ab78dabcff2e2e1a84c40ee12 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 01:25:05 -0700 Subject: Tutorial: added section on allowed return values from a command's execute() method --- ipalib/__init__.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 59d725f5..0c744414 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -486,6 +486,48 @@ For the full details on the parameter system, see the `frontend.Command` classes. +--------------------------------------- +Allowed return values from your command +--------------------------------------- + +The return values from your command can be rendered by different user +interfaces (CLI, web-UI); furthermore, a call to your command can be +transparently forwarded over the network (XML-RPC, JSON). As such, the return +values from your command must be usable by the least common denominator. + +Your command should return only simple data types and simple data structures, +the kind that can be represented in an XML-RPC request or in the JSON format. +The return values from your command's ``execute()`` method can include only +the following: + + Simple scalar values: + These can be ``str``, ``unicode``, ``int``, and ``float`` instances, + plus the ``True``, ``False``, and ``None`` constants. + + Simple compound values: + These can be ``dict``, ``list``, and ``tuple`` instances. These + compound values must contain only the simple scalar values above or + other simple compound values. These compound values can also be empty. + The ``list`` and ``tuple`` types are equivalent and can be used + interchangeably. + +Also note that your ``execute()`` method should not contain any ``print`` +statements or otherwise cause any output on ``sys.stdout``. Your command can +(and should) produce log messages by using ``self.log`` (see below). + +To learn more about XML-RPC (XML Remote Procedure Call), see: + + http://docs.python.org/library/xmlrpclib.html + + http://en.wikipedia.org/wiki/XML-RPC + +To learn more about JSON (Java Script Object Notation), see: + + http://docs.python.org/library/json.html + + http://www.json.org/ + + ------------------------ Logging from your plugin ------------------------ -- cgit From 0313bb7ec09071a9bb124ea625c9936921b8dde7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 12:19:18 -0700 Subject: Tutorial: added section on implementing an output_for_cli() method --- ipalib/__init__.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 0c744414..356482dc 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -496,7 +496,7 @@ transparently forwarded over the network (XML-RPC, JSON). As such, the return values from your command must be usable by the least common denominator. Your command should return only simple data types and simple data structures, -the kind that can be represented in an XML-RPC request or in the JSON format. +the kinds that can be represented in an XML-RPC request or in the JSON format. The return values from your command's ``execute()`` method can include only the following: @@ -508,8 +508,8 @@ the following: These can be ``dict``, ``list``, and ``tuple`` instances. These compound values must contain only the simple scalar values above or other simple compound values. These compound values can also be empty. - The ``list`` and ``tuple`` types are equivalent and can be used - interchangeably. + For our purposes here, the ``list`` and ``tuple`` types are equivalent + and can be used interchangeably. Also note that your ``execute()`` method should not contain any ``print`` statements or otherwise cause any output on ``sys.stdout``. Your command can @@ -528,6 +528,107 @@ To learn more about JSON (Java Script Object Notation), see: http://www.json.org/ +----------------------------------------- +How your command should output to the CLI +----------------------------------------- + +As noted above, your command should not print anything while in its +``execute()`` method. So how does your command format its output when +called from the ``ipa`` script? + +After the `cli.CLI.run_cmd()` method calls your command, it will call your +command's ``output_for_cli()`` method (if you have implemented one). + +If you implement an ``output_for_cli()`` method, it must have the following +signature: + + :: + + output_for_cli(textui, result, *args, **options) + + textui + An object implementing methods for outputting to the console. + Currently the `ipalib.cli.textui` plugin is passed, which your method + can also access as ``self.Backend.textui``. However, in case this + changes in the future, your method should use the instance passed to + it in this first argument. + + result + This is the return value from calling your command plugin. Depending + upon how your command is implemented, this is probably the return + value from your ``execute()`` method. + + args + The arguments your command was called with. If your command takes no + arguments, you can omit this. You can also explicitly list your + arguments rather than using the generic ``*args`` form. + + options + The options your command was called with. If your command takes no + options, you can omit this. If your command takes any options, you + must use the ``**options`` form as they will be provided strictly as + keyword arguments. + +For example, say we setup a command like this: + +>>> from ipalib import cli +>>> class show_items(Command): +... takes_options = [Param('reverse', type=Bool(), default=False)] +... +... def execute(self, **options): +... items = [ +... ('apple', 'fruit'), +... ('dog', 'pet'), +... ] +... if options['reverse']: +... items.reverse() +... return items +... +... def output_for_cli(self, textui, result, **options): +... textui.print_name(self.name) +... textui.print_keyval(result) +... format = '%d items' +... if options['reverse']: +... format += ' (in reverse order)' +... textui.print_count(result, format) +... +>>> api = create_api() +>>> api.env.in_server = True # We want to execute, not forward. +>>> api.register(show_items) +>>> api.finalize() +>>> textui = cli.textui() # We'll pass this to output_for_cli() + +Calling it through the ``ipa`` script would basically do the following: + +>>> options = dict(reverse=False) +>>> result = api.Command.show_items(**options) +>>> api.Command.show_items.output_for_cli(textui, result, **options) +----------- +show-items: +----------- + apple = 'fruit' + dog = 'pet' +------- +2 items +------- + +Similarly, calling it with ``reverse=True`` would result in the following: + +>>> options = dict(reverse=True) +>>> result = api.Command.show_items(**options) +>>> api.Command.show_items.output_for_cli(textui, result, **options) +----------- +show-items: +----------- + dog = 'pet' + apple = 'fruit' +-------------------------- +2 items (in reverse order) +-------------------------- + +See the `ipalib.cli.textui` plugin for a description of its methods. + + ------------------------ Logging from your plugin ------------------------ @@ -550,7 +651,7 @@ For example: Some basic knowledge of the Python ``logging`` module might be helpful. See: - http://www.python.org/doc/2.5.2/lib/module-logging.html + http://docs.python.org/library/logging.html The important thing to remember is that your plugin should not configure logging itself, but should instead simply use the ``self.log`` logger. -- cgit From 6d1ec6360cd5a7c2b5ad4a6089a1fe98c585036d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 12:43:10 -0700 Subject: Tutorial: small improvements to section on using output_for_cli() --- ipalib/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 356482dc..52ae6b53 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -528,9 +528,9 @@ To learn more about JSON (Java Script Object Notation), see: http://www.json.org/ ------------------------------------------ -How your command should output to the CLI ------------------------------------------ +--------------------------------------- +How your command should print to stdout +--------------------------------------- As noted above, your command should not print anything while in its ``execute()`` method. So how does your command format its output when @@ -571,7 +571,6 @@ signature: For example, say we setup a command like this: ->>> from ipalib import cli >>> class show_items(Command): ... takes_options = [Param('reverse', type=Bool(), default=False)] ... @@ -596,9 +595,15 @@ For example, say we setup a command like this: >>> api.env.in_server = True # We want to execute, not forward. >>> api.register(show_items) >>> api.finalize() + +Normally `cli.CLI.load_plugins()` will register the `cli.textui` plugin, but for +the sake of our example, we'll just create an instance here: + +>>> from ipalib import cli >>> textui = cli.textui() # We'll pass this to output_for_cli() -Calling it through the ``ipa`` script would basically do the following: +For what we are concerned with in this example, calling your command through +the ``ipa`` script basically will do the following: >>> options = dict(reverse=False) >>> result = api.Command.show_items(**options) -- cgit From 0f1ed3e904ace46411db549551753005363c30f9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 14:27:09 -0700 Subject: Tutorial: command in output_for_cli() example now also takes an argument --- ipalib/__init__.py | 78 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 31 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 52ae6b53..da2e711a 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -572,65 +572,81 @@ signature: For example, say we setup a command like this: >>> class show_items(Command): +... +... takes_args = ['key?'] +... ... takes_options = [Param('reverse', type=Bool(), default=False)] ... -... def execute(self, **options): -... items = [ -... ('apple', 'fruit'), -... ('dog', 'pet'), +... def execute(self, key, **options): +... items = dict( +... fruit='apple', +... pet='dog', +... city='Berlin', +... ) +... if key in items: +... return [(key, items[key])] +... return [ +... (k, items[k]) for k in sorted(items, reverse=options['reverse']) ... ] -... if options['reverse']: -... items.reverse() -... return items -... -... def output_for_cli(self, textui, result, **options): -... textui.print_name(self.name) -... textui.print_keyval(result) -... format = '%d items' -... if options['reverse']: -... format += ' (in reverse order)' -... textui.print_count(result, format) +... +... def output_for_cli(self, textui, result, key, **options): +... if key is not None: +... textui.print_keyval(result) +... else: +... textui.print_name(self.name) +... textui.print_keyval(result) +... format = '%d items' +... if options['reverse']: +... format += ' (in reverse order)' +... textui.print_count(result, format) ... >>> api = create_api() >>> api.env.in_server = True # We want to execute, not forward. >>> api.register(show_items) >>> api.finalize() -Normally `cli.CLI.load_plugins()` will register the `cli.textui` plugin, but for -the sake of our example, we'll just create an instance here: +Normally when you invoke the ``ipa`` script, `cli.CLI.load_plugins()` will +register the `cli.textui` backend plugin, but for the sake of our example, +we just create an instance here: >>> from ipalib import cli >>> textui = cli.textui() # We'll pass this to output_for_cli() -For what we are concerned with in this example, calling your command through -the ``ipa`` script basically will do the following: +Now for what we are concerned with in this example, calling your command +through the ``ipa`` script basically will do the following: ->>> options = dict(reverse=False) ->>> result = api.Command.show_items(**options) ->>> api.Command.show_items.output_for_cli(textui, result, **options) +>>> result = api.Command.show_items() +>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=False) ----------- show-items: ----------- - apple = 'fruit' - dog = 'pet' + city = 'Berlin' + fruit = 'apple' + pet = 'dog' ------- -2 items +3 items ------- Similarly, calling it with ``reverse=True`` would result in the following: ->>> options = dict(reverse=True) ->>> result = api.Command.show_items(**options) ->>> api.Command.show_items.output_for_cli(textui, result, **options) +>>> result = api.Command.show_items(reverse=True) +>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=True) ----------- show-items: ----------- - dog = 'pet' - apple = 'fruit' + pet = 'dog' + fruit = 'apple' + city = 'Berlin' -------------------------- -2 items (in reverse order) +3 items (in reverse order) -------------------------- +Lastly, providing a ``key`` would result in the following: + +>>> result = api.Command.show_items('city') +>>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False) + city = 'Berlin' + See the `ipalib.cli.textui` plugin for a description of its methods. -- cgit From 1abe3abb87dee628003301c307f4c0d06fe0aa0d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 14:35:52 -0700 Subject: Tutorial: another small change to section on using output_for_cli() --- ipalib/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index da2e711a..3e1dba9d 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -584,14 +584,14 @@ For example, say we setup a command like this: ... city='Berlin', ... ) ... if key in items: -... return [(key, items[key])] +... return items[key] ... return [ ... (k, items[k]) for k in sorted(items, reverse=options['reverse']) ... ] ... ... def output_for_cli(self, textui, result, key, **options): ... if key is not None: -... textui.print_keyval(result) +... textui.print_plain('%s = %r' % (key, result)) ... else: ... textui.print_name(self.name) ... textui.print_keyval(result) @@ -645,7 +645,7 @@ Lastly, providing a ``key`` would result in the following: >>> result = api.Command.show_items('city') >>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False) - city = 'Berlin' +city = 'Berlin' See the `ipalib.cli.textui` plugin for a description of its methods. -- cgit From caa98476f0abe72c387ec5809bb77568f39a1c33 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 14:49:48 -0700 Subject: Tutorial: fixed typo in 'First steps: A simple command plugin' section --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 3e1dba9d..5c1e4c3a 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -110,7 +110,7 @@ Notice that we are registering the ``my_command`` class itself, not an instance of ``my_command``. Until `plugable.API.finalize()` is called, your plugin class has not been -instantiated nor the does the ``Command`` namespace yet exist. For example: +instantiated nor does the ``Command`` namespace yet exist. For example: >>> hasattr(api, 'Command') False -- cgit From f8f4058014fda80f776bc177a5fba22009fb5836 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 14:53:55 -0700 Subject: Tutorial: fixed typo in 'How your command should print to stdout' section --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 5c1e4c3a..a04ce7f8 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -607,7 +607,7 @@ For example, say we setup a command like this: Normally when you invoke the ``ipa`` script, `cli.CLI.load_plugins()` will register the `cli.textui` backend plugin, but for the sake of our example, -we just create an instance here: +we will just create an instance here: >>> from ipalib import cli >>> textui = cli.textui() # We'll pass this to output_for_cli() -- cgit From 3433840692d294d8c16bd775cfea225b86b4d9e1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 19:48:01 -0700 Subject: Fixed doctest in tutorial --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index a04ce7f8..53462cff 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -718,7 +718,7 @@ This will typically be called before any plugins are registered. For example: 7 >>> api.bootstrap(in_server=True) # We want to execute, not forward >>> len(api.env) -34 +35 If your plugin requires new environment variables *and* will be included in the freeIPA built-in plugins, you should add the defaults for your variables -- cgit From 36737c2d913716eb99aece5cc1f6a21234abe46a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 21:29:46 -0700 Subject: Added frontend.LocalOrRemote command base class for commands like env --- ipalib/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 53462cff..b9a3c96d 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -873,7 +873,8 @@ freeIPA.org: import plugable from backend import Backend, Context -from frontend import Command, Object, Method, Property, Application +from frontend import Command, LocalOrRemote, Application +from frontend import Object, Method, Property from ipa_types import Bool, Int, Unicode, Enum from frontend import Param, DefaultFrom -- cgit From fd43b39145382b96cd2e0d0da3d5dcbe0d3a4a2a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Dec 2008 23:09:35 -0700 Subject: Moved setting of run-time variables from Env.__init__() to Env._bootstrap() --- ipalib/__init__.py | 53 ++++++++++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index b9a3c96d..e30b7fed 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -685,41 +685,40 @@ is configured. Environment variables --------------------- -Plugins access various environment variables and run-time information through -``self.api.env`` (for convenience, ``self.env`` is equivalent). +Plugins access configuration variables and run-time information through +``self.api.env`` (or for convenience, ``self.env`` is equivalent). This +attribute is a refences to the `ipalib.config.Env` instance created in +`plugable.API.__init__()`. -When you create a fresh `plugable.API` instance, its ``env`` attribute is -likewise a freshly created `config.Env` instance, which will already be -populated with certain run-time information. For example: +After `API.bootstrap()` has been called, the `Env` instance will be populated +with all the environment information used by the built-in plugins. +This will be called before any plugins are registered, so plugin authors can +assume these variables will all exist by the time the module containing their +plugin (or plugins) is imported. For example: >>> api = create_api() ->>> list(api.env) -['bin', 'dot_ipa', 'home', 'ipalib', 'mode', 'script', 'site_packages'] - -Here is a quick overview of the run-time information: - -============= ================================ ======================= -Key Source or example value Description -============= ================================ ======================= -bin /usr/bin Dir. containing script -dot_ipa ~/.ipa User config directory -home os.environ['HOME'] User home dir. -ipalib .../site-packages/ipalib Dir. of ipalib package -mode 'production' or 'unit_test' The mode ipalib is in -script sys.argv[0] Path of script -site_packages /usr/lib/python2.5/site-packages Dir. containing ipalib/ -============= ================================ ======================= - -After `plugable.API.bootstrap()` has been called, the env instance will be -populated with all the environment information used by the built-in plugins. -This will typically be called before any plugins are registered. For example: - >>> len(api.env) -7 +1 >>> api.bootstrap(in_server=True) # We want to execute, not forward >>> len(api.env) 35 +`Env._bootstrap()`, which is called by `API.bootstrap()`, will create several +run-time variables that connot be overriden in configuration files or through +command-line options. Here is an overview of this run-time information: + +============= ============================= ======================= +Key Example value Description +============= ============================= ======================= +bin '/usr/bin' Dir. containing script +dot_ipa '/home/jderose/.ipa' User config directory +home os.environ['HOME'] User home dir. +ipalib '.../site-packages/ipalib' Dir. of ipalib package +mode 'unit_test' The mode ipalib is in +script sys.argv[0] Path of script +site_packages '.../python2.5/site-packages' Dir. containing ipalib/ +============= ============================= ======================= + If your plugin requires new environment variables *and* will be included in the freeIPA built-in plugins, you should add the defaults for your variables in `ipalib.constants.DEFAULT_CONFIG`. Also, you should consider whether your -- cgit From 6fe78a4944f11d430b724103f7d8d49c92af9b63 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 4 Jan 2009 18:39:39 -0700 Subject: Renamed all references to 'ipa_server' to 'ipaserver' --- ipalib/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index e30b7fed..e1ef09c1 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -31,7 +31,7 @@ heavily cross-referenced with further documentation that (hopefully) fills in the missing details. In addition to this tutorial, the many built-in plugins in `ipalib.plugins` -and `ipa_server.plugins` provide real-life examples of how to write good +and `ipaserver.plugins` provide real-life examples of how to write good plugins. @@ -227,12 +227,12 @@ There are two types of plugins: 2. *Backend plugins* - These are only loaded in a *server* context and only need to be installed on the IPA server. The built-in backend - plugins can be found in `ipa_server.plugins`. + plugins can be found in `ipaserver.plugins`. Backend plugins should provide a set of methods that standardize how IPA interacts with some external system or library. For example, all interaction with LDAP is done through the ``ldap`` backend plugin defined in -`ipa_server.plugins.b_ldap`. As a good rule of thumb, anytime you need to +`ipaserver.plugins.b_ldap`. As a good rule of thumb, anytime you need to import some package that is not part of the Python standard library, you should probably interact with that package via a corresponding backend plugin you implement. @@ -824,7 +824,7 @@ To learn more about writing freeIPA plugins, you should: 1. Look at some of the built-in plugins, like the frontend plugins in `ipalib.plugins.f_user` and the backend plugins in - `ipa_server.plugins.b_ldap`. + `ipaserver.plugins.b_ldap`. 2. Learn about the base classes for frontend plugins in `ipalib.frontend`. -- cgit From f3e0900ebc01d8fae8ce4068b0fae8d14c8069bb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 11:32:32 -0700 Subject: New Param: ipalib.__init__ no longer import ipa_types and instead imports appropriate classes from parameters --- ipalib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index e1ef09c1..cf2e5a6a 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -874,8 +874,8 @@ import plugable from backend import Backend, Context from frontend import Command, LocalOrRemote, Application from frontend import Object, Method, Property -from ipa_types import Bool, Int, Unicode, Enum -from frontend import Param, DefaultFrom +from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str + def create_api(mode='dummy'): """ -- cgit From 0327b83899389e38aebde9de4219f64a716e611d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 20:36:17 -0700 Subject: New Param: all docstring examples now pass under doctests --- ipalib/__init__.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index cf2e5a6a..40d3a744 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -405,13 +405,13 @@ Defining arguments and options for your command You can define a command that will accept specific arguments and options. For example: ->>> from ipalib import Param +>>> from ipalib import Str >>> class nudge(Command): ... """Takes one argument, one option""" ... ... takes_args = ['programmer'] ... -... takes_options = [Param('stuff', default=u'documentation')] +... takes_options = [Str('stuff', default=u'documentation')] ... ... def execute(self, programmer, **kw): ... return '%s, go write more %s!' % (programmer, kw['stuff']) @@ -420,9 +420,9 @@ For example: >>> api.env.in_server = True >>> api.register(nudge) >>> api.finalize() ->>> api.Command.nudge('Jason') +>>> api.Command.nudge(u'Jason') u'Jason, go write more documentation!' ->>> api.Command.nudge('Jason', stuff='unit tests') +>>> api.Command.nudge(u'Jason', stuff=u'unit tests') u'Jason, go write more unit tests!' The ``args`` and ``options`` attributes are `plugable.NameSpace` instances @@ -431,11 +431,11 @@ containing a command's arguments and options, respectively, as you can see: >>> list(api.Command.nudge.args) # Iterates through argument names ['programmer'] >>> api.Command.nudge.args.programmer -Param('programmer') +Str('programmer') >>> list(api.Command.nudge.options) # Iterates through option names ['stuff'] >>> api.Command.nudge.options.stuff -Param('stuff', default=u'documentation') +Str('stuff', default=u'documentation') >>> api.Command.nudge.options.stuff.default u'documentation' @@ -451,7 +451,7 @@ NameSpace(<2 members>, sort=False) When calling a command, its positional arguments can also be provided as keyword arguments, and in any order. For example: ->>> api.Command.nudge(stuff='lines of code', programmer='Jason') +>>> api.Command.nudge(stuff=u'lines of code', programmer=u'Jason') u'Jason, go write more lines of code!' When a command plugin is called, the values supplied for its parameters are @@ -465,20 +465,20 @@ here is a quick teaser: ... takes_options = [ ... 'first', ... 'last', -... Param('nick', -... normalize=lambda value: value.lower(), +... Str('nick', +... normalizer=lambda value: value.lower(), ... default_from=lambda first, last: first[0] + last, ... ), -... Param('points', type=Int(), default=0), +... Int('points', default=0), ... ] ... >>> cp = create_player() >>> cp.finalize() ->>> cp.convert(points=" 1000 ") +>>> cp.convert(points=u' 1000 ') {'points': 1000} >>> cp.normalize(nick=u'NickName') {'nick': u'nickname'} ->>> cp.get_default(first='Jason', last='DeRose') +>>> cp.get_default(first=u'Jason', last=u'DeRose') {'nick': u'jderose', 'points': 0} For the full details on the parameter system, see the @@ -575,7 +575,7 @@ For example, say we setup a command like this: ... ... takes_args = ['key?'] ... -... takes_options = [Param('reverse', type=Bool(), default=False)] +... takes_options = [Flag('reverse')] ... ... def execute(self, key, **options): ... items = dict( @@ -643,7 +643,7 @@ show-items: Lastly, providing a ``key`` would result in the following: ->>> result = api.Command.show_items('city') +>>> result = api.Command.show_items(u'city') >>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False) city = 'Berlin' -- cgit From ec86208a9007ec9febca620c777b80b20e9c360d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 22:19:31 -0700 Subject: Updated passwd plugins module to where it can at least be imported --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 40d3a744..e5aa65d6 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -874,7 +874,7 @@ import plugable from backend import Backend, Context from frontend import Command, LocalOrRemote, Application from frontend import Object, Method, Property -from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str +from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password def create_api(mode='dummy'): -- cgit From 364e05def194b80714a5ea2a3e89598db9fb4892 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 18 Jan 2009 15:55:56 -0700 Subject: Added missing enumerable parameters --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index e5aa65d6..8abb9029 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -875,7 +875,7 @@ from backend import Backend, Context from frontend import Command, LocalOrRemote, Application from frontend import Object, Method, Property from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password - +from parameters import BytesEnum, StrEnum def create_api(mode='dummy'): """ -- cgit From e708765d61b73e1f8ccba266d7bc934f6f4c1277 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 16 Jan 2009 09:50:34 -0500 Subject: Include local copy of UUID generator for Python 2.4. Python 2.5+ has a built-in RFC 4122-compliant UUID generator. Include a copy of this file in our library and import it in a way that it will work with Python 2.4. --- ipalib/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'ipalib/__init__.py') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 8abb9029..29344e18 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -877,6 +877,11 @@ from frontend import Object, Method, Property from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password from parameters import BytesEnum, StrEnum +try: + import uuid +except ImportError: + import ipauuid as uuid + def create_api(mode='dummy'): """ Return standard `plugable.API` instance. -- cgit