Set the caller version as the default value for the version parameter i… by Raibaz · Pull Request #1096 · googleads/google-ads-python · GitHub
Skip to content

Set the caller version as the default value for the version parameter i… - #1096

Open
Raibaz wants to merge 3 commits into
mainfrom
default-version-in-raise-formatted
Open

Raibaz wants to merge 3 commits into
mainfrom
default-version-in-raise-formatted

Conversation

@Raibaz

@Raibaz Raibaz commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

…n raise_formatted_for_status and raise_formatted_ads_exception.

The newly generated code for the YouTubeVideoUploadServiceClient may not pass a version value to the error formatting methods, so we use the latest version as a default value when it's not passed.

…n raise_formatted_for_status and raise_formatted_ads_exception

Change-Id: I1f2b7b2a5654772a05a4349aa1d7177ff684d891
@Raibaz
Raibaz requested a review from BenRKarl October 19, 2026 13:40
Change-Id: I968729700a08028d34b76a3dccb96b355497cd32
@Raibaz
Raibaz requested a review from a team as a code owner October 20, 2026 09:26
@Raibaz
Raibaz requested a review from AnashOommen October 20, 2026 09:26
@Raibaz Raibaz changed the title Add _DEFAULT_VERSION as the default value for the version parameter i… Set the caller version as the default value for the version parameter i… Oct 20, 2026
Comment thread google/ads/googleads/client.py Outdated
Comment on lines +27 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circular Import Fragility & PEP 8 E402 Violation:

Moving _logger and the module-level constants (_VALID_API_VERSIONS, _DEFAULT_VERSION, etc.) above from google.ads.googleads import config, oauth2, util and from google.ads.googleads.interceptors import ... splits top-level imports in violation of PEP 8 E402 (module level import not at top of file) and Google Python Style.

More importantly, this creates an implicit circular import dependency (client -> interceptors -> exception_interceptor -> errors -> client) that relies on exact statement ordering in client.py. If an automated formatter (ruff, isort, flake8) regroups the imports at the top of client.py, or if google.ads.googleads.errors is ever imported/reloaded before client.py executes line 37, importing fails at runtime with an ImportError.

Suggestion: Revert the changes in client.py to keep all imports grouped at the top of the module, and import _DEFAULT_VERSION lazily inside _get_caller_version() in errors.py (only on the fallback path).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread google/ads/googleads/errors.py Outdated
Comment on lines +26 to +51
from google.ads.googleads.client import _DEFAULT_VERSION

_VERSION_REGEX = re.compile(r"^google\.ads\.googleads\.(v\d+)\.")


def _get_caller_version() -> str:
"""Determines the Google Ads API version from the call stack.

Walks the call stack looking for a frame originating from a versioned
``google.ads.googleads.<version>`` module. Falls back to
``_DEFAULT_VERSION`` if no versioned module is found.

Returns:
The detected API version string (e.g., "v25") or ``_DEFAULT_VERSION``.
"""
frame = inspect.currentframe()
try:
while frame is not None:
module_name = frame.f_globals.get("__name__", "")
match = _VERSION_REGEX.match(module_name)
if match:
return match.group(1)
frame = frame.f_back
finally:
del frame
return _DEFAULT_VERSION

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimization & Robustness in _get_caller_version():

  1. Break top-level circular import: Importing _DEFAULT_VERSION lazily on the fallback return avoids the top-level circular import with client.py.
  2. Skip internal frames & fast-reject non-matching modules: inspect.currentframe() starts at _get_caller_version itself (in google.ads.googleads.errors), and its immediate caller is always in errors.py. When falling back to _DEFAULT_VERSION, the loop currently executes _VERSION_REGEX.match on every stack frame all the way up the call stack (e.g., 20–40+ test/application frames). Starting at frame.f_back and checking module_name.startswith("google.ads.googleads.v") before matching avoids unnecessary regex overhead.
  3. Support point-release and root version module names: r"^google\.ads\.googleads\.(v\d+)\." will not match point versions (e.g., v26_1) or a top-level google.ads.googleads.v26 module.
