diff --git a/.gitignore b/.gitignore index 140ce783c264..fed7aaf56a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -248,6 +248,9 @@ io_file # IPython notebook checkpoints .ipynb_checkpoints +# Python virtual env +venv/ + # Editor temporaries *.swp *~ diff --git a/tools/pythonpkg/duckdb-stubs/__init__.pyi b/tools/pythonpkg/duckdb-stubs/__init__.pyi index b98763f7d3d9..38adc57fb875 100644 --- a/tools/pythonpkg/duckdb-stubs/__init__.pyi +++ b/tools/pythonpkg/duckdb-stubs/__init__.pyi @@ -75,6 +75,7 @@ __version__: str __interactive__: bool __jupyter__: bool +__formatted_python_version__: str class BinderException(ProgrammingError): ... diff --git a/tools/pythonpkg/duckdb/__init__.py b/tools/pythonpkg/duckdb/__init__.py index 04dfb7640dd2..c7dd2c05344f 100644 --- a/tools/pythonpkg/duckdb/__init__.py +++ b/tools/pythonpkg/duckdb/__init__.py @@ -252,6 +252,7 @@ __standard_vector_size__, __interactive__, __jupyter__, + __formatted_python_version__, __version__, apilevel, comment, @@ -269,6 +270,7 @@ "__standard_vector_size__", "__interactive__", "__jupyter__", + "__formatted_python_version__", "__version__", "apilevel", "comment", diff --git a/tools/pythonpkg/duckdb_python.cpp b/tools/pythonpkg/duckdb_python.cpp index 784269f4bd35..0f52ea408bb2 100644 --- a/tools/pythonpkg/duckdb_python.cpp +++ b/tools/pythonpkg/duckdb_python.cpp @@ -1079,6 +1079,7 @@ PYBIND11_MODULE(DUCKDB_PYTHON_LIB_NAME, m) { // NOLINT m.attr("__git_revision__") = DuckDB::SourceID(); m.attr("__interactive__") = DuckDBPyConnection::DetectAndGetEnvironment(); m.attr("__jupyter__") = DuckDBPyConnection::IsJupyter(); + m.attr("__formatted_python_version__") = DuckDBPyConnection::FormattedPythonVersion(); m.def("default_connection", &DuckDBPyConnection::DefaultConnection, "Retrieve the connection currently registered as the default to be used by the module"); m.def("set_default_connection", &DuckDBPyConnection::SetDefaultConnection, diff --git a/tools/pythonpkg/src/include/duckdb_python/pyconnection/pyconnection.hpp b/tools/pythonpkg/src/include/duckdb_python/pyconnection/pyconnection.hpp index aad8c44bc3ca..7beb13ae18c9 100644 --- a/tools/pythonpkg/src/include/duckdb_python/pyconnection/pyconnection.hpp +++ b/tools/pythonpkg/src/include/duckdb_python/pyconnection/pyconnection.hpp @@ -182,6 +182,7 @@ struct DuckDBPyConnection : public enable_shared_from_this { static bool DetectAndGetEnvironment(); static bool IsJupyter(); + static std::string FormattedPythonVersion(); static shared_ptr DefaultConnection(); static void SetDefaultConnection(shared_ptr conn); static PythonImportCache *ImportCache(); @@ -357,6 +358,7 @@ struct DuckDBPyConnection : public enable_shared_from_this { vector> GetStatements(const py::object &query); static PythonEnvironmentType environment; + static std::string formatted_python_version; static void DetectEnvironment(); }; diff --git a/tools/pythonpkg/src/pyconnection.cpp b/tools/pythonpkg/src/pyconnection.cpp index 13cd5e1512ba..8654f9bcfbc6 100644 --- a/tools/pythonpkg/src/pyconnection.cpp +++ b/tools/pythonpkg/src/pyconnection.cpp @@ -71,6 +71,7 @@ DefaultConnectionHolder DuckDBPyConnection::default_connection; DBInstanceCache instance_cache; // NOLINT: allow global shared_ptr DuckDBPyConnection::import_cache = nullptr; // NOLINT: allow global PythonEnvironmentType DuckDBPyConnection::environment = PythonEnvironmentType::NORMAL; // NOLINT: allow global +std::string DuckDBPyConnection::formatted_python_version = ""; DuckDBPyConnection::~DuckDBPyConnection() { try { @@ -83,6 +84,13 @@ DuckDBPyConnection::~DuckDBPyConnection() { } void DuckDBPyConnection::DetectEnvironment() { + // Get the formatted Python version + py::module_ sys = py::module_::import("sys"); + py::object version_info = sys.attr("version_info"); + int major = py::cast(version_info.attr("major")); + int minor = py::cast(version_info.attr("minor")); + DuckDBPyConnection::formatted_python_version = std::to_string(major) + "." + std::to_string(minor); + // If __main__ does not have a __file__ attribute, we are in interactive mode auto main_module = py::module_::import("__main__"); if (py::hasattr(main_module, "__file__")) { @@ -120,6 +128,10 @@ bool DuckDBPyConnection::IsJupyter() { return DuckDBPyConnection::environment == PythonEnvironmentType::JUPYTER; } +std::string DuckDBPyConnection::FormattedPythonVersion() { + return DuckDBPyConnection::formatted_python_version; +} + // NOTE: this function is generated by tools/pythonpkg/scripts/generate_connection_methods.py. // Do not edit this function manually, your changes will be overwritten! @@ -2146,9 +2158,9 @@ shared_ptr DuckDBPyConnection::Connect(const py::object &dat "If set, restores the old behavior of scanning all preceding frames to locate the referenced variable.", LogicalType::BOOLEAN, Value::BOOLEAN(false)); if (!DuckDBPyConnection::IsJupyter()) { - config_dict["duckdb_api"] = Value("python"); + config_dict["duckdb_api"] = Value("python/" + DuckDBPyConnection::FormattedPythonVersion()); } else { - config_dict["duckdb_api"] = Value("python jupyter"); + config_dict["duckdb_api"] = Value("python/" + DuckDBPyConnection::FormattedPythonVersion() + " jupyter"); } config.SetOptionsByName(config_dict); diff --git a/tools/pythonpkg/tests/fast/api/test_config.py b/tools/pythonpkg/tests/fast/api/test_config.py index b9772297163a..5db5f77b5054 100644 --- a/tools/pythonpkg/tests/fast/api/test_config.py +++ b/tools/pythonpkg/tests/fast/api/test_config.py @@ -71,7 +71,7 @@ def test_incorrect_parameter(self, duckdb_cursor): def test_user_agent_default(self, duckdb_cursor): con_regular = duckdb.connect(':memory:') - regex = re.compile("duckdb/.* python") + regex = re.compile("duckdb/.* python/.*") # Expands to: SELECT * FROM pragma_user_agent() assert regex.match(con_regular.sql("PRAGMA user_agent").fetchone()[0]) is not None custom_user_agent = con_regular.sql("SELECT current_setting('custom_user_agent')").fetchone() @@ -79,7 +79,7 @@ def test_user_agent_default(self, duckdb_cursor): def test_user_agent_custom(self, duckdb_cursor): con_regular = duckdb.connect(':memory:', config={'custom_user_agent': 'CUSTOM_STRING'}) - regex = re.compile("duckdb/.* python CUSTOM_STRING") + regex = re.compile("duckdb/.* python/.* CUSTOM_STRING") assert regex.match(con_regular.sql("PRAGMA user_agent").fetchone()[0]) is not None custom_user_agent = con_regular.sql("SELECT current_setting('custom_user_agent')").fetchone() assert custom_user_agent[0] == 'CUSTOM_STRING' diff --git a/tools/pythonpkg/tests/fast/test_duckdb_api.py b/tools/pythonpkg/tests/fast/test_duckdb_api.py new file mode 100644 index 000000000000..f5dcfb60e05e --- /dev/null +++ b/tools/pythonpkg/tests/fast/test_duckdb_api.py @@ -0,0 +1,8 @@ +import duckdb +import sys + + +def test_duckdb_api(): + res = duckdb.execute("SELECT name, value FROM duckdb_settings() WHERE name == 'duckdb_api'") + formatted_python_version = f"{sys.version_info.major}.{sys.version_info.minor}" + assert res.fetchall() == [('duckdb_api', f'python/{formatted_python_version}')] diff --git a/tools/pythonpkg/tests/fast/test_version.py b/tools/pythonpkg/tests/fast/test_version.py index 0038b7d5e51f..cdeb42b00056 100644 --- a/tools/pythonpkg/tests/fast/test_version.py +++ b/tools/pythonpkg/tests/fast/test_version.py @@ -1,5 +1,11 @@ import duckdb +import sys def test_version(): assert duckdb.__version__ != "0.0.0" + + +def test_formatted_python_version(): + formatted_python_version = f"{sys.version_info.major}.{sys.version_info.minor}" + assert duckdb.__formatted_python_version__ == formatted_python_version