bugsy: sync with Git
authorGregory Szorc <gps@mozilla.com>
Wed, 16 Jul 2014 10:33:30 -0700
changeset 359004 803478d545b226b46d4e67da1411217cad3667bb
parent 359003 271b7ff4edecbed3f8991a9508f9735616e3a44d
child 359005 3740050aa82f25d0ad52ab8c5fd3af40051d6635
push id16998
push userrwood@mozilla.com
push dateMon, 02 May 2016 19:42:03 +0000
bugsy: sync with Git Bugsy 187c6531a56f2dafea14b5ee80be5d6b83813eeb was imported into pylib/Bugsy. The bzpost extension has been updated to use the now-working Bugsy API for adding comments.
hgext/bzpost/__init__.py
pylib/Bugsy/History.md
pylib/Bugsy/bugsy/__init__.py
pylib/Bugsy/bugsy/bug.py
pylib/Bugsy/bugsy/bugsy.py
pylib/Bugsy/bugsy/search.py
pylib/Bugsy/docs/source/comment.rst
pylib/Bugsy/docs/source/index.rst
pylib/Bugsy/example/add_comments.py
pylib/Bugsy/setup.py
pylib/Bugsy/tests/test_bugs.py
pylib/Bugsy/tests/test_bugsy.py
pylib/Bugsy/tests/test_search.py
--- a/hgext/bzpost/__init__.py
+++ b/hgext/bzpost/__init__.py
@@ -164,14 +164,14 @@ def wrappedpushbookmark(orig, pushop):
                 bugnumber)
             continue
 
         lines = ['%s/rev/%s' % (baseuri, node) for node in missing_nodes]
 
         comment = '\n'.join(lines)
 
         ui.write(_('recording push in bug %s\n') % bugnumber)
-        bug.add_comment_working(comment)
+        bug.add_comment(comment)
 
     return result
 
 def extsetup(ui):
     extensions.wrapfunction(exchange, '_pushbookmark', wrappedpushbookmark)
--- a/pylib/Bugsy/History.md
+++ b/pylib/Bugsy/History.md
@@ -1,8 +1,17 @@
+
+0.3.0 / 2014-07-14
+==================
+
+ * Updated Documentation
+ * Fix adding comments to bugs that already exist. Fixes #2
+ * Give the ability to search for multiple bugs which allows changing the fields returned
+ * Only request a small number of fields from Bugzilla. Fixes #3
+ * Initial Comments API design
 
 0.2.0 / 2014-06-26
 ==================
 
  * Added the ability to search Bugzilla
     * Set include_fields to have defaults as used in Bugs object
     * Add the ability to search whiteboard
     * Add the ability to search summary fields
--- a/pylib/Bugsy/bugsy/__init__.py
+++ b/pylib/Bugsy/bugsy/__init__.py
@@ -1,3 +1,3 @@
-from bug import Bug, BugException
+from bug import Bug, BugException, Comment
 from bugsy import Bugsy, BugsyException, LoginException
 from search import Search
\ No newline at end of file
--- a/pylib/Bugsy/bugsy/bug.py
+++ b/pylib/Bugsy/bugsy/bug.py
@@ -35,158 +35,186 @@ class Bug(object):
         self._bugsy = bugsy
         self._bug = dict(**kwargs)
         self._bug['op_sys'] = kwargs.get('op_sys', 'All')
         self._bug['product'] = kwargs.get('product', 'core')
         self._bug['component'] = kwargs.get('component', 'general')
         self._bug['platform'] = kwargs.get('platform', 'All')
         self._bug['version'] = kwargs.get('version', 'unspecified')
 
-    def id():
-        doc = """
-            Property for getting the ID of a bug.
-
-            >>> bug.id
-            123456
+    @property
+    def id(self):
         """
-        def fget(self):
-            return self._bug.get('id', None)
-        return locals()
-    id = property(**id())
+        Property for getting the ID of a bug.
 
-    def summary():
-        doc = """
+        >>> bug.id
+        123456
+        """
+        return self._bug.get('id', None)
+
+    @property
+    def summary(self):
+        """
             Property for getting and setting the bug summary
 
-            >>> bug.summary = "I like cheese"
             >>> bug.summary
             "I like cheese"
         """
