diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 508ba98efe..9a7846675f 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:25de45b58e52021d3a24a6273964371a97a4efeefe6ad3845a64e697c63b6447 -# created: 2025-04-14T14:34:43.260858345Z + digest: sha256:4a9e5d44b98e8672e2037ee22bc6b4f8e844a2d75fcb78ea8a4b38510112abc6 +# created: 2025-10-07 diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 9920db74d5..389c3747c3 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -17,18 +17,18 @@ branchProtectionRules: # List of required status check contexts that must pass for commits to be accepted to matching branches. requiredStatusCheckContexts: - 'Kokoro' - - 'Kokoro system-3.7' + - 'Kokoro system' - 'cla/google' - 'OwlBot Post Processor' - 'docs' - 'docfx' - 'lint' - - 'unit (3.7)' - - 'unit (3.8)' - 'unit (3.9)' - 'unit (3.10)' - 'unit (3.11)' - 'unit (3.12)' + - 'unit (3.13)' + - 'unit (3.14)' - 'cover' - 'run-systests' # List of explicit permissions to add (additive only) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4866193af2..9a0598202b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.13" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 772186478f..27075146a1 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.13" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.github/workflows/system_emulated.yml b/.github/workflows/system_emulated.yml index 0f3a69224b..bb7986a0ab 100644 --- a/.github/workflows/system_emulated.yml +++ b/.github/workflows/system_emulated.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.7' + python-version: '3.13' # firestore emulator requires java 21+ - name: Setup Java diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index c66b757ced..494bb568fe 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - name: Checkout uses: actions/checkout@v4 @@ -45,7 +45,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.13" - name: Install coverage run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.kokoro/presubmit/system-3.14.cfg b/.kokoro/presubmit/system-3.14.cfg new file mode 100644 index 0000000000..86e7c5d776 --- /dev/null +++ b/.kokoro/presubmit/system-3.14.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "system-3.14" +} \ No newline at end of file diff --git a/.kokoro/presubmit/system-3.9.cfg b/.kokoro/presubmit/system-3.9.cfg new file mode 100644 index 0000000000..b8ae66b376 --- /dev/null +++ b/.kokoro/presubmit/system-3.9.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "system-3.9" +} \ No newline at end of file diff --git a/.kokoro/presubmit/system.cfg b/.kokoro/presubmit/system.cfg new file mode 100644 index 0000000000..bd1fb514b2 --- /dev/null +++ b/.kokoro/presubmit/system.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "system-3.9" +} diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1d0c00be3e..c917685242 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.7, 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:: @@ -238,6 +238,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/ @@ -245,7 +246,7 @@ We support: .. _Python 3.10: https://docs.python.org/3.10/ .. _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/google/cloud/firestore_bundle/bundle.py b/google/cloud/firestore_bundle/bundle.py index 0f9aaed976..e985a1e065 100644 --- a/google/cloud/firestore_bundle/bundle.py +++ b/google/cloud/firestore_bundle/bundle.py @@ -344,9 +344,10 @@ def build(self) -> str: BundleElement(document_metadata=bundled_document.metadata) ) document_count += 1 + bundle_pb = bundled_document.snapshot._to_protobuf() buffer += self._compile_bundle_element( BundleElement( - document=bundled_document.snapshot._to_protobuf()._pb, + document=bundle_pb._pb if bundle_pb else None, ) ) diff --git a/google/cloud/firestore_v1/async_transaction.py b/google/cloud/firestore_v1/async_transaction.py index 36509941ed..0dfa82e011 100644 --- a/google/cloud/firestore_v1/async_transaction.py +++ b/google/cloud/firestore_v1/async_transaction.py @@ -21,8 +21,10 @@ AsyncGenerator, Awaitable, Callable, + Generic, Optional, ) +from typing_extensions import Concatenate, ParamSpec, TypeVar from google.api_core import exceptions, gapic_v1 from google.api_core import retry_async as retries @@ -44,14 +46,14 @@ # Types needed only for Type Hints if TYPE_CHECKING: # pragma: NO COVER import datetime - from typing_extensions import TypeVar, ParamSpec, Concatenate from google.cloud.firestore_v1.async_stream_generator import AsyncStreamGenerator from google.cloud.firestore_v1.base_document import DocumentSnapshot from google.cloud.firestore_v1.query_profile import ExplainOptions - T = TypeVar("T") - P = ParamSpec("P") + +T = TypeVar("T") +P = ParamSpec("P") class AsyncTransaction(async_batch.AsyncWriteBatch, BaseTransaction): @@ -253,7 +255,7 @@ async def get( ) -class _AsyncTransactional(_BaseTransactional): +class _AsyncTransactional(_BaseTransactional, Generic[T, P]): """Provide a callable object to use as a transactional decorater. This is surfaced via diff --git a/google/cloud/firestore_v1/base_query.py b/google/cloud/firestore_v1/base_query.py index 14df886bcb..2de95b79ad 100644 --- a/google/cloud/firestore_v1/base_query.py +++ b/google/cloud/firestore_v1/base_query.py @@ -929,7 +929,7 @@ def _normalize_cursor(self, cursor, orders) -> Tuple[List, bool] | None: if isinstance(document_fields, document.DocumentSnapshot): snapshot = document_fields - document_fields = snapshot.to_dict() + document_fields = copy.deepcopy(snapshot._data) document_fields["__name__"] = snapshot.reference if isinstance(document_fields, dict): diff --git a/mypy.ini b/mypy.ini index beaa679a8d..59a6e4d37a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,3 +1,7 @@ [mypy] -python_version = 3.8 +python_version = 3.13 namespace_packages = True + +# ignore gapic files +[mypy-google.cloud.firestore_v1.services.*] +ignore_errors = True \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index 9e81d71795..ac1c7ee4d1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -33,7 +33,7 @@ ISORT_VERSION = "isort==5.11.0" LINT_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] -DEFAULT_PYTHON_VERSION = "3.8" +DEFAULT_PYTHON_VERSION = "3.13" UNIT_TEST_PYTHON_VERSIONS: List[str] = [ "3.7", @@ -43,6 +43,7 @@ "3.11", "3.12", "3.13", + "3.14", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", @@ -61,7 +62,7 @@ UNIT_TEST_EXTRAS: List[str] = [] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} -SYSTEM_TEST_PYTHON_VERSIONS: List[str] = ["3.7"] +SYSTEM_TEST_PYTHON_VERSIONS: List[str] = ["3.9", "3.14"] SYSTEM_TEST_STANDARD_DEPENDENCIES: List[str] = [ "mock", "pytest", @@ -79,7 +80,12 @@ CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() nox.options.sessions = [ - "unit", + "unit-3.9", + "unit-3.10", + "unit-3.11", + "unit-3.12", + "unit-3.13", + "unit-3.14", "system_emulated", "system", "mypy", @@ -170,7 +176,7 @@ def mypy(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def lint_setup_py(session): """Verify that setup.py is valid (including RST check).""" - session.install("docutils", "pygments") + session.install("setuptools", "docutils", "pygments") session.run("python", "setup.py", "check", "--restructuredtext", "--strict") @@ -210,7 +216,8 @@ def install_unittest_dependencies(session, *constraints): def unit(session, protobuf_implementation): # Install all test dependencies, then install this package in-place. - if protobuf_implementation == "cpp" and session.python in ("3.11", "3.12", "3.13"): + py_version = tuple([int(v) for v in session.python.split(".")]) + if protobuf_implementation == "cpp" and py_version >= (3, 11): session.skip("cpp implementation is not supported in python 3.11+") constraints_path = str( @@ -375,7 +382,13 @@ def cover(session): test runs (not system test runs), and then erases coverage data. """ session.install("coverage", "pytest-cov") - session.run("coverage", "report", "--show-missing", "--fail-under=100") + session.run( + "coverage", + "report", + "--show-missing", + "--fail-under=100", + "--omit=tests/*", + ) session.run("coverage", "erase") @@ -469,7 +482,8 @@ def docfx(session): def prerelease_deps(session, protobuf_implementation): """Run all tests with prerelease versions of dependencies installed.""" - if protobuf_implementation == "cpp" and session.python in ("3.11", "3.12", "3.13"): + py_version = tuple([int(v) for v in session.python.split(".")]) + if protobuf_implementation == "cpp" and py_version >= (3, 11): session.skip("cpp implementation is not supported in python 3.11+") # Install all dependencies diff --git a/owlbot.py b/owlbot.py index f08048fef7..a9323ce3c0 100644 --- a/owlbot.py +++ b/owlbot.py @@ -138,12 +138,14 @@ def update_fixup_scripts(library): # ---------------------------------------------------------------------------- templated_files = common.py_library( samples=False, # set to True only if there are samples - system_test_python_versions=["3.7"], unit_test_external_dependencies=["aiounittest", "six", "freezegun"], system_test_external_dependencies=["pytest-asyncio", "six"], microgenerator=True, cov_level=100, split_system_tests=True, + default_python_version="3.13", + system_test_python_versions=["3.9", "3.14"], + unit_test_python_versions=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"], ) s.move(templated_files, diff --git a/pytest.ini b/pytest.ini index 099cbd3ad2..eac8ea1233 100644 --- a/pytest.ini +++ b/pytest.ini @@ -18,3 +18,8 @@ filterwarnings = ignore:After January 1, 2024, new releases of this library will drop support for Python 3.7:DeprecationWarning # Remove warning once https://github.com/googleapis/gapic-generator-python/issues/1939 is fixed ignore:get_mtls_endpoint_and_cert_source is deprecated.:DeprecationWarning + # Remove once credential file support is removed + ignore:.*The \`credentials_file\` argument is deprecated.*:DeprecationWarning + # Remove after updating test dependencies that use asyncio.iscoroutinefunction + ignore:.*\'asyncio.iscoroutinefunction\' is deprecated.*:DeprecationWarning + ignore:.*\'asyncio.get_event_loop_policy\' is deprecated.*:DeprecationWarning \ No newline at end of file diff --git a/setup.py b/setup.py index 2a47080a15..8625abce96 100644 --- a/setup.py +++ b/setup.py @@ -79,6 +79,8 @@ "Programming Language :: Python :: 3.10", "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 :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/testing/constraints-3.14.txt b/testing/constraints-3.14.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/v1/test_base_query.py b/tests/unit/v1/test_base_query.py index 7f6b0e5e2e..7804b0430f 100644 --- a/tests/unit/v1/test_base_query.py +++ b/tests/unit/v1/test_base_query.py @@ -1400,6 +1400,19 @@ def test_basequery__normalize_cursor_as_snapshot_hit(): assert query._normalize_cursor(cursor, query._orders) == ([1], True) +def test_basequery__normalize_cursor_non_existant_snapshot(): + from google.cloud.firestore_v1 import document + + values = {"b": 1} + docref = _make_docref("here", "doc_id") + snapshot = document.DocumentSnapshot(docref, values, False, None, None, None) + cursor = (snapshot, True) + collection = _make_collection("here") + query = _make_base_query(collection).order_by("b", "ASCENDING") + + assert query._normalize_cursor(cursor, query._orders) == ([1], True) + + def test_basequery__normalize_cursor_w___name___w_reference(): db_string = "projects/my-project/database/(default)" client = mock.Mock(spec=["_database_string"])