Repository navigation
Conversation
…n raise_formatted_for_status and raise_formatted_ads_exception Change-Id: I1f2b7b2a5654772a05a4349aa1d7177ff684d891
Change-Id: I968729700a08028d34b76a3dccb96b355497cd32
There was a problem hiding this comment.
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).
| 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 |
There was a problem hiding this comment.
Optimization & Robustness in _get_caller_version():
- Break top-level circular import: Importing
_DEFAULT_VERSIONlazily on the fallback return avoids the top-level circular import withclient.py. - Skip internal frames & fast-reject non-matching modules:
inspect.currentframe()starts at_get_caller_versionitself (ingoogle.ads.googleads.errors), and its immediate caller is always inerrors.py. When falling back to_DEFAULT_VERSION, the loop currently executes_VERSION_REGEX.matchon every stack frame all the way up the call stack (e.g., 20–40+ test/application frames). Starting atframe.f_backand checkingmodule_name.startswith("google.ads.googleads.v")before matching avoids unnecessary regex overhead. - 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-levelgoogle.ads.googleads.v26module.
| 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 |
| if not response.ok: | ||
| if version is None: | ||
| version = _get_caller_version() | ||
| raise_formatted_ads_exception(response, version) |
There was a problem hiding this comment.
Guard Empty-String version & Redundant _get_caller_version() Resolution:
- Using
if not version:(both here and at line 150 inraise_formatted_ads_exception) is safer thanif version is None:, as an empty string""would otherwise bypass the check and fail withModuleNotFoundError: No module named 'google.ads.googleads..errors.types.errors'. - Note that
raise_formatted_ads_exceptionalready resolvesversion = _get_caller_version()whenversionis omitted; because_get_caller_version()walks the stack past allgoogle.ads.googleads.errorsframes, delegatingraise_formatted_ads_exception(response, version=version)directly produces the exact same result without duplicating the resolution logic (iftest_raise_formatted_for_status_default_versionassertsmock_raise.assert_called_once_with(self.response, None)or tests end-to-end).
There was a problem hiding this comment.
Test Performance Optimization & Edge-Case Coverage:
- Avoid loading the entire
v25.servicespackage tree in unit tests: Importinggoogle.ads.googleads.v25.services.services.you_tube_video_upload_service.transports.resumable_uploadexecutesgoogle/ads/googleads/v25/__init__.pyandgoogle/ads/googleads/v25/services/__init__.py(1,502 lines), which eagerly imports every service, resource, enum, and protobuf descriptor inv25on Python < 3.15. In addition, callingupload._process_initiate_responsecouples this test to a private method on a generated transport class.
You can test stack-frame version inference in <1ms without importingv25.servicesby 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 loadingresumable_upload.pywithout triggeringv25/services/__init__.py. - Additional edge cases: Consider adding a unit test verifying that
_get_caller_version()falls back toself.versionwheninspect.currentframe()returnsNone(with mock.patch("inspect.currentframe", return_value=None): ...), and a case verifyingraise_formatted_ads_exception(self.response)directly infers a non-default caller version.
…ing version and remove redundant resolution Change-Id: If8be593f7b18d57ae85bcb0d5746c0824e795f42

…n raise_formatted_for_status and raise_formatted_ads_exception.
The newly generated code for the
YouTubeVideoUploadServiceClientmay 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.