-        def fget(self):
-            return self._bug.get('summary', '')
-        def fset(self, value):
-            self._bug['summary'] = value
-        def fdel(self):
-            del self._bug['summary']
-        return locals()
-    summary = property(**summary())
+        return self._bug.get('summary', '')
+
+    @summary.setter
+    def summary(self, value):
+        """
+            Property for getting and setting the bug summary
 
-    def status():
-        doc = """
+            >>> bug.summary = "I like cheese"
+        """
+        self._bug['summary'] = value
+
+    @property
+    def status(self):
+        """
             Property for getting or setting the bug status
 
-            >>> bug.status = "REOPENED"
             >>> bug.status
             "REOPENED"
         """
-        def fget(self):
-            return self._bug.get('status', '')
-        def fset(self, value):
-            if self._bug.get('id', None):
-                if value in VALID_STATUS:
-                    self._bug['status'] = value
-                else:
-                    raise BugException("Invalid status type was used")
+        return self._bug.get('status', '')
+
+    @status.setter
+    def status(self, value):
+        """
+            Property for getting or setting the bug status
+
+            >>> bug.status = "REOPENED"
+        """
+        if self._bug.get('id', None):
+            if value in VALID_STATUS:
+                self._bug['status'] = value
             else:
-                raise BugException("Can not set status unless there is a bug id. Please call Update() before setting")
-        def fdel(self):
-            del self._bug['status']
-        return locals()
-    status = property(**status())
+                raise BugException("Invalid status type was used")
+        else:
+            raise BugException("Can not set status unless there is a bug id. Please call Update() before setting")
 
-    def OS():
-        doc = """
+    @property
+    def OS(self):
+        """
             Property for getting or setting the OS that the bug occured on
 
             >>> bug.OS
             "All"
+        """
+        return self._bug['op_sys']
+
+    @OS.setter
+    def OS(self, value):
+        """
+            Property for getting or setting the OS that the bug occured on
+
             >>> bug.OS = "Linux"
         """
-        def fget(self):
-            return self._bug['op_sys']
-        def fset(self, value):
-            self._bug['op_sys']
-        return locals()
-    OS = property(**OS())
+        self._bug['op_sys']
+
+    @property
+    def resolution(self):
+        """
+            Property for getting or setting the bug resolution
 
-    def resolution():
-        doc = """
+            >>> bug.resolution
+            "FIXED"
+        """
+        return self._bug['resolution']
+
+    @resolution.setter
+    def resolution(self, value):
+        """
             Property for getting or setting the bug resolution
 
             >>> bug.resolution = "FIXED"
-            >>> bug.resolution
-            "FIXED"
         """
-        def fget(self):
-            return self._bug['resolution']
-        def fset(self, value):
-            if value in VALID_RESOLUTION:
-                self._bug['resolution'] = value
-            else:
-                raise BugException("Invalid resolution type was used")
-        def fdel(self):
-            del self._bug['resolution']
-        return locals()
-    resolution = property(**resolution())
+        if value in VALID_RESOLUTION:
+            self._bug['resolution'] = value
+        else:
+            raise BugException("Invalid resolution type was used")
 
-    def product():
-        doc = """
+    @property
+    def product(self):
+        """
             Property for getting the bug product
 
             >>> bug.product
             Core
         """
-        def fget(self):
-            return self._bug['product']
-        def fset(self, value):
-            self._product = value
-        return locals()
-    product = property(**product())
+        return self._bug['product']
+
+    @product.setter
+    def product(self, value):
+        """
+            Property for getting the bug product
 
-    def component():
-        doc = """
+            >>> bug.product = "DOM"
+        """
+        self._bug['product'] = value
+
+    @property
+    def component(self):
+        """
             Property for getting the bug component
 
             >>> bug.component
             General
         """
-        def fget(self):
-            return self._bug['component']
-        def fset(self, value):
-            self._bug['component'] = value
-        return locals()
-    component = property(**component())
+        return self._bug['component']
+
+    @component.setter
+    def component(self, value):
+        """
+            Property for getting the bug component
 
-    def platform():
-        doc = """
+            >>> bug.component = "Marionette"
+        """
+        self._bug['component'] = value
+
+    @property
+    def platform(self):
+        """
             Property for getting the bug platform
 
             >>> bug.platform
             "ARM"
         """