Suggested change
from google.ads.googleads.client import _DEFAULT_VERSION
_VERSION_REGEX = re.compile(r"^google\.ads\.googleads\.(v\d+)\.")
def _get_caller_version() -> str:
"""Determines the Google Ads API version from the call stack.
Walks the call stack looking for a frame originating from a versioned
``google.ads.googleads.<version>`` module. Falls back to
``_DEFAULT_VERSION`` if no versioned module is found.
Returns:
The detected API version string (e.g., "v25") or ``_DEFAULT_VERSION``.
"""
frame = inspect.currentframe()
try:
while frame is not None:
module_name = frame.f_globals.get("__name__", "")
match = _VERSION_REGEX.match(module_name)
if match:
return match.group(1)
frame = frame.f_back
finally:
del frame
return _DEFAULT_VERSION
_VERSION_PREFIX = "google.ads.googleads.v"
_VERSION_REGEX = re.compile(r"^google\.ads\.googleads\.(v\d+(?:_\d+)?)(?:\.|$)")
def _get_caller_version() -> str:
"""Determines the Google Ads API version from the call stack.
Walks the call stack looking for a frame originating from a versioned
``google.ads.googleads.<version>`` module. Falls back to
``_DEFAULT_VERSION`` if no versioned module is found.
Returns:
The detected API version string (e.g., "v25") or ``_DEFAULT_VERSION``.
"""
frame = inspect.currentframe()
try:
if frame is not None:
frame = frame.f_back
while frame is not None:
module_name = frame.f_globals.get("__name__", "")
if module_name.startswith(_VERSION_PREFIX):
match = _VERSION_REGEX.match(module_name)
if match:
return match.group(1)
frame = frame.f_back
finally:
del frame
from google.ads.googleads.client import _DEFAULT_VERSION
return _DEFAULT_VERSION

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 192 to 195
if not response.ok:
if version is None:
version = _get_caller_version()
raise_formatted_ads_exception(response, version)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guard Empty-String version & Redundant _get_caller_version() Resolution:

  1. Using if not version: (both here and at line 150 in raise_formatted_ads_exception) is safer than if version is None:, as an empty string "" would otherwise bypass the check and fail with ModuleNotFoundError: No module named 'google.ads.googleads..errors.types.errors'.
  2. Note that raise_formatted_ads_exception already resolves version = _get_caller_version() when version is omitted; because _get_caller_version() walks the stack past all google.ads.googleads.errors frames, delegating raise_formatted_ads_exception(response, version=version) directly produces the exact same result without duplicating the resolution logic (if test_raise_formatted_for_status_default_version asserts mock_raise.assert_called_once_with(self.response, None) or tests end-to-end).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread tests/errors_test.py
Comment on lines +110 to +136

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Performance Optimization & Edge-Case Coverage:

  1. Avoid loading the entire v25.services package tree in unit tests: Importing google.ads.googleads.v25.services.services.you_tube_video_upload_service.transports.resumable_upload executes google/ads/googleads/v25/__init__.py and google/ads/googleads/v25/services/__init__.py (1,502 lines), which eagerly imports every service, resource, enum, and protobuf descriptor in v25 on Python < 3.15. In addition, calling upload._process_initiate_response couples this test to a private method on a generated transport class.
    You can test stack-frame version inference in <1ms without importing v25.services by executing a small helper with a versioned __name__ in its globals (e.g., exec("def call(r): errors.raise_formatted_for_status(r)", {"__name__": f"google.ads.googleads.{older_version}.services.test", "errors": errors})), or by loading resumable_upload.py without triggering v25/services/__init__.py.
  2. Additional edge cases: Consider adding a unit test verifying that _get_caller_version() falls back to self.version when inspect.currentframe() returns None (with mock.patch("inspect.currentframe", return_value=None): ...), and a case verifying raise_formatted_ads_exception(self.response) directly infers a non-default caller version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

…ing version and remove redundant resolution

Change-Id: If8be593f7b18d57ae85bcb0d5746c0824e795f42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants