Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 60 additions & 9 deletions google/genai/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,57 @@

from typing_extensions import Literal, Required, TypedDict

# Import triggers before interactions so that interactions.Interaction (the
# resource class) overrides triggers.Interaction (the TypeAliasType representing
# nested interactions inside triggers) in the exported namespace, resolving
# the name collision.
from ._gaos.types.triggers import * # noqa: F401,F403
from ._gaos.types.triggers import __all__ as _triggers_all
# Trigger create-params define nested `Interaction` / `InteractionParam`
# TypeAliasTypes that share names with the interactions resource class.
# Star-importing those aliases into this module makes mypy keep the first
# binding (the TypeAliasType), while runtime last-binding-wins to the resource
# class — see https://github.com/googleapis/python-genai/issues/2732.
# Exclude the colliding names here; they remain available from
# `google.genai._gaos.types.triggers`.
from ._gaos.types.triggers import __all__ as _triggers_all_raw

_TRIGGERS_NAMES_EXCLUDED_FROM_INTERACTIONS = frozenset({
'Interaction',
'InteractionParam',
})

_triggers_all = [
name
for name in _triggers_all_raw
if name not in _TRIGGERS_NAMES_EXCLUDED_FROM_INTERACTIONS
]

# Explicit imports so type checkers never bind the colliding TypeAliasTypes.
# Keep this list aligned with triggers.__all__ minus the excluded names; the
# assertion below fails if Speakeasy adds a new trigger export.
from ._gaos.types.triggers import ( # noqa: F401
ListTriggerExecutionsResponse,
ListTriggerExecutionsResponseTypedDict,
ListTriggersResponse,
ListTriggersResponseTypedDict,
Trigger,
TriggerCreateParams,
TriggerCreateParamsParam,
TriggerExecution,
TriggerExecutionStatus,
TriggerExecutionTypedDict,
TriggerStatus,
TriggerTypedDict,
TriggerUpdate,
TriggerUpdateParam,
TriggerUpdateStatus,
)

_missing_trigger_exports = [
name for name in _triggers_all if name not in globals()
]
if _missing_trigger_exports:
raise ImportError(
'google.genai.interactions is missing trigger exports after excluding '
f'colliding Interaction aliases: {_missing_trigger_exports}. Update the '
'explicit triggers import list in google/genai/interactions.py.'
)

from ._gaos.types.environments import * # noqa: F401,F403
from ._gaos.types.environments import __all__ as _environments_all
from ._gaos.types.interactions import * # noqa: F401,F403
Expand Down Expand Up @@ -137,6 +182,12 @@ class InteractionGetParamsStreaming(InteractionGetParamsBase):
"WebhookRotateSigningSecretParams",
"WebhookUpdateParams",
]
# Ensure _interactions_all is appended last so interactions.Interaction wins
# when doing wildcard imports from this module.
__all__ = __all__ + list(_triggers_all) + list(_resources_all) + list(_environments_all) + list(_interactions_all)
# Append interactions exports last so `Interaction` is the resource class for
# wildcard imports. Trigger Interaction aliases are intentionally omitted above.
__all__ = (
__all__
+ list(_triggers_all)
+ list(_resources_all)
+ list(_environments_all)
+ list(_interactions_all)
)
119 changes: 119 additions & 0 deletions google/genai/tests/interactions/test_interaction_type_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 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.
#

"""Tests for Interaction export typing/runtime consistency (#2732)."""

from __future__ import annotations

import os
import subprocess
import sys
import textwrap
from pathlib import Path

import pytest

from ...interactions import Interaction
from ..._gaos.types.interactions.interaction import (
Interaction as InteractionResource,
)
from ..._gaos.types.triggers.triggercreateparams import (
Interaction as TriggerInteractionAlias,
)


def test_interactions_module_exports_resource_class_at_runtime():
assert Interaction is InteractionResource
assert Interaction is not TriggerInteractionAlias
assert isinstance(Interaction, type)


def test_interactions_module_supports_isinstance_with_resource():
class _Dummy:
id = 'x'

# Resource class is a real type usable with isinstance.
assert not isinstance(_Dummy(), Interaction)


def test_trigger_interaction_alias_still_importable_from_triggers():
from ..._gaos.types.triggers import Interaction as TriggersInteraction

assert TriggersInteraction is TriggerInteractionAlias


def test_interactions_module_does_not_expose_trigger_interaction_param():
import google.genai.interactions as interactions_mod

assert getattr(interactions_mod, 'InteractionParam', None) is None
assert interactions_mod.Interaction.__name__ == 'Interaction'
assert interactions_mod.Interaction.__module__.endswith(
'types.interactions.interaction'
)


@pytest.mark.skipif(
subprocess.run(
[sys.executable, '-m', 'mypy', '--version'],
capture_output=True,
check=False,
).returncode
!= 0,
reason='mypy is not installed',
)
def test_mypy_resolves_interaction_to_resource_class(tmp_path: Path):
"""Regression for #2732: mypy must not treat Interaction as TypeAliasType."""
sample = tmp_path / 'check_interaction_export.py'
sample.write_text(
textwrap.dedent(
'''\
from google.genai.interactions import Interaction


def check(x: object) -> None:
if isinstance(x, Interaction):
print(x.id)
'''
),
encoding='utf-8',
)

repo_root = Path(__file__).resolve().parents[4]
env = os.environ.copy()
existing = env.get('PYTHONPATH', '')
env['PYTHONPATH'] = (
str(repo_root) + (os.pathsep + existing if existing else '')
)

result = subprocess.run(
[
sys.executable,
'-m',
'mypy',
'--strict',
'--follow-imports=silent',
str(sample),
],
capture_output=True,
text=True,
cwd=str(repo_root),
env=env,
)
combined = (result.stdout or '') + (result.stderr or '')
assert 'TypeAliasType' not in combined, combined
assert result.returncode == 0, (
'mypy failed to treat google.genai.interactions.Interaction as the '
f'resource class.\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}'
)
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,12 @@ module = [
# for imports. This will only affect the _gaos module.
ignore_errors = true

# This particularly for interactions.py.
# This particularly for interactions.py (star-imports from generated packages).
[[tool.mypy.overrides]]
module = "google.genai.interactions"
disable_error_code = [
"misc",
"assignment",
# Intentionally overriding imported alias, runtime safe.
"no-redef",
]

Loading