Repository: googleapis/python-bigquery

Issue:
Closing a connection tries to close already closed cursors
When closing a connection the library will call `.close()` on every cursor created for that connection, including closed ones. While the code works, it produces a lot of logs of error level, polluting the logs.

#### Environment details

  - OS type and version: Mac OS 10.15.7
  - Python version: Python 3.8.2
  - pip version: pip 20.3.3
  - `google-cloud-bigquery` version: 2.7.0

#### Steps to reproduce

  1. Close a cursor
  2. Close the connection
  3. Error log shows `Exception closing connection <google.cloud.bigquery.dbapi.connection.Connection object at 0x...>`

#### Code example

```python
from contextlib import closing

# using pybigquery
with closing(engine.raw_connection()) as conn:
    with closing(conn.cursor()) as cursor:
        cursor.execute(sql)
```

#### Stack trace
```
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/sqlalchemy/pool/base.py", line 270, in _close_connection
    self._dialect.do_close(connection)
  File "/usr/local/lib/python3.7/site-packages/sqlalchemy/engine/default.py", line 549, in do_close
    dbapi_connection.close()
  File "/usr/local/lib/python3.7/site-packages/google/cloud/bigquery/dbapi/_helpers.py", line 258, in with_closed_check
    return method(self, *args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/google/cloud/bigquery/dbapi/connection.py", line 79, in close
    cursor_.close()
  File "/usr/local/lib/python3.7/site-packages/google/cloud/bigquery/dbapi/_helpers.py", line 257, in with_closed_check
    raise exc_class(exc_msg)
google.cloud.bigquery.dbapi.exceptions.ProgrammingError: Operating on a closed cursor.
```

#### Suggested fix

```python
# google/cloud/bigquery/dbapi/connection.py
class Connection(object):
    ...
    def close(self):
        ...
        for cursor_ in self._cursors_created:
            if not cursor_._closed:
                cursor_.close()
```


Discussion:

Resolving patch:
diff --git a/google/cloud/bigquery/dbapi/connection.py b/google/cloud/bigquery/dbapi/connection.py
--- a/google/cloud/bigquery/dbapi/connection.py
+++ b/google/cloud/bigquery/dbapi/connection.py
@@ -76,7 +76,8 @@ def close(self):
             self._bqstorage_client._transport.grpc_channel.close()
 
         for cursor_ in self._cursors_created:
-            cursor_.close()
+            if not cursor_._closed:
+                cursor_.close()
 
     def commit(self):
         """No-op, but for consistency raise an error if connection is closed."""


Test patch:
diff --git a/tests/unit/test_dbapi_connection.py b/tests/unit/test_dbapi_connection.py
--- a/tests/unit/test_dbapi_connection.py
+++ b/tests/unit/test_dbapi_connection.py
@@ -176,6 +176,22 @@ def test_close_closes_all_created_cursors(self):
         self.assertTrue(cursor_1._closed)
         self.assertTrue(cursor_2._closed)
 
+    def test_close_closes_only_open_created_cursors(self):
+        connection = self._make_one(client=self._mock_client())
+        cursor_1 = connection.cursor()
+        cursor_2 = connection.cursor()
+        self.assertFalse(cursor_1._closed)
+        self.assertFalse(cursor_2._closed)
+
+        cursor_1.close()
+        self.assertTrue(cursor_1._closed)
+        cursor_1.close = mock.MagicMock()
+
+        connection.close()
+
+        self.assertFalse(cursor_1.close.called)
+        self.assertTrue(cursor_2._closed)
+
     def test_does_not_keep_cursor_instances_alive(self):
         from google.cloud.bigquery.dbapi import Cursor