Skip to content
2 changes: 2 additions & 0 deletions param/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ def _is_number(obj):


def _is_abstract(class_):
if inspect.isabstract(class_):
return True
try:
return class_.abstract
except AttributeError:
Expand Down
17 changes: 17 additions & 0 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
__init__.py (providing specialized Parameter types).
"""

import abc
import asyncio
import copy
import datetime as dt
Expand Down Expand Up @@ -4571,6 +4572,22 @@ def __str__(self):
return f"<{self.__class__.__name__} {self.name}>"


class ParameterizedABCMeta(abc.ABCMeta, ParameterizedMetaclass):
"""Metaclass for abstract base classes using Parameterized.

Ensures compatibility between ABCMeta and ParameterizedMetaclass.
"""


class ParameterizedABC(Parameterized, metaclass=ParameterizedABCMeta):
"""Base class for user-defined ABCs that extends Parameterized."""

def __init_subclass__(cls, **kwargs):
if cls.__bases__ and cls.__bases__[0] is ParameterizedABC:
setattr(cls, f'_{cls.__name__}__abstract', True)
super().__init_subclass__(**kwargs)


def print_all_param_defaults():
"""Print the default values for all imported Parameters."""
print("_______________________________________________________________________________")
Expand Down
19 changes: 18 additions & 1 deletion tests/testparameterizedobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,11 +365,28 @@ def test_instantiation_inheritance(self):
assert t.param['instPO'].instantiate is True
assert isinstance(t.instPO,AnotherTestPO)

def test_abstract_class(self):
def test_abstract_class_attribute(self):
"""Check that a class declared abstract actually shows up as abstract."""
self.assertEqual(TestAbstractPO.abstract, True)
self.assertEqual(_AnotherAbstractPO.abstract, True)
self.assertEqual(TestPO.abstract, False)
# Test subclasses are not abstract
class A(param.Parameterized):
__abstract = True
class B(A): pass
class C(A): pass
self.assertEqual(A.abstract, True)
self.assertEqual(B.abstract, False)
self.assertEqual(C.abstract, False)

def test_abstract_class_abc(self):
"""Check that an ABC class actually shows up as abstract."""
class A(param.parameterized.ParameterizedABC): pass
class B(A): pass
class C(A): pass
self.assertEqual(A.abstract, True)
self.assertEqual(B.abstract, False)
self.assertEqual(C.abstract, False)

def test_override_class_param_validation(self):
test = TestPOValidation()
Expand Down
33 changes: 31 additions & 2 deletions tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
import pytest

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.parameterized import bothmethod, Parameterized, ParameterizedABC
from param._utils import (
_is_abstract,
_is_mutable_container,
iscoroutinefunction,
gen_types,
)


try:
Expand Down Expand Up @@ -439,3 +444,27 @@ def _int_types():
assert next(iter(_int_types())) is int
assert next(iter(_int_types)) is int
assert isinstance(_int_types, Iterable)


def test_is_abstract_false():
class A: pass
class B(Parameterized): pass
assert not _is_abstract(A)
assert not _is_abstract(B)


def test_is_abstract_attribute():
class A(Parameterized):
__abstract = True
class B(A): pass

assert _is_abstract(A)
assert not _is_abstract(B)


def test_is_abstract_abc():
class A(ParameterizedABC): pass
class B(A): pass

assert _is_abstract(A)
assert not _is_abstract(B)