From 9bca909f249692d6446997b44c67bab06a71d0f5 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 12:13:05 -0800 Subject: [PATCH 01/13] feat: Add support for Python 3.14 --- .github/workflows/compliance.yml | 4 +- .github/workflows/unittest.yml | 2 +- .kokoro/samples/python3.14/common.cfg | 40 ++++++++++++++++++++ .kokoro/samples/python3.14/continuous.cfg | 6 +++ .kokoro/samples/python3.14/periodic-head.cfg | 11 ++++++ .kokoro/samples/python3.14/periodic.cfg | 6 +++ .kokoro/samples/python3.14/presubmit.cfg | 6 +++ CONTRIBUTING.rst | 6 ++- noxfile.py | 1 + samples/snippets/noxfile.py | 2 +- setup.py | 1 + testing/constraints-3.14.txt | 0 12 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 .kokoro/samples/python3.14/common.cfg create mode 100644 .kokoro/samples/python3.14/continuous.cfg create mode 100644 .kokoro/samples/python3.14/periodic-head.cfg create mode 100644 .kokoro/samples/python3.14/periodic.cfg create mode 100644 .kokoro/samples/python3.14/presubmit.cfg create mode 100644 testing/constraints-3.14.txt diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index 39b9ae7a..567460c6 100644 --- a/.github/workflows/compliance.yml +++ b/.github/workflows/compliance.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.13'] + python: ['3.14'] steps: - name: Checkout uses: actions/checkout@v5 @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.13'] + python: ['3.14'] steps: - name: Checkout uses: actions/checkout@v5 diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 6e05ae54..30ab5ae4 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.kokoro/samples/python3.14/common.cfg b/.kokoro/samples/python3.14/common.cfg new file mode 100644 index 00000000..2c3d47ba --- /dev/null +++ b/.kokoro/samples/python3.14/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.14" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-313" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-db-dtypes-pandas/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-db-dtypes-pandas/.kokoro/trampoline_v2.sh" diff --git a/.kokoro/samples/python3.14/continuous.cfg b/.kokoro/samples/python3.14/continuous.cfg new file mode 100644 index 00000000..a1c8d975 --- /dev/null +++ b/.kokoro/samples/python3.14/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.14/periodic-head.cfg b/.kokoro/samples/python3.14/periodic-head.cfg new file mode 100644 index 00000000..ee3d5640 --- /dev/null +++ b/.kokoro/samples/python3.14/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-db-dtypes-pandas/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.14/periodic.cfg b/.kokoro/samples/python3.14/periodic.cfg new file mode 100644 index 00000000..71cd1e59 --- /dev/null +++ b/.kokoro/samples/python3.14/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.14/presubmit.cfg b/.kokoro/samples/python3.14/presubmit.cfg new file mode 100644 index 00000000..a1c8d975 --- /dev/null +++ b/.kokoro/samples/python3.14/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c333038d..f52f2553 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -22,7 +22,7 @@ In order to add a feature: documentation. - The feature must work fully on the following CPython versions: - 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13 on both UNIX and Windows. + 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 and 3.14 on both UNIX and Windows. - The feature must not add unnecessary dependencies (where "unnecessary" is of course subjective, but new dependencies should @@ -72,7 +72,7 @@ We use `nox `__ to instrument our tests. - To run a single unit test:: - $ nox -s unit-3.13 -- -k + $ nox -s unit-3.14 -- -k .. note:: @@ -228,6 +228,7 @@ We support: - `Python 3.11`_ - `Python 3.12`_ - `Python 3.13`_ +- `Python 3.14`_ .. _Python 3.7: https://docs.python.org/3.7/ .. _Python 3.8: https://docs.python.org/3.8/ @@ -236,6 +237,7 @@ We support: .. _Python 3.11: https://docs.python.org/3.11/ .. _Python 3.12: https://docs.python.org/3.12/ .. _Python 3.13: https://docs.python.org/3.13/ +.. _Python 3.14: https://docs.python.org/3.14/ Supported versions can be found in our ``noxfile.py`` `config`_. diff --git a/noxfile.py b/noxfile.py index 125d4f1c..d3f1f265 100644 --- a/noxfile.py +++ b/noxfile.py @@ -41,6 +41,7 @@ "3.11", "3.12", "3.13", + "3.14", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index c9a3d1ec..c326375b 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -89,7 +89,7 @@ def get_pytest_env_vars() -> Dict[str, str]: # DO NOT EDIT - automatically generated. # All versions used to test samples. -ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] # Any default versions that should be ignored. IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] diff --git a/setup.py b/setup.py index 093bf2ed..5a60861b 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,7 @@ def readme(): "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Topic :: Database :: Front-Ends", ], diff --git a/testing/constraints-3.14.txt b/testing/constraints-3.14.txt new file mode 100644 index 00000000..e69de29b From 25128e30d7a1eb722480367154ce5c16a5c27ea5 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 12:24:50 -0800 Subject: [PATCH 02/13] improved test regex --- tests/unit/test_date.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_date.py b/tests/unit/test_date.py index a3ce43ee..4fb41a2e 100644 --- a/tests/unit/test_date.py +++ b/tests/unit/test_date.py @@ -137,9 +137,9 @@ def test_date_set_slice_null(): ("1-3", "Bad date string: '1-3'"), ("1", "Bad date string: '1'"), ("", "Bad date string: ''"), - ("2021-2-99", "day is out of range for month"), + ("2021-2-99", "day is out of range for month|day .* must be in range .*"), ("2021-99-1", "month must be in 1[.][.]12"), - ("10000-1-1", "year 10000 is out of range"), + ("10000-1-1", "year 10000 is out of range|year must be in .*"), ], ) def test_date_parsing_errors(value, error): From 66b697475f878e85dece19a29e353941d9e46036 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 13:22:24 -0800 Subject: [PATCH 03/13] fixed pandas backports tests --- tests/unit/test_pandas_backports.py | 30 +++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index cb783045..2f0308cb 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -12,16 +12,37 @@ # See the License for the specific language governing permissions and # limitations under the License. +import builtins import unittest.mock as mock import db_dtypes.pandas_backports as pandas_backports +REAL_IMPORT = builtins.__import__ + + +def _import_side_effect(module_name, result_return=None, result_raise=None): + """ + Builds a side-effect for mocking the import function. + If the imported package matches `name`, it will return or raise based on + arguments. Otherwise, it will default to regular import behaviour + """ + def _impl(name, *args, **kwargs): + if name == module_name: + if result_raise: + raise result_raise + else: + return result_return + else: + return REAL_IMPORT(name, *args, **kwargs) + return _impl + @mock.patch("builtins.__import__") def test_import_default_module_found(mock_import): mock_module = mock.MagicMock() - mock_module.OpsMixin = "OpsMixin_from_module" # Simulate successful import - mock_import.return_value = mock_module + mock_module.OpsMixin = "OpsMixin_from_module" + + mock_import.side_effect = _import_side_effect("module_name", mock_module) default_class = type("OpsMixin", (), {}) # Dummy class result = pandas_backports.import_default("module_name", default=default_class) @@ -29,8 +50,8 @@ def test_import_default_module_found(mock_import): @mock.patch("builtins.__import__") -def test_import_default_module_not_found(mock_import): - mock_import.side_effect = ModuleNotFoundError +def test_import_default_module_not_foundX(mock_import): + mock_import.side_effect = _import_side_effect("module_name", result_raise=ModuleNotFoundError) default_class = type("OpsMixin", (), {}) # Dummy class result = pandas_backports.import_default("module_name", default=default_class) @@ -48,6 +69,7 @@ def test_import_default_force_true(mock_import): result = pandas_backports.import_default( "any_module_name", force=True, default=default_class ) + assert mock_import.call_count == 0 # Assert that the returned value is the default class itself assert result is default_class From 7e8c60a50da56237627fe7ceb17ad87037a84828 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 13:29:12 -0800 Subject: [PATCH 04/13] addressed PR comments --- .kokoro/samples/python3.14/common.cfg | 2 +- samples/snippets/noxfile.py | 2 +- tests/unit/test_pandas_backports.py | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.kokoro/samples/python3.14/common.cfg b/.kokoro/samples/python3.14/common.cfg index 2c3d47ba..712b0a5e 100644 --- a/.kokoro/samples/python3.14/common.cfg +++ b/.kokoro/samples/python3.14/common.cfg @@ -16,7 +16,7 @@ env_vars: { # Declare build specific Cloud project. env_vars: { key: "BUILD_SPECIFIC_GCLOUD_PROJECT" - value: "python-docs-samples-tests-313" + value: "python-docs-samples-tests-314" } env_vars: { diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index c326375b..c94b48b0 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -89,7 +89,7 @@ def get_pytest_env_vars() -> Dict[str, str]: # DO NOT EDIT - automatically generated. # All versions used to test samples. -ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] +ALL_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] # Any default versions that should be ignored. IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index 2f0308cb..f3a03e53 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -22,10 +22,11 @@ def _import_side_effect(module_name, result_return=None, result_raise=None): """ - Builds a side-effect for mocking the import function. + Builds a side-effect for mocking the import function. If the imported package matches `name`, it will return or raise based on arguments. Otherwise, it will default to regular import behaviour """ + def _impl(name, *args, **kwargs): if name == module_name: if result_raise: @@ -34,6 +35,7 @@ def _impl(name, *args, **kwargs): return result_return else: return REAL_IMPORT(name, *args, **kwargs) + return _impl @@ -50,8 +52,10 @@ def test_import_default_module_found(mock_import): @mock.patch("builtins.__import__") -def test_import_default_module_not_foundX(mock_import): - mock_import.side_effect = _import_side_effect("module_name", result_raise=ModuleNotFoundError) +def test_import_default_module_not_found(mock_import): + mock_import.side_effect = _import_side_effect( + "module_name", result_raise=ModuleNotFoundError + ) default_class = type("OpsMixin", (), {}) # Dummy class result = pandas_backports.import_default("module_name", default=default_class) From ff16ec1e6733b382a8ad36132c9bdc2906baf6cc Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 13:31:17 -0800 Subject: [PATCH 05/13] reverted compliance changes --- .github/workflows/compliance.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index 567460c6..39b9ae7a 100644 --- a/.github/workflows/compliance.yml +++ b/.github/workflows/compliance.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.14'] + python: ['3.13'] steps: - name: Checkout uses: actions/checkout@v5 @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.14'] + python: ['3.13'] steps: - name: Checkout uses: actions/checkout@v5 From 1442da34877c1f34bd6ff0a072dac9dbac2e157b Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 13:36:20 -0800 Subject: [PATCH 06/13] moved compliance tests to 3.14 --- .github/workflows/compliance.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index 39b9ae7a..567460c6 100644 --- a/.github/workflows/compliance.yml +++ b/.github/workflows/compliance.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.13'] + python: ['3.14'] steps: - name: Checkout uses: actions/checkout@v5 @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.13'] + python: ['3.14'] steps: - name: Checkout uses: actions/checkout@v5 From 7c1f46b3dfdd57ab0112cae03e6111e6509a7a34 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 13:46:20 -0800 Subject: [PATCH 07/13] remove pandas 3.0.0 from supported versions --- noxfile.py | 5 +---- setup.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/noxfile.py b/noxfile.py index d3f1f265..e3659786 100644 --- a/noxfile.py +++ b/noxfile.py @@ -219,14 +219,11 @@ def prerelease(session, tests_path): "--upgrade", "pyarrow", ) - # Avoid pandas==2.2.0rc0 as this version causes PyArrow to fail. Once newer - # prerelease comes out, this constraint can be removed. See - # https://github.com/googleapis/python-db-dtypes-pandas/issues/234 session.install( "--prefer-binary", "--pre", "--upgrade", - "pandas!=2.2.0rc0", + "pandas<3.0.0rc0", ) session.install( "mock", diff --git a/setup.py b/setup.py index 5a60861b..186f31aa 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ "numpy >= 1.24.0, <= 2.2.6 ; python_version == '3.10'", "numpy >= 1.24.0 ; python_version != '3.10'", "packaging >= 24.2.0", - "pandas >= 1.5.3", + "pandas >= 1.5.3, < 3.0.0", "pyarrow >= 13.0.0", ] From f1ba856d02eb6a01928c7a16bd6e9070b8a6f5b8 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 13:48:07 -0800 Subject: [PATCH 08/13] fixed version in github actions tests --- .github/workflows/unittest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 30ab5ae4..970c47d2 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.13'] + python: ['3.14'] steps: - name: Checkout uses: actions/checkout@v4 @@ -68,7 +68,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.13'] + python: ['3.14'] steps: - name: Checkout uses: actions/checkout@v4 From e99483cdab7b94a96354a6c8c2e7425ee15f5b59 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 13:53:10 -0800 Subject: [PATCH 09/13] added no cover line --- tests/unit/test_pandas_backports.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index f3a03e53..191bb888 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -34,9 +34,7 @@ def _impl(name, *args, **kwargs): else: return result_return else: - return REAL_IMPORT(name, *args, **kwargs) - - return _impl + return REAL_IMPORT(name, *args, **kwargs) # pragma: no cover @mock.patch("builtins.__import__") From 09fe847c8e4a111fa80ce46ce8e198b58cccf2a1 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 14:04:38 -0800 Subject: [PATCH 10/13] added back accidental deletion --- tests/unit/test_pandas_backports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index 191bb888..0070c380 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -36,6 +36,8 @@ def _impl(name, *args, **kwargs): else: return REAL_IMPORT(name, *args, **kwargs) # pragma: no cover + return _impl + @mock.patch("builtins.__import__") def test_import_default_module_found(mock_import): From ffb9e2c15ceb85ef640c761beb63e5e22beab7ab Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 14:07:47 -0800 Subject: [PATCH 11/13] updated pragma --- tests/unit/test_pandas_backports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index 0070c380..2afdeb46 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -27,14 +27,14 @@ def _import_side_effect(module_name, result_return=None, result_raise=None): arguments. Otherwise, it will default to regular import behaviour """ - def _impl(name, *args, **kwargs): + def _impl(name, *args, **kwargs): # pragma: no cover if name == module_name: if result_raise: raise result_raise else: return result_return else: - return REAL_IMPORT(name, *args, **kwargs) # pragma: no cover + return REAL_IMPORT(name, *args, **kwargs) return _impl From abe2b62415f639ce8f682e40661e75ac664b1ab8 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 14:13:00 -0800 Subject: [PATCH 12/13] moved pragma --- tests/unit/test_pandas_backports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index 2afdeb46..bafd6b4e 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -27,13 +27,13 @@ def _import_side_effect(module_name, result_return=None, result_raise=None): arguments. Otherwise, it will default to regular import behaviour """ - def _impl(name, *args, **kwargs): # pragma: no cover + def _impl(name, *args, **kwargs): if name == module_name: if result_raise: raise result_raise else: return result_return - else: + else: # pragma: no cover return REAL_IMPORT(name, *args, **kwargs) return _impl From 79e20109d4f795db2321021e117b31d7e70e8b3f Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 12 Dec 2025 14:38:52 -0800 Subject: [PATCH 13/13] updated no cover --- tests/unit/test_pandas_backports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index bafd6b4e..e6d5cfaf 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -33,7 +33,7 @@ def _impl(name, *args, **kwargs): raise result_raise else: return result_return - else: # pragma: no cover + else: # pragma: NO COVER return REAL_IMPORT(name, *args, **kwargs) return _impl