Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
86 changes: 86 additions & 0 deletions doc/user_guide/References.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,92 @@
":::"
]
},
{
"cell_type": "markdown",
"id": "ae3232e6-91dd-4a09-8301-b5a79ae42f8f",
"metadata": {},
"source": [
"## Assigning refs literally with `LiteralRef`\n",
"\n",
"Sometimes you don’t want Param to resolve a ref-like value on assignment—you want to store the ref object itself.\n",
"\n",
"Use `LiteralRef` to assign a ref as-is (verbatim), so you can forward it, serialize it, or resolve it later under different conditions."
]
},
{
"cell_type": "markdown",
"id": "15985908-d821-41a6-a4e0-41a8a49d9aae",
"metadata": {},
"source": [
"\n",
"By default, assigning a ref-like value resolves and links it:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b2c80606-f60f-4534-b2da-608efd2c9ab8",
"metadata": {},
"outputs": [],
"source": [
"class Example(param.Parameterized):\n",
" value = param.Parameter(allow_refs=True)\n",
Copy link
Member

Choose a reason for hiding this comment

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

Just to avoid the possible confusion that this would only work with the base Parameter.

Suggested change
" value = param.Parameter(allow_refs=True)\n",
" value = param.String(allow_refs=True)\n",

Copy link
Member Author

Choose a reason for hiding this comment

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

Well this will error, because the ref value (i.e. the Parameter instance) is not a string.

"\n",
"src = Example(value=\"A\")\n",
"dst = Example()\n",
"\n",
"dst.value = src.param.value\n",
"assert dst.value == \"A\"\n",
"src.value = \"B\"\n",
"assert dst.value == \"B\" # still linked"
]
},
{
"cell_type": "markdown",
"id": "3942b3d3-3d0f-4f99-bb9e-b79ee991b635",
"metadata": {},
"source": [
"i.e. the `dst.value` tracks the `src.value`. Wrapping the reference in the `LiteralRef` skips the resolution:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cf3fae7b-7dc6-47b5-9ed9-78cd5799ff52",
"metadata": {},
"outputs": [],
"source": [
"dst.value = param.parameterized.LiteralRef(src.param.value)\n",
"\n",
"dst.value"
]
},
{
"cell_type": "markdown",
"id": "e7174aad-4751-436e-8b68-36557597fccc",
"metadata": {},
"source": [
"i.e. Param unwraps the `LiteralRef` object but does not resolve the `Parameter` reference. `dst` now holds the actual `Parameter` object."
]
},
{
"cell_type": "markdown",
"id": "481223e8-750e-41b4-8376-63381b2012c6",
"metadata": {},
"source": [
"#### Key points\n",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"#### Key points\n",
"### Key points\n",

"\n",
"- The wrapper is transient: it's unwrapped at assignment time; the stored value is the inner ref object.\n",
"- No subscription, no evaluation, no resolution occurs during assignment.\n",
"\n",
"#### When to use LiteralRef\n",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"#### When to use LiteralRef\n",
"### When to use LiteralRef\n",

"\n",
"- You're wiring a graph of components and need to pass a ref downstream without linking it yet.\n",
"- You're building a config/serialization format that stores refs (not their current values).\n",
"- You have reactive/async producers you don't want to start/subscribe/consume during assignment.\n",
"- You want to preserve intent (“this is a ref”) instead of collapsing it to a concrete value."
]
},
{
"cell_type": "markdown",
"id": "6ffd34f8-211c-4945-95b8-e87ec712e028",
Expand Down
45 changes: 41 additions & 4 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,36 @@ 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 LiteralRef:
"""
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 ``LiteralRef``
signals that the value should instead be stored as-is.

Example
-------
Comment on lines +175 to +176
Copy link
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.LiteralRef(other.param.value)
>>> assert obj.some_param is other.param.value

Notes
-----
- ``LiteralRef`` is only meaningful at assignment time; the wrapper is
unwrapped and not stored.
- The stored value is the inner object itself, not the ``LiteralRef`` 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"LiteralRef({self.value!r})"


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

"""
return _reference_transforms.append(transform)

Expand All @@ -182,6 +211,8 @@ def transform_reference(arg):
that are not simple Parameters or functions with dependency
definitions.
"""
if isinstance(arg, LiteralRef):
return arg.value
for transform in _reference_transforms:
if isinstance(arg, Parameter) or hasattr(arg, '_dinfo'):
break
Expand All @@ -207,7 +238,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, LiteralRef):
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 +264,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, LiteralRef):
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,10 +2477,12 @@ def _sync_refs(self_, *events):
self_.update(updates)

def _resolve_ref(self_, pobj, value):
if isinstance(value, LiteralRef):
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)
if not (deps or is_async or is_gen):
if not (deps or is_async or is_gen or pobj.nested_refs):
return None, None, value, False
ref = value
try:
Expand Down
140 changes: 139 additions & 1 deletion tests/testrefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import param
import pytest

from param.parameterized import Skip, resolve_ref
from param.parameterized import LiteralRef, Skip, resolve_ref
from param.reactive import bind, rx


Expand Down 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,137 @@ 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_literal_ref_parameter_ref_not_resolved_errors():
p = Parameters(string='base')

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

def test_literal_ref_plain_value_unchanged():
p = Parameters(allows_ref=LiteralRef('literal'))
assert p.allows_ref == 'literal'

def test_literal_ref_is_transient_unwrapped():
p0 = Parameters()
r = p0.param.string
p = Parameters(allows_ref=LiteralRef(r))
assert p.allows_ref is r

def test_literal_ref_nested_list_parameter_ref_preserved():
p_src = Parameters(string='alpha')
p = Parameters(list=LiteralRef([p_src.param.string, 'other']))
# With literal_ref, 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_literal_ref_nested_list_mixed_refs_preserved():
s = rx('x')
expr = s + '!'
p_src = Parameters(string='y')
p = Parameters(list=LiteralRef([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_literal_ref_nested_dict_value_parameter_ref_preserved():
p_src = Parameters(string='keyed')
p = Parameters(dictionary=LiteralRef({'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_literal_ref_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=LiteralRef(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_literal_ref_nested_refs_do_not_resolve_even_when_param_has_nested_refs_true():
p_src = Parameters(string='s')
obj = Parameters(
dictionary=LiteralRef({'inner': [p_src.param.string]}),
list=LiteralRef([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_literal_ref_inner_nested_refs_do_not_resolve_even_when_param_has_nested_refs_true():
p_src = Parameters(string='s')
obj = Parameters(
dictionary={'inner': [LiteralRef(p_src.param.string)]},
list=[LiteralRef(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_literal_ref_survives_param_update_context_and_reassignments():
p_src = Parameters(string='A')
p = Parameters(allows_ref=LiteralRef(p_src.param.string))
assert p.allows_ref is p_src.param.string

with p.param.update(allows_ref=LiteralRef(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'
assert p.allows_ref == 'C'

def test_literal_ref_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:
yield None

p = Parameters()
p.allows_ref = LiteralRef(gen)
assert p.allows_ref is gen
assert started['gen'] is False

p.allows_ref = LiteralRef(agen)
assert p.allows_ref is agen
assert started['async'] is False

def test_resolve_ref_hides_inner_when_given_literal_ref_directly():
p_src = Parameters()
refs = resolve_ref(LiteralRef(p_src.param.string))
assert len(refs) == 0

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