diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 713a508..761548a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,4 +5,5 @@ ### Added - Add class for existing directories -- Add function to format a list human-readable \ No newline at end of file +- Add function to format a list human-readable +- Add function to assert that an object is an instance of a specific type \ No newline at end of file diff --git a/src/jcloud_docsgen/utils.py b/src/jcloud_docsgen/utils.py index 3e91789..a9ad557 100644 --- a/src/jcloud_docsgen/utils.py +++ b/src/jcloud_docsgen/utils.py @@ -82,4 +82,37 @@ def human_readable_list(ls: list, final_separator: str = 'and', quotation_mark: _quote(str(obj), quotation_mark) # quote the string representation of the object for obj in ls[:-1] # do not include the last element of the list ] - ) + ' ' + final_separator + ' ' + _quote(str(ls[-1]), quotation_mark) \ No newline at end of file + ) + ' ' + final_separator + ' ' + _quote(str(ls[-1]), quotation_mark) + +def _list_type_names(types: list[type]) -> list[str]: + ''' + Converts a list of types into a list of their names (``__name__`` + attribute) + + :param types: The list of types + :type types: list[type] + + :return: The list of the names of the types + :rtype: list[str] + ''' + + return [tp.__name__ for tp in types] + +def assert_that_is_instance(obj: object, class_or_tuple: Union[type, types.UnionType, tuple[type, ...]]) -> None: + if not isinstance(class_or_tuple, Union[type, types.UnionType, tuple]): + raise TypeError(f'class_or_tuple: expected \'Union[type, types.UnionType, tuple[type, ...]]\', \'got {type(class_or_tuple).__name__}\'') + if not isinstance(obj, class_or_tuple): + if isinstance(class_or_tuple, (tuple, types.UnionType)): + print('MORE') + if isinstance(class_or_tuple, types.UnionType): + class_or_tuple = class_or_tuple.__args__ + print('LIST:', _list_type_names(class_or_tuple)) + if len(class_or_tuple) > 1: + exception_message_expected = 'either ' + else: + exception_message_expected = '' + exception_message_expected += human_readable_list(_list_type_names(class_or_tuple), 'or', '\'') + else: + print('SINGLE') + exception_message_expected = '\'' + class_or_tuple.__name__ + '\'' + raise TypeError(f'expected {exception_message_expected}, got \'{type(obj).__name__}\'') \ No newline at end of file diff --git a/tests/unit/_utils/test_assert_that_is_instance.py b/tests/unit/_utils/test_assert_that_is_instance.py new file mode 100644 index 0000000..580e121 --- /dev/null +++ b/tests/unit/_utils/test_assert_that_is_instance.py @@ -0,0 +1,29 @@ +from src.jcloud_docsgen.utils import assert_that_is_instance +import pytest +import types + +class TestType: ... +TestType.__name__ = 'NOT TestType' + +@pytest.mark.parametrize('obj,class_or_tuple', [ + (1, int), + (1, (int,)), + (1, str | int), + (1, int | str | list), + (TestType(), TestType), + (None, types.NoneType), +]) +def test_assert_that_is_instance(obj, class_or_tuple): + assert_that_is_instance(obj, class_or_tuple) + +@pytest.mark.parametrize('obj,class_or_tuple,expected_exception_msg', [ + (1, str, 'expected \'str\', got \'int\''), + (1, (str,), 'expected \'str\', got \'int\''), + (1, str | float, 'expected either \'str\' or \'float\', got \'int\''), + (1, str | float | list, 'expected either \'str\', \'float\' or \'list\', got \'int\''), + (1, TestType, 'expected \'NOT TestType\', got \'int\''), +]) +def test_assert_that_is_instance_exceptions(obj, class_or_tuple, expected_exception_msg): + with pytest.raises(TypeError) as exc_info: + assert_that_is_instance(obj, class_or_tuple) + assert str(exc_info.value) == expected_exception_msg \ No newline at end of file