-        def fget(self):
-            return self._bug['platform']
-        def fset(self, value):
-            self._bug['platform'] = value
-        return locals()
-    platform = property(**platform())
+        return self._bug['platform']
+
+    @platform.setter
+    def platform(self, value):
+        """
+            Property for getting the bug platform
 
-    def version():
-        doc = """
+            >>> bug.platform = "OSX"
+        """
+        self._bug['platform'] = value
+
+    @property
+    def version(self):
+        """
             Property for getting the bug platform
 
             >>> bug.version
             "TRUNK"
         """
-        def fget(self):
-            return self._bug['version']
-        def fset(self, value):
-            self._bug['version'] = value
-        return locals()
-    version = property(**version())
+        return self._bug['version']
+
+    @version.setter
+    def version(self, value):
+        """
+            Property for getting the bug platform
+
+            >>> bug.version = "0.3"
+        """
+        self._bug['version'] = value
 
     def to_dict(self):
         """
             Return the raw dict that is used inside this object
         """
         return self._bug
 
     def update(self):
@@ -210,58 +238,56 @@ class Bug(object):
         """
             Obtain comments for this bug.
 
             Returns a list of Comment instances.
         """
         bug = unicode(self._bug['id'])
         res = self._bugsy.request('bug/%s/comment' % bug).json()
 
-        return [Comment.from_json(c) for c in res['bugs'][bug]['comments']]
+        return [Comment(**comments) for comments in res['bugs'][bug]['comments']]
 
     def add_comment(self, comment):
         """
-            Adds a comment to a bug. Once you have added it you will need to
-            call put on the Bugsy object
+            Adds a comment to a bug. If a bug does not have a bug ID then you need
+            call `put` on the :class:`Bugsy` class.
 
             >>> bug.add_comment("I like sausages")
             >>> bugzilla.put(bug)
-        """
-        self._bug['comment'] = comment
+
+            If it does have a bug id then this will do a post to the server
 
-    def add_comment_working(self, comment):
-        """API to add a comment that actually works.
-
-        This is a temporary workaround until add_comment is fixed.
+            >>> bug.add_comment("I like eggs too")
         """
-        self._bugsy.request('bug/%s/comment' % self._bug['id'], 'POST',
-            data={'comment': comment})
+        # If we have a key post immediately otherwise hold onto it until put(bug)
+        # is called
+        if self._bug.has_key('id'):
+            self._bugsy.session.post('%s/bug/%s/comment' % (self._bugsy.bugzilla_url, self._bug['id']), data={"comment": comment}, )
+        else:
+            self._bug['comment'] = comment
 
     def to_dict(self):
         """
             Return the raw dict that is used inside this object
         """
         return self._bug
 
 
 class Comment(object):
     """
         Represents a single Bugzilla comment.
     """
 
-    @staticmethod
-    def from_json(j):
-        c = Comment()
+    def __init__(self, **kwargs):
 
-        c.attachment_id = j['attachment_id']
-        c.author = j['author']
-        c.bug_id = j['bug_id']
-        c.creation_time = str2datetime(j['creation_time'])
-        c.creator = j['creator']
-        c.id = j['id']
-        c.is_private = j['is_private']
-        c.text = j['text']
-        c.time = str2datetime(j['time'])
+        self.attachment_id = kwargs['attachment_id']
+        self.author = kwargs['author']
+        self.bug_id = kwargs['bug_id']
+        self.creation_time = str2datetime(kwargs['creation_time'])
+        self.creator = kwargs['creator']
+        self.id = kwargs['id']
+        self.is_private = kwargs['is_private']
+        self.text = kwargs['text']
+        self.time = str2datetime(kwargs['time'])
 
-        if 'tags' in j:
-            c.tags = set(j['tags'])
+        if 'tags' in kwargs:
+            self.tags = set(kwargs['tags'])
 
-        return c
--- a/pylib/Bugsy/bugsy/bugsy.py
+++ b/pylib/Bugsy/bugsy/bugsy.py
@@ -1,15 +1,17 @@
 import json
 
 import requests
 from bug import Bug
 from search import Search
 
 
+
+
 class BugsyException(Exception):
     """
         If while interacting with Bugzilla and we try do something that is not
         supported this error will be raised.
     """
     def __init__(self, msg):
         self.msg = msg
 
@@ -27,16 +29,19 @@ class LoginException(Exception):
     def __str__(self):
         return "Message: %s" % self.msg
 
 class Bugsy(object):
     """
         Bugsy allows easy getting and putting of Bugzilla bugs
     """
 
+    DEFAULT_SEARCH = ['version', 'id', 'summary', 'status', 'op_sys',
+                      'resolution', 'product', 'component', 'platform']
+
     def __init__(self, username=None, password=None, bugzilla_url='https://bugzilla.mozilla.org/rest'):
         """
             Initialises a new instance of Bugsy
 
             :param username: Username to login with. Defaults to None
             :param password: Password to login with. Defaults to None
             :param bugzilla_url: URL endpoint to interact with. Defaults to https://bugzilla.mozilla.org/rest
 
