Skip to content

Commit

Permalink
Fix infinite recursion on lazy_api_imports
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 697989132
  • Loading branch information
Conchylicultor authored and The etils Authors committed Nov 19, 2024
1 parent c65a09d commit 8a5685a
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Changelog follow https://keepachangelog.com/ format.
* `ecolab`:
* `ecolab.inspect`: Proto are better displayed (hide attributes
`DESCRIPTOR`, `Extensions` in sub-section)
* `epy`:
* `epy.lazy_api_imports`: Fix infinite recursion when importing sub-module
* `exm`:
* Add dummy implementation of the API to simplify open-sourcing.

Expand Down
25 changes: 20 additions & 5 deletions etils/epy/lazy_api_imports_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ def _getattr(
lazy_module = symbol_or_lazy_module
with lazy_module._maybe_adhoc(): # pylint: disable=protected-access
try:
symbol = _import_symbol(lazy_module.module_name)
symbol = _import_symbol(
import_qualname=lazy_module.module_name,
parent_module_name=module_name,
)
except ImportError as e:
if error_msg:
reraise_utils.reraise(
Expand All @@ -139,8 +142,20 @@ def _dir(
return list(globals_) + list(imported_symbols)


def _import_symbol(module_name: str) -> Any:
def _import_symbol(import_qualname: str, parent_module_name: str) -> Any:
"""Import the lazy-symbol."""
module_name, obj_name = module_name.rsplit('.', 1)
module = __import__(module_name, fromlist=[obj_name])
return getattr(module, obj_name)
module_name, obj_name = import_qualname.rsplit('.', 1)
if module_name == parent_module_name:
# To avoid infinite recursion, import sub-modules as
# `import parent_module.submodule` rather than
# `from parent_module import submodule`
module = __import__(f'{module_name}.{obj_name}')
parts = module_name.split('.')[1:] + [obj_name]
for name in parts:
module = getattr(module, name)
return module
else:
# Import symbols as `from module import obj` to supports functions,
# classes, etc.
module = __import__(module_name, fromlist=[obj_name])
return getattr(module, obj_name)
26 changes: 26 additions & 0 deletions etils/exm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2024 The etils Authors.
#
# 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.

"""XManager utils."""

# pylint: disable=g-importing-member

from etils.exm.dummy import current_experiment
from etils.exm.dummy import current_work_unit
from etils.exm.dummy import is_running_under_xmanager
from etils.exm.dummy import add_experiment_artifact
from etils.exm.dummy import add_work_unit_artifact
from etils.exm.dummy import curr_job_name
from etils.exm.dummy import url_to_python_only_logs
from etils.exm.dummy import set_citc_source
42 changes: 42 additions & 0 deletions etils/exm/dummy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2024 The etils Authors.
#
# 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.

"""Dummy implementation of the `exm` API to simplify open-sourcing."""

import functools
from typing import TypeVar

_T = TypeVar('_T')


def _noop(*args, return_value: _T, **kwargs) -> _T:
del args, kwargs
return return_value


def _error(*args, **kwargs):
del args, kwargs
raise NotImplementedError('This `exm` function does not work in open-source.')


current_experiment = _error
current_work_unit = _error
is_running_under_xmanager = functools.partial(_noop, return_value=False)
add_experiment_artifact = functools.partial(_noop, return_value=None)
add_work_unit_artifact = functools.partial(_noop, return_value=None)
curr_job_name = functools.partial(_noop, return_value='<unknown job name>')
url_to_python_only_logs = functools.partial(
_noop, return_value='<unknown log url>'
)
set_citc_source = functools.partial(_noop, return_value=None)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ exclude = [
"*/**/README.md",
"**/docs/**",
"**/*_test.py",
"ecolab/tests/",
"**/tests/**",
]

[tool.pyink]
Expand Down

0 comments on commit 8a5685a

Please sign in to comment.