Skip to content
Merged
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
1 change: 1 addition & 0 deletions doc/reference/param/parameterized.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ of {py:class}`Parameterized` classes and instances.
:toctree: generated/

parameterized.batch_call_watchers
parameterized.classlist
concrete_descendents
depends
descendents
Expand Down
11 changes: 9 additions & 2 deletions doc/user_guide/Parameter_Types.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,7 @@
"id": "fbd3c5d3",
"metadata": {},
"source": [
"As you can see, `D` is a type of `Parameterized`, and a `Parameterized` is a type of Python object. Conversely (and typically more usefully), `param.descendents` provides a list of the subclasses of the provided object, including itself:"
"As you can see, `D` is a type of `Parameterized`, and a `Parameterized` is a type of Python object. Conversely (and typically more usefully), `param.descendents(<class>, concrete=True)` provides a list of the concrete subclasses of the provided object, including itself:"
]
},
{
Expand All @@ -1160,7 +1160,14 @@
"metadata": {},
"outputs": [],
"source": [
"param.descendents(param.SelectorBase)"
"class Base(param.Parameterized):\n",
" __abstract = True\n",
"\n",
"class A1(Base): pass\n",
"class A2(Base): pass\n",
"class B1(A1): pass\n",
"\n",
"param.descendents(Base, concrete=True)"
]
},
{
Expand Down
58 changes: 42 additions & 16 deletions param/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import re
import traceback
import warnings
from collections import OrderedDict, abc, defaultdict
from collections import Counter, OrderedDict, abc, defaultdict
from contextlib import contextmanager
from numbers import Real
from textwrap import dedent
Expand Down Expand Up @@ -433,24 +433,30 @@ def _is_abstract(class_: type) -> bool:

def descendents(class_: type, concrete: bool = False) -> list[type]:
"""
Return a list of all descendant classes of a given class.
Return a list of all descendent classes of a given class.

This function performs a breadth-first traversal of the class hierarchy,
collecting all subclasses of `class_`. The result includes `class_` itself
and all of its subclasses. If `concrete=True`, abstract base classes
are excluded from the result.
collecting all subclasses of ``class_``. The result includes ``class_`` itself
and all of its subclasses. If ``concrete=True``, abstract base classes
are excluded from the result, including :class:`Parameterized` abstract
classes declared with ``__abstract = True``.

Parameters
----------
class_ : type
The base class whose descendants should be found.
concrete : bool, optional
If `True`, exclude abstract classes from the result. Default is `False`.
If ``True``, exclude abstract classes from the result. Default is ``False``.

.. versionadded:: 2.3.0

Added to encourage users to use :func:`descendents` in favor of
:func:`concrete_descendents` that clobbers classes sharing the same name.

Returns
-------
list of type
A list of descendant classes, ordered from the most base to the most derived.
list[type]
A list of descendent classes, ordered from the most base to the most derived.

Examples
--------
Expand Down Expand Up @@ -481,18 +487,38 @@ def descendents(class_: type, concrete: bool = False) -> list[type]:


# Could be a method of ClassSelector.
def concrete_descendents(parentclass):
def concrete_descendents(parentclass: type) -> dict[str, type]:
"""
Return a dictionary containing all subclasses of the specified
parentclass, including the parentclass. Only classes that are
defined in scripts that have been run or modules that have been
imported are included, so the caller will usually first do ``from
package import *``.
parentclass, including the parentclass (prefer :func:`descendents`).

Only classes that are defined in scripts that have been run or modules
that have been imported are included, so the caller will usually first
do ``from package import *``.

Only non-abstract classes will be included.
"""
return {c.__name__:c for c in descendents(parentclass)
if not _is_abstract(c)}

Warns
-----
ParamWarning
``concrete_descendents`` overrides descendents that share the same
class name. To avoid this, use :func:`descendents` with ``concrete=True``.
"""
desc = descendents(parentclass, concrete=True)
concrete_desc = {c.__name__: c for c in desc}
# Descendents with the same name are clobbered.
if len(desc) != len(concrete_desc):
class_count = Counter([kls.__name__ for kls in desc])
clobbered = [kls for kls, count in class_count.items() if count > 1]
warnings.warn(
'`concrete_descendents` overrides descendents that share the same '
'class name. Other descendents with the same name as the following '
f'classes exist but were not returned: {clobbered!r}\n'
'Use `descendents(parentclass, concrete=True)` instead.',
ParamWarning,
)
return concrete_desc


def _abbreviate_paths(pathspec,named_paths):
"""
Expand Down
8 changes: 4 additions & 4 deletions param/depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ def depends(
func: CallableT, /, *dependencies: Dependency, watch: bool = False, on_init: bool = False, **kw: Parameter
) -> Callable[[CallableT], DependsFunc[CallableT]]:
"""
Annotates a function or Parameterized method to express its dependencies.
Annotates a function or :class:`Parameterized` method to express its dependencies.

The specified dependencies can be either be Parameter instances or if a
The specified dependencies can be either be :class:`Parameter` instances or if a
method is supplied they can be defined as strings referring to Parameters
of the class, or Parameters of subobjects (Parameterized objects that are
values of this object's parameters). Dependencies can either be on
Expand All @@ -54,10 +54,10 @@ def depends(
----------
watch : bool, optional
Whether to invoke the function/method when the dependency is updated,
by default False
by default ``False``.
on_init : bool, optional
Whether to invoke the function/method when the instance is created,
by default False
by default ``False``.

"""
dependencies, kw = (
Expand Down
65 changes: 35 additions & 30 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,12 @@ def _batch_call_watchers(parameterized, enable=True, run=True):
@contextmanager
def batch_call_watchers(parameterized):
"""
Context manager to batch events to provide to Watchers on a
parameterized object. This context manager queues any events
triggered by setting a parameter on the supplied parameterized
object, saving them up to dispatch them all at once when the
context manager exits.
Context manager to batch events to provide to :class:`Watchers` on a
parameterized object.

This context manager queues any events triggered by setting a parameter value
on the supplied :class:`Parameterized` object, saving them up to dispatch
them all at once when the context manager exits.
"""
BATCH_WATCH = parameterized.param._BATCH_WATCH
parameterized.param._BATCH_WATCH = True
Expand All @@ -394,22 +395,22 @@ def _syncing(parameterized, parameters):
def edit_constant(parameterized: 'Parameterized') -> Generator[None, None, None]:
"""
Context manager to temporarily set parameters on a Parameterized object
to `constant=False` to allow editing them.
to ``constant=False`` to allow editing them.

The `edit_constant` context manager allows temporarily disabling the `constant`
property of all parameters on the given `Parameterized` object, enabling them
to be modified. Once the context exits, the original `constant` states are restored.
The ``edit_constant`` context manager allows temporarily disabling the ``constant``
property of all parameters on the given :class:`Parameterized` object, enabling them
to be modified. Once the context exits, the original ``constant`` states are restored.

Parameters
----------
parameterized : Parameterized
The `Parameterized` object whose parameters will have their `constant`
The :class:`Parameterized` object whose parameters will have their ``constant``
property temporarily disabled.

Yields
------
None
A context where all parameters of the `Parameterized` object can be modified.
A context where all parameters of the :class:`Parameterized` object can be modified.

Examples
--------
Expand All @@ -418,7 +419,7 @@ def edit_constant(parameterized: 'Parameterized') -> Generator[None, None, None]
... constant_param = param.Number(default=10, constant=True)
>>> p = MyClass()

Use `edit_constant` to modify the constant parameter:
Use ``edit_constant`` to modify the constant parameter:

>>> with param.edit_constant(p):
... p.constant_param = 20
Expand Down Expand Up @@ -450,8 +451,8 @@ def discard_events(parameterized: 'Parameterized') -> Generator[None, None, None
Context manager that discards any events within its scope triggered on the
supplied Parameterized object.

The `discard_events` context manager ensures that any events triggered
on the supplied `Parameterized` object during its scope are discarded.
The ``discard_events`` context manager ensures that any events triggered
on the supplied :class:`Parameterized` object during its scope are discarded.
This allows for silent changes to dependent parameters, making it useful
for initialization or setup phases where changes should not propagate
to watchers or dependencies.
Expand All @@ -464,12 +465,12 @@ def discard_events(parameterized: 'Parameterized') -> Generator[None, None, None
Parameters
----------
parameterized : Parameterized
The `Parameterized` object whose events will be suppressed.
The :class:`Parameterized` object whose events will be suppressed.

Yields
------
None
A context where events on the supplied `Parameterized` object are discarded.
A context where events on the supplied :class:`Parameterized` object are discarded.

References
----------
Expand All @@ -483,12 +484,15 @@ def discard_events(parameterized: 'Parameterized') -> Generator[None, None, None
>>> import param
>>> class MyClass(param.Parameterized):
... a = param.Number(default=1)
...
... @param.depends('a', watch=True)
... def on_a(self):
... print(self.a)
>>> p = MyClass()
>>> param.bind(print, p.param.a, watch=True)
>>> p.a=2
>>> p.a = 2
2

Use `discard_events` to suppress events:
Use ``discard_events`` to suppress events:

>>> with param.parameterized.discard_events(p):
... p.a = 3
Expand All @@ -510,7 +514,7 @@ def classlist(class_):
"""
Return a list of the class hierarchy above (and including) the given class.

Same as `inspect.getmro(class_)[::-1]`
Same as ``inspect.getmro(class_)[::-1]``
"""
return inspect.getmro(class_)[::-1]

Expand Down Expand Up @@ -641,9 +645,9 @@ def output(func, *output, **kw):
"""
Annotate a method to declare its outputs with specific types.

The `output` decorator allows annotating a method in a `Parameterized` class
The ``output`` decorator allows annotating a method in a :class:`Parameterized` class
to declare the types of the values it returns. This provides metadata for the
method's outputs, which can be queried using the `Parameterized.param.outputs`
method's outputs, which can be queried using the :meth:`~Parameters.outputs`
method. Outputs can be declared as unnamed, named, or typed, and the decorator
supports multiple outputs.

Expand All @@ -653,13 +657,15 @@ def output(func, *output, **kw):
The method being annotated.
*output : tuple or Parameter or type, optional
Positional arguments to declare outputs. Can include:
- `Parameter` instances or Python object types (e.g., `int`, `str`).
- Tuples of the form `(name, type)` to declare named outputs.

- :class:`Parameter` instances or Python object types (e.g., :class:`int`, :class:`str`).
- Tuples of the form ``(name, type)`` to declare named outputs.
- Multiple such tuples for declaring multiple outputs.
**kw : dict, optional
Keyword arguments mapping output names to types. Types can be:
- `Parameter` instances.
- Python object types, which will be converted to `ClassSelector`.

- :class:`Parameter` instances.
- Python object types, which will be converted to :class:`ClassSelector`.

Returns
-------
Expand All @@ -669,14 +675,13 @@ def output(func, *output, **kw):
Raises
------
ValueError
If:
- An invalid type is provided for an output.
- Duplicate names are used for multiple outputs.
If an invalid type is provided for an output or duplicate names are
used for multiple outputs.

Notes
-----
- Unnamed outputs default to the method name.
- Python types are converted to `ClassSelector` instances.
- Python types are converted to :class:`ClassSelector` instances.
- If no arguments are provided, the output is assumed to be an object
without a specific type.

Expand Down
14 changes: 9 additions & 5 deletions param/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
_produce_value,
_get_min_max_value,
_is_number,
concrete_descendents,
concrete_descendents, # noqa: F401
descendents as _descendents,
_abbreviate_paths,
_to_datetime,
)
Expand Down Expand Up @@ -2166,16 +2167,19 @@ def get_range(self):
"""
Return the possible types for this parameter's value.

(I.e. return `{name: <class>}` for all classes that are
concrete_descendents() of `self.class_`.)
(I.e. return ``{name: <class>}`` for all classes that are
:func:`param.parameterized.descendents` of ``self.class_``.)

Only classes from modules that have been imported are added
(see concrete_descendents()).
(see :func:`param.parameterized.descendents`).
"""
classes = self.class_ if isinstance(self.class_, tuple) else (self.class_,)
all_classes = {}
for cls in classes:
all_classes.update(concrete_descendents(cls))
desc = _descendents(cls, concrete=True)
# This will clobber separate classes with identical names.
# Known historical issue, see https://github.com/holoviz/param/pull/1035
all_classes.update({c.__name__: c for c in desc})
d = OrderedDict((name, class_) for name,class_ in all_classes.items())
if self.allow_None:
d['None'] = None
Expand Down
5 changes: 3 additions & 2 deletions tests/testdefaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import param

from param import concrete_descendents, Parameter
from param.parameterized import descendents, Parameter

# import all parameter types
from param import * # noqa
Expand Down Expand Up @@ -90,7 +90,8 @@ class P(param.Parameterized):

return test

for p_name, p_type in concrete_descendents(Parameter).items():
for p_type in descendents(Parameter):
p_name = p_type.__name__
dict_["test_default_of_unbound_%s"%p_name] = add_test_unbound(p_type) if p_name not in skip else test_skip
dict_["test_default_of_class_%s"%p_name] = add_test_class(p_type) if p_name not in skip else test_skip
dict_["test_default_of_inst_%s"%p_name] = add_test_inst(p_type) if p_name not in skip else test_skip
Expand Down
6 changes: 3 additions & 3 deletions tests/testsignatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import param
import pytest

from param import concrete_descendents, Parameter
from param import descendents, Parameter


SKIP_UPDATED = [
Expand All @@ -18,8 +18,8 @@

def custom_concrete_descendents(kls):
return {
pname: ptype
for pname, ptype in concrete_descendents(kls).items()
ptype.__name__: ptype
for ptype in descendents(kls)
if ptype.__module__.startswith('param')
}

Expand Down
Loading
Loading