Skip to content

Commit

Permalink
Allows Fiddle to resolve configs and fiddlers with dots relative to `…
Browse files Browse the repository at this point in the history
…default_module`. This permits config to be organized into modules.

Name resolution order is (first) module relative (second) global.

Ex.: `default_module=a, --config=config:b.config` tries `a.b.config` before `[root]b.config`.

PiperOrigin-RevId: 692281973
  • Loading branch information
Fiddle-Config Team authored and copybara-github committed Nov 1, 2024
1 parent 2071761 commit de9c408
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 4 deletions.
17 changes: 17 additions & 0 deletions docs/flags_code_lab.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,23 @@ fiddler and invoke it with the previous Fiddlers syntax.
</section>
## Name Resolution
Fiddle will attempt to resolve dotted names relative to `default_module`, if
provided. If no module is provided, Fiddle will attempt to resolve the name as
an absolute path.
Concretely, given the flag definition
```py
absl_flags.DEFINE_fiddle_config(
"config", help_string="Fiddle configuration.", default_module=m
)
```
resolving `--config=config:n.base` will first try to resolve or import
`m.n.base` but will fall back to `n.base`.
## Serializing and forwarding configurations
The new flags API provides a convenient way to serialize and forward a config.
Expand Down
34 changes: 34 additions & 0 deletions fiddle/_src/absl_flags/submodule_for_flags_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# coding=utf-8
# Copyright 2022 The Fiddle-Config 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.

"""Defines a config to check that we can resolve inside submodules."""

import dataclasses

import fiddle as fdl


@dataclasses.dataclass
class Bar:
b: int


def increment_b(config: fdl.Config[Bar]) -> fdl.Config[Bar]:
config.b += 1
return config


def config_bar() -> fdl.Config[Bar]:
return fdl.Config(Bar, b=1)
34 changes: 30 additions & 4 deletions fiddle/_src/absl_flags/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,19 @@ def error_prefix(self, name: str) -> str:
return f'Could not load fiddler {name!r}'


def import_dotted_name(name: str, mode: ImportDottedNameDebugContext) -> Any:
def _import_dotted_name(
name: str,
mode: ImportDottedNameDebugContext,
module: Optional[types.ModuleType],
) -> Any:
"""Returns the Python object with the given dotted name.
Args:
name: The dotted name of a Python object, including the module name.
mode: Whether we're looking for a base config function or a fiddler.
mode: Whether we're looking for a base config function or a fiddler, used
only for constructing error messages.
module: A common namespace to use as the basis for resolving the import, if
None, we will attempt to use absolute imports.
Returns:
The named value.
Expand All @@ -63,6 +70,9 @@ def import_dotted_name(name: str, mode: ImportDottedNameDebugContext) -> Any:
the indicated name.
"""
name_pieces = name.split('.')
if module is not None:
name_pieces = [module.__name__] + name_pieces

if len(name_pieces) < 2:
raise ValueError(
f'{mode.error_prefix(name)}: Expected a dotted name including the '
Expand Down Expand Up @@ -223,7 +233,8 @@ def resolve_function_reference(
module: A common namespace to use as the basis for finding configs and
fiddlers. May be `None`; if `None`, only fully qualified Fiddler imports
will be used (or alternatively a base configuration can be specified using
the `--fdl_config_file` flag.)
the `--fdl_config_file` flag.). Dotted imports are resolved relative to
`module` if not None, by preference, or else absolutely.
allow_imports: If true, then fully qualified dotted names may be used to
specify configs or fiddlers that should be automatically imported.
failure_msg_prefix: Prefix string to prefix log messages in case of
Expand All @@ -235,10 +246,25 @@ def resolve_function_reference(
if hasattr(module, function_name):
return getattr(module, function_name)
elif allow_imports:
# Try a relative import first.
if module is not None:
try:
return _import_dotted_name(
function_name,
mode=mode,
module=module,
)
except (ModuleNotFoundError, ValueError, AttributeError):
# Intentionally ignore the exception here. We will reraise after trying
# again without relative import.
pass

# Try absolute import for the provided function name / symbol.
try:
return import_dotted_name(
return _import_dotted_name(
function_name,
mode=mode,
module=None,
)
except ModuleNotFoundError as e:
raise ValueError(f'{failure_msg_prefix} {function_name!r}: {e}') from e
Expand Down
89 changes: 89 additions & 0 deletions fiddle/_src/absl_flags/utils_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# coding=utf-8
# Copyright 2022 The Fiddle-Config 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.

from absl.testing import absltest
from fiddle._src.absl_flags import submodule_for_flags_test
from fiddle._src.absl_flags import utils

_IRRELEVANT_MODE = utils.ImportDottedNameDebugContext.BASE_CONFIG


class ResolveFunctionReferenceTest(absltest.TestCase):

def test_module_relative_resolution(self):
import fiddle._src.absl_flags as parent # pylint: disable=g-import-not-at-top

self.assertIs(
utils.resolve_function_reference(
function_name='config_bar',
mode=_IRRELEVANT_MODE,
module=submodule_for_flags_test,
allow_imports=True,
failure_msg_prefix='',
),
submodule_for_flags_test.config_bar,
)
self.assertIs(
utils.resolve_function_reference(
function_name='submodule_for_flags_test.config_bar',
mode=_IRRELEVANT_MODE,
module=parent,
allow_imports=True,
failure_msg_prefix='',
),
submodule_for_flags_test.config_bar,
)

def test_module_relative_resolution_falls_back_to_absolute(self):
self.assertIs(
utils.resolve_function_reference(
function_name=(
'fiddle._src.absl_flags.submodule_for_flags_test.config_bar'
),
mode=_IRRELEVANT_MODE,
module=utils,
allow_imports=True,
failure_msg_prefix='',
),
submodule_for_flags_test.config_bar,
)

def test_raises_without_resolvable_name(self):
with self.assertRaisesRegex(
ValueError, "Could not init a buildable from 'config_bar'"
):
utils.resolve_function_reference(
function_name='config_bar',
mode=_IRRELEVANT_MODE,
module=None,
allow_imports=True,
failure_msg_prefix='',
)

def test_raises_with_imports_disabled(self):
with self.assertRaisesRegex(ValueError, 'available names: '):
utils.resolve_function_reference(
function_name=(
'fiddle._src.absl_flags.submodule_for_flags_test.config_bar'
),
mode=_IRRELEVANT_MODE,
module=utils,
allow_imports=False,
failure_msg_prefix='',
)


if __name__ == '__main__':
absltest.main()

0 comments on commit de9c408

Please sign in to comment.