From f97e3473a09c3eb745334ed44f32ecd7080d14c3 Mon Sep 17 00:00:00 2001 From: maximlt Date: Sun, 16 Feb 2025 17:37:22 +0100 Subject: [PATCH 1/5] better error handling --- param/_utils.py | 3 ++- tests/testutils.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/param/_utils.py b/param/_utils.py index 806b3f429..ed415ef85 100644 --- a/param/_utils.py +++ b/param/_utils.py @@ -532,7 +532,8 @@ def descendents(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) + if not isinstance(class_, type): + raise TypeError(f"descendents expected a class object, not {type(class_).__name__}") q = [class_] out = [] while len(q): diff --git a/tests/testutils.py b/tests/testutils.py index d060a314d..2c5c1fd3c 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -9,7 +9,12 @@ from param import guess_param_types, resolve_path from param.parameterized import bothmethod -from param._utils import _is_mutable_container, iscoroutinefunction, gen_types +from param._utils import ( + _is_mutable_container, + descendents, + iscoroutinefunction, + gen_types, +) try: @@ -439,3 +444,11 @@ def _int_types(): assert next(iter(_int_types())) is int assert next(iter(_int_types)) is int assert isinstance(_int_types, Iterable) + + +def test_descendents_bad_type(): + with pytest.raises( + TypeError, + match="descendents expected a class object, not int" + ): + descendents(1) From cbbe8582e2948bc829aee863ad0eebbb13648a2f Mon Sep 17 00:00:00 2001 From: maximlt Date: Sun, 16 Feb 2025 18:18:04 +0100 Subject: [PATCH 2/5] add concrete keyword --- param/_utils.py | 6 +++--- tests/testutils.py | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/param/_utils.py b/param/_utils.py index ed415ef85..ca7af5a04 100644 --- a/param/_utils.py +++ b/param/_utils.py @@ -525,7 +525,7 @@ def _is_abstract(class_): return False -def descendents(class_): +def descendents(class_, concrete=False): """ Return a list of the class hierarchy below (and including) the given class. @@ -538,11 +538,11 @@ def descendents(class_): out = [] while len(q): x = q.pop(0) - out.insert(0,x) + out.insert(0, x) for b in x.__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. diff --git a/tests/testutils.py b/tests/testutils.py index 2c5c1fd3c..68543cc64 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -8,7 +8,7 @@ import pytest from param import guess_param_types, resolve_path -from param.parameterized import bothmethod +from param.parameterized import bothmethod, Parameterized from param._utils import ( _is_mutable_container, descendents, @@ -452,3 +452,18 @@ def test_descendents_bad_type(): 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] From 390feaa2bc490ca17babf536a99f3eac8b20e2ea Mon Sep 17 00:00:00 2001 From: maximlt Date: Sun, 16 Feb 2025 18:19:54 +0100 Subject: [PATCH 3/5] docstring and type hints --- param/_utils.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/param/_utils.py b/param/_utils.py index ca7af5a04..df7282582 100644 --- a/param/_utils.py +++ b/param/_utils.py @@ -518,19 +518,42 @@ def _is_number(obj): else: return False -def _is_abstract(class_): +def _is_abstract(class_: type) -> bool: try: return class_.abstract except AttributeError: return False -def descendents(class_, concrete=False): +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. + 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__}") From caec65ab0c33b090315ac8687ba13db6e9590c40 Mon Sep 17 00:00:00 2001 From: maximlt Date: Sun, 16 Feb 2025 18:23:58 +0100 Subject: [PATCH 4/5] expose descendents in the API ref --- doc/reference/param/parameterized_helpers.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/reference/param/parameterized_helpers.md b/doc/reference/param/parameterized_helpers.md index 52fe56ec3..5b3a23677 100644 --- a/doc/reference/param/parameterized_helpers.md +++ b/doc/reference/param/parameterized_helpers.md @@ -14,6 +14,7 @@ param.parameterized.batch_call_watchers concrete_descendents depends + descendents discard_events edit_constant output From bfe9c63abcdf774e6aad049455258726c09eb1c0 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 18 Feb 2025 09:14:29 +0100 Subject: [PATCH 5/5] enable _is_abstract to check if the class is an ABC --- param/_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/param/_utils.py b/param/_utils.py index df7282582..2dde52c11 100644 --- a/param/_utils.py +++ b/param/_utils.py @@ -519,6 +519,8 @@ def _is_number(obj): def _is_abstract(class_: type) -> bool: + if inspect.isabstract(class_): + return True try: return class_.abstract except AttributeError: