Package command_system :: Module framework
[hide private]
[frames] | no frames]

Source Code for Module command_system.framework

  1  # Copyright (C) 2009-2010  Alexander Cherniuk <ts33kr@gmail.com> 
  2  # 
  3  # This program is free software: you can redistribute it and/or modify 
  4  # it under the terms of the GNU General Public License as published by 
  5  # the Free Software Foundation, either version 3 of the License, or 
  6  # (at your option) any later version. 
  7  # 
  8  # This program is distributed in the hope that it will be useful, 
  9  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 11  # GNU General Public License for more details. 
 12  # 
 13  # You should have received a copy of the GNU General Public License 
 14  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 15   
 16  """ 
 17  Provides a tiny framework with simple, yet powerful and extensible 
 18  architecture to implement commands in a streight and flexible, 
 19  declarative way. 
 20  """ 
 21   
 22  import re 
 23  from types import FunctionType 
 24  from inspect import getargspec, getdoc 
 25   
 26  from dispatcher import Host, Container 
 27  from dispatcher import get_command, list_commands 
 28  from mapping import parse_arguments, adapt_arguments 
 29  from errors import DefinitionError, CommandError, NoCommandError 
30 31 -class CommandHost(object):
32 """ 33 Command host is a hub between numerous command processors and 34 command containers. Aimed to participate in a dispatching process in 35 order to provide clean and transparent architecture. 36 37 The AUTOMATIC class variable, which must be defined by a command 38 host, specifies whether the command host should be automatically 39 dispatched and enabled by the dispatcher or not. 40 """ 41 __metaclass__ = Host
42
43 -class CommandContainer(object):
44 """ 45 Command container is an entity which holds defined commands, 46 allowing them to be dispatched and proccessed correctly. Each 47 command container may be bound to a one or more command hosts. 48 49 The AUTOMATIC class variable, which must be defined by a command 50 processor, specifies whether the command processor should be 51 automatically dispatched and enabled by the dispatcher or not. 52 53 Bounding is controlled by the HOSTS class variable, which must be 54 defined by the command container. This variable should contain a 55 sequence of hosts to bound to, as a tuple or list. 56 """ 57 __metaclass__ = Container
58
59 -class CommandProcessor(object):
60 """ 61 Command processor is an immediate command emitter. It does not 62 participate in the dispatching process directly, but must define a 63 host to bound to. 64 65 Bounding is controlled by the COMMAND_HOST variable, which must be 66 defined in the body of the command processor. This variable should 67 be set to a specific command host. 68 """ 69 70 # This defines a command prefix (or an initializer), which should 71 # preceede a a text in order it to be processed as a command. 72 COMMAND_PREFIX = '/' 73
74 - def process_as_command(self, text):
75 """ 76 Try to process text as a command. Returns True if it has been 77 processed as a command and False otherwise. 78 """ 79 prefix = text.startswith(self.COMMAND_PREFIX) 80 length = len(text) > len(self.COMMAND_PREFIX) 81 if not (prefix and length): 82 return False 83 84 body = text[len(self.COMMAND_PREFIX):] 85 body = body.strip() 86 87 parts = body.split(None, 1) 88 name, arguments = parts if len(parts) > 1 else (parts[0], None) 89 90 flag = self.looks_like_command(text, body, name, arguments) 91 if flag is not None: 92 return flag 93 94 self.execute_command(name, arguments) 95 96 return True
97
98 - def execute_command(self, name, arguments):
99 command = self.get_command(name) 100 101 args, opts = parse_arguments(arguments) if arguments else ([], []) 102 args, kwargs = adapt_arguments(command, arguments, args, opts) 103 104 if self.command_preprocessor(command, name, arguments, args, kwargs): 105 return 106 value = command(self, *args, **kwargs) 107 self.command_postprocessor(command, name, arguments, args, kwargs, value)
108
109 - def command_preprocessor(self, command, name, arguments, args, kwargs):
110 """ 111 Redefine this method in the subclass to execute custom code 112 before command gets executed. 113 114 If returns True then command execution will be interrupted and 115 command will not be executed. 116 """ 117 pass
118
119 - def command_postprocessor(self, command, name, arguments, args, kwargs, value):
120 """ 121 Redefine this method in the subclass to execute custom code 122 after command gets executed. 123 """ 124 pass
125
126 - def looks_like_command(self, text, body, name, arguments):
127 """ 128 This hook is being called before any processing, but after it 129 was determined that text looks like a command. 130 131 If returns value other then None - then further processing will 132 be interrupted and that value will be used to return from 133 process_as_command. 134 """ 135 pass
136
137 - def get_command(self, name):
138 command = get_command(self.COMMAND_HOST, name) 139 if not command: 140 raise NoCommandError("Command does not exist", name=name) 141 return command
142
143 - def list_commands(self):
144 commands = list_commands(self.COMMAND_HOST) 145 commands = dict(commands) 146 return sorted(set(commands.itervalues()))
147
148 -class Command(object):
149
150 - def __init__(self, handler, *names, **properties):
151 self.handler = handler 152 self.names = names 153 154 # Automatically set all the properties passed to a constructor 155 # by the command decorator. 156 for key, value in properties.iteritems(): 157 setattr(self, key, value)
158
159 - def __call__(self, *args, **kwargs):
160 try: 161 return self.handler(*args, **kwargs) 162 163 # This allows to use a shortcuted way of raising an exception 164 # inside a handler. That is to raise a CommandError without 165 # command or name attributes set. They will be set to a 166 # corresponding values right here in case if they was not set by 167 # the one who raised an exception. 168 except CommandError, error: 169 if not error.command and not error.name: 170 raise CommandError(error.message, self) 171 raise 172 173 # This one is a little bit too wide, but as Python does not have 174 # anything more constrained - there is no other choice. Take a 175 # look here if command complains about invalid arguments while 176 # they are ok. 177 except TypeError: 178 raise CommandError("Command received invalid arguments", self)
179
180 - def __repr__(self):
181 return "<Command %s>" % ', '.join(self.names)
182
183 - def __cmp__(self, other):
184 return cmp(self.first_name, other.first_name)
185 186 @property
187 - def first_name(self):
188 return self.names[0]
189 190 @property
191 - def native_name(self):
192 return self.handler.__name__
193
194 - def extract_documentation(self):
195 """ 196 Extract handler's documentation which is a doc-string and 197 transform it to a usable format. 198 """ 199 return getdoc(self.handler)
200
201 - def extract_description(self):
202 """ 203 Extract handler's description (which is a first line of the 204 documentation). Try to keep them simple yet meaningful. 205 """ 206 documentation = self.extract_documentation() 207 return documentation.split('\n', 1)[0] if documentation else None
208
209 - def extract_specification(self):
210 """ 211 Extract handler's arguments specification, as it was defined 212 preserving their order. 213 """ 214 names, var_args, var_kwargs, defaults = getargspec(self.handler) 215 216 # Behavior of this code need to be checked. Might yield 217 # incorrect results on some rare occasions. 218 spec_args = names[:-len(defaults) if defaults else len(names)] 219 spec_kwargs = list(zip(names[-len(defaults):], defaults)) if defaults else {} 220 221 # Removing self from arguments specification. Command handler 222 # should receive the processors as a first argument, which 223 # should be self by the canonical means. 224 if spec_args.pop(0) != 'self': 225 raise DefinitionError("First argument must be self", self) 226 227 return spec_args, spec_kwargs, var_args, var_kwargs
228
229 -def command(*names, **properties):
230 """ 231 A decorator for defining commands in a declarative way. Provides 232 facilities for setting command's names and properties. 233 234 Names should contain a set of names (aliases) by which the command 235 can be reached. If no names are given - the the native name (the one 236 extracted from the command handler) will be used. 237 238 If native=True is given (default) and names is non-empty - then the 239 native name of the command will be prepended in addition to the 240 given names. 241 242 If usage=True is given (default) - then command help will be 243 appended with autogenerated usage info, based of the command handler 244 arguments introspection. 245 246 If source=True is given - then the first argument of the command 247 will receive the source arguments, as a raw, unprocessed string. The 248 further mapping of arguments and options will not be affected. 249 250 If raw=True is given - then command considered to be raw and should 251 define positional arguments only. If it defines only one positional 252 argument - this argument will receive all the raw and unprocessed 253 arguments. If the command defines more then one positional argument 254 - then all the arguments except the last one will be processed 255 normally; the last argument will get what is left after the 256 processing as raw and unprocessed string. 257 258 If empty=True is given - this will allow to call a raw command 259 without arguments. 260 261 If extra=True is given - then all the extra arguments passed to a 262 command will be collected into a sequence and given to the last 263 positional argument. 264 265 If overlap=True is given - then all the extra arguments will be 266 mapped as if they were values for the keyword arguments. 267 268 If expand=True is given (default) - then short, one-letter options 269 will be expanded to a verbose ones, based of the comparison of the 270 first letter. If more then one option with the same first letter is 271 given - then only first one will be used in the expansion. 272 """ 273 names = list(names) 274 275 native = properties.get('native', True) 276 277 usage = properties.get('usage', True) 278 source = properties.get('source', False) 279 raw = properties.get('raw', False) 280 empty = properties.get('empty', False) 281 extra = properties.get('extra', False) 282 overlap = properties.get('overlap', False) 283 expand = properties.get('expand', True) 284 285 if empty and not raw: 286 raise DefinitionError("Empty option can be used only with raw commands") 287 288 if extra and overlap: 289 raise DefinitionError("Extra and overlap options can not be used together") 290 291 properties = { 292 'usage': usage, 293 'source': source, 294 'raw': raw, 295 'extra': extra, 296 'overlap': overlap, 297 'empty': empty, 298 'expand': expand 299 } 300 301 def decorator(handler): 302 """ 303 Decorator which receives handler as a first argument and then 304 wraps it in the command which then returns back. 305 """ 306 command = Command(handler, *names, **properties) 307 308 # Extract and inject a native name if either no other names are 309 # specified or native property is enabled, while making 310 # sure it is going to be the first one in the list. 311 if not names or native: 312 names.insert(0, command.native_name) 313 command.names = tuple(names) 314 315 return command
316 317 # Workaround if we are getting called without parameters. Keep in 318 # mind that in that case - first item in the names will be the 319 # handler. 320 if names and isinstance(names[0], FunctionType): 321 return decorator(names.pop(0)) 322 323 return decorator 324
325 -def doc(text):
326 """ 327 This decorator is used to bind a documentation (a help) to a 328 command. 329 """ 330 def decorator(target): 331 if isinstance(target, Command): 332 target.handler.__doc__ = text 333 else: 334 target.__doc__ = text 335 return target
336 337 return decorator 338