summaryrefslogtreecommitdiff
path: root/paste/wareweb
diff options
context:
space:
mode:
authorianb <devnull@localhost>2005-05-14 23:12:45 +0000
committerianb <devnull@localhost>2005-05-14 23:12:45 +0000
commit330dac21c0908b871fc73d9a17efefb7bd1e04be (patch)
treea30f8371508bf8ff03d8a7ecfa2e28ba9b0dfd41 /paste/wareweb
parentc5b9dee9b6ce25ff35a1a06858be781d07de294f (diff)
downloadpaste-330dac21c0908b871fc73d9a17efefb7bd1e04be.tar.gz
Added Wareweb, a new framework. Yea for more frameworks\!
Diffstat (limited to 'paste/wareweb')
-rw-r--r--paste/wareweb/__init__.py5
-rw-r--r--paste/wareweb/cgifields.py53
-rw-r--r--paste/wareweb/classinit.py75
-rw-r--r--paste/wareweb/cookie.py28
-rw-r--r--paste/wareweb/dispatch.py91
-rw-r--r--paste/wareweb/docs/wareweb.txt149
-rw-r--r--paste/wareweb/event.py34
-rw-r--r--paste/wareweb/interfaces.py196
-rw-r--r--paste/wareweb/notify.py67
-rw-r--r--paste/wareweb/servlet.py176
-rw-r--r--paste/wareweb/tests/__init__.py1
-rw-r--r--paste/wareweb/tests/test_unpack.py97
-rw-r--r--paste/wareweb/timeinterval.py70
-rw-r--r--paste/wareweb/unpack.py214
14 files changed, 1256 insertions, 0 deletions
diff --git a/paste/wareweb/__init__.py b/paste/wareweb/__init__.py
new file mode 100644
index 0000000..d82d368
--- /dev/null
+++ b/paste/wareweb/__init__.py
@@ -0,0 +1,5 @@
+from servlet import *
+from unpack import *
+from dispatch import *
+from notify import *
+
diff --git a/paste/wareweb/cgifields.py b/paste/wareweb/cgifields.py
new file mode 100644
index 0000000..fc7188f
--- /dev/null
+++ b/paste/wareweb/cgifields.py
@@ -0,0 +1,53 @@
+import cgi
+from UserDict import UserDict
+
+def parse_fields(environ):
+ fs = cgi.FieldStorage(
+ environ['wsgi.input'],
+ environ=environ,
+ keep_blank_values=True,
+ strict_parsing=False)
+ try:
+ keys = fs.keys()
+ except TypeError:
+ # Maybe an XML-RPC request
+ keys = []
+ d = {}
+ for key in keys:
+ value = fs[key]
+ if not isinstance(value, list):
+ if not value.filename:
+ # Turn the MiniFieldStorage into a string:
+ value = value.value
+ else:
+ value = [v.value for v in value]
+ d[key] = value
+ if environ['REQUEST_METHOD'].upper() == 'POST':
+ # Then we must also parse GET variables
+ getfields = cgi.parse_qs(
+ environ.get('QUERY_STRING', ''),
+ keep_blank_values=True,
+ strict_parsing=False)
+ for name, value in getfields.items():
+ if not d.has_key(name):
+ if isinstance(value, list) and len(value) == 1:
+ # parse_qs always returns a list of lists,
+ # while FieldStorage only uses lists for
+ # keys that actually repeat; this fixes that.
+ value = value[0]
+ d[name] = value
+ return d
+
+class Fields(UserDict):
+
+ def __init__(self, field_dict):
+ self.data = field_dict
+
+ def __getattr__(self, attr):
+ return self.data.get(attr)
+
+ def getlist(self, name):
+ v = self.data.get(name, [])
+ if isinstance(v, list):
+ return v
+ return [v]
diff --git a/paste/wareweb/classinit.py b/paste/wareweb/classinit.py
new file mode 100644
index 0000000..137f0d5
--- /dev/null
+++ b/paste/wareweb/classinit.py
@@ -0,0 +1,75 @@
+class classinstancemethod(object):
+ """
+ Acts like a class method when called from a class, like an
+ instance method when called by an instance. The method should
+ take two arguments, 'self' and 'cls'; one of these will be None
+ depending on how the method was called.
+ """
+
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, obj, type=None):
+ return _methodwrapper(self.func, obj=obj, type=type)
+
+class _methodwrapper(object):
+
+ def __init__(self, func, obj, type):
+ self.func = func
+ self.obj = obj
+ self.type = type
+
+ def __call__(self, *args, **kw):
+ assert not kw.has_key('self') and not kw.has_key('cls'), (
+ "You cannot use 'self' or 'cls' arguments to a "
+ "classinstancemethod")
+ return self.func(*((self.obj, self.type) + args), **kw)
+
+ def __repr__(self):
+ if self.obj is None:
+ return ('<bound class method %s.%s>'
+ % (self.type.__name__, self.func.func_name))
+ else:
+ return ('<bound method %s.%s of %r>'
+ % (self.type.__name__, self.func.func_name, self.obj))
+
+
+class ClassInitMeta(type):
+
+ def __new__(meta, class_name, bases, new_attrs):
+ cls = type.__new__(meta, class_name, bases, new_attrs)
+ if (new_attrs.has_key('__classinit__')
+ and not isinstance(cls.__classinit__, staticmethod)):
+ setattr(cls, '__classinit__',
+ staticmethod(cls.__classinit__.im_func))
+ if hasattr(cls, '__classinit__'):
+ cls.__classinit__(cls, new_attrs)
+ return cls
+
+def build_properties(cls, new_attrs):
+ """
+ Given a class and a new set of attributes (as passed in by
+ __classinit__), create or modify properties based on functions
+ with special names ending in __get, __set, and __del.
+ """
+ for name, value in new_attrs.items():
+ if (name.endswith('__get') or name.endswith('__set')
+ or name.endswith('__del')):
+ base = name[:-5]
+ if hasattr(cls, base):
+ old_prop = getattr(cls, base)
+ if not isinstance(old_prop, property):
+ raise ValueError(
+ "Attribute %s is a %s, not a property; function %s is named like a property"
+ % (base, type(old_prop), name))
+ attrs = {'fget': old_prop.fget,
+ 'fset': old_prop.fset,
+ 'fdel': old_prop.fdel,
+ 'doc': old_prop.__doc__}
+ else:
+ attrs = {}
+ attrs['f' + name[-3:]] = value
+ if name.endswith('__get') and value.__doc__:
+ attrs['doc'] = value.__doc__
+ new_prop = property(**attrs)
+ setattr(cls, base, new_prop)
diff --git a/paste/wareweb/cookie.py b/paste/wareweb/cookie.py
new file mode 100644
index 0000000..a1a2625
--- /dev/null
+++ b/paste/wareweb/cookie.py
@@ -0,0 +1,28 @@
+import timeinterval
+
+class Cookie(object):
+
+ def __init__(self, name, value, path, expires=None, secure=False):
+ self.name = name
+ self.value = value
+ self.path = path
+ self.secure = secure
+ if expires == 'ONCLOSE' or not expires:
+ expires = None
+ elif expires == 'NOW' or expires == 'NEVER':
+ expires = time.gmtime(time.time())
+ if expires == 'NEVER':
+ expires = (expires[0] + 10,) + expires[1:]
+ expires = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", expires)
+ else:
+ if isinstance(expires, (str, unicode)) and expires.startswith('+'):
+ interval = timeinterval.time_decode(expires[1:])
+ expires = time.time() + interval
+ if isinstance(expires, (int, long, float)):
+ expires = time.gmtime(expires)
+ if isinstance(expires, (tuple, time.struct_time)):
+ expires = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", expires)
+ self.expires = expires
+
+ def __str__(self):
+ raise NotImplementedError
diff --git a/paste/wareweb/dispatch.py b/paste/wareweb/dispatch.py
new file mode 100644
index 0000000..8cfc6ec
--- /dev/null
+++ b/paste/wareweb/dispatch.py
@@ -0,0 +1,91 @@
+from paste import httpexceptions
+import event
+
+__all__ = ['public', 'ActionDispatch', 'PathDispatch']
+
+def public(func):
+ func.public = True
+ return func
+
+class MethodDispatch(object):
+
+ prefix = None
+
+ def __addtoclass__(self, attr, cls):
+ cls.listeners.append(self.respond_event)
+
+ def respond_event(self, name, servlet, *args, **kw):
+ if name == 'end_awake':
+ return self.find_method(servlet, *args, **kw)
+ return event.Continue
+
+ def get_method(self, servlet, action):
+ try:
+ return action, getattr(servlet, action)
+ except AttributeError:
+ pass
+ if self.prefix:
+ try:
+ return (self.prefix + action,
+ getattr(servlet, self.prefix + action))
+ except AttributeError:
+ pass
+ return None, None
+
+ def valid_method(self, name, method):
+ if getattr(method, 'public', False):
+ return True
+ if self.prefix and name.startswith(self.prefix):
+ return True
+ return False
+
+class ActionDispatch(MethodDispatch):
+
+ prefix = 'action_'
+
+ def __init__(self, action_name='_action_'):
+ self.action_name = action_name
+
+ def find_method(self, servlet, ret_value):
+ possible_actions = []
+ for name, value in servlet.fields.items():
+ if name == self.action_name:
+ possible_actions.append(value)
+ elif name.startswith(self.action_name):
+ possible_actions.append(name[len(self.action_name):])
+ if not possible_actions:
+ return event.Continue
+ if len(possible_actions) > 1:
+ raise httpexceptions.HTTPBadRequest(
+ "More than one action received: %s"
+ % ', '.join(map(repr, possible_actions)))
+ action = possible_actions[0]
+ name, method = self.get_method(servlet, action)
+ if name is None:
+ raise httpexceptions.Forbidden(
+ "Action method not found: %r" % action)
+ if not self.valid_method(name, method):
+ raise httpexceptions.Forbidden(
+ "Method not allowed: %r" % action)
+ return method()
+
+class PathDispatch(MethodDispatch):
+
+ prefix = 'path_'
+
+ def find_method(self, servlet, ret_value):
+ parts = servlet.path_parts
+ if not parts:
+ action == 'index'
+ else:
+ action == parts[0]
+ servlet.path_parts = parts[1:]
+ name, method = self.get_method(servlet, action)
+ if name is None:
+ raise httpexceptions.Forbidden(
+ "Method not found: %r" % action)
+ if not self.valid_method(name, method):
+ raise httpexceptions.Forbidden(
+ "Method not allowed: %r" % action)
+ return method()
+
diff --git a/paste/wareweb/docs/wareweb.txt b/paste/wareweb/docs/wareweb.txt
new file mode 100644
index 0000000..ba554fd
--- /dev/null
+++ b/paste/wareweb/docs/wareweb.txt
@@ -0,0 +1,149 @@
+Wareweb
+=======
+
+.. contents::
+
+Introduction
+------------
+
+Wareweb is a rethinking of the ideas behind `Webware
+<http://webwareforpython.org>`_. Webware is a fairly old framework,
+written back in the days of Python 1.5.2, and some of the API
+decisions behind it now show their age. Also, many of the
+abstractions in Webware didn't turn out to be useful, while some other
+abstractions are missing.
+
+Wareweb tries to keep the basic model of Webware, while simplifying
+many portions and adding some new techniques. It's not backward
+compatible, but it doesn't require major revamping to apply to a
+Webware application.
+
+Wareweb also requires Python 2.4; as long as we're updating the API, I
+want to make it as modern as possible. Decorators in particular are
+widely used in the framework.
+
+Interface
+---------
+
+See ``paste/wareweb/interfaces.py`` for a description of the
+``IServlet`` interface.
+
+Components and Events
+---------------------
+
+There are a couple fairly simple metaprogramming techniques added to
+``Servlet`` to make it easier to add functionality.
+
+Active Attributes
+~~~~~~~~~~~~~~~~~
+
+When you create a class, attributes can be "active". That is, simply
+assigning the attribute can cause changes to the class.
+
+This is implemented by looking for a special method ``__addtoclass__``
+in all new attributes. The dispatchers, for instance, add themselves
+as "event listeners," so that simply by putting a dispatcher in your
+class definition somewhere you'll have changed the functionality of
+the class.
+
+Events
+~~~~~~
+
+Many methods throw events (with ``paste.wareweb.event.raise_event``).
+An event is sent to each listener on the object, and the handler can
+do nothing (returning ``event.Continue``) or can override or respond
+to the event in some way.
+
+Dispatching objects listen for the ``end_awake`` event (called at the
+end of ``awake()``), and may call an extra method on the servlet based
+on some criteria (like ``_awake_`` or ``path_info``).
+
+Templating systems can use an event to write a template. ZPTKit
+overrides ``start_respond`` to write the template.
+
+Unpacking Methods
+~~~~~~~~~~~~~~~~~
+
+On a per-method basis you can use the algorithms in
+``paste.wareweb.unpack`` to unpack your request. E.g., something
+like:
+
+ dispatch = MethodDispatch()
+
+ @public
+ @unpack
+ def edit_item(self, item_id_int_path, name):
+ Item.get(id=item_id_int_path).name = name
+
+This unpacks a request like ``servlet_name/edit_item/1?name=new_name``
+into the function call. Note that ``MethodDispatch`` makes
+``/edit_item`` call the ``edit_item()`` method. ``@public`` declares
+that the method is actually available to the public. ``@unpack`` uses
+the method signature to identify the parts -- ``item_id_int_path``
+ends with ``_path``, and so will be taken from ``path_info``; and has
+``_int`` and so will be coerced into an integer.
+
+Automatic Properties
+~~~~~~~~~~~~~~~~~~~~
+
+Methods that end with ``__get``, ``__set`` and ``__del`` will
+automatically be turned into properties. Subclasses can selectively
+replace just one of these implementations, while leaving the others in
+place.
+
+Differences from Webware
+------------------------
+
+This is a rough summary of the differences from Webware. There's lots
+of little things that are just plain gone; this doesn't enumerate all
+of them.
+
+No Request And Response
+~~~~~~~~~~~~~~~~~~~~~~~
+
+There are no longer request and response objects. There is only the
+servlet. The servlet contains all the useful methods from these
+objects.
+
+From the response, several methods:
+
+* set_cookie
+* set_header
+* add_header
+* write
+* redirect
+
+From the request, several attributes (note: no methods):
+
+* environ
+* config
+* request_method
+* app_url
+* path_info
+* path_parts
+* fields
+
+For more exact details, see ``paste.wareweb.interfaces.IServlet``
+
+Only One Servlet Class
+~~~~~~~~~~~~~~~~~~~~~~
+
+There's one ``Servlet`` for everyone -- no ``Servlet``,
+``HTTPServlet``, and ``Page``. All servlets instances are *not*
+threadsafe, because they contain information about the request and
+response in their instance variables. In practice no Webware servlets
+were threadsafe, nor was there anything to be gained by that.
+
+Also, reusable functionality is not meant to be added with subclasses
+of ``Servlet``. Subclassing is for the end-developer to add
+application-specific functionality; there's other techniques for
+adding reusable functionality.
+
+No _action_ by default
+~~~~~~~~~~~~~~~~~~~~~~
+
+URL dispatching is handled more generically; several dispatchers are
+available, one which implements the ``_action_` parsing of Webware.
+Dispatchers for XML-RPC and JSON-RPC are also possible, and they have
+their own built-in methods of indicating methods. Other forms of URL
+parsing are also implemented.
diff --git a/paste/wareweb/event.py b/paste/wareweb/event.py
new file mode 100644
index 0000000..5376d63
--- /dev/null
+++ b/paste/wareweb/event.py
@@ -0,0 +1,34 @@
+class Continue:
+ """
+ This class is a singleton (never meant to be instantiated) that
+ represents a kind of no-op return from a class.
+ """
+ def __init__(self):
+ assert False, (
+ "Continue cannot be instantiated (use the class object "
+ "itself)")
+
+def wrap_func(func):
+ name = func.__name__
+ def replacement_func(actor, *args, **kw):
+ # @@: It's really more of an "inner_value" than "next_method"
+ kw['next_method'] = func
+ result = raise_event('start_%s' % name, actor, *args, **kw)
+ del kw['next_method']
+ if result is Continue:
+ value = func(actor, *args, **kw)
+ else:
+ return result
+ result = raise_event('end_%s' % name, actor, value, *args, **kw)
+ if result is Continue:
+ return value
+ return result
+ replacement_func.__name__ = name
+ return replacement_func
+
+def raise_event(name, actor, *args, **kw):
+ for listener in actor.listeners:
+ value = listener(name, actor, *args, **kw)
+ if value is not Continue:
+ return value
+ return Continue
diff --git a/paste/wareweb/interfaces.py b/paste/wareweb/interfaces.py
new file mode 100644
index 0000000..9cd39b5
--- /dev/null
+++ b/paste/wareweb/interfaces.py
@@ -0,0 +1,196 @@
+"""
+Descriptions of the main interfaces in Wareweb. These aren't formal
+interfaces, they are just here for documentation purposes.
+"""
+
+class IServlet:
+
+ """
+ Response method sequence
+ ------------------------
+
+ These methods are called roughly in order to produce the response.
+ """
+
+ def __call__(environ, start_response):
+ """
+ Implementation of the WSGI application interface. Instances
+ of IServlet are WSGI applications.
+
+ Calls event ``call`` with ``environ`` and ``start_response``,
+ which may return (status_headers, app_iter) and thus abort the
+ rest of the call.
+ """
+
+ def run():
+ """
+ 'Runs' the request. This typically just calls ``awake()``,
+ ``respond()`` and ``sleep()``. Error handlers could be added
+ here by subclasses.
+
+ Wrapped as event (wrapped methods call ``start_method_name``
+ and ``end_method_name``).
+ """
+
+ def awake(call_setup=True):
+ """
+ Called at beginning of request. SHOULD NOT produce output.
+ If ``call_setup`` is true then ``.setup()`` will be called.
+ ``SitePage`` classes (that is, abstract subclasses of
+ ``Servlet``) should override this method and not ``setup``.
+
+ Wrapped as event.
+ """
+
+ def setup():
+ """
+ Called at beginning of requests. Subclasses do not need to
+ call the superclass implementation. This is where individual
+ (non-abstract) servlets typically do their setup.
+ """
+
+ def respond():
+ """
+ Called after ``.awake()``, this typically produces the body of
+ the response. Some components may intercept this method
+ (e.g., components that want to take over the body of the
+ response, like templating components).
+
+ Wrapped as event.
+ """
+
+ def sleep(call_teardown=True):
+ """
+ Called at the end of a request, to clean up resources. Called
+ regardless of exceptions in ``.awake()`` or ``.respond()``, so
+ implementations should be careful not to assume all resources
+ have been successfully set up. Like ``awake`` this calls
+ ``.teardown()`` if ``call_teardown`` is true; abstract classes
+ should override this method and not ``teardown``.
+
+ Wrapped as event.
+ """
+
+ def teardown():
+ """
+ Resource cleanup at the end of request. Subclasses do not
+ need to call the superclass implementation.
+ """
+
+ """
+ Request Attributes
+ ------------------
+
+ These attributes describe the request
+ """
+
+ environ = """
+ The WSGI environment. Contains typical CGI variables, in addition
+ to any WSGI extensions.
+ """
+
+ config = """
+ The Paste configuration object; a dictionary-like object.
+ """
+
+ app_url = """
+ The URL (usually not fully qualified) of this application. This
+ looks in the environmental variable ``<app_name>.base_url``.
+ The default ``app_name`` is ``"app"``, and this requires a
+ ``urlparser_hook`` in ``__init__.py`` to set accurately, which
+ would look like::
+
+ app_name = 'app'
+ def urlparse_hook(environ):
+ key = '%s.base_url' % app_name
+ if not key in environ:
+ environ[key] = environ['SCRIPT_NAME']
+ """
+
+ path_info = """
+ The value of environ['PATH_INFO'] -- all the URL that comes after
+ this servlet's location.
+ """
+
+ path_parts = """
+ A list of path parts; essentially ``path_info.split('/')``
+ """
+
+ fields = """
+ A dictionary-like object of all the request fields (both GET and
+ POST variables, folded together).
+
+ You can also get variables as attributes (which default to None).
+
+ Also has a ``.getlist(name)`` method, which returns the variable
+ as a list (i.e., if missing the return value is ``[]``; if one
+ string value the return value is ``[value]``)
+ """
+
+ """
+ Response Methods
+ ----------------
+
+ These methods are used to create the response
+ """
+
+ title = """
+ Attribute that returns the title of this page. Default
+ implementation returns the class's name.
+ """
+
+ session = """
+ The session object. This is a dictionary-like object where values
+ are persisted through multiple requests (using a cookie).
+ """
+
+ def set_cookie(cookie_name, value, path='/', expires='ONCLOSE',
+ secure=False):
+ """
+ Creates the named cookie in the response.
+
+ ``expires`` can be:
+ * ``ONCLOSE`` (default: expires when browser is closed)
+ * ``NOW`` (for immediate deletion)
+ * ``NEVER`` (some time far in the future)
+ * A integer value (expiration in seconds)
+ * A time.struct_time object
+ * A string that starts with ``+`` and describes the time, like
+ ``+1w`` (1 week) or ``+1b`` (1 month)
+ """
+
+ def set_header(header_name, header_value):
+ """
+ Sets the named header; overwriting any header that previously
+ existed. The ``Status`` header is specially used to change
+ the response status.
+ """
+
+ def add_header(header_name, header_value):
+ """
+ Adds the named header, appending to any previous header that
+ might have been set.
+ """
+
+ def write(*objs):
+ """
+ Writes the objects. ``None`` is written as the empty string,
+ and unicode objects are encoded with UTF-8.
+ """
+
+ def redirect(url, **query_vars):
+ """
+ Redirects to the given URL. If the URL is relative, then it
+ will be resolved to be absolute with respect to
+ ``self.app_url``.
+
+ The status is 303 by default; a status keyword argument can be
+ passed to override this.
+
+ Other variables are appended to the query string, e.g.::
+
+ self.redirect('edit', id=5)
+
+ Will redirect to ``edit?id=5``
+ """
+
diff --git a/paste/wareweb/notify.py b/paste/wareweb/notify.py
new file mode 100644
index 0000000..c4ced66
--- /dev/null
+++ b/paste/wareweb/notify.py
@@ -0,0 +1,67 @@
+import event
+
+__all__ = ['Notify']
+
+class Notify(object):
+
+ """
+ This allows you to queue messages to be displayed to the user on
+ the next page the user reads. This allows you to give messages in
+ response to POST requests, then redirect to a another page and
+ display that message (e.g, 'event added').
+
+ Usage::
+
+ class SitePage(Servlet):
+ message = Notify()
+
+ class MyPage(SitePage):
+ def setup(self):
+ if something_done:
+ self.message.write('All good!')
+ self.redirect(...)
+ def respond(self):
+ write header...
+ if self.message:
+ self.write('<div class="message">%s</div>\n'
+ % self.message.html)
+ """
+
+ def __addtoclass__(self, attr, cls):
+ cls.listeners.append(self.servlet_event)
+ self.name = attr
+
+ def servlet_event(self, name, servlet, *args, **kw):
+ if name == 'start_awake':
+ setattr(servlet, self.name, Message(servlet))
+ elif name == 'end_sleep':
+ if getattr(servlet, '_notify_written', False):
+ if 'notify.messages' in servlet.session:
+ del servlet.session['notify.messages']
+ return event.Continue
+
+class Message(object):
+
+ def __init__(self, servlet):
+ self.servlet = servlet
+
+ def write(self, message):
+ session = self.servlet.session
+ messages = session.get('notify.messages') or []
+ messages.append(message)
+ session['notify.messages'] = messages
+
+ def __nonzero__(self):
+ return bool(self.servlet.session.get('notify.messages'))
+
+ def html__get(self):
+ messages = self.servlet.session.get('notify.messages') or []
+ self.servlet._notify_written = True
+ return self.peek
+ html = property(html__get)
+
+ def peek__get(self):
+ messages = self.servlet.session.get('notify.messages') or []
+ return '<br>'.join(messages)
+ peek = property(peek__get)
+
diff --git a/paste/wareweb/servlet.py b/paste/wareweb/servlet.py
new file mode 100644
index 0000000..d52152e
--- /dev/null
+++ b/paste/wareweb/servlet.py
@@ -0,0 +1,176 @@
+import re
+import urllib
+from paste import httpexceptions
+import time
+import timeinterval
+import cgifields
+import event
+import classinit
+from UserDict import UserDict
+
+__all__ = ['Servlet']
+
+class Servlet(object):
+
+ app_name = 'app'
+ listeners = []
+
+ __metaclass__ = classinit.ClassInitMeta
+
+ def __classinit__(cls, new_attrs):
+ if not new_attrs.has_key('listeners'):
+ cls.listeners = cls.listeners[:]
+ for attr, value in new_attrs.items():
+ cls.add_attr(attr, value, set=False)
+ classinit.build_properties(cls, new_attrs)
+
+ @classmethod
+ def add_attr(cls, attr, value, set=True):
+ if set:
+ setattr(cls, attr, value)
+ if hasattr(value, '__addtoclass__'):
+ value.__addtoclass__(attr, cls)
+
+ def __call__(self, environ, start_response):
+ value = event.raise_event('call', self, environ, start_response)
+ if value is not event.Continue:
+ status, headers, app_iter = value
+ start_response(status, headers)
+ return app_iter
+ self.environ = environ
+ self.config = self.environ['paste.config']
+ if self.config.get('app_name'):
+ self.app_name = self.config['app_name']
+ self._cached_output = []
+ self.headers_out = {
+ 'Content-type': 'text/html; charset=UTF-8'}
+ self.status = '200 OK'
+ self.cookies_out = {}
+ self.request_method = environ['REQUEST_METHOD'].upper()
+ self.app_url = self.environ.get('%s.base_url' % self.app_name, '')
+ self.path_info = self.environ.get('PATH_INFO', '')
+ if self.path_info:
+ self.path_parts = filter(None, self.path_info[1:].split('/'))
+ else:
+ # Note that you have to look at self.path_info to
+ # distinguish between '' and '/'
+ self.path_parts = []
+ self.fields = cgifields.Fields(cgifields.parse_fields(environ))
+ self.run()
+ headers = []
+ for name, value in self.headers_out.items():
+ if isinstance(value, list):
+ for v in value:
+ headers.append((name, v))
+ else:
+ headers.append((name, value))
+ # @@: cookies
+ start_response(self.status, headers)
+ return self._cached_output
+
+ @event.wrap_func
+ def run(self):
+ try:
+ self.awake()
+ self.respond()
+ finally:
+ self.sleep()
+
+
+ @event.wrap_func
+ def awake(self, call_setup=True):
+ if call_setup:
+ self.setup()
+
+ def setup(self):
+ pass
+
+ @event.wrap_func
+ def respond(self):
+ pass
+
+ @event.wrap_func
+ def sleep(self, call_teardown=True):
+ if call_teardown:
+ self.teardown()
+
+ def teardown(self):
+ pass
+
+ ############################################################
+ ## Request
+ ############################################################
+
+ def session__get(self):
+ if 'paste.session.factory' in self.environ:
+ sess = self.environ['paste.session.factory']()
+ elif 'paste.flup_session_service' in self.environ:
+ sess = self.environ['paste.flup_session_service'].session
+ self.__dict__['session'] = sess
+ return sess
+
+ ############################################################
+ ## Response
+ ############################################################
+
+ def title__get(self):
+ return self.__class__.__name__
+
+ def set_cookie(self, cookie_name, value, path='/',
+ expires='ONCLOSE', secure=False):
+ c = cookie.Cookie(cookie_name, value, path=path,
+ expires=expires, secure=secure)
+ self._cookies_out[cookie_name] = c
+
+ def set_header(self, header_name, header_value):
+ header_name = header_name.lower()
+ if header_name == 'status':
+ self.status = header_value
+ return
+ self.headers_out[header_name] = header_value
+
+ def add_header(self, header_name, header_value):
+ header_name = header_name.lower()
+ if header_name == 'status':
+ self.status = header_value
+ return
+ if self.headers_out.has_key(header_name):
+ if not isinstance(self.headers_out[header_name], list):
+ self.headers_out[header_name] = [self.headers_out[header_name],
+ header_value]
+ else:
+ self.headers_out[header_name].append(header_value)
+ else:
+ self.headers_out[header_name] = header_value
+
+ def write(self, *obj):
+ for v in obj:
+ if v is None:
+ continue
+ elif isinstance(v, str):
+ self._cached_output.append(v)
+ elif isinstance(v, unicode):
+ self._cached_output.append(v.encode('utf-8'))
+ else:
+ self._cached_output.append(unicode(v).encode('utf-8'))
+
+ def redirect(self, url, **query_vars):
+ if not url.startswith('/') and not abs_regex.search(url):
+ url = self.app_url + '/' + url
+ if 'status' in query_vars:
+ status = query_vars.pop('status')
+ if isinstance(status, (str, unicode)):
+ status = int(status.split()[0])
+ else:
+ status = 303
+ if query_vars:
+ if '?' in url:
+ url += '&'
+ else:
+ url += '?'
+ url += urllib.urlencode(query_vars)
+ raise httpexceptions.get_exception(status)(
+ "This resource has been redirected",
+ headers={'Location': url})
+
+abs_regex = re.compile(r'^[a-zA-Z]:')
diff --git a/paste/wareweb/tests/__init__.py b/paste/wareweb/tests/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/paste/wareweb/tests/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/paste/wareweb/tests/test_unpack.py b/paste/wareweb/tests/test_unpack.py
new file mode 100644
index 0000000..560de03
--- /dev/null
+++ b/paste/wareweb/tests/test_unpack.py
@@ -0,0 +1,97 @@
+from paste.wareweb import unpack
+from paste.wareweb import cgifields
+from cStringIO import StringIO
+
+test_funcs = []
+
+def unpack_test(query, *args, **kw):
+ def decorate(func):
+ test_funcs.append((func, query, args, kw))
+ return func
+ return decorate
+
+def test_functions():
+ for a1, a2, a3, a4 in test_funcs:
+ yield function_test, a1, a2, a3, a4
+
+def function_test(func, query, expect_args, expect_kw):
+ spec = unpack.FunctionArgSpec(func)
+ if '?' in query:
+ path_info, query = query.split('?')
+ else:
+ path_info = ''
+ assert not path_info or path_info.startswith('/')
+ path_parts = filter(None, path_info[1:].split('/'))
+ fields = cgifields.parse_fields({
+ 'REQUEST_METHOD': 'GET',
+ 'QUERY_STRING': query,
+ 'wsgi.input': StringIO()})
+ try:
+ args, kw = spec.unpack_args(path_parts, fields)
+ except unpack.HTTPBadRequest, e:
+ print fields
+ print path_parts
+ if not expect_kw and len(expect_args) == 1:
+ print e
+ assert expect_args[0] == str(e)
+ return
+ raise
+ bad_args = []
+ if len(expect_args) > args:
+ for arg in expect_args[len(args):]:
+ bad_args.append('%s: missing' % arg)
+ if len(args) > expect_args:
+ for arg in args[len(expect_args):]:
+ bad_args.append('Bad input positional arg: %s' % arg)
+ for i, (got, expected) in enumerate(zip(args, expect_args)):
+ if got != expected:
+ bad_args.append('Arg %i; got %r != %r'
+ % (i, got, expected))
+ for name, value in kw.iteritems():
+ if name not in expect_kw:
+ bad_args.append('kw %s: not expected' % name)
+ elif expect_kw[name] != value:
+ bad_args.append('kw %s; got %r != %r'
+ % (name, value, expect_kw[name]))
+ for name in expect_kw:
+ if name not in kw:
+ bad_args.append('kw %s; expected, not gotten' % name)
+ if bad_args:
+ print "Expected:"
+ print expect_args
+ print expect_kw
+ print "Got:"
+ print args
+ print kw
+ for line in bad_args:
+ print line
+ assert 0, "Bad arguments"
+
+
+@unpack_test('name=bob&age=5', name='bob', age_int=5)
+@unpack_test('name=joe&age=ten', "Bad variable 'age': invalid literal for int(): ten")
+@unpack_test('age=10', "Variable 'name' required")
+@unpack_test('name=name&bob=bob', "Variable 'bob' not expected")
+@unpack_test('name=name1&name=name2&age=10', "Bad variable 'name': List not expected")
+def t(name, age_int):
+ pass
+
+
+@unpack_test('name1=x&name2=x&name3=x', name1='x', name2='x', name3='x')
+@unpack_test('/test/this/out?stuff', 'test', 'this', 'out', stuff='')
+def t2(*args, **kw):
+ pass
+
+@unpack_test('/this/here?x', 'this', 'here', x='')
+@unpack_test('/this?x', 'this', x='')
+@unpack_test('/?x', 'Not enough parameters on the URL (expected 1 more path segments)')
+@unpack_test('/this/here/bad?x', 'Too many parameters on the URL (expected 1 less path segments)')
+def t3(arg1_path, arg2_path=None, x=None):
+ pass
+
+@unpack_test('a=1', a_list_int=[1])
+@unpack_test('')
+@unpack_test('a=1&a=2', a_list_int=[1, 2])
+@unpack_test('a=b', "Bad variable 'a': invalid literal for int(): b")
+def t4(a_list_int=[]):
+ pass
diff --git a/paste/wareweb/timeinterval.py b/paste/wareweb/timeinterval.py
new file mode 100644
index 0000000..f2d14b8
--- /dev/null
+++ b/paste/wareweb/timeinterval.py
@@ -0,0 +1,70 @@
+"""
+timeinterval
+
+Convert interval strings (in the form of 1w2d, etc) to
+seconds, and back again. Is not exactly about months or
+years (leap years in particular).
+
+Accepts (y)ear, (b)month, (w)eek, (d)ay, (h)our, (m)inute, (s)econd.
+
+Exports only time_encode and time_decode functions.
+"""
+
+import re
+
+second = 1
+minute = second*60
+hour = minute*60
+day = hour*24
+week = day*7
+month = day*30
+year = day*365
+time_values = {
+ 'y': year,
+ 'b': month,
+ 'w': week,
+ 'd': day,
+ 'h': hour,
+ 'm': minute,
+ 's': second,
+ }
+time_ordered = time_values.items()
+time_ordered.sort(lambda a, b: -cmp(a[1], b[1]))
+
+def time_encode(seconds):
+ """Encodes a number of seconds (representing a time interval)
+ into a form like 1h2d3s."""
+ s = ''
+ for char, amount in time_ordered:
+ if seconds >= amount:
+ i, seconds = divmod(seconds, amount)
+ s += '%i%s' % (i, char)
+ return s
+
+time_re = re.compile(r'[0-9]+[a-zA-Z]')
+def time_decode(s):
+ """Decodes a number in the format 1h4d3m (1 hour, 3 days, 3 minutes)
+ into a number of seconds"""
+ time = 0
+ for match in all_matches(s, time_re):
+ char = match.group(0)[-1].lower()
+ if not time_values.has_key(char):
+ # @@: should signal error
+ continue
+ time += int(match.group(0)[:-1]) * time_values[char]
+ return time
+
+def all_matches(source, regex):
+ """
+ Return a list of matches for regex in source
+ """
+ pos = 0
+ end = len(source)
+ rv = []
+ match = regex.search(source, pos)
+ while match:
+ rv.append(match)
+ match = regex.search(source, match.end() )
+ return rv
+
+__all__ = ['time_encode', 'time_decode']
diff --git a/paste/wareweb/unpack.py b/paste/wareweb/unpack.py
new file mode 100644
index 0000000..a2b3217
--- /dev/null
+++ b/paste/wareweb/unpack.py
@@ -0,0 +1,214 @@
+import inspect
+import xmlrpclib
+import traceback
+from paste.httpexceptions import HTTPBadRequest
+json = None
+
+__all__ = ['unpack', 'unpack_xmlrpc', 'unpack_json']
+
+def unpack(func):
+ argspec = FunctionArgSpec(func)
+ def replacement_func(self):
+ args, kw = argspec.unpack_args(self.path_parts, self.fields)
+ return func(self, *args, **kw)
+ replacement_func.__doc__ = func.__doc__
+ replacement_func.__name__ = func.__name__
+ return replacement_func
+
+def unpack_xmlrpc(func):
+ def replacement_func(self):
+ assert self.environ['CONTENT_TYPE'].startswith('text/xml')
+ data = self.environ['wsgi.input'].read()
+ xmlargs, method_name = xmlrpclib.loads(data)
+ if method_name:
+ kw = {'method_name': method_name}
+ else:
+ kw = {}
+ self.set_header('content-type', 'text/xml; charset=UTF-8')
+ try:
+ result = func(self, *xmlargs, **kw)
+ except:
+ body = make_rpc_exception(environ, sys.exc_info())
+ body = xmlrpclib.dumps(
+ xmlrpclib.Fault(1, fault), encoding='utf-8')
+ else:
+ if not isinstance(result, tuple):
+ result = (result,)
+ body = xmlrpclib.dumps(
+ result, methodresponse=True, encoding='utf-8')
+ self.write(body)
+ replacement_func.__doc__ = func.__doc__
+ replacement_func.__name__ = func.__name__
+ return replacement_func
+
+def make_rpc_exception(environ, exc_info):
+ config = environ['paste.config']
+ rpc_exception = config.get('rpc_exception', None)
+ if rpc_exception not in (None, 'occurred', 'exception', 'traceback'):
+ environ['wsgi.errors'].write(
+ "Bad 'rpc_exception' setting: %r\n" % rpc_exception)
+ rpc_exception = None
+ if rpc_exception is None:
+ if config.get('debug'):
+ rpc_exception = 'traceback'
+ else:
+ rpc_exception = 'exception'
+ if rpc_exception == 'occurred':
+ fault = 'unhandled exception'
+ elif rpc_exception == 'exception':
+ fault = str(e)
+ elif rpc_exception == 'traceback':
+ out = StringIO()
+ traceback.print_exception(*exc_info, **{'file': out})
+ fault = out.getvalue()
+ return fault
+
+def unpack_json(func):
+ global json
+ if json is None:
+ import json
+ def replacement_func(self):
+ data = self.environ['wsgi.input'].read()
+ jsonrpc = json.jsonToObj(data)
+ method = jsonrpc['method']
+ params = jsonrpc['params']
+ id = jsonrpc['id']
+ if method:
+ kw = {'method_name': method}
+ else:
+ kw = {}
+ self.set_header('content-type', 'text/plain; charset: UTF-8')
+ try:
+ result = func(self, *params, **kw)
+ except:
+ body = make_rpc_exception(environ, sys.exc_info())
+ response = {
+ 'result': None,
+ 'error': body,
+ 'id': id}
+ else:
+ response = {
+ 'result': result,
+ 'error': None,
+ 'id': id}
+ self.write(json.objToJson(response))
+ replacement_func.__doc__ = func.__doc__
+ replacement_func.__name__ = func.__name__
+ return replacement_func
+
+
+class FunctionArgSpec(object):
+
+ def __init__(self, func):
+ self.funcargs, self.varargs, self.varkw, self.defaults = (
+ inspect.getargspec(func))
+ self.positional = []
+ self.optional_pos = []
+ self.coersions = self.collect_coersions(self.funcargs)
+ while self.funcargs and self.funcargs[0].endswith('_path'):
+ if len(self.defaults or ()) == len(self.funcargs):
+ # This is an optional path segment
+ self.optional_pos.append(self.funcargs.pop(0))
+ self.defaults = self.defaults[1:]
+ else:
+ self.positional.append(self.funcargs.pop(0))
+ self.reqargs = []
+ if not self.defaults:
+ self.reqargs = self.funcargs
+ else:
+ self.reqargs = self.funcargs[:-len(self.defaults)]
+
+ def unpack_args(self, path_parts, fields):
+ args = []
+ kw = {}
+ fields = fields.copy()
+ if len(self.positional) > len(path_parts):
+ raise HTTPBadRequest(
+ "Not enough parameters on the URL (expected %i more "
+ "path segments)" % (len(self.positional)-len(path_parts)))
+ if (not self.varargs
+ and (len(self.positional)+len(self.optional_pos))
+ < len(path_parts)):
+ raise HTTPBadRequest(
+ "Too many parameters on the URL (expected %i less path "
+ "segments)" % (len(path_parts)-len(self.positional)
+ -len(self.optional_pos)))
+ for name, value in fields.iteritems():
+ if not self.varkw and name not in self.coersions:
+ raise HTTPBadRequest(
+ "Variable %r not expected" % name)
+ if name not in self.coersions:
+ kw[name] = value
+ continue
+ orig_name, coercer = self.coersions[name]
+ if coercer:
+ try:
+ value = coercer(value)
+ except (ValueError, TypeError), e:
+ raise HTTPBadRequest(
+ "Bad variable %r: %s" % (name, e))
+ kw[orig_name] = value
+ for arg in self.reqargs:
+ if arg not in kw:
+ raise HTTPBadRequest(
+ "Variable %r required" % arg)
+ return args, kw
+
+ def collect_coersions(self, funcargs):
+ coersions = {}
+ for name in funcargs:
+ coercer = normal
+ orig = name
+ while 1:
+ if name.endswith('_int'):
+ coercer = self.add_coercer(coercer, make_int)
+ name = name[:-4]
+ elif name.endswith('_list'):
+ coercer = self.add_coercer(coercer, make_list)
+ name = name[:-5]
+ elif name.endswith('_float'):
+ coercer = self.add_coercer(coercer, make_float)
+ name = name[:-6]
+ elif name.endswith('_req'):
+ coercer = self.add_coercer(coercer, make_required)
+ name = name[:-4]
+ else:
+ break
+ coersions[name] = (orig, coercer)
+ return coersions
+
+ def add_coercer(self, coercer, new_coercer):
+ if not coercer or coercer is normal:
+ return new_coercer
+ else:
+ def coerce(val):
+ return new_coercer(coercer(val))
+ return coerce
+
+def make_int(v):
+ if isinstance(v, list):
+ return map(int, v)
+ else:
+ return int(v)
+
+def make_float(v):
+ if isinstance(v, list):
+ return map(float, v)
+ else:
+ return float(v)
+
+def make_list(v):
+ if isinstance(v, list):
+ return v
+ else:
+ return [v]
+
+def make_required(s):
+ if s is None:
+ raise TypeError
+ return s
+
+def normal(v):
+ if isinstance(v, list):
+ raise ValueError("List not expected")
+ return v