# 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 errno import os import pathlib import types from typing import Union try: from warnings import deprecated except ImportError: from typing_extensions import deprecated @deprecated('Use pathlib.Path instead.') 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) @deprecated('Use pathlib.Path instead.') 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: 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, " f"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: str | None) -> 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: str | None :return: The value or ``None``. :rtype: 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, )