@@ -65,17 +70,17 @@ class Bugsy(object):
             Get a bug from Bugzilla. If there is a login token created during object initialisation
             it will be part of the query string passed to Bugzilla
 
             :param bug_number: Bug Number that will be searched. If found will return a Bug object.
 
             >>> bugzilla = Bugsy()
             >>> bug = bugzilla.get(123456)
         """
-        bug = self.request('bug/%s' % bug_number).json()
+        bug = self.request('bug/%s' % bug_number, params={"include_fields" : self. DEFAULT_SEARCH}).json()
         return Bug(self, **bug['bugs'][0])
 
     def put(self, bug):
         """
             This method allows you to create or update a bug on Bugzilla. You will have had to pass
             in a valid username and password to the object initialisation and recieved back a token.
 
             :param bug: A Bug object either created by hand or by using get()
@@ -94,31 +99,31 @@ class Bugsy(object):
 
         if not isinstance(bug, Bug):
             raise BugsyException("Please pass in a Bug object when posting to Bugzilla")
 
         if not bug.id:
             result = self.request('bug', 'POST', data=bug.to_dict()).json()
             if not result.has_key('error'):
                 bug._bug['id'] = result['id']
+                bug._bugsy = self
             else:
                 raise BugsyException(result['message'])
         else:
             self.session.post('%s/bug/%s' % (self.bugzilla_url, bug.id),
                 data=bug.to_dict())
 
-    def search_for():
-        doc = "The search_for property."
-        def fget(self):
-            return Search(self)
-        return locals()
-    search_for = property(**search_for())
+    @property
+    def search_for(self):
+        return Search(self)
 
     def request(self, path, method='GET', **kwargs):
         """Perform a HTTP request.
 
         Given a relative Bugzilla URL path, an optional request method,
         and arguments suitable for requests.Request(), perform a
         HTTP request.
         """
+        headers = {"User-Agent": "Bugsy"}
+        kwargs['headers'] = headers
         url = '%s/%s' % (self.bugzilla_url, path)
         return self.session.request(method, url, **kwargs)
 
--- a/pylib/Bugsy/bugsy/search.py
+++ b/pylib/Bugsy/bugsy/search.py
@@ -1,29 +1,32 @@
+import copy
+
 import requests
 from bug import Bug
+import bugsy as Bugsy
 
 
 class Search(object):
     """
         This allows searching for bugs in Bugzilla
     """
     def __init__(self, bugsy):
         """
             Initialises the search object
 
             :param bugsy: Bugsy instance to use to connect to Bugzilla.
         """
         self._bugsy = bugsy
-        self._includefields = ['version', 'id', 'summary', 'status', 'op_sys',
-                              'resolution', 'product', 'component', 'platform']
+        self._includefields = copy.copy(bugsy.DEFAULT_SEARCH)
         self._keywords = []
         self._assigned = []
         self._summaries = []
         self._whiteboard = []
+        self._bug_numbers = []
 
     def include_fields(self, *args):
         r"""
             Include fields is the fields that you want to be returned when searching. These
             are in addition to the fields that are always included below.
 
             :param args: items passed in will be turned into a list
             :returns: :class:`Search`
@@ -83,35 +86,55 @@ class Search(object):
             :param args: items passed in will be turned into a list
             :returns: :class:`Search`
 
             >>> bugzilla.search_for.whiteboard("affects")
         """
         self._whiteboard = list(args)
         return self
 
+    def bug_number(self, bug_numbers):
+        r"""
+            When you want to search for a bugs and be able to change the fields returned.
+
+            :param bug_numbers: A string for the bug number or a list of strings
+            :returns: :class:`Search`
+
+            >>> bugzilla.search_for.bug_number(['123123', '123456'])
+        """
+        self._bug_numbers = list(bug_numbers)
+        return self
+
     def search(self):
         r"""
             Call the Bugzilla endpoint that will do the search. It will take the information
             used in other methods on the Search object and build up the query string. If no
             bugs are found then an empty list is returned.
 
             >>> bugs = bugzilla.search_for\
             ...                .keywords("checkin-needed")\
             ...                .include_fields("flags")\
             ...                .search()
         """
         params = {}
         if self._includefields:
             params['include_fields'] = list(self._includefields)
