254 lines
7.4 KiB
Python
254 lines
7.4 KiB
Python
"""
|
|
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
|
|
"""
|
|
|
|
import enum
|
|
from typing import Dict, Optional
|
|
|
|
from ._const import Platform
|
|
|
|
|
|
def _to_error_code(code: int) -> str:
|
|
return f"PV{code:04d}"
|
|
|
|
|
|
class ErrorAttrKey:
|
|
BYTE_COUNT = "byte_count"
|
|
DESCRIPTION = "description"
|
|
FS_ENCODING = "fs_encoding"
|
|
PLATFORM = "platform"
|
|
REASON = "reason"
|
|
RESERVED_NAME = "reserved_name"
|
|
REUSABLE_NAME = "reusable_name"
|
|
|
|
|
|
@enum.unique
|
|
class ErrorReason(enum.Enum):
|
|
"""
|
|
Validation error reasons.
|
|
"""
|
|
|
|
NULL_NAME = (_to_error_code(1001), "NULL_NAME", "the value must not be an empty string")
|
|
RESERVED_NAME = (
|
|
_to_error_code(1002),
|
|
"RESERVED_NAME",
|
|
"found a reserved name by a platform",
|
|
)
|
|
INVALID_CHARACTER = (
|
|
_to_error_code(1100),
|
|
"INVALID_CHARACTER",
|
|
"invalid characters found",
|
|
)
|
|
INVALID_LENGTH = (
|
|
_to_error_code(1101),
|
|
"INVALID_LENGTH",
|
|
"found an invalid string length",
|
|
)
|
|
FOUND_ABS_PATH = (
|
|
_to_error_code(1200),
|
|
"FOUND_ABS_PATH",
|
|
"found an absolute path where must be a relative path",
|
|
)
|
|
MALFORMED_ABS_PATH = (
|
|
_to_error_code(1201),
|
|
"MALFORMED_ABS_PATH",
|
|
"found a malformed absolute path",
|
|
)
|
|
INVALID_AFTER_SANITIZE = (
|
|
_to_error_code(2000),
|
|
"INVALID_AFTER_SANITIZE",
|
|
"found invalid value after sanitizing",
|
|
)
|
|
|
|
@property
|
|
def code(self) -> str:
|
|
"""str: Error code."""
|
|
return self.__code
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""str: Error reason name."""
|
|
return self.__name
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
"""str: Error reason description."""
|
|
return self.__description
|
|
|
|
def __init__(self, code: str, name: str, description: str) -> None:
|
|
self.__name = name
|
|
self.__code = code
|
|
self.__description = description
|
|
|
|
def __str__(self) -> str:
|
|
return f"[{self.__code}] {self.__description}"
|
|
|
|
|
|
class ValidationError(ValueError):
|
|
"""
|
|
Exception class of validation errors.
|
|
"""
|
|
|
|
@property
|
|
def platform(self) -> Optional[Platform]:
|
|
"""
|
|
:py:class:`~pathvalidate.Platform`: Platform information.
|
|
"""
|
|
return self.__platform
|
|
|
|
@property
|
|
def reason(self) -> ErrorReason:
|
|
"""
|
|
:py:class:`~pathvalidate.error.ErrorReason`: The cause of the error.
|
|
"""
|
|
return self.__reason
|
|
|
|
@property
|
|
def description(self) -> Optional[str]:
|
|
"""Optional[str]: Error description."""
|
|
return self.__description
|
|
|
|
@property
|
|
def reserved_name(self) -> str:
|
|
"""str: Reserved name."""
|
|
return self.__reserved_name
|
|
|
|
@property
|
|
def reusable_name(self) -> Optional[bool]:
|
|
"""Optional[bool]: Whether the name is reusable or not."""
|
|
return self.__reusable_name
|
|
|
|
@property
|
|
def fs_encoding(self) -> Optional[str]:
|
|
"""Optional[str]: File system encoding."""
|
|
return self.__fs_encoding
|
|
|
|
@property
|
|
def byte_count(self) -> Optional[int]:
|
|
"""Optional[int]: Byte count of the path."""
|
|
return self.__byte_count
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # type: ignore
|
|
if ErrorAttrKey.REASON not in kwargs:
|
|
raise ValueError(f"{ErrorAttrKey.REASON} must be specified")
|
|
|
|
self.__reason: ErrorReason = kwargs.pop(ErrorAttrKey.REASON)
|
|
self.__byte_count: Optional[int] = kwargs.pop(ErrorAttrKey.BYTE_COUNT, None)
|
|
self.__platform: Optional[Platform] = kwargs.pop(ErrorAttrKey.PLATFORM, None)
|
|
self.__description: Optional[str] = kwargs.pop(ErrorAttrKey.DESCRIPTION, None)
|
|
self.__reserved_name: str = kwargs.pop(ErrorAttrKey.RESERVED_NAME, "")
|
|
self.__reusable_name: Optional[bool] = kwargs.pop(ErrorAttrKey.REUSABLE_NAME, None)
|
|
self.__fs_encoding: Optional[str] = kwargs.pop(ErrorAttrKey.FS_ENCODING, None)
|
|
|
|
try:
|
|
super().__init__(*args[0], **kwargs)
|
|
except IndexError:
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def as_slog(self) -> Dict[str, str]:
|
|
"""Return a dictionary representation of the error.
|
|
|
|
Returns:
|
|
Dict[str, str]: A dictionary representation of the error.
|
|
"""
|
|
|
|
slog: Dict[str, str] = {
|
|
"code": self.reason.code,
|
|
ErrorAttrKey.DESCRIPTION: self.reason.description,
|
|
}
|
|
if self.platform:
|
|
slog[ErrorAttrKey.PLATFORM] = self.platform.value
|
|
if self.description:
|
|
slog[ErrorAttrKey.DESCRIPTION] = self.description
|
|
if self.__reusable_name is not None:
|
|
slog[ErrorAttrKey.REUSABLE_NAME] = str(self.__reusable_name)
|
|
if self.__fs_encoding:
|
|
slog[ErrorAttrKey.FS_ENCODING] = self.__fs_encoding
|
|
if self.__byte_count:
|
|
slog[ErrorAttrKey.BYTE_COUNT] = str(self.__byte_count)
|
|
|
|
return slog
|
|
|
|
def __str__(self) -> str:
|
|
item_list = []
|
|
header = str(self.reason)
|
|
|
|
if Exception.__str__(self):
|
|
item_list.append(Exception.__str__(self))
|
|
|
|
if self.platform:
|
|
item_list.append(f"{ErrorAttrKey.PLATFORM}={self.platform.value}")
|
|
if self.description:
|
|
item_list.append(f"{ErrorAttrKey.DESCRIPTION}={self.description}")
|
|
if self.__reusable_name is not None:
|
|
item_list.append(f"{ErrorAttrKey.REUSABLE_NAME}={self.reusable_name}")
|
|
if self.__fs_encoding:
|
|
item_list.append(f"{ErrorAttrKey.FS_ENCODING}={self.__fs_encoding}")
|
|
if self.__byte_count is not None:
|
|
item_list.append(f"{ErrorAttrKey.BYTE_COUNT}={self.__byte_count:,d}")
|
|
|
|
if item_list:
|
|
header += ": "
|
|
|
|
return header + ", ".join(item_list).strip()
|
|
|
|
def __repr__(self) -> str:
|
|
return self.__str__()
|
|
|
|
|
|
class NullNameError(ValidationError):
|
|
"""[Deprecated]
|
|
Exception raised when a name is empty.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # type: ignore
|
|
kwargs[ErrorAttrKey.REASON] = ErrorReason.NULL_NAME
|
|
|
|
super().__init__(args, **kwargs)
|
|
|
|
|
|
class InvalidCharError(ValidationError):
|
|
"""
|
|
Exception raised when includes invalid character(s) within a string.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
kwargs[ErrorAttrKey.REASON] = ErrorReason.INVALID_CHARACTER
|
|
|
|
super().__init__(args, **kwargs)
|
|
|
|
|
|
class ReservedNameError(ValidationError):
|
|
"""
|
|
Exception raised when a string matched a reserved name.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
kwargs[ErrorAttrKey.REASON] = ErrorReason.RESERVED_NAME
|
|
|
|
super().__init__(args, **kwargs)
|
|
|
|
|
|
class ValidReservedNameError(ReservedNameError):
|
|
"""[Deprecated]
|
|
Exception raised when a string matched a reserved name.
|
|
However, it can be used as a name.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
kwargs[ErrorAttrKey.REUSABLE_NAME] = True
|
|
|
|
super().__init__(args, **kwargs)
|
|
|
|
|
|
class InvalidReservedNameError(ReservedNameError):
|
|
"""[Deprecated]
|
|
Exception raised when a string matched a reserved name.
|
|
Moreover, the reserved name is invalid as a name.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
kwargs[ErrorAttrKey.REUSABLE_NAME] = False
|
|
|
|
super().__init__(args, **kwargs)
|