Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion param/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
Parameterized, Parameter, Skip, String, ParameterizedFunction,
ParamOverrides, Undefined, get_logger, ParameterizedABC,
)
from .parameterized import (output, script_repr,
from .parameterized import (output, script_repr, raw,
discard_events, edit_constant)
from .parameterized import shared_parameters
from .parameterized import logging_level
Expand Down Expand Up @@ -224,6 +224,7 @@
'param_union',
'parameterized_class',
'random_seed',
'raw',
'resolve_path',
'rx',
'script_repr',
Expand Down
67 changes: 64 additions & 3 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,60 @@ def get_logger(name: Optional[str] = None)->"logging.Logger":
# Hook to apply to depends and bind arguments to turn them into valid parameters
_reference_transforms = []

class Raw:
"""
Wrapper type used to assign ref-like objects to Parameters *without*
triggering automatic resolution.

Normally, when a ref-like value (e.g. a Parameter, reactive expression,
async generator, etc.) is assigned to a Parameter attribute, Param
resolves it to its underlying value. Wrapping the object in ``Raw``
signals that the value should instead be stored as-is.

Example
-------
Comment on lines +175 to +176
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
Example
-------
Examples
--------

>>> obj.some_param = param.Raw(other.param.value)
>>> assert obj.some_param is other.param.value

Notes
-----
- ``Raw`` is only meaningful at assignment time; the wrapper is
unwrapped and not stored.
- The stored value is the inner object itself, not the ``Raw`` instance.
- This allows safe serialization, forwarding, or deferred resolution of
ref-like values.
"""

__slots__ = ["value"]

def __init__(self, value): self.value = value
def __repr__(self): return f"Raw({self.value!r})"


def raw(value: Any):
"""
Mark a value to be assigned *as-is*, skipping Param’s automatic
resolution of ref-like objects.

This allows storing a Parameter, reactive expression, or other
ref-like value directly, without evaluating or resolving it at
assignment time.

Examples
--------
>>> c = MyComponent()
>>> c.target = param.raw(other.param.value)
>>> assert c.target is other.param.value

Notes
-----
- The wrapper is unwrapped during assignment and not stored.
- The stored value is the inner object itself.
- Useful when serializing, forwarding, or deferring resolution of
ref-like values.
"""
return Raw(value)

def register_reference_transform(transform):
"""
Append a transform to extract potential parameter dependencies
Expand All @@ -170,7 +224,6 @@ def register_reference_transform(transform):
Parameters
----------
transform: Callable[Any, Any]

"""
return _reference_transforms.append(transform)

Expand All @@ -182,6 +235,8 @@ def transform_reference(arg):
that are not simple Parameters or functions with dependency
definitions.
"""
if isinstance(arg, Raw):
return arg.value
for transform in _reference_transforms:
if isinstance(arg, Parameter) or hasattr(arg, '_dinfo'):
break
Expand All @@ -207,7 +262,9 @@ def eval_function_with_deps(function):

def resolve_value(value, recursive=True):
"""Resolve the current value of a dynamic reference."""
if not recursive:
if isinstance(value, Raw):
return value.value
elif not recursive:
pass
elif isinstance(value, (list, tuple)):
return type(value)(resolve_value(v) for v in value)
Expand All @@ -231,7 +288,9 @@ def resolve_value(value, recursive=True):

def resolve_ref(reference, recursive=False):
"""Resolve all parameters a dynamic reference depends on."""
if recursive:
if isinstance(reference, Raw):
return []
elif recursive:
if isinstance(reference, (list, tuple, set)):
return [r for v in reference for r in resolve_ref(v, recursive)]
elif isinstance(reference, dict):
Expand Down Expand Up @@ -2442,6 +2501,8 @@ def _sync_refs(self_, *events):
self_.update(updates)

def _resolve_ref(self_, pobj, value):
if isinstance(value, Raw):
return None, None, value.value, False
is_gen = inspect.isgeneratorfunction(value)
is_async = iscoroutinefunction(value) or is_gen
deps = resolve_ref(value, recursive=pobj.nested_refs)
Expand Down
129 changes: 129 additions & 0 deletions tests/testrefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ class Parameters(param.Parameterized):

string_list = param.List(default=[], item_type=str, allow_refs=True, nested_refs=True)

list = param.List(default=[], allow_refs=True, nested_refs=True)

no_refs = param.Parameter(allow_refs=False)

allows_ref = param.Parameter(allow_refs=True)

@param.depends('string')
def formatted_string(self):
if self.string.endswith('?'):
Expand Down Expand Up @@ -363,3 +367,128 @@ def test_resolve_ref_recursive_slice():
refs = resolve_ref(nested, recursive=True)
assert len(refs) == 1
assert refs[0] is p.param.string

def test_raw_parameter_ref_not_resolved_errors():
p = Parameters(string='base')

with pytest.raises(ValueError):
Parameters(string=param.raw(p.param.string))

def test_raw_plain_value_unchanged():
p = Parameters(allows_ref=param.raw('literal'))
assert p.allows_ref == 'literal'

def test_raw_is_transient_unwrapped():
p0 = Parameters()
r = p0.param.string
p = Parameters(allows_ref=param.raw(r))
assert p.allows_ref is r

def test_raw_nested_list_parameter_ref_preserved():
p_src = Parameters(string='alpha')
p = Parameters(list=param.raw([p_src.param.string, 'other']))
# With raw, nested refs are preserved (not resolved)
assert isinstance(p.list, list)
assert p.list[0] is p_src.param.string
assert p.list[1] == 'other'

# Changing the source has no effect — we stored the ref object, not a live link
p_src.string = 'beta'
assert p.list[0] is p_src.param.string

def test_raw_nested_list_mixed_refs_preserved():
s = rx('x')
expr = s + '!'
p_src = Parameters(string='y')
p = Parameters(list=param.raw([expr, p_src.param.string, 'z']))

assert p.list[0] is expr
assert p.list[1] is p_src.param.string
assert p.list[2] == 'z'

s.rx.value = 'xx'
p_src.string = 'yy'
# Nothing auto-updates because we stored verbatim objects
assert p.list[0] is expr
assert p.list[1] is p_src.param.string

def test_raw_nested_dict_value_parameter_ref_preserved():
p_src = Parameters(string='keyed')
p = Parameters(dictionary=param.raw({'k': p_src.param.string, 'n': 1}))
# Values kept as-is
assert p.dictionary['k'] is p_src.param.string
assert p.dictionary['n'] == 1
# Changing source does not propagate
p_src.string = 'changed'
assert p.dictionary['k'] is p_src.param.string

def test_raw_nested_dict_deep_structure_preserved():
p_src = Parameters(string='deep')
expr = (rx('a') + rx('b'))
nested = {
'level1': {
'list': [p_src.param.string, expr, {'leaf': p_src.param.string}]
}
}
p = Parameters(dictionary=param.raw(nested))

got = p.dictionary
assert got['level1']['list'][0] is p_src.param.string
assert got['level1']['list'][1] is expr
assert got['level1']['list'][2]['leaf'] is p_src.param.string

def test_raw_nested_refs_do_not_resolve_even_when_param_has_nested_refs_true():
p_src = Parameters(string='s')
obj = Parameters(
dictionary=param.raw({'inner': [p_src.param.string]}),
list=param.raw([p_src.param.string, 'x'])
)
assert obj.dictionary['inner'][0] is p_src.param.string
assert obj.list[0] is p_src.param.string

def test_raw_survives_param_update_context_and_reassignments():
p_src = Parameters(string='A')
p = Parameters(allows_ref=param.raw(p_src.param.string))
assert p.allows_ref is p_src.param.string

with p.param.update(allows_ref=param.raw(p_src.param.string)):
assert p.allows_ref is p_src.param.string
p_src.string = 'B'
assert p.allows_ref is p_src.param.string

p.allows_ref = p_src.param.string
assert p.allows_ref == 'B'
p_src.string = 'C'
Comment thread
philippjfr marked this conversation as resolved.
assert p.allows_ref == 'C'

def test_raw_stores_callables_or_generators_without_consuming():
started = {'gen': False, 'async': False}

def gen():
started['gen'] = True
yield 'x'

async def agen():
started['async'] = True
if False: # pragma: no cover (keep as async generator)
Comment thread
philippjfr marked this conversation as resolved.
Outdated
yield None

p = Parameters()
p.allows_ref = param.raw(gen) # store the generator *function* itself
Comment thread
philippjfr marked this conversation as resolved.
Outdated
assert p.allows_ref is gen
assert started['gen'] is False # not invoked
Comment thread
philippjfr marked this conversation as resolved.
Outdated

p.allows_ref = param.raw(agen) # store async generator *function* itself
Comment thread
philippjfr marked this conversation as resolved.
Outdated
assert p.allows_ref is agen
assert started['async'] is False # not invoked
Comment thread
philippjfr marked this conversation as resolved.
Outdated

def test_resolve_ref_hides_inner_when_given_raw_directly():
p_src = Parameters()
refs = resolve_ref(param.raw(p_src.param.string))
assert len(refs) == 0

def test_resolve_ref_recursive_on_container_from_raw():
p_src = Parameters()
nested = param.raw([{'k': (p_src.param.string,)}])
refs = resolve_ref(nested, recursive=True)
assert len(refs) == 0
Loading