-        if self._keywords:
-            params['keywords'] = list(self._keywords)
-        if self._assigned:
-            params['assigned_to'] = list(self._assigned)
-        if self._summaries:
-            params['short_desc_type'] = 'allwordssubstr'
-            params['short_desc'] = list(self._summaries)
-        if self._whiteboard:
-            params['short_desc_type'] = 'allwordssubstr'
-            params['whiteboard'] = list(self._whiteboard)
+        if self._bug_numbers:
+            bugs = []
+            for bug in self._bug_numbers:
+                result = self._bugsy.request('bug/%s' % bug, params=params).json()
+                bugs.append(Bug(self._bugsy, **result['bugs'][0]))
 
-        results = self._bugsy.request('bug', params=params).json()
-        return [Bug(self._bugsy, **bug) for bug in results['bugs']]
+            return bugs
+        else:
+            if self._keywords:
+                params['keywords'] = list(self._keywords)
+            if self._assigned:
+                params['assigned_to'] = list(self._assigned)
+            if self._summaries:
+                params['short_desc_type'] = 'allwordssubstr'
+                params['short_desc'] = list(self._summaries)
+            if self._whiteboard:
+                params['short_desc_type'] = 'allwordssubstr'
+                params['whiteboard'] = list(self._whiteboard)
+
+            results = self._bugsy.request('bug', params=params).json()
+            return [Bug(self._bugsy, **bug) for bug in results['bugs']]
new file mode 100644
--- /dev/null
+++ b/pylib/Bugsy/docs/source/comment.rst
@@ -0,0 +1,9 @@
+.. toctree::
+   :maxdepth: 2
+
+:mod:`Comment`
+---------------------
+.. versionchanged:: 0.3
+.. automodule:: bugsy
+.. autoclass:: Comment
+   :members:
\ No newline at end of file
--- a/pylib/Bugsy/docs/source/index.rst
+++ b/pylib/Bugsy/docs/source/index.rst
@@ -82,24 +82,46 @@ o you just chain the items that you need
     bugzilla = bugsy.Bugsy()
     bugs = bugzilla.search_for\
                     .keywords("checkin-needed")\
                     .include_fields("flags")\
                     .search()
 
 More details can be found in from the :class:`Search` class
 
+Comments
+~~~~~~~~
+
+Getting comments from a bug
+
+.. code-block:: python
+
+    import bugsy
+    bugzilla = bugsy.Bugsy()
+    bug = bugzilla.get(123456)
+    comments = bug.get_comments()
+    comments[0].text # Returns  "I <3 Sausages"
+
+Adding comments to a bug
+
+.. code-block:: python
+
+    import bugsy
+    bugzilla = bugsy.Bugsy()
+    bug = bugzilla.get(123456)
+    bug.add_comment("And I love bacon too!")
 
 To see further details look at:
 
 .. toctree::
    :maxdepth: 2
 
    bugsy.rst
    bug.rst
+   comment.rst
    search_bug.rst
 
 
 Indices and tables
 ==================
 
 * :ref:`genindex`
 * :ref:`modindex`
