From 3ac17808a363629e1630367d625925beada66a66 Mon Sep 17 00:00:00 2001 From: Sonica-B Date: Sat, 24 Oct 2026 21:36:51 -0700 Subject: [PATCH 1/2] docs: use keyword argument in the Video.from_file README example Video.from_file is keyword-only (def from_file(cls, *, location, mime_type=None)), so the Video-to-Video example in README.md and docs/source/index.rst raised TypeError: from_file() takes 1 positional argument but 2 were given. The Image.from_file example next to it already passes location=. --- README.md | 2 +- docs/source/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6cca3cd0a..7c683ab5e 100644 --- a/README.md +++ b/README.md @@ -1477,7 +1477,7 @@ previously generated videos. Gemini Enterprise Agent Platform supports video ext from google.genai import types # Read local video (uses mimetypes.guess_type to infer mime type) -video = types.Video.from_file("local/path/video.mp4") +video = types.Video.from_file(location="local/path/video.mp4") # Create operation operation = client.models.generate_videos( diff --git a/docs/source/index.rst b/docs/source/index.rst index a285fcf80..274a6e82a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1468,7 +1468,7 @@ Currently, only Gemini Enterprise Agent Platform supports Video to Video generat from google.genai import types # Read local video (uses mimetypes.guess_type to infer mime type) - video = types.Video.from_file("local/path/video.mp4") + video = types.Video.from_file(location="local/path/video.mp4") # Create operation operation = client.models.generate_videos( From 5953cc85c8eccfc2f7b6fe7700cbce813706d47a Mon Sep 17 00:00:00 2001 From: Sonica-B Date: Sat, 24 Oct 2026 21:38:35 -0700 Subject: [PATCH 2/2] test: add offline check that README examples match the types API Parse the Python blocks in README.md and docs/source/index.rst and check, without any API call, that every types.X(...) and types.X.method(...) call names an existing type, passes only model fields, and binds to the real signature. It reports the Video.from_file example fixed in the previous commit, which no existing test covered. --- google/genai/tests/readme/__init__.py | 16 ++ .../tests/readme/test_readme_examples.py | 177 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 google/genai/tests/readme/__init__.py create mode 100644 google/genai/tests/readme/test_readme_examples.py diff --git a/google/genai/tests/readme/__init__.py b/google/genai/tests/readme/__init__.py new file mode 100644 index 000000000..66e21cef8 --- /dev/null +++ b/google/genai/tests/readme/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + diff --git a/google/genai/tests/readme/test_readme_examples.py b/google/genai/tests/readme/test_readme_examples.py new file mode 100644 index 000000000..839d25ddd --- /dev/null +++ b/google/genai/tests/readme/test_readme_examples.py @@ -0,0 +1,177 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Offline checks that the README examples still match the public SDK. + +Running the README needs credentials and network access, so these tests only +parse its Python blocks (and the copy in docs/source/index.rst) and check, with +no API call, that every `types.X(...)` and `types.X.method(...)` call is valid +for the installed SDK: the name exists, keyword arguments are model fields, and +method calls bind to the real signature. +""" + +import ast +import inspect +import pathlib +import re +import textwrap +from typing import Any, Callable +import unittest + +import pydantic + +from ... import types + +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[4] +_MARKDOWN_PYTHON_BLOCK = re.compile(r'```python\n(.*?)```', re.DOTALL) +# An indented reStructuredText block: blank lines or lines with 4+ spaces. +_RST_PYTHON_BLOCK = re.compile( + r'^\.\. code:: python\n\n((?:(?:[ ]{4}.*)?\n)+)', re.MULTILINE +) + + +def _markdown_blocks(text: str) -> list[str]: + return _MARKDOWN_PYTHON_BLOCK.findall(text) + + +def _rst_blocks(text: str) -> list[str]: + return [textwrap.dedent(b) for b in _RST_PYTHON_BLOCK.findall(text)] + + +def _types_path(func: ast.expr) -> tuple[str, ...]: + """Returns ('Video', 'from_file') for `types.Video.from_file`, else ().""" + names = [] + while isinstance(func, ast.Attribute): + names.append(func.attr) + func = func.value + if isinstance(func, ast.Name) and func.id == 'types' and names: + return tuple(reversed(names)) + return () + + +def _check_call(name: str, obj: Any, node: ast.Call) -> list[str]: + if inspect.isclass(obj) and issubclass(obj, pydantic.BaseModel): + fields = set(obj.model_fields) + fields.update(f.alias for f in obj.model_fields.values() if f.alias) + return [ + f'{name}() has no field {kw.arg!r}' + for kw in node.keywords + if kw.arg not in fields + ] + if not callable(obj): + return [] + try: + signature = inspect.signature(obj) + except (TypeError, ValueError): + return [] + try: + signature.bind( + *[None] * len(node.args), **{kw.arg: None for kw in node.keywords} + ) + except TypeError as e: + return [f'{name}(): {e}'] + return [] + + +def _find_problems(source: str) -> tuple[int, list[str]]: + """Returns (number of calls checked, problems found) for one code block.""" + try: + tree = ast.parse(source) + except SyntaxError: + return 0, [] # Fragments such as a bare list of contents cannot be parsed. + checked = 0 + problems = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if any(isinstance(a, ast.Starred) for a in node.args) or any( + kw.arg is None for kw in node.keywords + ): + continue + path = _types_path(node.func) + if not path: + continue + name = '.'.join(('types',) + path) + obj: Any = types + for attribute in path: + obj = getattr(obj, attribute, None) + if obj is None: + problems.append(f'{name} does not exist') + break + else: + checked += 1 + problems.extend(_check_call(name, obj, node)) + return checked, problems + + +class ReadmeExamplesTest(unittest.TestCase): + + def _assert_examples_match_sdk( + self, relative_path: str, extract: Callable[[str], list[str]] + ) -> None: + path = _REPO_ROOT / relative_path + if not path.is_file(): + self.skipTest(f'{relative_path} is not part of this checkout.') + checked = 0 + problems = [] + for block in extract(path.read_text(encoding='utf-8')): + block_checked, block_problems = _find_problems(block) + checked += block_checked + problems.extend(block_problems) + self.assertGreater(checked, 0, f'No examples found in {relative_path}.') + self.assertEqual(problems, [], f'Stale examples in {relative_path}.') + + def test_readme_examples_match_sdk(self): + self._assert_examples_match_sdk('README.md', _markdown_blocks) + + def test_docs_index_examples_match_sdk(self): + self._assert_examples_match_sdk('docs/source/index.rst', _rst_blocks) + + def test_accepts_valid_calls(self): + source = ( + 'types.Video.from_file(location="a.mp4")\n' + 'types.Part.from_text(text="hi")\n' + 'types.GenerateContentConfig(temperature=0.1)\n' + ) + self.assertEqual(_find_problems(source), (3, [])) + + def test_flags_positional_argument_to_keyword_only_classmethod(self): + _, problems = _find_problems('types.Video.from_file("a.mp4")') + self.assertEqual(len(problems), 1) + self.assertIn('types.Video.from_file', problems[0]) + + def test_flags_unknown_model_field(self): + _, problems = _find_problems('types.GenerateContentConfig(no_such_field=1)') + self.assertEqual( + problems, ["types.GenerateContentConfig() has no field 'no_such_field'"] + ) + + def test_flags_unknown_type(self): + _, problems = _find_problems('types.NoSuchType(x=1)') + self.assertEqual(problems, ['types.NoSuchType does not exist']) + + def test_skips_fragments_that_do_not_parse(self): + self.assertEqual(_find_problems('[\n types.Part(text="a"),\n'), (0, [])) + + def test_extracts_rst_code_blocks(self): + text = ( + 'Title\n=====\n\n.. code:: python\n\n x = types.Part()\n\n' + ' y = 1\n\nNot code.\n' + ) + self.assertEqual(_rst_blocks(text), ['x = types.Part()\n\ny = 1\n\n']) + + +if __name__ == '__main__': + unittest.main()