io.py 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. """Defines any IO utilities used by isort"""
  2. import re
  3. import tokenize
  4. from contextlib import contextmanager
  5. from io import BytesIO, StringIO, TextIOWrapper
  6. from pathlib import Path
  7. from typing import Any, Callable, Iterator, TextIO, Union
  8. from isort._future import dataclass
  9. from isort.exceptions import UnsupportedEncoding
  10. _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)")
  11. @dataclass(frozen=True)
  12. class File:
  13. stream: TextIO
  14. path: Path
  15. encoding: str
  16. @staticmethod
  17. def detect_encoding(filename: Union[str, Path], readline: Callable[[], bytes]) -> str:
  18. try:
  19. return tokenize.detect_encoding(readline)[0]
  20. except Exception:
  21. raise UnsupportedEncoding(filename)
  22. @staticmethod
  23. def from_contents(contents: str, filename: str) -> "File":
  24. encoding = File.detect_encoding(filename, BytesIO(contents.encode("utf-8")).readline)
  25. return File( # type: ignore
  26. stream=StringIO(contents), path=Path(filename).resolve(), encoding=encoding
  27. )
  28. @property
  29. def extension(self) -> str:
  30. return self.path.suffix.lstrip(".")
  31. @staticmethod
  32. def _open(filename: Union[str, Path]) -> TextIOWrapper:
  33. """Open a file in read only mode using the encoding detected by
  34. detect_encoding().
  35. """
  36. buffer = open(filename, "rb")
  37. try:
  38. encoding = File.detect_encoding(filename, buffer.readline)
  39. buffer.seek(0)
  40. text = TextIOWrapper(buffer, encoding, line_buffering=True, newline="")
  41. text.mode = "r" # type: ignore
  42. return text
  43. except Exception:
  44. buffer.close()
  45. raise
  46. @staticmethod
  47. @contextmanager
  48. def read(filename: Union[str, Path]) -> Iterator["File"]:
  49. file_path = Path(filename).resolve()
  50. stream = None
  51. try:
  52. stream = File._open(file_path)
  53. yield File(stream=stream, path=file_path, encoding=stream.encoding) # type: ignore
  54. finally:
  55. if stream is not None:
  56. stream.close()
  57. class _EmptyIO(StringIO):
  58. def write(self, *args: Any, **kwargs: Any) -> None: # type: ignore # skipcq: PTC-W0049
  59. pass
  60. Empty = _EmptyIO()