Repository: googleapis/google-cloud-python

Issue:
BigQuery: magic cell in IPython/Jupyter should hide bad query tracebacks
Incorrect user queries using IPython/Jupyter magic cells generate a large traceback. For example:

```
In [1]:  %%bigquery
           SELECT badname
           FROM mytable
---------------------------------------------------------------------------
BadRequest                                Traceback (most recent call last)
<ipython-input-4-f03917bb6736> in <module>
----> 1 get_ipython().run_cell_magic('bigquery', '', 'SELECT badname\nFROM mytable\n')

~/cloud/bigmerge/venv/lib/python3.6/site-packages/IPython/core/interactiveshell.py in run_cell_magic(self, magic_name, line, cell)
   2321             magic_arg_s = self.var_expand(line, stack_depth)
   2322             with self.builtin_trap:
-> 2323                 result = fn(magic_arg_s, cell)
   2324             return result
   2325 

~/cloud/bigmerge/venv/lib/python3.6/site-packages/google/cloud/bigquery/magics.py in _cell_magic(line, query)
    322     job_config.query_parameters = params
    323     job_config.use_legacy_sql = args.use_legacy_sql
--> 324     query_job = _run_query(client, query, job_config)
    325 
    326     if not args.verbose:

~/cloud/bigmerge/venv/lib/python3.6/site-packages/google/cloud/bigquery/magics.py in _run_query(client, query, job_config)
    251             time.time() - start_time), end='')
    252         try:
--> 253             query_job.result(timeout=0.5)
    254             break
    255         except futures.TimeoutError:

~/cloud/bigmerge/venv/lib/python3.6/site-packages/google/cloud/bigquery/job.py in result(self, timeout, retry)
   2735             not complete in the given timeout.
   2736         """
-> 2737         super(QueryJob, self).result(timeout=timeout)
   2738         # Return an iterator instead of returning the job.
   2739         if not self._query_results:

~/cloud/bigmerge/venv/lib/python3.6/site-packages/google/cloud/bigquery/job.py in result(self, timeout)
    697             self._begin()
    698         # TODO: modify PollingFuture so it can pass a retry argument to done().
--> 699         return super(_AsyncJob, self).result(timeout=timeout)
    700 
    701     def cancelled(self):

~/cloud/bigmerge/venv/lib/python3.6/site-packages/google/api_core/future/polling.py in result(self, timeout)
    125             # pylint: disable=raising-bad-type
    126             # Pylint doesn't recognize that this is valid in this case.
--> 127             raise self._exception
    128 
    129         return self._result

BadRequest: 400 Unrecognized name: badname at [1:8]
```

Since the above code is executed implicitly, it should be hidden from the user and, instead, only the user-relevant part of the error should be shown. Like:
```
In [1]:  %%bigquery
           SELECT badname
           FROM mytable

Your query contained an error. Unrecognized name: badname at [1:8]
```



Discussion:

Resolving patch:
diff --git a/bigquery/google/cloud/bigquery/magics.py b/bigquery/google/cloud/bigquery/magics.py
--- a/bigquery/google/cloud/bigquery/magics.py
+++ b/bigquery/google/cloud/bigquery/magics.py
@@ -130,6 +130,7 @@
 from __future__ import print_function
 
 import ast
+import sys
 import time
 from concurrent import futures
 
@@ -417,11 +418,20 @@ def _cell_magic(line, query):
     elif args.maximum_bytes_billed is not None:
         value = int(args.maximum_bytes_billed)
         job_config.maximum_bytes_billed = value
-    query_job = _run_query(client, query, job_config)
+
+    error = None
+    try:
+        query_job = _run_query(client, query, job_config)
+    except Exception as ex:
+        error = str(ex)
 
     if not args.verbose:
         display.clear_output()
 
+    if error:
+        print("\nERROR:\n", error, file=sys.stderr)
+        return
+
     result = query_job.to_dataframe(bqstorage_client=bqstorage_client)
     if args.destination_var:
         IPython.get_ipython().push({args.destination_var: result})


Test patch:
diff --git a/bigquery/tests/unit/test_magics.py b/bigquery/tests/unit/test_magics.py
--- a/bigquery/tests/unit/test_magics.py
+++ b/bigquery/tests/unit/test_magics.py
@@ -32,6 +32,7 @@
 except ImportError:  # pragma: NO COVER
     IPython = None
 
+from google.api_core import exceptions
 import google.auth.credentials
 
 try:
@@ -815,3 +816,30 @@ def test_bigquery_magic_with_improperly_formatted_params():
 
     with pytest.raises(SyntaxError):
         ip.run_cell_magic("bigquery", "--params {17}", sql)
+
+
+@pytest.mark.usefixtures("ipython_interactive")
+def test_bigquery_magic_omits_tracebacks_from_error_message():
+    ip = IPython.get_ipython()
+    ip.extension_manager.load_extension("google.cloud.bigquery")
+
+    credentials_mock = mock.create_autospec(
+        google.auth.credentials.Credentials, instance=True
+    )
+    default_patch = mock.patch(
+        "google.auth.default", return_value=(credentials_mock, "general-project")
+    )
+
+    run_query_patch = mock.patch(
+        "google.cloud.bigquery.magics._run_query",
+        autospec=True,
+        side_effect=exceptions.BadRequest("Syntax error in SQL query"),
+    )
+
+    with run_query_patch, default_patch, io.capture_output() as captured_io:
+        ip.run_cell_magic("bigquery", "", "SELECT foo FROM WHERE LIMIT bar")
+
+    output = captured_io.stderr
+    assert "400 Syntax error in SQL query" in output
+    assert "Traceback (most recent call last)" not in output
+    assert "Syntax error" not in captured_io.stdout