new file mode 100644
--- /dev/null
+++ b/pylib/Bugsy/example/add_comments.py
@@ -0,0 +1,8 @@
+import bugsy
+bz = bugsy.Bugsy("username", "password", "https://bugzilla-dev.allizom.org/rest")
+bug = bugsy.Bug()
+bug.summary = "I love cheese"
+bug.add_comment('I do love sausages too')
+bz.put(bug)
+
+bug.add_comment('I do love eggs too')
--- a/pylib/Bugsy/setup.py
+++ b/pylib/Bugsy/setup.py
@@ -1,12 +1,12 @@
 from setuptools import setup, find_packages
 
 setup(name='bugsy',
-      version='0.2.0',
+      version='0.3.0',
       description='A library for interacting Bugzilla Native REST API',
       author='David Burns',
       author_email='david.burns at theautomatedtester dot co dot uk',
       url='http://oss.theautomatedtester.co.uk/bugzilla',
       classifiers=['Development Status :: 3 - Alpha',
                   'Intended Audience :: Developers',
                   'License :: OSI Approved :: Apache Software License',
                   'Operating System :: POSIX',
--- a/pylib/Bugsy/tests/test_bugs.py
+++ b/pylib/Bugsy/tests/test_bugs.py
@@ -1,8 +1,9 @@
+import datetime
 import responses
 import json
 
 from bugsy import Bugsy, Bug
 from bugsy import BugException
 
 example_return = {u'faults': [], u'bugs': [{u'cf_tracking_firefox29': u'---', u'classification': u'Other', u'creator': u'jgriffin@mozilla.com', u'cf_status_firefox30':
 u'---', u'depends_on': [], u'cf_status_firefox32': u'---', u'creation_time': u'2014-05-28T23:57:58Z', u'product': u'Release Engineering', u'cf_user_story': u'', u'dupe_of': None, u'cf_tracking_firefox_relnote': u'---', u'keywords': [], u'cf_tracking_b2g18': u'---', u'summary': u'Schedule Mn tests on o\
@@ -17,16 +18,51 @@ 32': u'---', u'cf_tracking_firefox31': u
 status_b2g18': u'---', u'cf_status_b2g_1_4': u'---', u'url': u'', u'creator_detail': {u'id': 347295, u'email': u'jgriffin@mozilla.com', u'name': u'jgri\
 ffin@mozilla.com', u'real_name': u'Jonathan Griffin (:jgriffin)'}, u'whiteboard': u'', u'cf_status_b2g_2_0': u'---', u'cc_detail': [{u'id': 30066, u'em\
 ail': u'coop@mozilla.com', u'name': u'coop@mozilla.com', u'real_name': u'Chris Cooper [:coop]'}, {u'id': 397261, u'email': u'dburns@mozilla.com', u'nam\
 e': u'dburns@mozilla.com', u'real_name': u'David Burns :automatedtester'}, {u'id': 438921, u'email': u'jlund@mozilla.com', u'name': u'jlund@mozilla.com ', u'real_name': u'Jordan Lund (:jlund)'}, {u'id': 418814, u'email': u'mdas@mozilla.com', u'name': u'mdas@mozilla.com', u'real_name': u'Malini Das [:md\
 as]'}], u'alias': None, u'cf_tracking_b2g_v1_2': u'---', u'cf_tracking_b2g_v1_3': u'---', u'flags': [], u'assigned_to': u'jgriffin@mozilla.com', u'cf_s\
 tatus_firefox_esr24': u'---', u'resolution': u'FIXED', u'last_change_time': u'2014-05-30T21:20:17Z', u'cc': [u'coop@mozilla.com', u'dburns@mozilla.com'
 , u'jlund@mozilla.com', u'mdas@mozilla.com'], u'cf_blocking_fennec': u'---'}]}
 
+comments_return = {
+    u'bugs': {
+        u'1017315': {
+            u'comments': [
+                {
+                   u'attachment_id': None,
+                   u'author': u'gps@mozilla.com',
+                   u'bug_id': 1017315,
+                   u'creation_time': u'2014-03-27T23:47:45Z',
+                   u'creator': u'gps@mozilla.com',
+                   u'id': 8589785,
+                   u'is_private': False,
+                   u'raw_text': u'raw text 1',
+                   u'tags': [u'tag1', u'tag2'],
+                   u'text': u'text 1',
+                   u'time': u'2014-03-27T23:47:45Z'
+                },
+                {
+                   u'attachment_id': 8398207,
+                   u'author': u'gps@mozilla.com',
+                   u'bug_id': 1017315,
+                   u'creation_time': u'2014-03-27T23:56:34Z',
+                   u'creator': u'gps@mozilla.com',
+                   u'id': 8589812,
+                   u'is_private': True,
+                   u'raw_text': u'raw text 2',
+                   u'tags': [],
+                   u'text': u'text 2',
+                   u'time': u'2014-03-27T23:56:34Z'
+                },
+            ],
+        },
+    },
+}
+
 def test_can_create_bug_and_set_summary_afterwards():
     bug = Bug()
     assert bug.id == None, "Id has been set"
     assert bug.summary == '', "Summary is not set to nothing on plain initialisation"
     bug.summary = "Foo"
     assert bug.summary == 'Foo', "Summary is not being set"
     assert bug.status == '', 'Status has been set'
 
@@ -86,17 +122,17 @@ def test_we_can_pass_in_dict_and_get_a_b
 
 def test_we_can_get_a_dict_version_of_the_bug():
     bug = Bug(**example_return['bugs'][0])
     result = bug.to_dict()
     assert example_return['bugs'][0]['id'] == result['id']
 
 @responses.activate
 def test_we_can_update_a_bug_from_bugzilla():
-    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315',
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
                       body=json.dumps(example_return), status=200,
                       content_type='application/json', match_querystring=True)
     bugzilla = Bugsy()
     bug = bugzilla.get(1017315)
     import copy
     bug_dict = copy.deepcopy(example_return)
     bug_dict['bugs'][0]['status'] = "REOPENED"
     responses.reset()
