OLD | NEW |
(Empty) | |
| 1 """CherryPy is a pythonic, object-oriented HTTP framework. |
| 2 |
| 3 |
| 4 CherryPy consists of not one, but four separate API layers. |
| 5 |
| 6 The APPLICATION LAYER is the simplest. CherryPy applications are written as |
| 7 a tree of classes and methods, where each branch in the tree corresponds to |
| 8 a branch in the URL path. Each method is a 'page handler', which receives |
| 9 GET and POST params as keyword arguments, and returns or yields the (HTML) |
| 10 body of the response. The special method name 'index' is used for paths |
| 11 that end in a slash, and the special method name 'default' is used to |
| 12 handle multiple paths via a single handler. This layer also includes: |
| 13 |
| 14 * the 'exposed' attribute (and cherrypy.expose) |
| 15 * cherrypy.quickstart() |
| 16 * _cp_config attributes |
| 17 * cherrypy.tools (including cherrypy.session) |
| 18 * cherrypy.url() |
| 19 |
| 20 The ENVIRONMENT LAYER is used by developers at all levels. It provides |
| 21 information about the current request and response, plus the application |
| 22 and server environment, via a (default) set of top-level objects: |
| 23 |
| 24 * cherrypy.request |
| 25 * cherrypy.response |
| 26 * cherrypy.engine |
| 27 * cherrypy.server |
| 28 * cherrypy.tree |
| 29 * cherrypy.config |
| 30 * cherrypy.thread_data |
| 31 * cherrypy.log |
| 32 * cherrypy.HTTPError, NotFound, and HTTPRedirect |
| 33 * cherrypy.lib |
| 34 |
| 35 The EXTENSION LAYER allows advanced users to construct and share their own |
| 36 plugins. It consists of: |
| 37 |
| 38 * Hook API |
| 39 * Tool API |
| 40 * Toolbox API |
| 41 * Dispatch API |
| 42 * Config Namespace API |
| 43 |
| 44 Finally, there is the CORE LAYER, which uses the core API's to construct |
| 45 the default components which are available at higher layers. You can think |
| 46 of the default components as the 'reference implementation' for CherryPy. |
| 47 Megaframeworks (and advanced users) may replace the default components |
| 48 with customized or extended components. The core API's are: |
| 49 |
| 50 * Application API |
| 51 * Engine API |
| 52 * Request API |
| 53 * Server API |
| 54 * WSGI API |
| 55 |
| 56 These API's are described in the CherryPy specification: |
| 57 http://www.cherrypy.org/wiki/CherryPySpec |
| 58 """ |
| 59 |
| 60 __version__ = "3.2.2" |
| 61 |
| 62 from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode |
| 63 from cherrypy._cpcompat import basestring, unicodestr, set |
| 64 |
| 65 from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect |
| 66 from cherrypy._cperror import NotFound, CherryPyException, TimeoutError |
| 67 |
| 68 from cherrypy import _cpdispatch as dispatch |
| 69 |
| 70 from cherrypy import _cptools |
| 71 tools = _cptools.default_toolbox |
| 72 Tool = _cptools.Tool |
| 73 |
| 74 from cherrypy import _cprequest |
| 75 from cherrypy.lib import httputil as _httputil |
| 76 |
| 77 from cherrypy import _cptree |
| 78 tree = _cptree.Tree() |
| 79 from cherrypy._cptree import Application |
| 80 from cherrypy import _cpwsgi as wsgi |
| 81 |
| 82 from cherrypy import process |
| 83 try: |
| 84 from cherrypy.process import win32 |
| 85 engine = win32.Win32Bus() |
| 86 engine.console_control_handler = win32.ConsoleCtrlHandler(engine) |
| 87 del win32 |
| 88 except ImportError: |
| 89 engine = process.bus |
| 90 |
| 91 |
| 92 # Timeout monitor. We add two channels to the engine |
| 93 # to which cherrypy.Application will publish. |
| 94 engine.listeners['before_request'] = set() |
| 95 engine.listeners['after_request'] = set() |
| 96 |
| 97 class _TimeoutMonitor(process.plugins.Monitor): |
| 98 |
| 99 def __init__(self, bus): |
| 100 self.servings = [] |
| 101 process.plugins.Monitor.__init__(self, bus, self.run) |
| 102 |
| 103 def before_request(self): |
| 104 self.servings.append((serving.request, serving.response)) |
| 105 |
| 106 def after_request(self): |
| 107 try: |
| 108 self.servings.remove((serving.request, serving.response)) |
| 109 except ValueError: |
| 110 pass |
| 111 |
| 112 def run(self): |
| 113 """Check timeout on all responses. (Internal)""" |
| 114 for req, resp in self.servings: |
| 115 resp.check_timeout() |
| 116 engine.timeout_monitor = _TimeoutMonitor(engine) |
| 117 engine.timeout_monitor.subscribe() |
| 118 |
| 119 engine.autoreload = process.plugins.Autoreloader(engine) |
| 120 engine.autoreload.subscribe() |
| 121 |
| 122 engine.thread_manager = process.plugins.ThreadManager(engine) |
| 123 engine.thread_manager.subscribe() |
| 124 |
| 125 engine.signal_handler = process.plugins.SignalHandler(engine) |
| 126 |
| 127 |
| 128 from cherrypy import _cpserver |
| 129 server = _cpserver.Server() |
| 130 server.subscribe() |
| 131 |
| 132 |
| 133 def quickstart(root=None, script_name="", config=None): |
| 134 """Mount the given root, start the builtin server (and engine), then block. |
| 135 |
| 136 root: an instance of a "controller class" (a collection of page handler |
| 137 methods) which represents the root of the application. |
| 138 script_name: a string containing the "mount point" of the application. |
| 139 This should start with a slash, and be the path portion of the URL |
| 140 at which to mount the given root. For example, if root.index() will |
| 141 handle requests to "http://www.example.com:8080/dept/app1/", then |
| 142 the script_name argument would be "/dept/app1". |
| 143 |
| 144 It MUST NOT end in a slash. If the script_name refers to the root |
| 145 of the URI, it MUST be an empty string (not "/"). |
| 146 config: a file or dict containing application config. If this contains |
| 147 a [global] section, those entries will be used in the global |
| 148 (site-wide) config. |
| 149 """ |
| 150 if config: |
| 151 _global_conf_alias.update(config) |
| 152 |
| 153 tree.mount(root, script_name, config) |
| 154 |
| 155 if hasattr(engine, "signal_handler"): |
| 156 engine.signal_handler.subscribe() |
| 157 if hasattr(engine, "console_control_handler"): |
| 158 engine.console_control_handler.subscribe() |
| 159 |
| 160 engine.start() |
| 161 engine.block() |
| 162 |
| 163 |
| 164 from cherrypy._cpcompat import threadlocal as _local |
| 165 |
| 166 class _Serving(_local): |
| 167 """An interface for registering request and response objects. |
| 168 |
| 169 Rather than have a separate "thread local" object for the request and |
| 170 the response, this class works as a single threadlocal container for |
| 171 both objects (and any others which developers wish to define). In this |
| 172 way, we can easily dump those objects when we stop/start a new HTTP |
| 173 conversation, yet still refer to them as module-level globals in a |
| 174 thread-safe way. |
| 175 """ |
| 176 |
| 177 request = _cprequest.Request(_httputil.Host("127.0.0.1", 80), |
| 178 _httputil.Host("127.0.0.1", 1111)) |
| 179 """ |
| 180 The request object for the current thread. In the main thread, |
| 181 and any threads which are not receiving HTTP requests, this is None.""" |
| 182 |
| 183 response = _cprequest.Response() |
| 184 """ |
| 185 The response object for the current thread. In the main thread, |
| 186 and any threads which are not receiving HTTP requests, this is None.""" |
| 187 |
| 188 def load(self, request, response): |
| 189 self.request = request |
| 190 self.response = response |
| 191 |
| 192 def clear(self): |
| 193 """Remove all attributes of self.""" |
| 194 self.__dict__.clear() |
| 195 |
| 196 serving = _Serving() |
| 197 |
| 198 |
| 199 class _ThreadLocalProxy(object): |
| 200 |
| 201 __slots__ = ['__attrname__', '__dict__'] |
| 202 |
| 203 def __init__(self, attrname): |
| 204 self.__attrname__ = attrname |
| 205 |
| 206 def __getattr__(self, name): |
| 207 child = getattr(serving, self.__attrname__) |
| 208 return getattr(child, name) |
| 209 |
| 210 def __setattr__(self, name, value): |
| 211 if name in ("__attrname__", ): |
| 212 object.__setattr__(self, name, value) |
| 213 else: |
| 214 child = getattr(serving, self.__attrname__) |
| 215 setattr(child, name, value) |
| 216 |
| 217 def __delattr__(self, name): |
| 218 child = getattr(serving, self.__attrname__) |
| 219 delattr(child, name) |
| 220 |
| 221 def _get_dict(self): |
| 222 child = getattr(serving, self.__attrname__) |
| 223 d = child.__class__.__dict__.copy() |
| 224 d.update(child.__dict__) |
| 225 return d |
| 226 __dict__ = property(_get_dict) |
| 227 |
| 228 def __getitem__(self, key): |
| 229 child = getattr(serving, self.__attrname__) |
| 230 return child[key] |
| 231 |
| 232 def __setitem__(self, key, value): |
| 233 child = getattr(serving, self.__attrname__) |
| 234 child[key] = value |
| 235 |
| 236 def __delitem__(self, key): |
| 237 child = getattr(serving, self.__attrname__) |
| 238 del child[key] |
| 239 |
| 240 def __contains__(self, key): |
| 241 child = getattr(serving, self.__attrname__) |
| 242 return key in child |
| 243 |
| 244 def __len__(self): |
| 245 child = getattr(serving, self.__attrname__) |
| 246 return len(child) |
| 247 |
| 248 def __nonzero__(self): |
| 249 child = getattr(serving, self.__attrname__) |
| 250 return bool(child) |
| 251 # Python 3 |
| 252 __bool__ = __nonzero__ |
| 253 |
| 254 # Create request and response object (the same objects will be used |
| 255 # throughout the entire life of the webserver, but will redirect |
| 256 # to the "serving" object) |
| 257 request = _ThreadLocalProxy('request') |
| 258 response = _ThreadLocalProxy('response') |
| 259 |
| 260 # Create thread_data object as a thread-specific all-purpose storage |
| 261 class _ThreadData(_local): |
| 262 """A container for thread-specific data.""" |
| 263 thread_data = _ThreadData() |
| 264 |
| 265 |
| 266 # Monkeypatch pydoc to allow help() to go through the threadlocal proxy. |
| 267 # Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve. |
| 268 # The only other way would be to change what is returned from type(request) |
| 269 # and that's not possible in pure Python (you'd have to fake ob_type). |
| 270 def _cherrypy_pydoc_resolve(thing, forceload=0): |
| 271 """Given an object or a path to an object, get the object and its name.""" |
| 272 if isinstance(thing, _ThreadLocalProxy): |
| 273 thing = getattr(serving, thing.__attrname__) |
| 274 return _pydoc._builtin_resolve(thing, forceload) |
| 275 |
| 276 try: |
| 277 import pydoc as _pydoc |
| 278 _pydoc._builtin_resolve = _pydoc.resolve |
| 279 _pydoc.resolve = _cherrypy_pydoc_resolve |
| 280 except ImportError: |
| 281 pass |
| 282 |
| 283 |
| 284 from cherrypy import _cplogging |
| 285 |
| 286 class _GlobalLogManager(_cplogging.LogManager): |
| 287 """A site-wide LogManager; routes to app.log or global log as appropriate. |
| 288 |
| 289 This :class:`LogManager<cherrypy._cplogging.LogManager>` implements |
| 290 cherrypy.log() and cherrypy.log.access(). If either |
| 291 function is called during a request, the message will be sent to the |
| 292 logger for the current Application. If they are called outside of a |
| 293 request, the message will be sent to the site-wide logger. |
| 294 """ |
| 295 |
| 296 def __call__(self, *args, **kwargs): |
| 297 """Log the given message to the app.log or global log as appropriate.""" |
| 298 # Do NOT use try/except here. See http://www.cherrypy.org/ticket/945 |
| 299 if hasattr(request, 'app') and hasattr(request.app, 'log'): |
| 300 log = request.app.log |
| 301 else: |
| 302 log = self |
| 303 return log.error(*args, **kwargs) |
| 304 |
| 305 def access(self): |
| 306 """Log an access message to the app.log or global log as appropriate.""" |
| 307 try: |
| 308 return request.app.log.access() |
| 309 except AttributeError: |
| 310 return _cplogging.LogManager.access(self) |
| 311 |
| 312 |
| 313 log = _GlobalLogManager() |
| 314 # Set a default screen handler on the global log. |
| 315 log.screen = True |
| 316 log.error_file = '' |
| 317 # Using an access file makes CP about 10% slower. Leave off by default. |
| 318 log.access_file = '' |
| 319 |
| 320 def _buslog(msg, level): |
| 321 log.error(msg, 'ENGINE', severity=level) |
| 322 engine.subscribe('log', _buslog) |
| 323 |
| 324 # Helper functions for CP apps # |
| 325 |
| 326 |
| 327 def expose(func=None, alias=None): |
| 328 """Expose the function, optionally providing an alias or set of aliases.""" |
| 329 def expose_(func): |
| 330 func.exposed = True |
| 331 if alias is not None: |
| 332 if isinstance(alias, basestring): |
| 333 parents[alias.replace(".", "_")] = func |
| 334 else: |
| 335 for a in alias: |
| 336 parents[a.replace(".", "_")] = func |
| 337 return func |
| 338 |
| 339 import sys, types |
| 340 if isinstance(func, (types.FunctionType, types.MethodType)): |
| 341 if alias is None: |
| 342 # @expose |
| 343 func.exposed = True |
| 344 return func |
| 345 else: |
| 346 # func = expose(func, alias) |
| 347 parents = sys._getframe(1).f_locals |
| 348 return expose_(func) |
| 349 elif func is None: |
| 350 if alias is None: |
| 351 # @expose() |
| 352 parents = sys._getframe(1).f_locals |
| 353 return expose_ |
| 354 else: |
| 355 # @expose(alias="alias") or |
| 356 # @expose(alias=["alias1", "alias2"]) |
| 357 parents = sys._getframe(1).f_locals |
| 358 return expose_ |
| 359 else: |
| 360 # @expose("alias") or |
| 361 # @expose(["alias1", "alias2"]) |
| 362 parents = sys._getframe(1).f_locals |
| 363 alias = func |
| 364 return expose_ |
| 365 |
| 366 def popargs(*args, **kwargs): |
| 367 """A decorator for _cp_dispatch |
| 368 (cherrypy.dispatch.Dispatcher.dispatch_method_name). |
| 369 |
| 370 Optional keyword argument: handler=(Object or Function) |
| 371 |
| 372 Provides a _cp_dispatch function that pops off path segments into |
| 373 cherrypy.request.params under the names specified. The dispatch |
| 374 is then forwarded on to the next vpath element. |
| 375 |
| 376 Note that any existing (and exposed) member function of the class that |
| 377 popargs is applied to will override that value of the argument. For |
| 378 instance, if you have a method named "list" on the class decorated with |
| 379 popargs, then accessing "/list" will call that function instead of popping |
| 380 it off as the requested parameter. This restriction applies to all |
| 381 _cp_dispatch functions. The only way around this restriction is to create |
| 382 a "blank class" whose only function is to provide _cp_dispatch. |
| 383 |
| 384 If there are path elements after the arguments, or more arguments |
| 385 are requested than are available in the vpath, then the 'handler' |
| 386 keyword argument specifies the next object to handle the parameterized |
| 387 request. If handler is not specified or is None, then self is used. |
| 388 If handler is a function rather than an instance, then that function |
| 389 will be called with the args specified and the return value from that |
| 390 function used as the next object INSTEAD of adding the parameters to |
| 391 cherrypy.request.args. |
| 392 |
| 393 This decorator may be used in one of two ways: |
| 394 |
| 395 As a class decorator: |
| 396 @cherrypy.popargs('year', 'month', 'day') |
| 397 class Blog: |
| 398 def index(self, year=None, month=None, day=None): |
| 399 #Process the parameters here; any url like |
| 400 #/, /2009, /2009/12, or /2009/12/31 |
| 401 #will fill in the appropriate parameters. |
| 402 |
| 403 def create(self): |
| 404 #This link will still be available at /create. Defined functions |
| 405 #take precedence over arguments. |
| 406 |
| 407 Or as a member of a class: |
| 408 class Blog: |
| 409 _cp_dispatch = cherrypy.popargs('year', 'month', 'day') |
| 410 #... |
| 411 |
| 412 The handler argument may be used to mix arguments with built in functions. |
| 413 For instance, the following setup allows different activities at the |
| 414 day, month, and year level: |
| 415 |
| 416 class DayHandler: |
| 417 def index(self, year, month, day): |
| 418 #Do something with this day; probably list entries |
| 419 |
| 420 def delete(self, year, month, day): |
| 421 #Delete all entries for this day |
| 422 |
| 423 @cherrypy.popargs('day', handler=DayHandler()) |
| 424 class MonthHandler: |
| 425 def index(self, year, month): |
| 426 #Do something with this month; probably list entries |
| 427 |
| 428 def delete(self, year, month): |
| 429 #Delete all entries for this month |
| 430 |
| 431 @cherrypy.popargs('month', handler=MonthHandler()) |
| 432 class YearHandler: |
| 433 def index(self, year): |
| 434 #Do something with this year |
| 435 |
| 436 #... |
| 437 |
| 438 @cherrypy.popargs('year', handler=YearHandler()) |
| 439 class Root: |
| 440 def index(self): |
| 441 #... |
| 442 |
| 443 """ |
| 444 |
| 445 #Since keyword arg comes after *args, we have to process it ourselves |
| 446 #for lower versions of python. |
| 447 |
| 448 handler = None |
| 449 handler_call = False |
| 450 for k,v in kwargs.items(): |
| 451 if k == 'handler': |
| 452 handler = v |
| 453 else: |
| 454 raise TypeError( |
| 455 "cherrypy.popargs() got an unexpected keyword argument '{0}'" \ |
| 456 .format(k) |
| 457 ) |
| 458 |
| 459 import inspect |
| 460 |
| 461 if handler is not None \ |
| 462 and (hasattr(handler, '__call__') or inspect.isclass(handler)): |
| 463 handler_call = True |
| 464 |
| 465 def decorated(cls_or_self=None, vpath=None): |
| 466 if inspect.isclass(cls_or_self): |
| 467 #cherrypy.popargs is a class decorator |
| 468 cls = cls_or_self |
| 469 setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated) |
| 470 return cls |
| 471 |
| 472 #We're in the actual function |
| 473 self = cls_or_self |
| 474 parms = {} |
| 475 for arg in args: |
| 476 if not vpath: |
| 477 break |
| 478 parms[arg] = vpath.pop(0) |
| 479 |
| 480 if handler is not None: |
| 481 if handler_call: |
| 482 return handler(**parms) |
| 483 else: |
| 484 request.params.update(parms) |
| 485 return handler |
| 486 |
| 487 request.params.update(parms) |
| 488 |
| 489 #If we are the ultimate handler, then to prevent our _cp_dispatch |
| 490 #from being called again, we will resolve remaining elements through |
| 491 #getattr() directly. |
| 492 if vpath: |
| 493 return getattr(self, vpath.pop(0), None) |
| 494 else: |
| 495 return self |
| 496 |
| 497 return decorated |
| 498 |
| 499 def url(path="", qs="", script_name=None, base=None, relative=None): |
| 500 """Create an absolute URL for the given path. |
| 501 |
| 502 If 'path' starts with a slash ('/'), this will return |
| 503 (base + script_name + path + qs). |
| 504 If it does not start with a slash, this returns |
| 505 (base + script_name [+ request.path_info] + path + qs). |
| 506 |
| 507 If script_name is None, cherrypy.request will be used |
| 508 to find a script_name, if available. |
| 509 |
| 510 If base is None, cherrypy.request.base will be used (if available). |
| 511 Note that you can use cherrypy.tools.proxy to change this. |
| 512 |
| 513 Finally, note that this function can be used to obtain an absolute URL |
| 514 for the current request path (minus the querystring) by passing no args. |
| 515 If you call url(qs=cherrypy.request.query_string), you should get the |
| 516 original browser URL (assuming no internal redirections). |
| 517 |
| 518 If relative is None or not provided, request.app.relative_urls will |
| 519 be used (if available, else False). If False, the output will be an |
| 520 absolute URL (including the scheme, host, vhost, and script_name). |
| 521 If True, the output will instead be a URL that is relative to the |
| 522 current request path, perhaps including '..' atoms. If relative is |
| 523 the string 'server', the output will instead be a URL that is |
| 524 relative to the server root; i.e., it will start with a slash. |
| 525 """ |
| 526 if isinstance(qs, (tuple, list, dict)): |
| 527 qs = _urlencode(qs) |
| 528 if qs: |
| 529 qs = '?' + qs |
| 530 |
| 531 if request.app: |
| 532 if not path.startswith("/"): |
| 533 # Append/remove trailing slash from path_info as needed |
| 534 # (this is to support mistyped URL's without redirecting; |
| 535 # if you want to redirect, use tools.trailing_slash). |
| 536 pi = request.path_info |
| 537 if request.is_index is True: |
| 538 if not pi.endswith('/'): |
| 539 pi = pi + '/' |
| 540 elif request.is_index is False: |
| 541 if pi.endswith('/') and pi != '/': |
| 542 pi = pi[:-1] |
| 543 |
| 544 if path == "": |
| 545 path = pi |
| 546 else: |
| 547 path = _urljoin(pi, path) |
| 548 |
| 549 if script_name is None: |
| 550 script_name = request.script_name |
| 551 if base is None: |
| 552 base = request.base |
| 553 |
| 554 newurl = base + script_name + path + qs |
| 555 else: |
| 556 # No request.app (we're being called outside a request). |
| 557 # We'll have to guess the base from server.* attributes. |
| 558 # This will produce very different results from the above |
| 559 # if you're using vhosts or tools.proxy. |
| 560 if base is None: |
| 561 base = server.base() |
| 562 |
| 563 path = (script_name or "") + path |
| 564 newurl = base + path + qs |
| 565 |
| 566 if './' in newurl: |
| 567 # Normalize the URL by removing ./ and ../ |
| 568 atoms = [] |
| 569 for atom in newurl.split('/'): |
| 570 if atom == '.': |
| 571 pass |
| 572 elif atom == '..': |
| 573 atoms.pop() |
| 574 else: |
| 575 atoms.append(atom) |
| 576 newurl = '/'.join(atoms) |
| 577 |
| 578 # At this point, we should have a fully-qualified absolute URL. |
| 579 |
| 580 if relative is None: |
| 581 relative = getattr(request.app, "relative_urls", False) |
| 582 |
| 583 # See http://www.ietf.org/rfc/rfc2396.txt |
| 584 if relative == 'server': |
| 585 # "A relative reference beginning with a single slash character is |
| 586 # termed an absolute-path reference, as defined by <abs_path>..." |
| 587 # This is also sometimes called "server-relative". |
| 588 newurl = '/' + '/'.join(newurl.split('/', 3)[3:]) |
| 589 elif relative: |
| 590 # "A relative reference that does not begin with a scheme name |
| 591 # or a slash character is termed a relative-path reference." |
| 592 old = url(relative=False).split('/')[:-1] |
| 593 new = newurl.split('/') |
| 594 while old and new: |
| 595 a, b = old[0], new[0] |
| 596 if a != b: |
| 597 break |
| 598 old.pop(0) |
| 599 new.pop(0) |
| 600 new = (['..'] * len(old)) + new |
| 601 newurl = '/'.join(new) |
| 602 |
| 603 return newurl |
| 604 |
| 605 |
| 606 # import _cpconfig last so it can reference other top-level objects |
| 607 from cherrypy import _cpconfig |
| 608 # Use _global_conf_alias so quickstart can use 'config' as an arg |
| 609 # without shadowing cherrypy.config. |
| 610 config = _global_conf_alias = _cpconfig.Config() |
| 611 config.defaults = { |
| 612 'tools.log_tracebacks.on': True, |
| 613 'tools.log_headers.on': True, |
| 614 'tools.trailing_slash.on': True, |
| 615 'tools.encode.on': True |
| 616 } |
| 617 config.namespaces["log"] = lambda k, v: setattr(log, k, v) |
| 618 config.namespaces["checker"] = lambda k, v: setattr(checker, k, v) |
| 619 # Must reset to get our defaults applied. |
| 620 config.reset() |
| 621 |
| 622 from cherrypy import _cpchecker |
| 623 checker = _cpchecker.Checker() |
| 624 engine.subscribe('start', checker) |
OLD | NEW |