From 91b33659b562f26665cebc3c7d8aba6d57e9386c Mon Sep 17 00:00:00 2001 From: spiq Date: Fri, 4 Mar 2011 12:28:20 +0200 Subject: [PATCH 01/13] fix for PUT files --- djangorestframework/content.py | 35 +++++++++++++-- djangorestframework/parsers.py | 81 ++++++++++++++++++++-------------- 2 files changed, 80 insertions(+), 36 deletions(-) diff --git a/djangorestframework/content.py b/djangorestframework/content.py index d612a2ee0..abe2069ed 100644 --- a/djangorestframework/content.py +++ b/djangorestframework/content.py @@ -24,6 +24,27 @@ class StandardContentMixin(ContentMixin): return None return (request.META.get('CONTENT_TYPE', None), request.raw_post_data) +from django.core.files.base import File +class SocketFile(File): + # Only forward access is allowed + def __init__(self, socket, size): + super(SocketFile, self).__init__(socket) + self._size = int(size) + self._pos = 0 + + def read(self, num_bytes=None): + if num_bytes is None: + num_bytes = self._size - self._pos + else: + num_bytes = min(num_bytes, self._size - self._pos) + self._pos += num_bytes + return self.file.read(num_bytes) + + def tell(self): + return self._pos + + def seek(self, position): + pass class OverloadedContentMixin(ContentMixin): """HTTP request content behaviour that also allows arbitrary content to be tunneled in form data.""" @@ -39,7 +60,7 @@ class OverloadedContentMixin(ContentMixin): Note that content_type may be None if it is unset.""" if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None): return None - + content_type = request.META.get('CONTENT_TYPE', None) if (request.method == 'POST' and self.CONTENT_PARAM and @@ -51,5 +72,13 @@ class OverloadedContentMixin(ContentMixin): content_type = request.POST.get(self.CONTENTTYPE_PARAM, None) return (content_type, request.POST[self.CONTENT_PARAM]) - - return (content_type, request.raw_post_data) \ No newline at end of file + elif request.method == 'PUT': + f = SocketFile(request.environ['wsgi.input'], request.META['CONTENT_LENGTH']) + returned = (content_type, f.read()) + return returned + #try: + # f.close() + #except Exception as e: + # print 'exception', e + else: + return (content_type, request.raw_post_data) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 7c686ca81..e5dd7df47 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -1,3 +1,7 @@ +from StringIO import StringIO + +from django.http.multipartparser import MultiPartParser as DjangoMPParser + from djangorestframework.response import ResponseException from djangorestframework import status @@ -6,6 +10,10 @@ try: except ImportError: import simplejson as json +try: + from urlparse import parse_qs +except ImportError: + from cgi import parse_qs class ParserMixin(object): parsers = () @@ -75,50 +83,57 @@ class FormParser(BaseParser): """The default parser for form data. Return a dict containing a single value for each non-reserved parameter. """ - + # TODO: not good, because posted/put lists are flattened !!! media_type = 'application/x-www-form-urlencoded' def parse(self, input): - # The FormParser doesn't parse the input as other parsers would, since Django's already done the - # form parsing for us. We build the content object from the request directly. request = self.resource.request if request.method == 'PUT': - # Fix from piston to force Django to give PUT requests the same - # form processing that POST requests get... - # - # Bug fix: if _load_post_and_files has already been called, for - # example by middleware accessing request.POST, the below code to - # pretend the request is a POST instead of a PUT will be too late - # to make a difference. Also calling _load_post_and_files will result - # in the following exception: - # AttributeError: You cannot set the upload handlers after the upload has been processed. - # The fix is to check for the presence of the _post field which is set - # the first time _load_post_and_files is called (both by wsgi.py and - # modpython.py). If it's set, the request has to be 'reset' to redo - # the query value parsing in POST mode. - if hasattr(request, '_post'): - del request._post - del request._files - - try: - request.method = "POST" - request._load_post_and_files() - request.method = "PUT" - except AttributeError: - request.META['REQUEST_METHOD'] = 'POST' - request._load_post_and_files() - request.META['REQUEST_METHOD'] = 'PUT' + data = parse_qs(input) + # Flattening the parsed query data + for key, val in data.items(): + data[key] = val[0] + + if request.method == 'POST': + # Django has already done the form parsing for us. + data = dict(request.POST.items()) # Strip any parameters that we are treating as reserved - data = {} - for (key, val) in request.POST.items(): - if key not in self.resource.RESERVED_FORM_PARAMS: - data[key] = val - + for key in data: + if key in self.resource.RESERVED_FORM_PARAMS: + data.pop(key) return data # TODO: Allow parsers to specify multiple media_types class MultipartParser(FormParser): media_type = 'multipart/form-data' + def parse(self, input): + request = self.resource.request + + if request.method == 'PUT': + upload_handlers = request._get_upload_handlers() + django_mpp = DjangoMPParser(request.META, StringIO(input), upload_handlers) + data, files = django_mpp.parse() + data = dict(data) + files = dict(files) + + if request.method == 'POST': + # Django has already done the form parsing for us. + data = dict(request.POST) + files = dict(request.FILES) + + # Flattening, then merging the POSTED/PUT data/files + for key, val in dict(data).items(): + data[key] = val[0] + for key, val in dict(files).items(): + files[key] = val[0].read() + data.update(files) + + # Strip any parameters that we are treating as reserved + for key in data: + if key in self.resource.RESERVED_FORM_PARAMS: + data.pop(key) + return data + From 033c0ae797e0bbb39a0687592d5b5f8eceb4d4dd Mon Sep 17 00:00:00 2001 From: spiq Date: Fri, 4 Mar 2011 17:06:44 +0200 Subject: [PATCH 02/13] forms and multipat parsers now flaten data --- djangorestframework/content.py | 8 +---- djangorestframework/parsers.py | 53 ++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/djangorestframework/content.py b/djangorestframework/content.py index abe2069ed..dd40f8f1a 100644 --- a/djangorestframework/content.py +++ b/djangorestframework/content.py @@ -60,7 +60,6 @@ class OverloadedContentMixin(ContentMixin): Note that content_type may be None if it is unset.""" if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None): return None - content_type = request.META.get('CONTENT_TYPE', None) if (request.method == 'POST' and self.CONTENT_PARAM and @@ -74,11 +73,6 @@ class OverloadedContentMixin(ContentMixin): return (content_type, request.POST[self.CONTENT_PARAM]) elif request.method == 'PUT': f = SocketFile(request.environ['wsgi.input'], request.META['CONTENT_LENGTH']) - returned = (content_type, f.read()) - return returned - #try: - # f.close() - #except Exception as e: - # print 'exception', e + return (content_type, f.read()) else: return (content_type, request.raw_post_data) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index e5dd7df47..c334f7292 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -78,12 +78,36 @@ class JSONParser(BaseParser): class XMLParser(BaseParser): media_type = 'application/xml' +class DataFlatener(object): -class FormParser(BaseParser): + def flatten_data(self, data): + """Given a data dictionary ``{: }``, returns a flattened dictionary according to :meth:`FormParser.is_a_list`. + """ + #TODO : document + test + flatdata = dict() + for attr_name, attr_value in data.items(): + if self.is_a_list(attr_name): + if isinstance(attr_value, list): + flatdata[attr_name] = attr_value + else: + flatdata[attr_name] = [attr_value] + else: + if isinstance(attr_value, list): + flatdata[attr_name] = attr_value[0] + else: + flatdata[attr_name] = attr_value + return flatdata + + def is_a_list(self, attr_name): + """ """ + #TODO: document + return False + +class FormParser(BaseParser, DataFlatener): """The default parser for form data. Return a dict containing a single value for each non-reserved parameter. """ - # TODO: not good, because posted/put lists are flattened !!! + # TODO: writing tests for PUT files + normal data media_type = 'application/x-www-form-urlencoded' def parse(self, input): @@ -91,13 +115,12 @@ class FormParser(BaseParser): if request.method == 'PUT': data = parse_qs(input) - # Flattening the parsed query data - for key, val in data.items(): - data[key] = val[0] if request.method == 'POST': # Django has already done the form parsing for us. - data = dict(request.POST.items()) + data = request.POST + + data = self.flatten_data(data) # Strip any parameters that we are treating as reserved for key in data: @@ -106,7 +129,7 @@ class FormParser(BaseParser): return data # TODO: Allow parsers to specify multiple media_types -class MultipartParser(FormParser): +class MultipartParser(BaseParser, DataFlatener): media_type = 'multipart/form-data' def parse(self, input): @@ -116,19 +139,14 @@ class MultipartParser(FormParser): upload_handlers = request._get_upload_handlers() django_mpp = DjangoMPParser(request.META, StringIO(input), upload_handlers) data, files = django_mpp.parse() - data = dict(data) - files = dict(files) - if request.method == 'POST': + elif request.method == 'POST': # Django has already done the form parsing for us. - data = dict(request.POST) - files = dict(request.FILES) + data = request.POST + files = request.FILES - # Flattening, then merging the POSTED/PUT data/files - for key, val in dict(data).items(): - data[key] = val[0] - for key, val in dict(files).items(): - files[key] = val[0].read() + data = self.flatten_data(data) + files = self.flatten_data(files) data.update(files) # Strip any parameters that we are treating as reserved @@ -136,4 +154,3 @@ class MultipartParser(FormParser): if key in self.resource.RESERVED_FORM_PARAMS: data.pop(key) return data - From 2169c85dbb011beb411a5c37ebc8d700b0984ce8 Mon Sep 17 00:00:00 2001 From: spiq Date: Fri, 4 Mar 2011 17:23:18 +0200 Subject: [PATCH 03/13] FomrParser now implements a work around for empty values in a list --- djangorestframework/content.py | 5 ++++- djangorestframework/parsers.py | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/djangorestframework/content.py b/djangorestframework/content.py index dd40f8f1a..fe1a56d93 100644 --- a/djangorestframework/content.py +++ b/djangorestframework/content.py @@ -49,6 +49,9 @@ class SocketFile(File): class OverloadedContentMixin(ContentMixin): """HTTP request content behaviour that also allows arbitrary content to be tunneled in form data.""" + #TODO: test PUT + #TODO: rewrite cleaner + """The name to use for the content override field in the POST form.""" CONTENT_PARAM = '_content' @@ -59,7 +62,7 @@ class OverloadedContentMixin(ContentMixin): """If the request contains content return a tuple of (content_type, content) otherwise return None. Note that content_type may be None if it is unset.""" if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None): - return None + return None # TODO : Breaks, because determine_content should return a tuple. content_type = request.META.get('CONTENT_TYPE', None) if (request.method == 'POST' and self.CONTENT_PARAM and diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index c334f7292..38f3db4b4 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -79,11 +79,11 @@ class XMLParser(BaseParser): media_type = 'application/xml' class DataFlatener(object): +#TODO : document + test def flatten_data(self, data): """Given a data dictionary ``{: }``, returns a flattened dictionary according to :meth:`FormParser.is_a_list`. """ - #TODO : document + test flatdata = dict() for attr_name, attr_value in data.items(): if self.is_a_list(attr_name): @@ -100,27 +100,32 @@ class DataFlatener(object): def is_a_list(self, attr_name): """ """ - #TODO: document return False class FormParser(BaseParser, DataFlatener): """The default parser for form data. Return a dict containing a single value for each non-reserved parameter. """ + # TODO: document flatening # TODO: writing tests for PUT files + normal data + # TODO: document EMPTY workaround media_type = 'application/x-www-form-urlencoded' + EMPTY_VALUE = 'EMPTY' + def parse(self, input): request = self.resource.request if request.method == 'PUT': data = parse_qs(input) - - if request.method == 'POST': + elif request.method == 'POST': # Django has already done the form parsing for us. data = request.POST + # Flatening data and removing EMPTY_VALUEs from the lists data = self.flatten_data(data) + for key in filter(lambda k: self.is_a_list(k), data): + self.remove_empty_val(data[key]) # Strip any parameters that we are treating as reserved for key in data: @@ -128,6 +133,16 @@ class FormParser(BaseParser, DataFlatener): data.pop(key) return data + def remove_empty_val(self, val_list): + """ """ + while(1): # Because there might be several times EMPTY_VALUE in the list + try: + ind = val_list.index(self.EMPTY_VALUE) + except ValueError: + break + else: + val_list.pop(ind) + # TODO: Allow parsers to specify multiple media_types class MultipartParser(BaseParser, DataFlatener): media_type = 'multipart/form-data' @@ -139,12 +154,12 @@ class MultipartParser(BaseParser, DataFlatener): upload_handlers = request._get_upload_handlers() django_mpp = DjangoMPParser(request.META, StringIO(input), upload_handlers) data, files = django_mpp.parse() - elif request.method == 'POST': # Django has already done the form parsing for us. data = request.POST files = request.FILES + # Flatening data, files and combining them data = self.flatten_data(data) files = self.flatten_data(files) data.update(files) From d053cc892ddddd6faeb231c8f8f25a1c31507f42 Mon Sep 17 00:00:00 2001 From: spiq Date: Tue, 8 Mar 2011 17:19:55 +0200 Subject: [PATCH 04/13] corrected a small bug --- djangorestframework/parsers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 38f3db4b4..b75876728 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -128,7 +128,7 @@ class FormParser(BaseParser, DataFlatener): self.remove_empty_val(data[key]) # Strip any parameters that we are treating as reserved - for key in data: + for key in data.keys(): if key in self.resource.RESERVED_FORM_PARAMS: data.pop(key) return data @@ -165,7 +165,7 @@ class MultipartParser(BaseParser, DataFlatener): data.update(files) # Strip any parameters that we are treating as reserved - for key in data: + for key in data.keys(): if key in self.resource.RESERVED_FORM_PARAMS: data.pop(key) return data From 899233bf99bfb07e820e4c636bfabc62cfea981e Mon Sep 17 00:00:00 2001 From: spiq Date: Wed, 9 Mar 2011 14:07:06 +0200 Subject: [PATCH 05/13] renamings + corrected a bug --- djangorestframework/parsers.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index b75876728..b7b73408c 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -82,23 +82,23 @@ class DataFlatener(object): #TODO : document + test def flatten_data(self, data): - """Given a data dictionary ``{: }``, returns a flattened dictionary according to :meth:`FormParser.is_a_list`. + """Given a data dictionary ``{: }``, returns a flattened dictionary according to :meth:`FormParser.is_a_list`. """ flatdata = dict() - for attr_name, attr_value in data.items(): - if self.is_a_list(attr_name): + for key, attr_value in data.items(): + if self.is_a_list(key): if isinstance(attr_value, list): - flatdata[attr_name] = attr_value + flatdata[key] = attr_value else: - flatdata[attr_name] = [attr_value] + flatdata[key] = [attr_value] else: if isinstance(attr_value, list): - flatdata[attr_name] = attr_value[0] + flatdata[key] = attr_value[0] else: - flatdata[attr_name] = attr_value + flatdata[key] = attr_value return flatdata - def is_a_list(self, attr_name): + def is_a_list(self, key): """ """ return False @@ -120,7 +120,7 @@ class FormParser(BaseParser, DataFlatener): data = parse_qs(input) elif request.method == 'POST': # Django has already done the form parsing for us. - data = request.POST + data = dict(request.POST.iterlists()) # Flatening data and removing EMPTY_VALUEs from the lists data = self.flatten_data(data) From 26e10d0e3fadf1ff87da1650e30c1de34392b0d4 Mon Sep 17 00:00:00 2001 From: sebpiq Date: Thu, 10 Mar 2011 16:03:46 +0200 Subject: [PATCH 06/13] removed useless stuff, request.POST and FILES not used + some doc --- djangorestframework/content.py | 35 ++++--------------------------- djangorestframework/parsers.py | 19 ++++------------- djangorestframework/utils.py | 38 ---------------------------------- 3 files changed, 8 insertions(+), 84 deletions(-) diff --git a/djangorestframework/content.py b/djangorestframework/content.py index fe1a56d93..96050029d 100644 --- a/djangorestframework/content.py +++ b/djangorestframework/content.py @@ -24,42 +24,18 @@ class StandardContentMixin(ContentMixin): return None return (request.META.get('CONTENT_TYPE', None), request.raw_post_data) -from django.core.files.base import File -class SocketFile(File): - # Only forward access is allowed - def __init__(self, socket, size): - super(SocketFile, self).__init__(socket) - self._size = int(size) - self._pos = 0 - - def read(self, num_bytes=None): - if num_bytes is None: - num_bytes = self._size - self._pos - else: - num_bytes = min(num_bytes, self._size - self._pos) - self._pos += num_bytes - return self.file.read(num_bytes) - - def tell(self): - return self._pos - - def seek(self, position): - pass class OverloadedContentMixin(ContentMixin): """HTTP request content behaviour that also allows arbitrary content to be tunneled in form data.""" - - #TODO: test PUT - #TODO: rewrite cleaner - """The name to use for the content override field in the POST form.""" + """The name to use for the content override field in the POST form. Set this to *None* to desactivate content overloading.""" CONTENT_PARAM = '_content' - """The name to use for the content-type override field in the POST form.""" + """The name to use for the content-type override field in the POST form. Taken into account only if content overloading is activated.""" CONTENTTYPE_PARAM = '_contenttype' def determine_content(self, request): - """If the request contains content return a tuple of (content_type, content) otherwise return None. + """If the request contains content, returns a tuple of (content_type, content) otherwise returns None. Note that content_type may be None if it is unset.""" if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None): return None # TODO : Breaks, because determine_content should return a tuple. @@ -68,14 +44,11 @@ class OverloadedContentMixin(ContentMixin): if (request.method == 'POST' and self.CONTENT_PARAM and request.POST.get(self.CONTENT_PARAM, None) is not None): - # Set content type if form contains a none empty FORM_PARAM_CONTENTTYPE field + # Set content type if form contains a non-empty CONTENTTYPE_PARAM field content_type = None if self.CONTENTTYPE_PARAM and request.POST.get(self.CONTENTTYPE_PARAM, None): content_type = request.POST.get(self.CONTENTTYPE_PARAM, None) return (content_type, request.POST[self.CONTENT_PARAM]) - elif request.method == 'PUT': - f = SocketFile(request.environ['wsgi.input'], request.META['CONTENT_LENGTH']) - return (content_type, f.read()) else: return (content_type, request.raw_post_data) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index b7b73408c..4659f24bf 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -114,13 +114,7 @@ class FormParser(BaseParser, DataFlatener): EMPTY_VALUE = 'EMPTY' def parse(self, input): - request = self.resource.request - - if request.method == 'PUT': - data = parse_qs(input) - elif request.method == 'POST': - # Django has already done the form parsing for us. - data = dict(request.POST.iterlists()) + data = parse_qs(input) # Flatening data and removing EMPTY_VALUEs from the lists data = self.flatten_data(data) @@ -150,14 +144,9 @@ class MultipartParser(BaseParser, DataFlatener): def parse(self, input): request = self.resource.request - if request.method == 'PUT': - upload_handlers = request._get_upload_handlers() - django_mpp = DjangoMPParser(request.META, StringIO(input), upload_handlers) - data, files = django_mpp.parse() - elif request.method == 'POST': - # Django has already done the form parsing for us. - data = request.POST - files = request.FILES + upload_handlers = request._get_upload_handlers() + django_mpp = DjangoMPParser(request.META, StringIO(input), upload_handlers) + data, files = django_mpp.parse() # Flatening data, files and combining them data = self.flatten_data(data) diff --git a/djangorestframework/utils.py b/djangorestframework/utils.py index 266d1ca6e..d45e5acf8 100644 --- a/djangorestframework/utils.py +++ b/djangorestframework/utils.py @@ -34,44 +34,6 @@ def url_resolves(url): return False return True -# From piston -def coerce_put_post(request): - """ - Django doesn't particularly understand REST. - In case we send data over PUT, Django won't - actually look at the data and load it. We need - to twist its arm here. - - The try/except abominiation here is due to a bug - in mod_python. This should fix it. - """ - if request.method != 'PUT': - return - - # Bug fix: if _load_post_and_files has already been called, for - # example by middleware accessing request.POST, the below code to - # pretend the request is a POST instead of a PUT will be too late - # to make a difference. Also calling _load_post_and_files will result - # in the following exception: - # AttributeError: You cannot set the upload handlers after the upload has been processed. - # The fix is to check for the presence of the _post field which is set - # the first time _load_post_and_files is called (both by wsgi.py and - # modpython.py). If it's set, the request has to be 'reset' to redo - # the query value parsing in POST mode. - if hasattr(request, '_post'): - del request._post - del request._files - - try: - request.method = "POST" - request._load_post_and_files() - request.method = "PUT" - except AttributeError: - request.META['REQUEST_METHOD'] = 'POST' - request._load_post_and_files() - request.META['REQUEST_METHOD'] = 'PUT' - - request.PUT = request.POST # From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml #class object_dict(dict): From 2d8e4e056b1b6f822d02d614955ee1f220e2fdda Mon Sep 17 00:00:00 2001 From: sebpiq Date: Thu, 10 Mar 2011 16:49:11 +0200 Subject: [PATCH 07/13] documentation --- djangorestframework/content.py | 7 +++++-- djangorestframework/parsers.py | 28 +++++++++++++++++++--------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/djangorestframework/content.py b/djangorestframework/content.py index 96050029d..48f327b6f 100644 --- a/djangorestframework/content.py +++ b/djangorestframework/content.py @@ -28,10 +28,12 @@ class StandardContentMixin(ContentMixin): class OverloadedContentMixin(ContentMixin): """HTTP request content behaviour that also allows arbitrary content to be tunneled in form data.""" - """The name to use for the content override field in the POST form. Set this to *None* to desactivate content overloading.""" + """The name to use for the content override field in the POST form. + Set this to *None* to desactivate content overloading.""" CONTENT_PARAM = '_content' - """The name to use for the content-type override field in the POST form. Taken into account only if content overloading is activated.""" + """The name to use for the content-type override field in the POST form. + Taken into account only if content overloading is activated.""" CONTENTTYPE_PARAM = '_contenttype' def determine_content(self, request): @@ -48,6 +50,7 @@ class OverloadedContentMixin(ContentMixin): content_type = None if self.CONTENTTYPE_PARAM and request.POST.get(self.CONTENTTYPE_PARAM, None): content_type = request.POST.get(self.CONTENTTYPE_PARAM, None) + request.META['CONTENT_TYPE'] = content_type return (content_type, request.POST[self.CONTENT_PARAM]) else: diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 4659f24bf..081fa6897 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -79,11 +79,11 @@ class XMLParser(BaseParser): media_type = 'application/xml' class DataFlatener(object): -#TODO : document + test + """Utility object for flatening dictionaries of lists. Useful for "urlencoded" decoded data.""" def flatten_data(self, data): - """Given a data dictionary ``{: }``, returns a flattened dictionary according to :meth:`FormParser.is_a_list`. - """ + """Given a data dictionary {: }, returns a flattened dictionary + with information provided by the method "is_a_list".""" flatdata = dict() for key, attr_value in data.items(): if self.is_a_list(key): @@ -98,20 +98,27 @@ class DataFlatener(object): flatdata[key] = attr_value return flatdata - def is_a_list(self, key): - """ """ + def is_a_list(self, key, val): + """Returns True if the parameter with name *key* is expected to be a list, or False otherwise. + *val* which is the received value for parameter *key* can be used to guess the answer.""" return False class FormParser(BaseParser, DataFlatener): """The default parser for form data. Return a dict containing a single value for each non-reserved parameter. + + In order to handle select multiple (and having possibly more than a single value for each parameter), + you can customize the output by subclassing the method 'is_a_list'. + """ - # TODO: document flatening # TODO: writing tests for PUT files + normal data - # TODO: document EMPTY workaround media_type = 'application/x-www-form-urlencoded' - EMPTY_VALUE = 'EMPTY' + """The value of the parameter when the select multiple is empty. + Browsers are usually stripping the select multiple that have no option selected from the parameters sent. + A common hack to avoid this is to send the parameter with a value specifying that the list is empty. + This value will always be stripped before the data is returned.""" + EMPTY_VALUE = '_empty' def parse(self, input): data = parse_qs(input) @@ -142,8 +149,11 @@ class MultipartParser(BaseParser, DataFlatener): media_type = 'multipart/form-data' def parse(self, input): - request = self.resource.request + request = self.resource.request + #TODO : that's pretty dumb : files are loaded with + #upload_handlers, but as we read the request body completely (input), + #then it kind of misses the point. Why not input as a stream ? upload_handlers = request._get_upload_handlers() django_mpp = DjangoMPParser(request.META, StringIO(input), upload_handlers) data, files = django_mpp.parse() From b117f7bd4503e74160c42b4ac1de3f8ea1132731 Mon Sep 17 00:00:00 2001 From: sebpiq Date: Thu, 10 Mar 2011 16:59:21 +0200 Subject: [PATCH 08/13] added a test --- djangorestframework/content.py | 2 +- djangorestframework/tests/content.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/djangorestframework/content.py b/djangorestframework/content.py index 48f327b6f..f9a3c6de6 100644 --- a/djangorestframework/content.py +++ b/djangorestframework/content.py @@ -40,7 +40,7 @@ class OverloadedContentMixin(ContentMixin): """If the request contains content, returns a tuple of (content_type, content) otherwise returns None. Note that content_type may be None if it is unset.""" if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None): - return None # TODO : Breaks, because determine_content should return a tuple. + return None content_type = request.META.get('CONTENT_TYPE', None) if (request.method == 'POST' and self.CONTENT_PARAM and diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py index 9052f677e..ee7af486d 100644 --- a/djangorestframework/tests/content.py +++ b/djangorestframework/tests/content.py @@ -111,6 +111,7 @@ class TestContentMixins(TestCase): OverloadedContentMixin.CONTENTTYPE_PARAM: content_type} request = self.req.post('/', form_data) self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content)) + self.assertEqual(request.META['CONTENT_TYPE'], content_type) def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self): """Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set""" From 94199a484783d91317c8decb273ab60447ffdfd7 Mon Sep 17 00:00:00 2001 From: sebpiq Date: Fri, 11 Mar 2011 12:00:51 +0200 Subject: [PATCH 09/13] modified tests.__init__ so it also test module docstrings --- djangorestframework/tests/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/djangorestframework/tests/__init__.py b/djangorestframework/tests/__init__.py index 7ac544196..55e386b52 100644 --- a/djangorestframework/tests/__init__.py +++ b/djangorestframework/tests/__init__.py @@ -5,5 +5,10 @@ modules = [filename.rsplit('.', 1)[0] for filename in os.listdir(os.path.dirname(__file__)) if filename.endswith('.py') and not filename.startswith('_')] +__test__ = dict() + for module in modules: - exec("from djangorestframework.tests.%s import *" % module) \ No newline at end of file + exec("from djangorestframework.tests.%s import __doc__ as module_doc" % module) + exec("from djangorestframework.tests.%s import *" % module) + __test__['%s' % module] = module_doc or "" + From d6c13a9e5caee653f7a8f3150c00aa574aa15700 Mon Sep 17 00:00:00 2001 From: sebpiq Date: Fri, 11 Mar 2011 14:34:39 +0200 Subject: [PATCH 10/13] documentation + tests + debugging for formparsers --- djangorestframework/content.py | 2 +- djangorestframework/parsers.py | 30 +++++------ djangorestframework/tests/__init__.py | 4 +- djangorestframework/tests/parsers.py | 72 +++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 djangorestframework/tests/parsers.py diff --git a/djangorestframework/content.py b/djangorestframework/content.py index f9a3c6de6..cfdd33be5 100644 --- a/djangorestframework/content.py +++ b/djangorestframework/content.py @@ -50,7 +50,7 @@ class OverloadedContentMixin(ContentMixin): content_type = None if self.CONTENTTYPE_PARAM and request.POST.get(self.CONTENTTYPE_PARAM, None): content_type = request.POST.get(self.CONTENTTYPE_PARAM, None) - request.META['CONTENT_TYPE'] = content_type + request.META['CONTENT_TYPE'] = content_type # TODO : VERY BAD, avoid modifying original request. return (content_type, request.POST[self.CONTENT_PARAM]) else: diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 081fa6897..d324f687b 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -84,23 +84,23 @@ class DataFlatener(object): def flatten_data(self, data): """Given a data dictionary {: }, returns a flattened dictionary with information provided by the method "is_a_list".""" + data = data.copy() flatdata = dict() - for key, attr_value in data.items(): - if self.is_a_list(key): - if isinstance(attr_value, list): - flatdata[key] = attr_value - else: - flatdata[key] = [attr_value] + for key, val_list in data.items(): + if self.is_a_list(key, val_list): + flatdata[key] = val_list else: - if isinstance(attr_value, list): - flatdata[key] = attr_value[0] + if val_list: + flatdata[key] = val_list[0] else: - flatdata[key] = attr_value + # If the list is empty, but the parameter is not a list, + # we strip this parameter. + data.pop(key) return flatdata - def is_a_list(self, key, val): + def is_a_list(self, key, val_list): """Returns True if the parameter with name *key* is expected to be a list, or False otherwise. - *val* which is the received value for parameter *key* can be used to guess the answer.""" + *val_list* which is the received value for parameter *key* can be used to guess the answer.""" return False class FormParser(BaseParser, DataFlatener): @@ -121,12 +121,12 @@ class FormParser(BaseParser, DataFlatener): EMPTY_VALUE = '_empty' def parse(self, input): - data = parse_qs(input) + data = parse_qs(input, keep_blank_values=True) - # Flatening data and removing EMPTY_VALUEs from the lists + # removing EMPTY_VALUEs from the lists and flatening the data + for key, val_list in data.items(): + self.remove_empty_val(val_list) data = self.flatten_data(data) - for key in filter(lambda k: self.is_a_list(k), data): - self.remove_empty_val(data[key]) # Strip any parameters that we are treating as reserved for key in data.keys(): diff --git a/djangorestframework/tests/__init__.py b/djangorestframework/tests/__init__.py index 55e386b52..5d5b652ab 100644 --- a/djangorestframework/tests/__init__.py +++ b/djangorestframework/tests/__init__.py @@ -10,5 +10,5 @@ __test__ = dict() for module in modules: exec("from djangorestframework.tests.%s import __doc__ as module_doc" % module) exec("from djangorestframework.tests.%s import *" % module) - __test__['%s' % module] = module_doc or "" - + __test__[module] = module_doc or "" + diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py new file mode 100644 index 000000000..a2831b63b --- /dev/null +++ b/djangorestframework/tests/parsers.py @@ -0,0 +1,72 @@ +""" +.. + >>> from djangorestframework.parsers import FormParser + >>> from djangorestframework.resource import Resource + >>> from djangorestframework.compat import RequestFactory + >>> from urllib import urlencode + >>> req = RequestFactory().get('/') + >>> some_resource = Resource() + >>> trash = some_resource.dispatch(req)# Some variables are set only when calling dispatch + +Data flatening +---------------- + +Here is some example data, which would eventually be sent along with a post request : + + >>> inpt = urlencode([ + ... ('key1', 'bla1'), + ... ('key2', 'blo1'), ('key2', 'blo2'), + ... ]) + +Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter : + + >>> FormParser(some_resource).parse(inpt) == {'key1': 'bla1', 'key2': 'blo1'} + True + +However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` : + + >>> class MyFormParser(FormParser): + ... + ... def is_a_list(self, key, val_list): + ... return len(val_list) > 1 + +This new parser only flattens the lists of parameters that contain a single value. + + >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} + True + +Submitting an empty list +-------------------------- + +When submitting an empty select multiple, like this one :: + + + +The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty :: + + + +:class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data : + + >>> inpt = urlencode([ + ... ('key1', 'blo1'), ('key1', '_empty'), + ... ('key2', '_empty'), + ... ]) + +:class:`parsers.FormParser` strips the values ``_empty`` from all the lists. + + >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'blo1'} + True + +Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it. + + >>> class MyFormParser(FormParser): + ... + ... def is_a_list(self, key, val_list): + ... return key == 'key2' + ... + >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'blo1', 'key2': []} + True + +Better like that. Note also that you can configure something else than ``_empty`` for the empty value by setting :class:`parsers.FormParser.EMPTY_VALUE`. +""" From ee74aec27cdc8ca9934f93c828ffbdc7da3c426c Mon Sep 17 00:00:00 2001 From: sebpiq Date: Fri, 11 Mar 2011 15:05:35 +0200 Subject: [PATCH 11/13] debug + tests for MultipartParser --- djangorestframework/parsers.py | 9 ++--- djangorestframework/tests/parsers.py | 57 +++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index d324f687b..3bd020ad4 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -84,7 +84,6 @@ class DataFlatener(object): def flatten_data(self, data): """Given a data dictionary {: }, returns a flattened dictionary with information provided by the method "is_a_list".""" - data = data.copy() flatdata = dict() for key, val_list in data.items(): if self.is_a_list(key, val_list): @@ -108,10 +107,8 @@ class FormParser(BaseParser, DataFlatener): Return a dict containing a single value for each non-reserved parameter. In order to handle select multiple (and having possibly more than a single value for each parameter), - you can customize the output by subclassing the method 'is_a_list'. + you can customize the output by subclassing the method 'is_a_list'.""" - """ - # TODO: writing tests for PUT files + normal data media_type = 'application/x-www-form-urlencoded' """The value of the parameter when the select multiple is empty. @@ -159,8 +156,8 @@ class MultipartParser(BaseParser, DataFlatener): data, files = django_mpp.parse() # Flatening data, files and combining them - data = self.flatten_data(data) - files = self.flatten_data(files) + data = self.flatten_data(dict(data.iterlists())) + files = self.flatten_data(dict(files.iterlists())) data.update(files) # Strip any parameters that we are treating as reserved diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py index a2831b63b..d4cd1e87f 100644 --- a/djangorestframework/tests/parsers.py +++ b/djangorestframework/tests/parsers.py @@ -8,6 +8,9 @@ >>> some_resource = Resource() >>> trash = some_resource.dispatch(req)# Some variables are set only when calling dispatch +FormParser +============ + Data flatening ---------------- @@ -35,6 +38,8 @@ This new parser only flattens the lists of parameters that contain a single valu >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} True +.. note:: The same functionality is available for :class:`parsers.MultipartParser`. + Submitting an empty list -------------------------- @@ -68,5 +73,55 @@ Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a lis >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'blo1', 'key2': []} True -Better like that. Note also that you can configure something else than ``_empty`` for the empty value by setting :class:`parsers.FormParser.EMPTY_VALUE`. +Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`. """ +import httplib, mimetypes +from tempfile import TemporaryFile +from django.test import TestCase +from djangorestframework.compat import RequestFactory +from djangorestframework.parsers import MultipartParser +from djangorestframework.resource import Resource + +def encode_multipart_formdata(fields, files): + """For testing multipart parser. + fields is a sequence of (name, value) elements for regular form fields. + files is a sequence of (name, filename, value) elements for data to be uploaded as files + Return (content_type, body).""" + BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + +def get_content_type(filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + +class TestMultipartParser(TestCase): + def setUp(self): + self.req = RequestFactory() + self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], + [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) + + def test_multipartparser(self): + """Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters.""" + post_req = RequestFactory().post('/', self.body, content_type=self.content_type) + some_resource = Resource() + some_resource.dispatch(post_req) + parsed = MultipartParser(some_resource).parse(self.body) + self.assertEqual(parsed['key1'], 'val1') + self.assertEqual(parsed['file1'].read(), 'blablabla') + From bed7d8c3a99425b8dfa5c5a9c6b1d49a46f5284f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 13 Mar 2011 17:08:46 +0000 Subject: [PATCH 12/13] tox, runcoverage, and setup.py test --- .hgignore | 4 ++ README | 16 ++++-- .../runtests}/__init__.py | 0 djangorestframework/runtests/runcoverage.py | 53 +++++++++++++++++++ djangorestframework/runtests/runtests.py | 35 ++++++++++++ .../runtests}/settings.py | 0 djangorestframework/runtests/urls.py | 7 +++ setup.py | 10 +++- testproject/manage.py | 11 ---- testproject/urls.py | 16 ------ tox.ini | 40 ++++++++++++++ 11 files changed, 160 insertions(+), 32 deletions(-) rename {testproject => djangorestframework/runtests}/__init__.py (100%) create mode 100644 djangorestframework/runtests/runcoverage.py create mode 100644 djangorestframework/runtests/runtests.py rename {testproject => djangorestframework/runtests}/settings.py (100%) create mode 100644 djangorestframework/runtests/urls.py delete mode 100755 testproject/manage.py delete mode 100644 testproject/urls.py create mode 100644 tox.ini diff --git a/.hgignore b/.hgignore index e5154a849..b02605e93 100644 --- a/.hgignore +++ b/.hgignore @@ -6,12 +6,16 @@ syntax: glob env docs/build html +htmlcov examples/media/pygments/* examples/media/objectstore/* build/* dist/* +djangorestframework.egg-info/* MANIFEST .project .pydevproject .settings .cache +.coverage +.tox diff --git a/README b/README index a7dc3e925..d74aa474b 100644 --- a/README +++ b/README @@ -8,9 +8,14 @@ pip install -r requirements.txt # django # To run the tests... -cd testproject -export PYTHONPATH=.. -python manage.py test djangorestframework +export PYTHONPATH=. # Ensure djangorestframework is on the PYTHONPATH +python djangorestframework/runtests/runtests.py + + +# To run the test coverage report... + +export PYTHONPATH=. # Ensure djangorestframework is on the PYTHONPATH +python djangorestframework/runtests/runcoverage.py # To run the examples... @@ -24,3 +29,8 @@ python manage.py runserver pip install -r docs/requirements.txt # sphinx sphinx-build -c docs -b html -d docs/build docs html + +# To run the tests against the full set of supported configurations + +deactivate # Ensure we are not currently running in a virtualenv +tox diff --git a/testproject/__init__.py b/djangorestframework/runtests/__init__.py similarity index 100% rename from testproject/__init__.py rename to djangorestframework/runtests/__init__.py diff --git a/djangorestframework/runtests/runcoverage.py b/djangorestframework/runtests/runcoverage.py new file mode 100644 index 000000000..1deb10805 --- /dev/null +++ b/djangorestframework/runtests/runcoverage.py @@ -0,0 +1,53 @@ +""" +Useful tool to run the test suite for djangorestframework and generate a coverage report. +""" + +# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/ +# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/ +# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py +import os +import sys +os.environ['DJANGO_SETTINGS_MODULE'] = 'djangorestframework.runtests.settings' + +from django.conf import settings +from django.test.utils import get_runner +from coverage import coverage +from itertools import chain +import djangorestframework + +def main(): + """Run the tests for djangorestframework and generate a coverage report.""" + + # Discover the list of all modules that we should test coverage for + project_dir = os.path.dirname(djangorestframework.__file__) + cov_files = [] + for (path, dirs, files) in os.walk(project_dir): + # Drop tests and runtests directories from the test coverage report + if os.path.basename(path) == 'tests' or os.path.basename(path) == 'runtests': + continue + cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')]) + + cov = coverage() + cov.erase() + cov.start() + TestRunner = get_runner(settings) + + if hasattr(TestRunner, 'func_name'): + # Pre 1.2 test runners were just functions, + # and did not support the 'failfast' option. + import warnings + warnings.warn( + 'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.', + DeprecationWarning + ) + failures = TestRunner(['djangorestframework']) + else: + test_runner = TestRunner() + failures = test_runner.run_tests(['djangorestframework']) + + cov.stop() + cov.report(cov_files) + sys.exit(failures) + +if __name__ == '__main__': + main() diff --git a/djangorestframework/runtests/runtests.py b/djangorestframework/runtests/runtests.py new file mode 100644 index 000000000..a3cdfa67d --- /dev/null +++ b/djangorestframework/runtests/runtests.py @@ -0,0 +1,35 @@ +''' +Created on Mar 10, 2011 + +@author: tomchristie +''' +# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/ +# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/ +# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py +import os +import sys +os.environ['DJANGO_SETTINGS_MODULE'] = 'djangorestframework.runtests.settings' + +from django.conf import settings +from django.test.utils import get_runner + +def main(): + TestRunner = get_runner(settings) + + if hasattr(TestRunner, 'func_name'): + # Pre 1.2 test runners were just functions, + # and did not support the 'failfast' option. + import warnings + warnings.warn( + 'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.', + DeprecationWarning + ) + failures = TestRunner(['djangorestframework']) + else: + test_runner = TestRunner() + failures = test_runner.run_tests(['djangorestframework']) + + sys.exit(failures) + +if __name__ == '__main__': + main() diff --git a/testproject/settings.py b/djangorestframework/runtests/settings.py similarity index 100% rename from testproject/settings.py rename to djangorestframework/runtests/settings.py diff --git a/djangorestframework/runtests/urls.py b/djangorestframework/runtests/urls.py new file mode 100644 index 000000000..455558138 --- /dev/null +++ b/djangorestframework/runtests/urls.py @@ -0,0 +1,7 @@ +""" +Blank URLConf just to keep runtests.py happy. +""" +from django.conf.urls.defaults import * + +urlpatterns = patterns('', +) \ No newline at end of file diff --git a/setup.py b/setup.py index 0015f0d9b..4c5ff054e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env/python # -*- coding: utf-8 -*- -from distutils.core import setup +from setuptools import setup setup( name = "djangorestframework", @@ -14,9 +14,11 @@ setup( author_email = 'tom@tomchristie.com', packages = ['djangorestframework', 'djangorestframework.templatetags', - 'djangorestframework.tests'], + 'djangorestframework.tests', + 'djangorestframework.runtests'], package_dir={'djangorestframework': 'djangorestframework'}, package_data = {'djangorestframework': ['templates/*', 'static/*']}, + test_suite = 'djangorestframework.runtests.runtests.main', classifiers = [ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', @@ -29,3 +31,7 @@ setup( ] ) +import os, shutil +shutil.rmtree(os.path.join(os.path.dirname(__file__), 'djangorestframework.egg-info'), True) + + diff --git a/testproject/manage.py b/testproject/manage.py deleted file mode 100755 index 5e78ea979..000000000 --- a/testproject/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) - -if __name__ == "__main__": - execute_manager(settings) diff --git a/testproject/urls.py b/testproject/urls.py deleted file mode 100644 index d13127899..000000000 --- a/testproject/urls.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.conf.urls.defaults import * - -# Uncomment the next two lines to enable the admin: -# from django.contrib import admin -# admin.autodiscover() - -urlpatterns = patterns('', - # Example: - # (r'^testproject/', include('testproject.foo.urls')), - - # Uncomment the admin/doc line below to enable admin documentation: - # (r'^admin/doc/', include('django.contrib.admindocs.urls')), - - # Uncomment the next line to enable the admin: - # (r'^admin/', include(admin.site.urls)), -) diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..b586ab074 --- /dev/null +++ b/tox.ini @@ -0,0 +1,40 @@ +[tox] +envlist= + py25-django12, py26-django12, py27-django12, py25-django13, py26-django13, py27-django13 + +[testenv] +commands= + python setup.py test + +[testenv:py25-django12] +basepython=python2.5 +deps= + django==1.2.4 + simplejson + +[testenv:py26-django12] +basepython=python2.6 +deps= + django==1.2.4 + +[testenv:py27-django12] +basepython=python2.7 +deps= + django==1.2.4 + +[testenv:py25-django13] +basepython=python2.5 +deps= + http://www.djangoproject.com/download/1.3-rc-1/tarball/ + simplejson + +[testenv:py26-django13] +basepython=python2.6 +deps= + http://www.djangoproject.com/download/1.3-rc-1/tarball/ + +[testenv:py27-django13] +basepython=python2.7 +deps= + http://www.djangoproject.com/download/1.3-rc-1/tarball/ + From 0f0fd13d1358863c2fd92bb75d32d411e84730eb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 13 Mar 2011 17:13:18 +0000 Subject: [PATCH 13/13] test refactoring --- djangorestframework/tests/runtests.py | 29 --------------------------- djangorestframework/tests/urls.py | 7 ------- 2 files changed, 36 deletions(-) delete mode 100644 djangorestframework/tests/runtests.py delete mode 100644 djangorestframework/tests/urls.py diff --git a/djangorestframework/tests/runtests.py b/djangorestframework/tests/runtests.py deleted file mode 100644 index 3043c7b6a..000000000 --- a/djangorestframework/tests/runtests.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Created on Mar 10, 2011 - -@author: tomchristie -''' -# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/ -# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/ -from django.conf import settings -from django.core.management import call_command - -def runtests(): - settings.configure( - INSTALLED_APPS=( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'djangorestframework', - ), - ROOT_URLCONF='djangorestframework.tests.urls', - # Django replaces this, but it still wants it. *shrugs* - DATABASE_ENGINE='sqlite3' - ) - call_command('test', 'djangorestframework') - - -if __name__ == '__main__': - runtests() diff --git a/djangorestframework/tests/urls.py b/djangorestframework/tests/urls.py deleted file mode 100644 index 455558138..000000000 --- a/djangorestframework/tests/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Blank URLConf just to keep runtests.py happy. -""" -from django.conf.urls.defaults import * - -urlpatterns = patterns('', -) \ No newline at end of file