Skip to content

Commit

Permalink
Add concrete keyword to descendents (#1027)
Browse files Browse the repository at this point in the history
  • Loading branch information
maximlt authored Feb 20, 2025
1 parent 274632a commit 30edc3f
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 12 deletions.
1 change: 1 addition & 0 deletions doc/reference/param/parameterized_helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
param.parameterized.batch_call_watchers
concrete_descendents
depends
descendents
discard_events
edit_constant
output
Expand Down
44 changes: 35 additions & 9 deletions param/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,26 +518,52 @@ def _is_number(obj):
else: return False


def _is_abstract(class_):
def _is_abstract(class_: type) -> bool:
if inspect.isabstract(class_):
return True
try:
return class_.abstract
except AttributeError:
return False


def descendents(class_):
def descendents(class_: type, concrete: bool = False) -> list[type]:
"""
Return a list of the class hierarchy below (and including) the given class.
Return a list of all descendant classes of a given class.
The list is ordered from least- to most-specific. Can be useful for
printing the contents of an entire class hierarchy.
"""
assert isinstance(class_,type)
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.
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`.
Returns
-------
list of type
A list of descendant classes, ordered from the most base to the most derived.
Examples
--------
>>> class A: pass
>>> class B(A): pass
>>> class C(A): pass
>>> class D(B): pass
>>> descendents(A)
[A, B, C, D]
"""
if not isinstance(class_, type):
raise TypeError(f"descendents expected a class object, not {type(class_).__name__}")
q = [class_]
out = []
while len(q):
x = q.pop(0)
out.insert(0,x)
out.insert(0, x)
try:
subclasses = x.__subclasses__()
except TypeError:
Expand All @@ -547,7 +573,7 @@ def descendents(class_):
for b in subclasses:
if b not in q and b not in out:
q.append(b)
return out[::-1]
return [kls for kls in out if not (concrete and _is_abstract(kls))][::-1]


# Could be a method of ClassSelector.
Expand Down
34 changes: 31 additions & 3 deletions tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
import param
import pytest

from param import descendents, guess_param_types, resolve_path
from param.parameterized import bothmethod
from param._utils import _is_mutable_container, iscoroutinefunction, gen_types
from param import guess_param_types, resolve_path
from param.parameterized import bothmethod, Parameterized
from param._utils import (
_is_mutable_container,
descendents,
iscoroutinefunction,
gen_types,
)


try:
Expand Down Expand Up @@ -444,3 +449,26 @@ def _int_types():
def test_descendents_object():
# Used to raise an unhandled error, see https://github.com/holoviz/param/issues/1013.
assert descendents(object)


def test_descendents_bad_type():
with pytest.raises(
TypeError,
match="descendents expected a class object, not int"
):
descendents(1)

class A(Parameterized):
__abstract = True
class B(A): pass
class C(A): pass
class X(B): pass
class Y(B): pass


def test_descendents():
assert descendents(A) == [A, B, C, X, Y]


def test_descendents_concrete():
assert descendents(A, concrete=True) == [B, C, X, Y]

0 comments on commit 30edc3f

Please sign in to comment.