changeset 9:295c54288dd1

Added Bug and Attachment classes.
author Atul Varma <avarma@mozilla.com>
date Tue, 13 Apr 2010 15:52:28 -0700
parents 993fc3b27263
children b85bd0fc50e4
files bugzilla.py test_bugzilla.py
diffstat 2 files changed, 191 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/bugzilla.py	Tue Apr 13 11:36:56 2010 -0700
+++ b/bugzilla.py	Tue Apr 13 15:52:28 2010 -0700
@@ -6,6 +6,7 @@
 import httplib
 import urllib
 import mimetypes
+import datetime
 from urlparse import urlparse
 from getpass import getpass
 
@@ -100,7 +101,7 @@
 
     return password
 
-def load_config(filename=None, getpass=getpass_or_die):
+def load_config(filename=None, getpass=None):
     config = {}
     config.update(DEFAULT_CONFIG)
 
@@ -118,7 +119,8 @@
     return config
 
 class BugzillaApi(object):
-    def __init__(self, config=None, jsonreq=None):
+    def __init__(self, config=None, jsonreq=None,
+                 getpass=getpass_or_die):
         if config is None:
             config = load_config()
 
@@ -136,6 +138,33 @@
                         content_type=None, is_patch=False, is_private=False,
                         is_obsolete=False,
                         guess_mime_type=mimetypes.guess_type):
+        """
+        >>> jsonreq = Mock('jsonreq')
+        >>> jsonreq.mock_returns = {
+        ...   "status": 201,
+        ...   "body": {"ref": "http://foo/latest/attachment/1"},
+        ...   "reason": "Created",
+        ...   "content_type": "application/json"
+        ... }
+        >>> bzapi = BugzillaApi(config=TEST_CFG_WITH_LOGIN,
+        ...                     jsonreq=jsonreq)
+        >>> bzapi.post_attachment(bug_id=536619,
+        ...                       contents="testing!",
+        ...                       filename="contents.txt",
+        ...                       description="test upload")
+        Called jsonreq(
+            body={'is_obsolete': False, 'flags': [],
+                  'description': 'test upload',
+                  'content_type': 'text/plain', 'encoding': 'base64',
+                  'file_name': 'contents.txt', 'is_patch': False,
+                  'data': 'dGVzdGluZyE=', 'is_private': False,
+                  'size': 8},
+            method='POST',
+            query_args={'username': 'bar', 'password': 'baz'},
+            url='http://foo/latest/bug/536619/attachment')
+        {'ref': 'http://foo/latest/attachment/1'}
+        """
+
         if content_type is None:
             content_type = guess_mime_type(filename)[0]
             if not content_type:
@@ -183,6 +212,103 @@
 class BugzillaApiError(Exception):
     pass
 