@@ -114,17 +150,17 @@ def test_we_cant_update_unless_we_have_a
         assert str(e) == "Message: Unable to update bug that isn't in Bugzilla"
 
 @responses.activate
 def test_we_can_update_a_bug_with_login_token():
   responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                         body='{"token": "foobar"}', status=200,
                         content_type='application/json', match_querystring=True)
 
-  responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar',
+  responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
                     body=json.dumps(example_return), status=200,
                     content_type='application/json', match_querystring=True)
   bugzilla = Bugsy("foo", "bar")
   bug = bugzilla.get(1017315)
   import copy
   bug_dict = copy.deepcopy(example_return)
   bug_dict['bugs'][0]['status'] = "REOPENED"
   responses.reset()
@@ -132,24 +168,77 @@ def test_we_can_update_a_bug_with_login_
                     body=json.dumps(bug_dict), status=200,
                     content_type='application/json', match_querystring=True)
   bug.update()
   assert bug.id == 1017315
   assert bug.status == 'REOPENED'
   assert bug.summary == 'Schedule Mn tests on opt Linux builds on cedar'
 
 @responses.activate
-def test_that_we_can_add_a_comment_to_a_bug():
+def test_that_we_can_add_a_comment_to_a_bug_before_it_is_put():
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
+                          body='{"token": "foobar"}', status=200,
+                          content_type='application/json', match_querystring=True)
+
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
+                      body=json.dumps(example_return), status=200,
+                      content_type='application/json', match_querystring=True)
+    bugzilla = Bugsy("foo", "bar")
+    bug = Bug()
+    bug.summary = "I like cheese"
+    bug.add_comment("I like sausages")
+
+    bug_dict = bug.to_dict().copy()
+    bug_dict['id'] = 123123
+
+    responses.add(responses.POST, 'https://bugzilla.mozilla.org/rest/bug?token=foobar',
+                      body=json.dumps(bug_dict), status=200,
+                      content_type='application/json', match_querystring=True)
+    bugzilla.put(bug)
+
+@responses.activate
+def test_that_we_can_add_a_comment_to_an_existing_bug():
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                           body='{"token": "foobar"}', status=200,
                           content_type='application/json', match_querystring=True)
 
-    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar',
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
                       body=json.dumps(example_return), status=200,
                       content_type='application/json', match_querystring=True)
     bugzilla = Bugsy("foo", "bar")
     bug = bugzilla.get(1017315)
+
+    responses.add(responses.POST, 'https://bugzilla.mozilla.org/rest/bug/1017315/comment?token=foobar',
+                      body=json.dumps({}), status=200,
+                      content_type='application/json', match_querystring=True)
+
     bug.add_comment("I like sausages")
 
-    responses.add(responses.POST, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar',
+    assert len(responses.calls) == 3
+
+@responses.activate
+def test_comment_retrieval():
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
+                        body='{"token": "foobar"}', status=200,
+                        content_type='application/json', match_querystring=True)
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
                       body=json.dumps(example_return), status=200,
                       content_type='application/json', match_querystring=True)
-    bugzilla.put(bug)
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315/comment?token=foobar',
+                    body=json.dumps(comments_return), status=200,
+                    content_type='application/json', match_querystring=True)
+
+    bugzilla = Bugsy("foo", "bar")
+    bug = bugzilla.get(1017315)
+    comments = bug.get_comments()
+    assert len(comments) == 2
+    c1 = comments[0]
+    assert c1.attachment_id is None
+    assert c1.author == u'gps@mozilla.com'
+    assert c1.bug_id == 1017315
+    assert c1.creation_time == datetime.datetime(2014, 03, 27, 23, 47, 45)
+    assert c1.creator == u'gps@mozilla.com'
+    assert c1.id == 8589785
+    assert c1.is_private is False
+    assert c1.text == u'text 1'
+    assert c1.tags == set([u'tag1', u'tag2'])
+    assert c1.time == datetime.datetime(2014, 03, 27, 23, 47, 45)
+
--- a/pylib/Bugsy/tests/test_bugsy.py
+++ b/pylib/Bugsy/tests/test_bugsy.py
@@ -51,32 +51,32 @@ def test_we_cant_post_without_passing_a_
     try:
         bugzilla.put("foo")
         assert 1 == 0, "Should have thrown an error about type when calling put"
     except BugsyException as e:
         assert str(e) == "Message: Please pass in a Bug object when posting to Bugzilla"
 
 @responses.activate
 def test_we_can_get_a_bug():
