error.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. """
  2. .. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
  3. """
  4. import enum
  5. from typing import Final, Optional
  6. from ._const import Platform
  7. def _to_error_code(code: int) -> str:
  8. return f"PV{code:04d}"
  9. class ErrorAttrKey:
  10. BYTE_COUNT: Final = "byte_count"
  11. DESCRIPTION: Final = "description"
  12. FS_ENCODING: Final = "fs_encoding"
  13. PLATFORM: Final = "platform"
  14. REASON: Final = "reason"
  15. RESERVED_NAME: Final = "reserved_name"
  16. REUSABLE_NAME: Final = "reusable_name"
  17. VALUE: Final = "value"
  18. @enum.unique
  19. class ErrorReason(enum.Enum):
  20. """
  21. Validation error reasons.
  22. """
  23. NULL_NAME = (_to_error_code(1001), "NULL_NAME", "the value must not be an empty string")
  24. RESERVED_NAME = (
  25. _to_error_code(1002),
  26. "RESERVED_NAME",
  27. "found a reserved name by a platform",
  28. )
  29. INVALID_CHARACTER = (
  30. _to_error_code(1100),
  31. "INVALID_CHARACTER",
  32. "invalid characters found",
  33. )
  34. INVALID_LENGTH = (
  35. _to_error_code(1101),
  36. "INVALID_LENGTH",
  37. "found an invalid string length",
  38. )
  39. FOUND_ABS_PATH = (
  40. _to_error_code(1200),
  41. "FOUND_ABS_PATH",
  42. "found an absolute path where must be a relative path",
  43. )
  44. MALFORMED_ABS_PATH = (
  45. _to_error_code(1201),
  46. "MALFORMED_ABS_PATH",
  47. "found a malformed absolute path",
  48. )
  49. INVALID_AFTER_SANITIZE = (
  50. _to_error_code(2000),
  51. "INVALID_AFTER_SANITIZE",
  52. "found invalid value after sanitizing",
  53. )
  54. @property
  55. def code(self) -> str:
  56. """str: Error code."""
  57. return self.__code
  58. @property
  59. def name(self) -> str:
  60. """str: Error reason name."""
  61. return self.__name
  62. @property
  63. def description(self) -> str:
  64. """str: Error reason description."""
  65. return self.__description
  66. def __init__(self, code: str, name: str, description: str) -> None:
  67. self.__name = name
  68. self.__code = code
  69. self.__description = description
  70. def __str__(self) -> str:
  71. return f"[{self.__code}] {self.__description}"
  72. class ValidationError(ValueError):
  73. """
  74. Exception class of validation errors.
  75. """
  76. @property
  77. def platform(self) -> Optional[Platform]:
  78. """
  79. :py:class:`~pathvalidate.Platform`: Platform information.
  80. """
  81. return self.__platform
  82. @property
  83. def reason(self) -> ErrorReason:
  84. """
  85. :py:class:`~pathvalidate.error.ErrorReason`: The cause of the error.
  86. """
  87. return self.__reason
  88. @property
  89. def description(self) -> Optional[str]:
  90. """Optional[str]: Error description."""
  91. return self.__description
  92. @property
  93. def reserved_name(self) -> str:
  94. """str: Reserved name."""
  95. return self.__reserved_name
  96. @property
  97. def reusable_name(self) -> Optional[bool]:
  98. """Optional[bool]: Whether the name is reusable or not."""
  99. return self.__reusable_name
  100. @property
  101. def fs_encoding(self) -> Optional[str]:
  102. """Optional[str]: File system encoding."""
  103. return self.__fs_encoding
  104. @property
  105. def byte_count(self) -> Optional[int]:
  106. """Optional[int]: Byte count of the path."""
  107. return self.__byte_count
  108. def __init__(self, *args, **kwargs) -> None: # type: ignore
  109. if ErrorAttrKey.REASON not in kwargs:
  110. raise ValueError(f"{ErrorAttrKey.REASON} must be specified")
  111. self.__reason: ErrorReason = kwargs.pop(ErrorAttrKey.REASON)
  112. self.__byte_count: Optional[int] = kwargs.pop(ErrorAttrKey.BYTE_COUNT, None)
  113. self.__platform: Optional[Platform] = kwargs.pop(ErrorAttrKey.PLATFORM, None)
  114. self.__description: Optional[str] = kwargs.pop(ErrorAttrKey.DESCRIPTION, None)
  115. self.__reserved_name: str = kwargs.pop(ErrorAttrKey.RESERVED_NAME, "")
  116. self.__reusable_name: Optional[bool] = kwargs.pop(ErrorAttrKey.REUSABLE_NAME, None)
  117. self.__fs_encoding: Optional[str] = kwargs.pop(ErrorAttrKey.FS_ENCODING, None)
  118. self.__value: Optional[str] = kwargs.pop(ErrorAttrKey.VALUE, None)
  119. try:
  120. super().__init__(*args[0], **kwargs)
  121. except IndexError:
  122. super().__init__(*args, **kwargs)
  123. def as_slog(self) -> dict[str, str]:
  124. """Return a dictionary representation of the error.
  125. Returns:
  126. Dict[str, str]: A dictionary representation of the error.
  127. """
  128. slog: dict[str, str] = {
  129. "code": self.reason.code,
  130. ErrorAttrKey.DESCRIPTION: self.reason.description,
  131. }
  132. if self.platform:
  133. slog[ErrorAttrKey.PLATFORM] = self.platform.value
  134. if self.description:
  135. slog[ErrorAttrKey.DESCRIPTION] = self.description
  136. if self.__reusable_name is not None:
  137. slog[ErrorAttrKey.REUSABLE_NAME] = str(self.__reusable_name)
  138. if self.__fs_encoding:
  139. slog[ErrorAttrKey.FS_ENCODING] = self.__fs_encoding
  140. if self.__byte_count:
  141. slog[ErrorAttrKey.BYTE_COUNT] = str(self.__byte_count)
  142. if self.__value:
  143. slog[ErrorAttrKey.VALUE] = self.__value
  144. return slog
  145. def __str__(self) -> str:
  146. item_list = []
  147. header = str(self.reason)
  148. if Exception.__str__(self):
  149. item_list.append(Exception.__str__(self))
  150. if self.platform:
  151. item_list.append(f"{ErrorAttrKey.PLATFORM}={self.platform.value}")
  152. if self.description:
  153. item_list.append(f"{ErrorAttrKey.DESCRIPTION}={self.description}")
  154. if self.__reusable_name is not None:
  155. item_list.append(f"{ErrorAttrKey.REUSABLE_NAME}={self.reusable_name}")
  156. if self.__fs_encoding:
  157. item_list.append(f"{ErrorAttrKey.FS_ENCODING}={self.__fs_encoding}")
  158. if self.__byte_count is not None:
  159. item_list.append(f"{ErrorAttrKey.BYTE_COUNT}={self.__byte_count:,d}")
  160. if self.__value:
  161. item_list.append(f"{ErrorAttrKey.VALUE}={self.__value!r}")
  162. if item_list:
  163. header += ": "
  164. return header + ", ".join(item_list).strip()
  165. def __repr__(self) -> str:
  166. return self.__str__()
  167. class NullNameError(ValidationError):
  168. """[Deprecated]
  169. Exception raised when a name is empty.
  170. """
  171. def __init__(self, *args, **kwargs) -> None: # type: ignore
  172. kwargs[ErrorAttrKey.REASON] = ErrorReason.NULL_NAME
  173. super().__init__(args, **kwargs)
  174. class InvalidCharError(ValidationError):
  175. """
  176. Exception raised when includes invalid character(s) within a string.
  177. """
  178. def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
  179. kwargs[ErrorAttrKey.REASON] = ErrorReason.INVALID_CHARACTER
  180. super().__init__(args, **kwargs)
  181. class ReservedNameError(ValidationError):
  182. """
  183. Exception raised when a string matched a reserved name.
  184. """
  185. def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
  186. kwargs[ErrorAttrKey.REASON] = ErrorReason.RESERVED_NAME
  187. super().__init__(args, **kwargs)
  188. class ValidReservedNameError(ReservedNameError):
  189. """[Deprecated]
  190. Exception raised when a string matched a reserved name.
  191. However, it can be used as a name.
  192. """
  193. def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
  194. kwargs[ErrorAttrKey.REUSABLE_NAME] = True
  195. super().__init__(args, **kwargs)
  196. class InvalidReservedNameError(ReservedNameError):
  197. """[Deprecated]
  198. Exception raised when a string matched a reserved name.
  199. Moreover, the reserved name is invalid as a name.
  200. """
  201. def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
  202. kwargs[ErrorAttrKey.REUSABLE_NAME] = False
  203. super().__init__(args, **kwargs)