diff --git a/CHANGELOG.md b/CHANGELOG.md index 014de0b65..17b8ff0be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - #710, #561 Implement `except*` syntax (@lieryan) - #711 allow building documentation without having rope module installed (@kloczek) +- #719 Allows the in-memory db to be shared across threads (@tkrabel) - #720 create one sqlite3.Connection per thread using a thread local (@tkrabel) # Release 1.10.0 diff --git a/rope/contrib/autoimport/sqlite.py b/rope/contrib/autoimport/sqlite.py index 9cc9647b5..f02d426ba 100644 --- a/rope/contrib/autoimport/sqlite.py +++ b/rope/contrib/autoimport/sqlite.py @@ -2,6 +2,8 @@ import contextlib import json +from hashlib import sha256 +import secrets import re import sqlite3 import sys @@ -153,14 +155,29 @@ def create_database_connection( memory : bool if true, don't persist to disk """ + + def calculate_project_hash(data: str) -> str: + return sha256(data.encode()).hexdigest() + if not memory and project is None: raise Exception("if memory=False, project must be provided") - db_path: str if memory or project is None or project.ropefolder is None: - db_path = ":memory:" + # Allows the in-memory db to be shared across threads + # See https://www.sqlite.org/inmemorydb.html + project_hash: str + if project is None: + project_hash = secrets.token_hex() + elif project.ropefolder is None: + project_hash = calculate_project_hash(project.address) + else: + project_hash = calculate_project_hash(project.ropefolder.real_path) + return sqlite3.connect( + f"file:rope-{project_hash}:?mode=memory&cache=shared", uri=True + ) else: - db_path = str(Path(project.ropefolder.real_path) / "autoimport.db") - return sqlite3.connect(db_path) + return sqlite3.connect( + str(Path(project.ropefolder.real_path) / "autoimport.db") + ) @property def connection(self): diff --git a/ropetest/conftest.py b/ropetest/conftest.py index 32a35aaa6..ce8a1eab5 100644 --- a/ropetest/conftest.py +++ b/ropetest/conftest.py @@ -13,6 +13,13 @@ def project(): testutils.remove_project(project) +@pytest.fixture +def project2(): + project = testutils.sample_project("another_project") + yield project + testutils.remove_project(project) + + @pytest.fixture def project_path(project): yield pathlib.Path(project.address) diff --git a/ropetest/contrib/autoimport/autoimporttest.py b/ropetest/contrib/autoimport/autoimporttest.py index d65d8b2bf..d8bbd87ee 100644 --- a/ropetest/contrib/autoimport/autoimporttest.py +++ b/ropetest/contrib/autoimport/autoimporttest.py @@ -1,3 +1,4 @@ +import sqlite3 from concurrent.futures import ThreadPoolExecutor from contextlib import closing, contextmanager from textwrap import dedent @@ -27,6 +28,20 @@ def database_list(connection): return list(connection.execute("PRAGMA database_list")) +def test_in_memory_database_share_cache(project, project2): + ai_1 = AutoImport(project, memory=True) + ai_2 = AutoImport(project, memory=True) + + ai_3 = AutoImport(project2, memory=True) + + with ai_1.connection: + ai_1.connection.execute("CREATE TABLE shared(data)") + ai_1.connection.execute("INSERT INTO shared VALUES(28)") + assert ai_2.connection.execute("SELECT data FROM shared").fetchone() == (28,) + with pytest.raises(sqlite3.OperationalError, match="no such table: shared"): + ai_3.connection.execute("SELECT data FROM shared").fetchone() + + def test_autoimport_connection_parameter_with_in_memory( project: Project, autoimport: AutoImport,