Commits

Jesper Nøhr committed f50c817

supports re compilations for exlusion, and well, exclusion, documenting the big things

Comments (0)

Files changed (5)

piston/authentication.py

         return False
 
 class HttpBasicAuthentication(object):
+    """
+    Basic HTTP authenticater. Synopsis:
+    
+    Authentication handlers must implement two methods:
+     - `is_authenticated`: Will be called when checking for
+        authentication. Receives a `request` object, please
+        set your `User` object on `request.user`, otherwise
+        return False (or something that evaluates to False.)
+     - `challenge`: In cases where `is_authenticated` returns
+        False, the result of this method will be returned.
+        This will usually be a `HttpResponse` object with
+        some kind of challenge headers and 401 code on it.
+    """
     def __init__(self, auth_func=django_auth, realm='Bitbucket.org API'):
         self.auth_func = auth_func
         self.realm = realm

piston/emitters.py

-import types, decimal, yaml, copy
+import types, decimal, yaml, types, re
 from django.db.models.query import QuerySet
 from django.db.models import Model, permalink
 from django.utils import simplejson
     import StringIO
 
 class Emitter(object):
+    """
+    Super emitter. All other emitters should subclass
+    this one. It has the `construct` method which
+    conveniently returns a serialized `dict`. This is
+    usually the only method you want to use in your
+    emitter. See below for examples.
+    """
     def __init__(self, payload, typemapper):
         self.typemapper = typemapper
         self.data = payload
             raise
     
     def construct(self):
+        """
+        Recursively serialize a lot of types, and
+        in cases where it doesn't recognize the type,
+        it will fall back to Django's `smart_unicode`.
         
+        Returns `dict`.
+        """
         def _any(thing):
-
+            """
+            Dispatch, all types are routed through here.
+            """
             ret = None
             
             if isinstance(thing, (tuple, list, QuerySet)):
                 ret = str(thing)
             elif isinstance(thing, Model):
                 ret = _model(thing)
-            elif isinstance(thing, unicode):
-                ret = thing.encode('utf-8')
+            elif isinstance(thing, types.FunctionType):
+                pass
             else:
-                ret = thing
+                ret = smart_unicode(thing, strings_only=True)
 
             return ret
 
         def _fk(data, field):
+            """
+            Foreign keys.
+            """
             return _any(getattr(data, field.name))
             
         def _m2m(data, field):
+            """
+            Many to many (re-route to `_model`.)
+            """
             return [ _model(m) for m in getattr(data, field.name).iterator() ]
 
         def _model(data):
+            """
+            Models. Will respect the `fields` and/or
+            `exclude` on the handler (see `typemapper`.)
+            """
             ret = { }
             
             if type(data) in self.typemapper.keys():
 
                 v = lambda f: getattr(data, f.attname)
-                want_fields = copy.copy(list(self.typemapper.get(type(data)).fields))
+                get_fields = set(self.typemapper.get(type(data)).fields)
+                exclude_fields = set(self.typemapper.get(type(data)).exclude)
+                
+                if not get_fields:
+                    get_fields = set([ f.attname.replace("_id", "", 1)
+                        for f in data._meta.fields ])
+
+                # sets can be negated.
+                for exclude in exclude_fields:
+                    if isinstance(exclude, basestring):
+                        get_fields.discard(exclude)
+                    elif isinstance(exclude, re._pattern_type):
+                        for field in get_fields.copy():
+                            if exclude.match(field):
+                                get_fields.discard(field)
                 
                 for f in data._meta.local_fields:
                     if f.serialize:
                         if not f.rel:
-                            if f.attname in want_fields:
+                            if f.attname in get_fields:
                                 ret[f.attname] = _any(v(f))
-                                want_fields.remove(f.attname)
+                                get_fields.remove(f.attname)
                         else:
-                            if f.attname[:-3] in want_fields:
+                            if f.attname[:-3] in get_fields:
                                 ret[f.name] = _fk(data, f)
-                                want_fields.remove(f.name)
+                                get_fields.remove(f.name)
 
                 for mf in data._meta.many_to_many:
                     if mf.serialize:
-                        if mf.attname in want_fields:
+                        if mf.attname in get_fields:
                             ret[mf.name] = _m2m(data, mf)
