diff --git a/fiddle/_src/building.py b/fiddle/_src/building.py index 6212a6fb..ecbcdd2c 100644 --- a/fiddle/_src/building.py +++ b/fiddle/_src/building.py @@ -96,14 +96,21 @@ def _make_message(current_path: daglish.Path, buildable: config_lib.Buildable, def call_buildable( buildable: config_lib.Buildable, - arguments: Dict[str, Any], + kwargs: Dict[str, Any], *, current_path: daglish.Path, ) -> Any: - make_message = functools.partial(_make_message, current_path, buildable, - arguments) + """Run the __build__ method on a Buildable given keyword arguments.""" + make_message = functools.partial( + _make_message, current_path, buildable, kwargs + ) + args = [] + for name in buildable.__signature_info__.positional_arg_names: + if name in kwargs: + args.append(kwargs.pop(name)) + args.extend(kwargs.pop('__args__', [])) with reraised_exception.try_with_lazy_message(make_message): - return buildable.__build__(**arguments) + return buildable.__build__(*args, **kwargs) # Define typing overload for `build(Partial[T])` diff --git a/fiddle/_src/config.py b/fiddle/_src/config.py index 037780cd..2ad66c1a 100644 --- a/fiddle/_src/config.py +++ b/fiddle/_src/config.py @@ -22,8 +22,9 @@ import copy import dataclasses import functools +import inspect import types -from typing import Any, Callable, Collection, Dict, FrozenSet, Generic, Iterable, Mapping, NamedTuple, Optional, Set, Tuple, Type, TypeVar, Union +from typing import Any, Callable, Collection, Dict, FrozenSet, Generic, Iterable, List, Mapping, NamedTuple, Optional, Set, Tuple, Type, TypeVar, Union from fiddle._src import daglish from fiddle._src import history @@ -242,10 +243,15 @@ def __init__( arg_history.add_new_value('__fn_or_cls__', fn_or_cls) super().__setattr__('__argument_history__', arg_history) super().__setattr__('__argument_tags__', collections.defaultdict(set)) - arguments = signatures.SignatureInfo.signature_binding( - fn_or_cls, *args, **kwargs + arguments, positional_arguments = ( + signatures.SignatureInfo.signature_binding(fn_or_cls, *args, **kwargs) ) + if positional_arguments: + self.__arguments__['__args__'] = list(positional_arguments) + for i, value in enumerate(positional_arguments): + self[i] = value + for name, value in arguments.items(): setattr(self, name, value) @@ -258,6 +264,7 @@ def __init__( def __init_callable__( self, fn_or_cls: Union['Buildable[T]', TypeOrCallableProducingT[T]] ) -> None: + """Save information on `fn_or_cls` to the `Buildable`.""" if isinstance(fn_or_cls, Buildable): raise ValueError( 'Using the Buildable constructor to convert a buildable to a new ' @@ -273,9 +280,11 @@ def __init_callable__( super().__setattr__('__fn_or_cls__', fn_or_cls) super().__setattr__('__arguments__', {}) signature = signatures.get_signature(fn_or_cls) + # Several attributes are computed automatically by SignatureInfo during + # `__post_init__`. super().__setattr__( '__signature_info__', - signatures.SignatureInfo(signature), + signatures.SignatureInfo(signature=signature), ) def __init_subclass__(cls): @@ -311,6 +320,14 @@ def __path_elements__(self) -> Tuple[daglish.Attr]: def __getattr__(self, name: str): """Get parameter with given ``name``.""" + if name == 'posargs': + if not self.__signature_info__.has_var_positional: + raise TypeError( + "This function doesn't have variadic positional arguments (*args). " + 'Please set other (including positional-only) arguments by name.' + ) + + name = '__args__' value = self.__arguments__.get(name, _UNSET_SENTINEL) if value is not _UNSET_SENTINEL: @@ -340,9 +357,39 @@ def __getattr__(self, name: str): ) raise AttributeError(msg) + def __setitem__(self, key: Any, value: Any): + if not isinstance(key, (int, slice)): + raise TypeError( + 'Setting arguments by index is only supported for variadic ' + "arguments (*args), like my_config[4] = 'foo'." + ) + if not self.__signature_info__.has_var_positional: + raise TypeError( + "This function doesn't have variadic positional arguments (*args). " + 'Please set other (including positional-only) arguments by name.' + ) + + if '__args__' not in self.__arguments__: + self.__arguments__['__args__'] = [] + self.__argument_history__.add_new_value('__args__', []) + self.__arguments__['__args__'][key] = value + self.__argument_history__.add_new_value( + '__args__', self.__arguments__['__args__'] + ) + + def __getitem__(self, key: Any): + if not isinstance(key, slice): + raise TypeError( + 'Getting arguments by index is only supported when using slice, ' + 'for example `v = my_config[:2]`, or using the `posargs` attr ' + f'instead, like v = my_config[0]. Got {type(key)} type as key.' + ) + return self.posargs[key] + def __setattr__(self, name: str, value: Any): """Sets parameter ``name`` to ``value``.""" - + if name == 'posargs': + name = '__args__' self.__signature_info__.validate_param_name(name, self.__fn_or_cls__) if isinstance(value, TaggedValueCls): @@ -362,6 +409,8 @@ def __setattr__(self, name: str, value: Any): def __delattr__(self, name): """Unsets parameter ``name``.""" + if name == 'posargs': + name = '__args__' try: del self.__arguments__[name] self.__argument_history__.add_deleted_value(name) @@ -488,9 +537,7 @@ def __getstate__(self): Dict of serialized state. """ result = dict(self.__dict__) - result['__signature_info__'] = signatures.SignatureInfo( # pytype: disable=wrong-arg-types - None, result['__signature_info__'].has_var_keyword - ) + result['__signature_info__'] = signatures.SignatureInfo(None) # pytype: disable=wrong-arg-types return result def __setstate__(self, state) -> None: @@ -503,8 +550,10 @@ def __setstate__(self, state) -> None: """ self.__dict__.update(state) # Support unpickle. if self.__signature_info__.signature is None: - self.__signature_info__.signature = signatures.get_signature( - self.__fn_or_cls__ + signature = signatures.get_signature(self.__fn_or_cls__) + super().__setattr__( + '__signature_info__', + signatures.SignatureInfo(signature=signature), ) @@ -637,6 +686,51 @@ def _field_uses_default_factory(dataclass_type: Type[Any], field_name: str): return False +def _align_var_positional_args( + new_signature: inspect.Signature, + original_args: Dict[str, Any], + drop_invalid_args: bool, +) -> List[str]: + """Returns the list of positional arguments to unpack.""" + args_start_index = -1 + for index, arg in enumerate(new_signature.parameters.keys()): + if arg not in original_args.keys(): + args_start_index = index + break + if (args_start_index == -1 and original_args['__args__']) or ( + len(new_signature.parameters) + < args_start_index + 1 + len(original_args['__args__']) + ): + if not drop_invalid_args: + raise ValueError( + 'new_callable does not have enough arguments when unpack' + f' *args: {original_args["__args__"]} from the original' + ' buildable.' + ) + arg_keys = list(new_signature.parameters.keys())[args_start_index:] + return arg_keys + + +def _expand_args_history( + arg_keys: List[str], buildable: Buildable +) -> List[List[history.HistoryEntry]]: + """Returns expanded history entries for positional arguments.""" + args_history = buildable.__argument_history__['__args__'] + expaneded_history = [] + for index in range(len(arg_keys)): + expanded_entries = [] + for entry in args_history: + new_entry = copy.copy(entry) + if isinstance(new_entry.new_value, list): + if index >= len(new_entry.new_value): + new_entry.new_value = history.NOTSET + else: + new_entry.new_value = new_entry.new_value[index] + expanded_entries.append(new_entry) + expaneded_history.append(expanded_entries) + return expaneded_history + + def update_callable( buildable: Buildable, new_callable: TypeOrCallableProducingT, @@ -667,23 +761,40 @@ def update_callable( # Note: can't call `setattr` on all the args to validate them, because that # will result in duplicate history entries. original_args = buildable.__arguments__ - signature = signatures.get_signature(new_callable) - if any( - param.kind == param.VAR_POSITIONAL - for param in signature.parameters.values() - ): - raise NotImplementedError( - 'Variable positional arguments (aka `*args`) not supported.' - ) - signature_info = signatures.SignatureInfo(signature) - object.__setattr__( - buildable, - '__signature_info__', - signature_info, - ) - if not signature_info.has_var_keyword: + new_signature = signatures.get_signature(new_callable) + # Update the signature early so that we can set arguments by position. + # Otherwise, parameter validation logics would complain about argument + # name not exists. + object.__setattr__(buildable, '__signature__', new_signature) + new_signature_info = signatures.SignatureInfo(signature=new_signature) + original_signature_info = buildable.__signature_info__ + object.__setattr__(buildable, '__signature_info__', new_signature_info) + + if new_signature_info.has_var_positional: + # If only new callable has positional arguments + if not original_signature_info.has_var_positional: + buildable.__arguments__['__args__'] = [] + buildable.__argument_history__.add_new_value('__args__', []) + else: + # If only the original config has *args + if original_signature_info.has_var_positional: + arg_keys = _align_var_positional_args( + new_signature, original_args, drop_invalid_args + ) + expanded_history = _expand_args_history(arg_keys, buildable) + + for arg, value, history_extries in zip( + arg_keys, original_args['__args__'], expanded_history + ): + buildable.__setattr__(arg, value) + buildable.__argument_history__[arg] = history_extries + buildable.__delattr__('__args__') + + if not new_signature_info.has_var_keyword: invalid_args = [ - arg for arg in original_args.keys() if arg not in signature.parameters + arg + for arg in original_args.keys() + if arg not in new_signature.parameters and arg != '__args__' ] if invalid_args: if drop_invalid_args: diff --git a/fiddle/_src/config_test.py b/fiddle/_src/config_test.py index c61f24ae..c8399a74 100644 --- a/fiddle/_src/config_test.py +++ b/fiddle/_src/config_test.py @@ -73,6 +73,10 @@ def fn_with_var_args_and_kwargs(arg1, *args, kwarg1=None, **kwargs): # pylint: return locals() +def fn_with_args_and_kwargs_only(*args, **kwargs): + return args, kwargs + + def make_typed_config() -> fdl.Config[SampleClass]: """Helper function which returns a fdl.Config whose type is known.""" return fdl.Config(SampleClass, arg1=1, arg2=2) @@ -212,6 +216,84 @@ def test_config_for_functions_with_var_args_and_kwargs(self): 'kwargs': 'kwarg_called_kwarg' }) + # Test variadic positional arguments *args support. + def test_args_config_access(self): + fn_config = fdl.Config(fn_with_var_args, 'foo', 'bar', 'baz') + + with self.subTest('ordered_arguments'): + self.assertEqual( + fdl.ordered_arguments(fn_config), + { + 'arg1': 'foo', + '__args__': ['bar', 'baz'], + }, + ) + + with self.subTest('posargs_access'): + self.assertEqual(fn_config.posargs[0], 'bar') + self.assertEqual(fn_config.posargs[1], 'baz') + self.assertSequenceEqual(fn_config.posargs, ['bar', 'baz']) + + with self.subTest('index_access'): + with self.assertRaisesRegex( + TypeError, + 'Getting arguments by index is only supported when using slice', + ): + _ = fn_config[0] + + with self.subTest('slice_access'): + self.assertEmpty(fn_config[:0]) + self.assertSequenceEqual(fn_config[:1], ['bar']) + self.assertSequenceEqual(fn_config[:], ['bar', 'baz']) + + def test_args_config_posargs_append(self): + fn_config = fdl.Config(fn_with_var_args, 'foo', 'bar', 'baz') + fn_config.posargs.append('foo') + self.assertSequenceEqual(fn_config.posargs, ['bar', 'baz', 'foo']) + + def test_args_config_slice_mutation(self): + fn_config = fdl.Config(fn_with_var_args, 'foo', 'bar', 'baz') + self.assertSequenceEqual(fn_config[:], ['bar', 'baz']) + fn_config[:1] = ['zero', 'one'] + self.assertSequenceEqual(fn_config[:], ['zero', 'one', 'baz']) + + def test_args_config_shallow_copy(self): + fn_config = fdl.Config(fn_with_var_args, 'foo', 'bar', 'baz') + self.assertLen(fn_config[:], 2) + a_copy = fn_config[:] + a_copy.append('foo') + self.assertLen(fn_config[:], 2) + self.assertLen(a_copy, 3) + + def test_index_mutation(self): + fn_config = fdl.Config(fn_with_var_args, 'foo', 'bar', 'baz') + fn_config[0] = 'foo' + self.assertEqual(fn_config.posargs[0], 'foo') + fn_config[-1] = 'last' + self.assertLen(fn_config.posargs, 2) + self.assertEqual(fn_config.posargs[1], 'last') + self.assertEqual(fn_config.posargs[-1], 'last') + + def test_index_out_of_range(self): + fn_config = fdl.Config(fn_with_var_args, 'foo', 'bar', 'baz') + self.assertLen(fn_config[:], 2) + with self.assertRaisesRegex( + IndexError, 'list assignment index out of range' + ): + fn_config[2] = 'index-2' + + def test_args_config_build(self): + fn_config = fdl.Config(fn_with_var_args, 'foo', 'bar', 'baz') + fn_args = fdl.build(fn_config) + self.assertEqual( + fn_args, + { + 'arg1': 'foo', + 'args': ('bar', 'baz'), + 'kwarg1': None, + }, + ) + def test_config_for_dicts(self): dict_config = fdl.Config(dict, a=1, b=2) dict_config.c = 3 @@ -745,12 +827,6 @@ def test_nonexistent_var_args_parameter_error(self): with self.assertRaisesRegex(TypeError, expected_msg): fn_config.args = (1, 2, 3) - def test_unsupported_var_args_error(self): - expected_msg = (r'Variable positional arguments \(aka `\*args`\) not ' - r'supported\.') - with self.assertRaisesRegex(NotImplementedError, expected_msg): - fdl.Config(fn_with_var_args, 1, 2, 3) - def test_build_inside_build(self): def inner_build(x: int) -> str: @@ -1074,11 +1150,108 @@ def test_update_callable_new_kwargs(self): } }, fdl.build(cfg)) - def test_update_callable_varargs(self): - cfg = fdl.Config(fn_with_var_kwargs, 1, 2) - with self.assertRaisesRegex(NotImplementedError, - 'Variable positional arguments'): - fdl.update_callable(cfg, fn_with_var_args_and_kwargs) + # For `update_callable` involves variadic positional arguments, we test + # three patterns below. + # Pattern 1: *args -> *args + def test_update_args_to_args(self): + cfg = fdl.Config(fn_with_var_args, 1, 2, kwarg1=3) + fdl.update_callable(cfg, fn_with_var_args_and_kwargs) + self.assertEqual( + cfg.__arguments__, {'arg1': 1, '__args__': [2], 'kwarg1': 3} + ) + self.assertEqual( + fdl.build(cfg), + {'arg1': 1, 'args': (2,), 'kwarg1': 3, 'kwargs': {}}, + ) + + def test_update_args_kwargs_only_to_args(self): + cfg = fdl.Config(fn_with_args_and_kwargs_only, 1, 2, 3, kwarg1=4, kwarg2=5) + cfg.posargs[0] = 10 + cfg.kwarg1 = 40 + config_lib.update_callable(cfg, fn_with_var_args_and_kwargs) + self.assertEqual( + cfg.__arguments__, + {'__args__': [10, 2, 3], 'kwarg1': 40, 'kwarg2': 5}, + ) + self.assertEqual( + fdl.build(cfg), + {'arg1': 10, 'args': (2, 3), 'kwarg1': 40, 'kwargs': {'kwarg2': 5}}, + ) + + # Pattern 2: *args -> no *args + def test_update_args_to_no_args(self): + cfg = fdl.Config(fn_with_var_args, 1, 2, kwarg1=3) + fdl.update_callable(cfg, basic_fn) + cfg.arg2 = 22 + self.assertEqual(cfg.__arguments__, {'arg1': 1, 'arg2': 22, 'kwarg1': 3}) + self.assertEqual( + fdl.build(cfg), {'arg1': 1, 'arg2': 22, 'kwarg1': 3, 'kwarg2': None} + ) + + # Pattern 3: no *args -> *args + def test_update_no_args_to_args(self): + cfg = fdl.Config(basic_fn, 1, 2, kwarg1=3) + fdl.update_callable(cfg, fn_with_var_args_and_kwargs) + self.assertEqual( + cfg.__arguments__, {'__args__': [], 'arg1': 1, 'arg2': 2, 'kwarg1': 3} + ) + self.assertEqual( + fdl.build(cfg), + {'arg1': 1, 'args': (), 'kwarg1': 3, 'kwargs': {'arg2': 2}}, + ) + + # Test history tracking for *args + def test_update_args_to_no_args_history(self): + def no_args_fn(arg1, arg2, arg3, kwarg1): + del arg1 + del arg2 + del arg3 + del kwarg1 + + cfg = fdl.Config(fn_with_var_args, 1, 2, kwarg1=3) + cfg.posargs = [10, 20] + del cfg.posargs + cfg.posargs = [100, 200] + fdl.update_callable(cfg, no_args_fn) + + self.assertEqual( + set(['arg1', 'arg2', 'arg3', 'kwarg1', '__args__', '__fn_or_cls__']), + set(cfg.__argument_history__.keys()), + ) + self.assertLen(cfg.__argument_history__['arg1'], 1) + self.assertLen(cfg.__argument_history__['arg2'], 4) + self.assertLen(cfg.__argument_history__['arg3'], 4) + self.assertLen(cfg.__argument_history__['kwarg1'], 1) + self.assertLen(cfg.__argument_history__['__args__'], 5) + self.assertLen(cfg.__argument_history__['__fn_or_cls__'], 2) + + expected_new_value = { + 'arg2': [2, 10, history.DELETED, 100], + 'arg3': [history.NOTSET, 20, history.DELETED, 200], + } + for arg_name in ('arg2', 'arg3'): + for index in range(4): + self.assertEqual( + cfg.__argument_history__[arg_name][index].new_value, + expected_new_value[arg_name][index], + ) + self.assertRegex( + str(cfg.__argument_history__[arg_name][index].location), + r'config_test.py:\d+:test_update_args_to_no_args_history', + ) + + self.assertLess( + cfg.__argument_history__[arg_name][0].sequence_id, + cfg.__argument_history__[arg_name][1].sequence_id, + ) + self.assertEqual( + cfg.__argument_history__[arg_name][1].sequence_id + 1, + cfg.__argument_history__[arg_name][2].sequence_id, + ) + self.assertEqual( + cfg.__argument_history__[arg_name][2].sequence_id + 1, + cfg.__argument_history__[arg_name][3].sequence_id, + ) def test_get_callable(self): cfg = fdl.Config(basic_fn) diff --git a/fiddle/_src/history.py b/fiddle/_src/history.py index 89fd77f7..ddba94d2 100644 --- a/fiddle/_src/history.py +++ b/fiddle/_src/history.py @@ -129,14 +129,23 @@ def _stacktrace_location_provider() -> Location: class _Deleted: - """A marker object to indicated deletion.""" + """A marker object to indicate deletion.""" def __repr__(self): return "DELETED" +class _NotSet: + """A marker object to indicate a value is not set for *args.""" + + def __repr__(self): + return "NOT_SET" + + # A marker object to record when a field was deleted. DELETED = _Deleted() +# A marker object to record this value is not set for *args. +NOTSET = _NotSet() class ChangeKind(enum.Enum): @@ -149,7 +158,7 @@ class ChangeKind(enum.Enum): UPDATE_TAGS = 2 -@dataclasses.dataclass(frozen=True) +@dataclasses.dataclass(frozen=False) class HistoryEntry: """An entry in the history table for a config object. @@ -166,7 +175,7 @@ class HistoryEntry: sequence_id: int param_name: str kind: ChangeKind - new_value: Union[Any, FrozenSet[tag_type.TagType], _Deleted] + new_value: Union[Any, FrozenSet[tag_type.TagType], _Deleted, _NotSet] location: Location def __deepcopy__(self, memo): diff --git a/fiddle/_src/signatures.py b/fiddle/_src/signatures.py index f975bba2..68dedd64 100644 --- a/fiddle/_src/signatures.py +++ b/fiddle/_src/signatures.py @@ -17,7 +17,7 @@ import dataclasses import inspect -from typing import Any, Callable, Dict, Generic, Mapping, Tuple, Type +from typing import Any, Callable, Dict, Generic, List, Mapping, Tuple, Type import weakref import typing_extensions @@ -136,48 +136,71 @@ class SignatureInfo: signature: inspect.Signature has_var_keyword: bool = None + has_var_positional: bool = None + positional_arg_names: List[str] = dataclasses.field(default_factory=list) def __post_init__(self): + # During serilization, signature is set to None so no action is needed. + if self.signature is None: + return + + if self.has_var_positional is None: + self.has_var_positional = any( + param.kind == param.VAR_POSITIONAL + for param in self.signature.parameters.values() + ) if self.has_var_keyword is None: - has_var_keyword = any( + self.has_var_keyword = any( param.kind == param.VAR_KEYWORD for param in self.signature.parameters.values() ) - self.has_var_keyword = has_var_keyword + + # If *args exists, we must pass things before it in positional format. This + # list tracks those arguments. + maybe_positional_args = [] + positional_only_args = [] + for param in self.signature.parameters.values(): + if param.kind == param.POSITIONAL_ONLY: + positional_only_args.append(param.name) + elif param.kind == param.POSITIONAL_OR_KEYWORD: + maybe_positional_args.append(param.name) + elif param.kind == param.VAR_POSITIONAL: + positional_only_args.extend(maybe_positional_args) + self.positional_arg_names = positional_only_args @staticmethod def signature_binding(fn_or_cls, *args, **kwargs) -> Any: """Bind partial for arguments and return arguments.""" signature = get_signature(fn_or_cls) + positional_arguments = () arguments = signature.bind_partial(*args, **kwargs).arguments for name in list(arguments.keys()): # Make a copy in case we mutate. param = signature.parameters[name] if param.kind == param.VAR_POSITIONAL: - # TODO(b/197367863): Add *args support. - err_msg = ( - 'Variable positional arguments (aka `*args`) not supported. ' - f'Found param `{name}` in `{fn_or_cls}`.' - ) - raise NotImplementedError(err_msg) + positional_arguments = arguments.pop(param.name) elif param.kind == param.VAR_KEYWORD: arguments.update(arguments.pop(param.name)) - return arguments + return arguments, positional_arguments def validate_param_name(self, name, fn_or_cls) -> None: """Raises an error if ``name`` is not a valid parameter name.""" + if name == '__args__': + if self.has_var_positional: + return + else: + raise ValueError( + 'This Buildable does not have variadic positional argument.' + ) + param = self.signature.parameters.get(name) - if param is not None: - if param.kind == param.POSITIONAL_ONLY: - # TODO(b/197367863): Add positional-only arg support. - raise NotImplementedError( - 'Positional only arguments not supported. ' - f'Tried to set {name!r} on {fn_or_cls}' - ) - elif param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD): - # Just pretend it doesn't correspond to a valid parameter name... below - # a TypeError will be thrown unless there is a **kwargs parameter. - param = None + if param is not None and param.kind in ( + param.VAR_POSITIONAL, + param.VAR_KEYWORD, + ): + # Just pretend it doesn't correspond to a valid parameter name... below + # a TypeError will be thrown unless there is a **kwargs parameter. + param = None if param is None and not self.has_var_keyword: if name in self.signature.parameters: diff --git a/fiddle/examples/colabs/basic_api.ipynb b/fiddle/examples/colabs/basic_api.ipynb index 05cb2024..62877ed1 100644 --- a/fiddle/examples/colabs/basic_api.ipynb +++ b/fiddle/examples/colabs/basic_api.ipynb @@ -400,50 +400,7 @@ "id": "G3IVzfktqAIu" }, "source": [ - "but `*args` are currently unsupported," - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "height": 34 - }, - "executionInfo": { - "elapsed": 58, - "status": "ok", - "timestamp": 1666724790041, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": 420 - }, - "id": "p-r9vED0qNib", - "outputId": "aba61202-1149-4aee-dd1c-a56e9a4f31f3" - }, - "outputs": [ - { - "data": { - "text/html": [ - "\u003cspan style=\"color: red\"\u003eNotImplementedError: Variable positional arguments (aka `*args`) not supported.\u003c/span\u003e" - ], - "text/plain": [ - "\u003cIPython.core.display.HTML object\u003e" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "try:\n", - " fdl.Config(args_and_kwargs, 4, 7)\n", - "except NotImplementedError as e:\n", - " %html \u003cspan style=\"color: red\"\u003eNotImplementedError: {e}\u003c/span\u003e\n", - "else:\n", - " raise AssertionError(\"This should raise an error!\")" + "# TODO(b/288893692): Update docs for posistional args." ] }, { diff --git a/fiddle/history.py b/fiddle/history.py index a6d7c826..f7f321da 100644 --- a/fiddle/history.py +++ b/fiddle/history.py @@ -24,6 +24,7 @@ from fiddle._src.history import HistoryEntry from fiddle._src.history import Location from fiddle._src.history import LocationProvider +from fiddle._src.history import NOTSET from fiddle._src.history import set_tracking from fiddle._src.history import suspend_tracking from fiddle._src.history import tracking_enabled