Repository: conan-io/conan

Issue:
Conan database is not formerly closed
Coming from https://github.com/conan-io/hooks/pull/17#discussion_r251386053

Seems the Conan database is not being closed when a Conan command finishes and this makes some commands raise in some situations.

The above links shows a workaround to overcome the issue but this would need a proper fix in Conan itself as this could be also important when using the conan client API.

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.



Discussion:

Resolving patch:
diff --git a/conans/client/cmd/user.py b/conans/client/cmd/user.py
--- a/conans/client/cmd/user.py
+++ b/conans/client/cmd/user.py
@@ -6,7 +6,7 @@ def users_list(localdb_file, remotes):
     if not remotes:
         raise ConanException("No remotes defined")
 
-    localdb = LocalDB(localdb_file)
+    localdb = LocalDB.create(localdb_file)
     remotes_info = []
     for remote in remotes:
         user_info = {}
@@ -19,12 +19,11 @@ def users_list(localdb_file, remotes):
 
 
 def users_clean(localdb_file):
-    localdb = LocalDB(localdb_file)
-    localdb.init(clean=True)
+    LocalDB.create(localdb_file, clean=True)
 
 
 def user_set(localdb_file, user, remote_name=None):
-    localdb = LocalDB(localdb_file)
+    localdb = LocalDB.create(localdb_file)
 
     if user.lower() == "none":
         user = None
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -158,7 +158,7 @@ def instance_remote_manager(requester, cache, user_io, hook_manager):
                                         revisions_enabled=cache.config.revisions_enabled,
                                         put_headers=put_headers)
         # To store user and token
-        localdb = LocalDB(cache.localdb)
+        localdb = LocalDB.create(cache.localdb)
         # Wraps RestApiClient to add authentication support (same interface)
         auth_manager = ConanApiAuthManager(rest_api_client, user_io, localdb)
         # Handle remote connections
diff --git a/conans/client/store/localdb.py b/conans/client/store/localdb.py
--- a/conans/client/store/localdb.py
+++ b/conans/client/store/localdb.py
@@ -3,6 +3,7 @@
 from sqlite3 import OperationalError
 
 from conans.errors import ConanException
+from contextlib import contextmanager
 
 REMOTES_USER_TABLE = "users_remotes"
 
@@ -10,32 +11,28 @@
 class LocalDB(object):
 
     def __init__(self, dbfile):
+        self.dbfile = dbfile
+
+    @staticmethod
+    def create(dbfile, clean=False):
+        # Create the database file if it doesn't exist
         if not os.path.exists(dbfile):
             par = os.path.dirname(dbfile)
             if not os.path.exists(par):
                 os.makedirs(par)
             db = open(dbfile, 'w+')
             db.close()
-        self.dbfile = dbfile
-        try:
-            self.connection = sqlite3.connect(self.dbfile,
-                                              detect_types=sqlite3.PARSE_DECLTYPES)
-            self.connection.text_factory = str
-        except Exception as e:
-            raise ConanException('Could not connect to local cache', e)
-        self.init()
 
-    def init(self, clean=False):
-        cursor = None
+        connection = sqlite3.connect(dbfile, detect_types=sqlite3.PARSE_DECLTYPES)
+
         try:
-            cursor = self.connection.cursor()
+            cursor = connection.cursor()
             if clean:
                 cursor.execute("drop table if exists %s" % REMOTES_USER_TABLE)
                 try:
                     # https://github.com/ghaering/pysqlite/issues/109
-                    self.connection.isolation_level = None
+                    connection.isolation_level = None
                     cursor.execute('VACUUM')  # Make sure the DB is cleaned, drop doesn't do that
-                    self.connection.isolation_level = ''  # default value of isolation_level
                 except OperationalError:
                     pass
 
@@ -45,36 +42,46 @@ def init(self, clean=False):
             message = "Could not initialize local sqlite database"
             raise ConanException(message, e)
         finally:
-            if cursor:
-                cursor.close()
+            connection.close()
 
-    def get_login(self, remote_url):
-        '''Returns login credentials.
-        This method is also in charge of expiring them.
-        '''
+        return LocalDB(dbfile)
+
+    @contextmanager
+    def _connect(self):
+        connection = sqlite3.connect(self.dbfile, detect_types=sqlite3.PARSE_DECLTYPES)
+        connection.text_factory = str
         try:
-            statement = self.connection.cursor()
-            statement.execute('select user, token from %s where remote_url="%s"'
-                              % (REMOTES_USER_TABLE, remote_url))
-            rs = statement.fetchone()
-            if not rs:
-                return None, None
-            name = rs[0]
-            token = rs[1]
-            return name, token
-        except Exception:
-            raise ConanException("Couldn't read login\n Try removing '%s' file" % self.dbfile)
+            yield connection
+        finally:
+            connection.close()
+
+    def get_login(self, remote_url):
+        """ Returns login credentials. This method is also in charge of expiring them. """
+        with self._connect() as connection:
+            try:
+                statement = connection.cursor()
+                statement.execute('select user, token from %s where remote_url="%s"'
+                                  % (REMOTES_USER_TABLE, remote_url))
+                rs = statement.fetchone()
+                if not rs:
+                    return None, None
+                name = rs[0]
+                token = rs[1]
+                return name, token
+            except Exception:
+                raise ConanException("Couldn't read login\n Try removing '%s' file" % self.dbfile)
 
     def get_username(self, remote_url):
         return self.get_login(remote_url)[0]
 
     def set_login(self, login, remote_url):
-        """Login is a tuple of (user, token)"""
-        try:
-            statement = self.connection.cursor()
-            statement.execute("INSERT OR REPLACE INTO %s (remote_url, user, token) "
-                              "VALUES (?, ?, ?)" % REMOTES_USER_TABLE,
-                              (remote_url, login[0], login[1]))
-            self.connection.commit()
-        except Exception as e:
-            raise ConanException("Could not store credentials %s" % str(e))
+        """ Login is a tuple of (user, token) """
+        with self._connect() as connection:
+            try:
+                statement = connection.cursor()
+                statement.execute("INSERT OR REPLACE INTO %s (remote_url, user, token) "
+                                  "VALUES (?, ?, ?)" % REMOTES_USER_TABLE,
+                                  (remote_url, login[0], login[1]))
+                connection.commit()
+            except Exception as e:
+                raise ConanException("Could not store credentials %s" % str(e))


Test patch:
diff --git a/conans/test/unittests/util/local_db_test.py b/conans/test/unittests/util/local_db_test.py
--- a/conans/test/unittests/util/local_db_test.py
+++ b/conans/test/unittests/util/local_db_test.py
@@ -10,10 +10,9 @@ class LocalStoreTest(unittest.TestCase):
     def localdb_test(self):
         tmp_dir = temp_folder()
         db_file = os.path.join(tmp_dir, "dbfile")
-        localdb = LocalDB(db_file)
+        localdb = LocalDB.create(db_file)
 
         # Test write and read login
-        localdb.init()
         user, token = localdb.get_login("myurl1")
         self.assertIsNone(user)
         self.assertIsNone(token)