-                            want_fields.remove(mf.name)
+                            get_fields.remove(mf.name)
                             
                 # try to get the remainder of fields
-                for maybe_field in want_fields:
+                for maybe_field in get_fields:
                     maybe = getattr(data, maybe_field, None)
 
                     if maybe:
             return ret
             
         def _list(data):
+            """
+            Lists.
+            """
             return [ _any(v) for v in data ]
             
         def _dict(data):
+            """
+            Dictionaries.
+            """
             return dict([ (k, _any(v)) for k, v in data.iteritems() ])
             
         return _any(self.data)
     
     def render(self):
+        """
+        This super emitter does not implement `render`,
+        this is a job for the specific emitter below.
+        """
         raise NotImplementedError("Please implement render.")
         
     @classmethod
     def get(cls, format):
+        """
+        Gets an emitter, returns the class and a content-type.
+        """
         if format == 'xml':
             return XMLEmitter, 'text/plain; charset=utf-8'
         elif format == 'json':
         return stream.getvalue()
 
 class JSONEmitter(Emitter):
+    """
+    JSON emitter, understands timestamps.
+    """
     # TODO: callback functions
     def render(self):
         return simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder)
     
 class YAMLEmitter(Emitter):
+    """
+    YAML emitter, uses `safe_dump` to omit the
+    specific types when outputting to non-Python.
+    """
     def render(self):
         return yaml.safe_dump(self.construct())
     pass
     
 class ModelForm(forms.ModelForm):
+    """
+    Subclass of `forms.ModelForm` which makes sure
+    that the initial values are present in the form
+    data, so you don't have to send all old values
+    for the form to actually validate. Django does not
+    do this on its own, which is really annoying.
+    """
     def merge_from_initial(self):
         self.data._mutable = True
         filt = lambda v: v not in self.data.keys()

piston/handler.py

 typemapper = { }
 
 class HandlerType(type):
+    """
+    Metaclass that keeps a registry of class -> handler
+    mappings. It uses a global variable, so it is *not*
+    thread-safe, although that's probably not a concern.
+    """
     def __init__(cls, name, bases, dct):
         model = dct.get('model', None)
         
         return super(HandlerType, cls).__init__(name, bases, dct)
 
 class BaseHandler(object):
+    """
+    Basehandler that gives you CRUD for free.
+    You are supposed to subclass this for specific
+    functionality.
+    
+    All CRUD methods (`read`/`update`/`create`/`delete`)
+    receive a request as the first argument from the
+    resource. Use this for checking `request.user`, etc.
+    """
     __metaclass__ = HandlerType
 
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
+    exclude = ( 'id' )
+    fields =  ( )
 
     def flatten_dict(self, dct):
         return dict([ (str(k), dct.get(k)) for k in dct.keys() ])

piston/resource.py

-"""
-Piston resource.
-"""
 from django.http import HttpResponse, Http404, HttpResponseNotAllowed, HttpResponseForbidden
 from emitters import Emitter
 from handler import typemapper
 from utils import coerce_put_post, FormValidationError
 
 class NoAuthentication(object):
+    """
+    Authentication handler that always returns
+    True, so no authentication is needed, nor
+    initiated (`challenge` is missing.)
+    """
     def is_authenticated(self, request):
         return True
 
 class Resource(object):
+    """
+    Resource. Create one for your URL mappings, just
+    like you would with Django. Takes one argument,
+    the handler. The second argument is optional, and
+    is an authentication handler. If not specified,
+    `NoAuthentication` will be used by default.
+    """
     callmap = { 'GET': 'read', 'POST': 'create', 
                 'PUT': 'update', 'DELETE': 'delete' }
 
             return HttpResponseNotAllowed(self.handler.allowed_methods)
 
         meth = getattr(self.handler, Resource.callmap.get(rm), None)
-        format = request.GET.get('format', 'json')
-        
+
         if not meth:        
             raise Http404
 
         except Exception, e:
             result = e
         
-        emitter, ct = Emitter.get(format)
+        emitter, ct = Emitter.get(request.GET.get('format', 'json'))
         srl = emitter(result, typemapper)
         
         return HttpResponse(srl.render(), mimetype=ct)