convert.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. from __future__ import annotations
  2. import logging
  3. import os
  4. from typing import TYPE_CHECKING, ClassVar
  5. if TYPE_CHECKING:
  6. from argparse import Action
  7. from typing import Any
  8. LOGGER = logging.getLogger(__name__)
  9. class TypeData:
  10. def __init__(self, default_type: type, as_type: type) -> None:
  11. self.default_type = default_type
  12. self.as_type = as_type
  13. def __repr__(self) -> str:
  14. return f"{self.__class__.__name__}(base={self.default_type}, as={self.as_type})"
  15. def convert(self, value: str) -> Any: # noqa: ANN401
  16. return self.default_type(value)
  17. class BoolType(TypeData):
  18. BOOLEAN_STATES: ClassVar[dict[str, bool]] = {
  19. "1": True,
  20. "yes": True,
  21. "true": True,
  22. "on": True,
  23. "0": False,
  24. "no": False,
  25. "false": False,
  26. "off": False,
  27. }
  28. def convert(self, value: str) -> bool:
  29. if value.lower() not in self.BOOLEAN_STATES:
  30. msg = f"Not a boolean: {value}"
  31. raise ValueError(msg)
  32. return self.BOOLEAN_STATES[value.lower()]
  33. class NoneType(TypeData):
  34. def convert(self, value: str) -> str | None:
  35. if not value:
  36. return None
  37. return str(value)
  38. class ListType(TypeData):
  39. def _validate(self) -> None:
  40. """no op."""
  41. def convert(self, value: str | list[str], flatten: bool = True) -> list[Any]: # noqa: ARG002, FBT002
  42. values = self.split_values(value)
  43. result = []
  44. for a_value in values:
  45. sub_values = a_value.split(os.pathsep)
  46. result.extend(sub_values)
  47. return [self.as_type(i) for i in result]
  48. def split_values(self, value: str | bytes | list[str]) -> list[str]:
  49. """Split the provided value into a list.
  50. First this is done by newlines. If there were no newlines in the text, then we next try to split by comma.
  51. """
  52. if isinstance(value, (str, bytes)):
  53. # Use `splitlines` rather than a custom check for whether there is
  54. # more than one line. This ensures that the full `splitlines()`
  55. # logic is supported here.
  56. values = value.splitlines()
  57. if len(values) <= 1:
  58. values = value.split(",") # ty: ignore[invalid-argument-type]
  59. values = filter(None, [x.strip() for x in values])
  60. else:
  61. values = list(value)
  62. return values # ty: ignore[invalid-return-type]
  63. def convert(value: str, as_type: TypeData, source: str) -> Any: # noqa: ANN401
  64. """Convert the value as a given type where the value comes from the given source."""
  65. try:
  66. return as_type.convert(value)
  67. except Exception as exception:
  68. LOGGER.warning("%s failed to convert %r as %r because %r", source, value, as_type, exception)
  69. raise
  70. _CONVERT = {bool: BoolType, type(None): NoneType, list: ListType}
  71. def get_type(action: Action) -> TypeData:
  72. default_type = type(action.default)
  73. as_type = default_type if action.type is None else action.type
  74. return _CONVERT.get(default_type, TypeData)(default_type, as_type) # ty: ignore[invalid-argument-type]
  75. __all__ = [
  76. "convert",
  77. "get_type",
  78. ]