+def iso8601_to_datetime(timestamp):
+    """
+    >>> iso8601_to_datetime('2010-04-11T19:16:59Z')
+    datetime.datetime(2010, 4, 11, 19, 16, 59)
+    """
+
+    return datetime.datetime.strptime(timestamp,
+                                      "%Y-%m-%dT%H:%M:%SZ")
+
+class Attachment(object):
+    """
+    >>> bzapi = Mock('bzapi')
+    >>> bzapi.request.mock_returns = TEST_ATTACHMENT_WITH_DATA
+    >>> a = Attachment(TEST_ATTACHMENT_WITHOUT_DATA, bzapi)
+    >>> a.data
+    Called bzapi.request(
+        'GET',
+        '/attachment/438797',
+        query_args={'attachmentdata': '1'})
+    'testing!'
+    """
+
+    def __init__(self, jsonobj, bzapi=None):
+        self.bzapi = bzapi
+        self.id = int(jsonobj['id'])
+        self.description = jsonobj['description']
+        self.content_type = jsonobj['content_type']
+        for timeprop in ['last_change_time']:
+            setattr(self, timeprop, jsonobj[timeprop])
+        if 'data' in jsonobj:
+            self.__data = self.__decode_data(jsonobj)
+        else:
+            self.__data = None
+
+    @property
+    def data(self):
+        if self.__data is None and self.bzapi:
+            jsonobj = self.__get_full_attachment(self.bzapi, self.id)
+            self.__data = self.__decode_data(jsonobj)
+        return self.__data
+
+    def __decode_data(self, jsonobj):
+        if jsonobj['encoding'] != 'base64':
+            raise NotImplementedError("unrecognized encoding: %s" %
+                                      jsonobj['encoding'])
+        return base64.b64decode(jsonobj['data'])
+
+    def __repr__(self):
+        return '<Attachment %d - %s>' % (self.id, repr(self.description))
+
+    @staticmethod
+    def __get_full_attachment(bzapi, attach_id):
+        return bzapi.request('GET', '/attachment/%d' % attach_id,
+                             query_args={'attachmentdata': '1'})
+
+    @classmethod
+    def fetch(klass, bzapi, attach_id):
+        """
+        >>> bzapi = Mock('bzapi')
+        >>> bzapi.request.mock_returns = TEST_ATTACHMENT_WITH_DATA
+        >>> Attachment.fetch(bzapi, 438797)
+        Called bzapi.request(
+            'GET',
+            '/attachment/438797',
+            query_args={'attachmentdata': '1'})
+        <Attachment 438797 - u'test upload'>
+        """
+
+        return klass(klass.__get_full_attachment(bzapi, attach_id))
+
+class Bug(object):
+    """
+    >>> Bug(TEST_BUG)
+    <Bug 558680 - u'Here is a summary'>
+    """
+
+    def __init__(self, jsonobj):
+        self.id = int(jsonobj['id'])
+        self.summary = jsonobj['summary']
+        self.attachments = [Attachment(attach)
+                            for attach in jsonobj['attachments']]
+
+    def __repr__(self):
+        return '<Bug %d - %s>' % (self.id, repr(self.summary))
+
+    @classmethod
+    def fetch(klass, bzapi, bug_id):
+        """
+        >>> bzapi = Mock('bzapi')
+        >>> bzapi.request.mock_returns = TEST_BUG
+        >>> Bug.fetch(bzapi, 558680)
+        Called bzapi.request('GET', '/bug/558680')
+        <Bug 558680 - u'Here is a summary'>
+        """
+
+        return klass(bzapi.request('GET', '/bug/%d' % bug_id))
+
 if __name__ == '__main__':
     bzapi = BugzillaApi()
     print bzapi.request('GET', '/attachment/436897',
--- a/test_bugzilla.py	Tue Apr 13 11:36:56 2010 -0700
+++ b/test_bugzilla.py	Tue Apr 13 15:52:28 2010 -0700
@@ -1,44 +1,65 @@
+import os
 import doctest
 import unittest
 
 from minimock import Mock
 import bugzilla
 
-CFG_WITH_LOGIN = {'api_server': 'http://foo/latest',
-                  'username': 'bar',
-                  'password': 'baz'}
+TEST_CFG_WITH_LOGIN = {'api_server': 'http://foo/latest',
+                       'username': 'bar',
+                       'password': 'baz'}
+
+TEST_ATTACHMENT_WITHOUT_DATA = {
+    u'is_obsolete': u'0',
+    u'description': u'test upload', u'encoding': u'base64',
+    u'file_name': u'contents.txt', u'is_patch': u'0',
+    u'creation_time': u'2010-04-13T18:02:00Z',
+    u'bug_id': u'536619',
+    u'last_change_time': u'2010-04-13T18:02:00Z',
+    u'content_type': u'text/plain',
+    u'attacher': {u'name': u'avarma'}, u'is_url': u'0',
+    u'id': u'438797', u'is_private': u'0',
+    u'size': u'8'
+}
+
+TEST_ATTACHMENT_WITH_DATA = {
+    u'is_obsolete': u'0',
+    u'description': u'test upload', u'encoding': u'base64',
+    u'file_name': u'contents.txt', u'is_patch': u'0',
+    u'creation_time': u'2010-04-13T18:02:00Z',
+    u'data': u'dGVzdGluZyE=', u'bug_id': u'536619',
+    u'last_change_time': u'2010-04-13T18:02:00Z',
+    u'content_type': u'text/plain',
+    u'attacher': {u'name': u'avarma'}, u'is_url': u'0',
+    u'id': u'438797', u'is_private': u'0',
+    u'size': u'8'
+}
+
+TEST_BUG = {
+    u'attachments':
+        [{u'is_obsolete': u'0',
+          u'description': u'here is a description',
+          u'file_name': u'traceback-on-package-exception',
+          u'is_patch': u'1', u'creation_time': u'2010-04-11T19:16:00Z',
+          u'last_change_time': u'2010-04-11T19:16:59Z',
+          u'bug_id': u'558680', u'content_type': u'text/plain',
+          u'attacher': {u'name': u'asqueella'},
+          u'id': u'438381', u'size': u'1320'}],
+    u'summary': u'Here is a summary',
+    u'id': u'558680'
+    }
+
+DOCTEST_EXTRA_GLOBS = {
+    'Mock': Mock
+    }
+
+for globname in globals().keys():
+    if globname.startswith('TEST_'):
+        DOCTEST_EXTRA_GLOBS[globname] = globals()[globname]
+del globname
 
 class Tests(unittest.TestCase):
-    pass
-
-def test_upload(self):
-    """
-    >>> jsonreq = Mock('jsonreq')
-    >>> jsonreq.mock_returns = {
-    ...   "status": 201,
-    ...   "body": {"ref": "http://foo/latest/attachment/1"},
-    ...   "reason": "Created",
-    ...   "content_type": "application/json"
-    ... }
-    >>> bzapi = bugzilla.BugzillaApi(config=CFG_WITH_LOGIN,
-    ...                              jsonreq=jsonreq)
-    >>> bzapi.post_attachment(bug_id=536619,
-    ...                       contents="testing!",
-    ...                       filename="contents.txt",
-    ...                       description="test upload")
-    Called jsonreq(
-        body={'is_obsolete': False, 'flags': [],
-              'description': 'test upload',
-              'content_type': 'text/plain', 'encoding': 'base64',
-              'file_name': 'contents.txt', 'is_patch': False,
-              'data': 'dGVzdGluZyE=', 'is_private': False,
-              'size': 8},
-        method='POST',
-        query_args={'username': 'bar', 'password': 'baz'},
-        url='http://foo/latest/bug/536619/attachment')
-    {'ref': 'http://foo/latest/attachment/1'}
-    """
-
+    # TODO: Add unit tests here.
     pass
 
 def get_tests_in_module(module):
@@ -52,7 +73,7 @@
     optionflags = (doctest.NORMALIZE_WHITESPACE |
                    doctest.REPORT_UDIFF)
     finder = doctest.DocTestFinder()
-    doctests = finder.find(module)
+    doctests = finder.find(module, extraglobs=DOCTEST_EXTRA_GLOBS)
     for test in doctests:
         if len(test.examples) > 0:
             tests.append(doctest.DocTestCase(test,
@@ -61,8 +82,14 @@
     return tests
 
 def run_tests(verbosity=2):
-    module = __import__(__name__)
-    tests = get_tests_in_module(module)
+    modules = [filename[:-3] for filename in os.listdir('.')
+               if filename.endswith('.py')
+               and filename not in ['minimock.py']]
+    tests = []
+    for modulename in modules:
+        module = __import__(modulename)
+        tests.extend(get_tests_in_module(module))
+
     suite = unittest.TestSuite(tests)
     runner = unittest.TextTestRunner(verbosity=verbosity)
     runner.run(suite)