Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pip/_vendor/cachecontrol/filewrapper.py: 24%

49 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-26 06:33 +0000

1# SPDX-FileCopyrightText: 2015 Eric Larson 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4from __future__ import annotations 

5 

6import mmap 

7from tempfile import NamedTemporaryFile 

8from typing import TYPE_CHECKING, Any, Callable 

9 

10if TYPE_CHECKING: 

11 from http.client import HTTPResponse 

12 

13 

14class CallbackFileWrapper: 

15 """ 

16 Small wrapper around a fp object which will tee everything read into a 

17 buffer, and when that file is closed it will execute a callback with the 

18 contents of that buffer. 

19 

20 All attributes are proxied to the underlying file object. 

21 

22 This class uses members with a double underscore (__) leading prefix so as 

23 not to accidentally shadow an attribute. 

24 

25 The data is stored in a temporary file until it is all available. As long 

26 as the temporary files directory is disk-based (sometimes it's a 

27 memory-backed-``tmpfs`` on Linux), data will be unloaded to disk if memory 

28 pressure is high. For small files the disk usually won't be used at all, 

29 it'll all be in the filesystem memory cache, so there should be no 

30 performance impact. 

31 """ 

32 

33 def __init__( 

34 self, fp: HTTPResponse, callback: Callable[[bytes], None] | None 

35 ) -> None: 

36 self.__buf = NamedTemporaryFile("rb+", delete=True) 

37 self.__fp = fp 

38 self.__callback = callback 

39 

40 def __getattr__(self, name: str) -> Any: 

41 # The vaguaries of garbage collection means that self.__fp is 

42 # not always set. By using __getattribute__ and the private 

43 # name[0] allows looking up the attribute value and raising an 

44 # AttributeError when it doesn't exist. This stop thigns from 

45 # infinitely recursing calls to getattr in the case where 

46 # self.__fp hasn't been set. 

47 # 

48 # [0] https://docs.python.org/2/reference/expressions.html#atom-identifiers 

49 fp = self.__getattribute__("_CallbackFileWrapper__fp") 

50 return getattr(fp, name) 

51 

52 def __is_fp_closed(self) -> bool: 

53 try: 

54 return self.__fp.fp is None 

55 

56 except AttributeError: 

57 pass 

58 

59 try: 

60 closed: bool = self.__fp.closed 

61 return closed 

62 

63 except AttributeError: 

64 pass 

65 

66 # We just don't cache it then. 

67 # TODO: Add some logging here... 

68 return False 

69 

70 def _close(self) -> None: 

71 if self.__callback: 

72 if self.__buf.tell() == 0: 

73 # Empty file: 

74 result = b"" 

75 else: 

76 # Return the data without actually loading it into memory, 

77 # relying on Python's buffer API and mmap(). mmap() just gives 

78 # a view directly into the filesystem's memory cache, so it 

79 # doesn't result in duplicate memory use. 

80 self.__buf.seek(0, 0) 

81 result = memoryview( 

82 mmap.mmap(self.__buf.fileno(), 0, access=mmap.ACCESS_READ) 

83 ) 

84 self.__callback(result) 

85 

86 # We assign this to None here, because otherwise we can get into 

87 # really tricky problems where the CPython interpreter dead locks 

88 # because the callback is holding a reference to something which 

89 # has a __del__ method. Setting this to None breaks the cycle 

90 # and allows the garbage collector to do it's thing normally. 

91 self.__callback = None 

92 

93 # Closing the temporary file releases memory and frees disk space. 

94 # Important when caching big files. 

95 self.__buf.close() 

96 

97 def read(self, amt: int | None = None) -> bytes: 

98 data: bytes = self.__fp.read(amt) 

99 if data: 

100 # We may be dealing with b'', a sign that things are over: 

101 # it's passed e.g. after we've already closed self.__buf. 

102 self.__buf.write(data) 

103 if self.__is_fp_closed(): 

104 self._close() 

105 

106 return data 

107 

108 def _safe_read(self, amt: int) -> bytes: 

109 data: bytes = self.__fp._safe_read(amt) # type: ignore[attr-defined] 

110 if amt == 2 and data == b"\r\n": 

111 # urllib executes this read to toss the CRLF at the end 

112 # of the chunk. 

113 return data 

114 

115 self.__buf.write(data) 

116 if self.__is_fp_closed(): 

117 self._close() 

118 

119 return data