diff --git a/packages/gapic-generator/gapic/codegen/components/package_init.py b/packages/gapic-generator/gapic/codegen/components/package_init.py new file mode 100644 index 000000000000..ba277bfd10e8 --- /dev/null +++ b/packages/gapic-generator/gapic/codegen/components/package_init.py @@ -0,0 +1,34 @@ +# 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 +# +# https://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. + +from gapic.codegen.writer import CodeWriter +from gapic.schema.api import API + + +class PackageInitGenerator: + """Pure Python generator for package initialization files (__init__.py, gapic_version.py, py.typed).""" + + @staticmethod + def generate_gapic_version(api_schema: API) -> str: + """Generates gapic_version.py content.""" + writer = CodeWriter() + writer.write_license_header() + writer.write_line(f'__version__ = "{api_schema.gapic_version}" # {{x-release-please-version}}') + return writer.dump() + + @staticmethod + def generate_py_typed(api_schema: API) -> str: + """Generates py.typed file (PEP 561 marker).""" + pkg_name = api_schema.naming.warehouse_package_name + return f"# Marker file for PEP 561.\n# The {pkg_name} package uses inline types.\n" diff --git a/packages/gapic-generator/gapic/codegen/engine.py b/packages/gapic-generator/gapic/codegen/engine.py new file mode 100644 index 000000000000..f8a1b0d837bc --- /dev/null +++ b/packages/gapic-generator/gapic/codegen/engine.py @@ -0,0 +1,39 @@ +# 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 +# +# https://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. + +from typing import Any, Optional +from gapic.codegen.components.package_init import PackageInitGenerator + + +class PurePythonEngine: + """Pure-Python template-free code generation engine.""" + + @staticmethod + def render(template_name: str, context: dict[str, Any]) -> Optional[str]: + """Dispatches template rendering to pure-Python component generators. + + Returns None to fall back to Jinja rendering if the template is not yet migrated. + """ + api_schema = context.get("api") + if not api_schema: + return None + + # Package Init & Versioning Component + if template_name.endswith("gapic_version.py.j2"): + return PackageInitGenerator.generate_gapic_version(api_schema) + if template_name.endswith("py.typed.j2"): + return PackageInitGenerator.generate_py_typed(api_schema) + + # Fallback to Jinja for un-migrated templates + return None diff --git a/packages/gapic-generator/gapic/generator/generator.py b/packages/gapic-generator/gapic/generator/generator.py index 9fe56aa9de1d..a26456af9209 100644 --- a/packages/gapic-generator/gapic/generator/generator.py +++ b/packages/gapic-generator/gapic/generator/generator.py @@ -22,6 +22,7 @@ from typing import Any, DefaultDict, Dict, Mapping, Optional, Tuple from hashlib import sha256 from collections import OrderedDict, defaultdict +from gapic.codegen.engine import PurePythonEngine from gapic.samplegen_utils.utils import ( coerce_response_name, is_valid_sample_cfg, @@ -402,12 +403,16 @@ def _get_file( ) # Render the file contents. + rendered = PurePythonEngine.render( + template_name, {"api": api_schema, "opts": opts, **context} + ) + if rendered is None: + rendered = self._env.get_template(template_name).render( + api=api_schema, opts=opts, **context + ) + cgr_file = CodeGeneratorResponse.File( - content=formatter.fix_whitespace( - self._env.get_template(template_name).render( - api=api_schema, opts=opts, **context - ), - ), + content=formatter.fix_whitespace(rendered), name=fn, ) diff --git a/packages/gapic-generator/tests/unit/codegen/test_package_init.py b/packages/gapic-generator/tests/unit/codegen/test_package_init.py new file mode 100644 index 000000000000..905874ce7553 --- /dev/null +++ b/packages/gapic-generator/tests/unit/codegen/test_package_init.py @@ -0,0 +1,51 @@ +# 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 +# +# https://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. + +from unittest.mock import MagicMock +from gapic.codegen.components.package_init import PackageInitGenerator + + +def test_generate_gapic_version(): + mock_api = MagicMock() + mock_api.gapic_version = "0.1.0" + content = PackageInitGenerator.generate_gapic_version(mock_api) + + assert "# Copyright 2026 Google LLC" in content + assert '__version__ = "0.1.0" # {x-release-please-version}' in content + + +def test_generate_py_typed(): + mock_api = MagicMock() + mock_api.naming.warehouse_package_name = "google-iam-credentials" + content = PackageInitGenerator.generate_py_typed(mock_api) + assert "# Marker file for PEP 561." in content + assert "google-iam-credentials" in content + + +def test_pure_python_engine_render(): + from gapic.codegen.engine import PurePythonEngine + + assert PurePythonEngine.render("gapic_version.py.j2", {}) is None + + mock_api = MagicMock() + mock_api.gapic_version = "0.1.0" + mock_api.naming.warehouse_package_name = "google-iam-credentials" + + v_out = PurePythonEngine.render("gapic_version.py.j2", {"api": mock_api}) + assert '__version__ = "0.1.0"' in v_out + + t_out = PurePythonEngine.render("py.typed.j2", {"api": mock_api}) + assert "google-iam-credentials" in t_out + + assert PurePythonEngine.render("other_file.py.j2", {"api": mock_api}) is None