io.py 2.2 KB

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