diff --git a/tools/gh_auth.py b/tools/gh_api.py
similarity index 61%
rename from tools/gh_auth.py
rename to tools/gh_api.py
index 60c1e6e..6b042ed 100644
--- a/tools/gh_auth.py
+++ b/tools/gh_api.py
@@ -7,7 +7,6 @@ except NameError:
     pass
 
 import requests
-import keyring
 import getpass
 import json
 
@@ -15,7 +14,14 @@ import json
 # password
 fake_username = 'ipython_tools'
 
+token = None
 def get_auth_token():
+    global token
+    
+    if token is not None:
+        return token
+    
+    import keyring
     token = keyring.get_password('github', fake_username)
     if token is not None:
         return token
@@ -28,9 +34,11 @@ def get_auth_token():
     
     auth_request = {
       "scopes": [
-        "public_repo"
+        "public_repo",
+        "gist"
       ],
-      "note": "IPython tools"
+      "note": "IPython tools",
+      "note_url": "https://github.com/ipython/ipython/tree/master/tools",
     }
     response = requests.post('https://api.github.com/authorizations',
                             auth=(user, pw), data=json.dumps(auth_request))
@@ -39,4 +47,34 @@ def get_auth_token():
     keyring.set_password('github', fake_username, token)
     return token
 
+def make_auth_header():
+    return {'Authorization': 'token ' + get_auth_token()}
+
+def post_issue_comment(project, num, body):
+    url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
+    payload = json.dumps({'body': body})
+    r = requests.post(url, data=payload, headers=make_auth_header())
+
+def post_gist(content, description='', filename='file', auth=False):
+    """Post some text to a Gist, and return the URL."""
+    post_data = json.dumps({
+      "description": description,
+      "public": True,
+      "files": {
+        filename: {
+          "content": content
+        }
+      }
+    }).encode('utf-8')
+    
+    headers = make_auth_header() if auth else {}
+    response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
+    response.raise_for_status()
+    response_data = json.loads(response.text)
+    return response_data['html_url']
     
+def get_pull_request(project, num):
+    url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
+    response = requests.get(url)
+    response.raise_for_status()
+    return json.loads(response.text)
diff --git a/tools/test_pr.py b/tools/test_pr.py
index 3ce4766..de53fb1 100755
--- a/tools/test_pr.py
+++ b/tools/test_pr.py
@@ -10,14 +10,16 @@ from __future__ import print_function
 
 import errno
 from glob import glob
+import io
 import json
 import os
 import re
 import requests
 import shutil
 from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProcessError
+import sys
 
-import gh_auth
+import gh_api
 
 basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests")
 repodir = os.path.join(basedir, "ipython")
@@ -66,10 +68,6 @@ def setup():
     check_call(['git', 'pull', ipy_repository, 'master'])
     os.chdir(basedir)
 
-def get_pull_request(num):
-    url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=gh_project, num=num)
-    return json.loads(requests.get(url).text)
-
 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
                              r"\s*(.*?)\n")
 def get_missing_libraries(log):
@@ -108,22 +106,6 @@ def run_tests(venv):
     except CalledProcessError as e:
         return False, e.output.decode('utf-8')
 
-def post_gist(content, description='IPython test log', filename="results.log"):
-    """Post some text to a Gist, and return the URL."""
-    post_data = json.dumps({
-      "description": description,
-      "public": True,
-      "files": {
-        filename: {
-          "content": content
-        }
-      }
-    }).encode('utf-8')
-    
-    response = requests.post("https://api.github.com/gists", data=post_data)
-    response_data = json.loads(response.text)
-    return response_data['html_url']
-
 def markdown_format(pr, results):
     def format_result(py, passed, gist_url, missing_libraries):
         s = "* %s: " % py
@@ -149,18 +131,33 @@ def markdown_format(pr, results):
 
 def post_results_comment(pr, results, num):
     body = markdown_format(pr, results)
-    url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=gh_project, num=num)
-    payload = json.dumps({'body': body})
-    auth_token = gh_auth.get_auth_token()
-    headers = {'Authorization': 'token ' + auth_token}
-    r = requests.post(url, data=payload, headers=headers)
+    gh_api.post_issue_comment(gh_project, num, body)
 
+def print_results(pr, results):
+    print("\n")
+    if pr['mergeable']:
+        print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
+    else:
+        print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
+    print("Platform:", sys.platform)
+    for py, passed, gist_url, missing_libraries in results:
+        if passed:
+            print(py, ":", "OK")
+        else:
+            print(py, ":", "Failed")
+            print("    Test log:", gist_url)
+        if missing_libraries:
+            print("    Libraries not available:", missing_libraries)
+    print("Not available for testing:", ", ".join(unavailable_pythons))
 
-if __name__ == '__main__':
-    import sys
-    num = sys.argv[1]
+def test_pr(num, post_results=True):
+    # Get Github authorisation first, so that the user is prompted straight away
+    # if their login is needed.
+    if post_results:
+        gh_api.get_auth_token()
+    
     setup()
-    pr = get_pull_request(num)
+    pr = gh_api.get_pull_request(gh_project, num)
     get_branch(repo=pr['head']['repo']['clone_url'], 
                  branch=pr['head']['ref'],
                  owner=pr['head']['repo']['owner']['login'],
@@ -174,23 +171,27 @@ if __name__ == '__main__':
         if passed:
             results.append((py, True, None, missing_libraries))
         else:
-            gist_url = post_gist(log)
-            results.append((py, False, gist_url, missing_libraries))
+            if post_results:
+                result_locn = gh_api.post_gist(log, description='IPython test log',
+                                    filename="results.log", auth=True)
+            else:
+                result_locn = os.path.join(venv, pr['head']['sha'][:7]+".log")
+                with io.open(result_locn, 'w', encoding='utf-8') as f:
+                    f.write(log)
+            results.append((py, False, result_locn, missing_libraries))
     
-    print("\n")
-    if pr['mergeable']:
-        print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
-    else:
-        print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
-    print("Platform:", sys.platform)
-    for py, passed, gist_url, missing_libraries in results:
-        if passed:
-            print(py, ":", "OK")
-        else:
-            print(py, ":", "Failed")
-            print("    Test log:", gist_url)
-        if missing_libraries:
-            print("    Libraries not available:", missing_libraries)
-    print("Not available for testing:", ", ".join(unavailable_pythons))
-    post_results_comment(pr, results, num)
-    print("(Posted to Github)")
+    print_results(pr, results)
+    if post_results:
+        post_results_comment(pr, results, num)
+        print("(Posted to Github)")
+    
+
+if __name__ == '__main__':
+    import argparse
+    parser = argparse.ArgumentParser(description="Test an IPython pull request")
+    parser.add_argument('-l', '--local', action='store_true',
+                        help="Don't publish the results to Github")
+    parser.add_argument('number', type=int, help="The pull request number")
+    
+    args = parser.parse_args()
+    test_pr(args.number, post_results=(not args.local))