-    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315',
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
                       body=json.dumps(example_return), status=200,
                       content_type='application/json', match_querystring=True)
     bugzilla = Bugsy()
     bug = bugzilla.get(1017315)
     assert bug.id == 1017315
     assert bug.status == 'RESOLVED'
     assert bug.summary == 'Schedule Mn tests on opt Linux builds on cedar'
 
 @responses.activate
 def test_we_can_get_a_bug_with_login_token():
   responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                         body='{"token": "foobar"}', status=200,
                         content_type='application/json', match_querystring=True)
 
-  responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar',
+  responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?token=foobar&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
                     body=json.dumps(example_return), status=200,
                     content_type='application/json', match_querystring=True)
   bugzilla = Bugsy("foo", "bar")
   bug = bugzilla.get(1017315)
   assert bug.id == 1017315
   assert bug.status == 'RESOLVED'
   assert bug.summary == 'Schedule Mn tests on opt Linux builds on cedar'
 
@@ -143,8 +143,18 @@ def test_we_handle_errors_from_bugzilla_
 
   bug_dict = example_return.copy()
   bug_dict['summary'] = 'I love foo but hate bar'
   bug = Bug(**bug_dict['bugs'][0])
   try:
       bugzilla.put(bug)
   except BugsyException as e:
       assert str(e) == "Message: You must select/enter a component."
+
+@responses.activate
+def test_we_can_set_the_user_agent_to_bugsy():
+  responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
+                    body='{"token": "foobar"}', status=200,
+                    content_type='application/json', match_querystring=True)
+  Bugsy("foo", "bar")
+  assert responses.calls[0].request.headers['User-Agent'] == "Bugsy"
+
+
--- a/pylib/Bugsy/tests/test_search.py
+++ b/pylib/Bugsy/tests/test_search.py
@@ -42,17 +42,16 @@ def test_we_only_ask_for_the_include_fie
                "resolution" : "",
                "status" : "NEW",
                "summary" : "Marionette thinks that the play button in the music app is not displayed",
                "version" : "unspecified"
             }
          ]
       }
 
-
   responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug?assigned_to=dburns@mozilla.com&whiteboard=affects&short_desc_type=allwordssubstr&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform&include_fields=flags',
                     body=json.dumps(include_return), status=200,
                     content_type='application/json', match_querystring=True)
 
   bugzilla = Bugsy()
   bugs = bugzilla.search_for\
           .include_fields('flags')\
           .assigned_to("dburns@mozilla.com")\
@@ -257,9 +256,47 @@ def test_we_can_search_whiteboard_fields
     bugs = bugzilla.search_for\
             .assigned_to('dburns@mozilla.com')\
             .whiteboard("affects")\
             .search()
 
     assert len(responses.calls) == 1
     assert len(bugs) == 2
     assert bugs[0].product == whiteboard_return['bugs'][0]['product']
-    assert bugs[0].summary == whiteboard_return['bugs'][0]['summary']
\ No newline at end of file
+    assert bugs[0].summary == whiteboard_return['bugs'][0]['summary']
+
+@responses.activate
+def test_we_can_search_for_a_list_of_bug_numbers():
+    return_1 = {
+     "bugs" : [
+        {
+           "component" : "CSS Parsing and Computation",
+           "product" : "Core",
+           "summary" : "Map \"rebeccapurple\" to #663399 in named color list."
+        }
+      ]
+    }
+
+    return_2 = {
+     "bugs" : [
+        {
+           "component" : "Marionette",
+           "product" : "Testing",
+           "summary" : "Marionette thinks that the play button in the music app is not displayed"
+        }
+      ]
+    }
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315?include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
+                      body=json.dumps(return_1), status=200,
+                      content_type='application/json', match_querystring=True)
+
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017316?include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
+                      body=json.dumps(return_2), status=200,
+                      content_type='application/json', match_querystring=True)
+    bugzilla = Bugsy()
+    bugs = bugzilla.search_for\
+            .bug_number(['1017315', '1017316'])\
+            .search()
+
+    assert len(responses.calls) == 2
+    assert len(bugs) == 2
+    assert bugs[0].product == return_1['bugs'][0]['product']
+    assert bugs[0].summary == return_1['bugs'][0]['summary']
\ No newline at end of file