Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain/plain/internal/files/base.py: 30%

1 import os

2 from io import BytesIO, StringIO, UnsupportedOperation

3

4 from plain.internal.files.utils import FileProxyMixin

5 from plain.utils.functional import cached_property

6

7

8 class File(FileProxyMixin):

9 DEFAULT_CHUNK_SIZE = 64 * 2**10

10

11 def __init__(self, file, name=None):

12 self.file = file

13 if name is None:

14 name = getattr(file, "name", None)

15 self.name = name

16 if hasattr(file, "mode"):

17 self.mode = file.mode

18

19 def __str__(self):

20 return self.name or ""

21

22 def __repr__(self):

23 return "<{}: {}>".format(self.__class__.__name__, self or "None")

24

25 def __bool__(self):

26 return bool(self.name)

27

28 def __len__(self):

29 return self.size

30

31 @cached_property

32 def size(self):

33 if hasattr(self.file, "size"):

34 return self.file.size

35 if hasattr(self.file, "name"):

36 try:

37 return os.path.getsize(self.file.name)

38 except (OSError, TypeError):

39 pass

40 if hasattr(self.file, "tell") and hasattr(self.file, "seek"):

41 pos = self.file.tell()

42 self.file.seek(0, os.SEEK_END)

43 size = self.file.tell()

44 self.file.seek(pos)

45 return size

46 raise AttributeError("Unable to determine the file's size.")

47

48 def chunks(self, chunk_size=None):

49 """

50 Read the file and yield chunks of ``chunk_size`` bytes (defaults to

51 ``File.DEFAULT_CHUNK_SIZE``).

52 """

53 chunk_size = chunk_size or self.DEFAULT_CHUNK_SIZE

54 try:

55 self.seek(0)

56 except (AttributeError, UnsupportedOperation):

57 pass

58

59 while True:

60 data = self.read(chunk_size)

61 if not data:

62 break

63 yield data

64

65 def multiple_chunks(self, chunk_size=None):

66 """

67 Return ``True`` if you can expect multiple chunks.

68

69 NB: If a particular file representation is in memory, subclasses should

70 always return ``False`` -- there's no good reason to read from memory in

71 chunks.

72 """

73 return self.size > (chunk_size or self.DEFAULT_CHUNK_SIZE)

74

75 def __iter__(self):

76 # Iterate over this file-like object by newlines

77 buffer_ = None

78 for chunk in self.chunks():

79 for line in chunk.splitlines(True):

80 if buffer_:

81 if endswith_cr(buffer_) and not equals_lf(line):

82 # Line split after a \r newline; yield buffer_.

83 yield buffer_

84 # Continue with line.

85 else:

86 # Line either split without a newline (line

87 # continues after buffer_) or with \r\n

88 # newline (line == b'\n').

89 line = buffer_ + line

90 # buffer_ handled, clear it.

91 buffer_ = None

92

93 # If this is the end of a \n or \r\n line, yield.

94 if endswith_lf(line):

95 yield line

96 else:

97 buffer_ = line

98

99 if buffer_ is not None:

100 yield buffer_

101

102 def __enter__(self):

103 return self

104

105 def __exit__(self, exc_type, exc_value, tb):

106 self.close()

107

108 def open(self, mode=None):

109 if not self.closed:

110 self.seek(0)

111 elif self.name and os.path.exists(self.name):

112 self.file = open(self.name, mode or self.mode)

113 else:

114 raise ValueError("The file cannot be reopened.")

115 return self

116

117 def close(self):

118 self.file.close()

119

120

121 class ContentFile(File):

122 """

123 A File-like object that takes just raw content, rather than an actual file.

124 """

125

126 def __init__(self, content, name=None):

127 stream_class = StringIO if isinstance(content, str) else BytesIO

128 super().__init__(stream_class(content), name=name)

129 self.size = len(content)

130

131 def __str__(self):

132 return "Raw content"

133

134 def __bool__(self):

135 return True

136

137 def open(self, mode=None):

138 self.seek(0)

139 return self

140

141 def close(self):

142 pass

143

144 def write(self, data):

145 self.__dict__.pop("size", None) # Clear the computed size.

146 return self.file.write(data)

147

148

149 def endswith_cr(line):

150 """Return True if line (a text or bytestring) ends with '\r'."""

151 return line.endswith("\r" if isinstance(line, str) else b"\r")

152

153

154 def endswith_lf(line):

155 """Return True if line (a text or bytestring) ends with '\n'."""

156 return line.endswith("\n" if isinstance(line, str) else b"\n")

157

158

159 def equals_lf(line):

160 """Return True if line (a text or bytestring) equals '\n'."""

161 return line == ("\n" if isinstance(line, str) else b"\n")