# Copyright 2026 jCloud # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ''' The utilities for jcloud_docsgen ''' import pathlib import os import errno from typing import Union import types class ExistingDirectory(pathlib.Path): ''' pathlib.Path subclass that is a directory. ExistingDirectory represents a filesystem path but it also checks whether the path exists and is not a file but a directory. It inherits from pathlib.Path. ''' def __init__(self, path, *args, **kwargs) -> None: if not os.path.exists(path): raise FileNotFoundError(errno.ENOENT, 'no such file or directory', path) if not os.path.isdir(path): raise NotADirectoryError(errno.ENOTDIR, 'not a directory', path) super().__init__(*(path, *args), **kwargs) class ExistingFile(pathlib.Path): ''' pathlib.Path subclass that is a file. ExistingFile represents a filesystem path but it also checks whether the path exists and is not a directory but a file. It inherits from pathlib.Path. ''' def __init__(self, path, *args, **kwargs) -> None: if not os.path.exists(path): raise FileNotFoundError(errno.ENOENT, 'no such file or directory', path) if not os.path.isfile(path): raise IsADirectoryError(errno.EISDIR, 'is a directory', path) super().__init__(*(path, *args), **kwargs) def _quote(string: str, quotation_mark: str) -> str: ''' Quotes a string. :param string: The string to quote :type string: str :param quotation_mark: The quotation mark :type quotation_mark: str :return: The quoted string :rtype: str ''' return quotation_mark + string + quotation_mark def human_readable_list(ls: list, final_separator: str = 'and', quotation_mark: str = '') -> str: ''' Generates a human-readable list from a list. :param ls: The list. :type ls: list :param final_separator: The seperator between the last and the second-to-last element of the list. Default is 'and'. :type final_separator: str :param quotation_mark: The characters that are inserted before and after an element. Default is ''. :return: The human-readable list. :rtype: str ''' # empty list if not ls: return '' # list with one element if len(ls) == 1: return _quote(str(ls[0]), quotation_mark) return ', '.join( [ _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) 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)): if isinstance(class_or_tuple, types.UnionType): class_or_tuple = class_or_tuple.__args__ 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: exception_message_expected = '\'' + class_or_tuple.__name__ + '\'' raise TypeError(f'expected {exception_message_expected}, got \'{type(obj).__name__}\'') def non_empty_str(value: Union[str, None]) -> Union[str, None]: ''' Returns the value or ``None``. Returns the value if it is not empty and not `Ǹone``; otherwise, returns ``None``. Please note: values are stripped, i. e. if the value consists only of spaces, it is also considered empty. :param value: The value. :type value: Union[str, None] :return: The value or ``None``. :rtype: Union[str, None] ''' if value is None: return None if value.strip() == '': return None return value def get_relative_path_segments(path: pathlib.Path, root: pathlib.Path) -> list[str]: ''' Returns the path segments relative to ``root``. :param path: The path. :type path: pathlib.Path :param root: The root path. :type root: pathlib.Path :return: The path segments relative to ``root``. :rtype: list[str] ''' return list( path.resolve() .relative_to( root.resolve() ) .parts )