Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/urllib3/_request_methods.py: 34%

53 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:40 +0000

1from __future__ import annotations 

2 

3import json as _json 

4import typing 

5from urllib.parse import urlencode 

6 

7from ._base_connection import _TYPE_BODY 

8from ._collections import HTTPHeaderDict 

9from .filepost import _TYPE_FIELDS, encode_multipart_formdata 

10from .response import BaseHTTPResponse 

11 

12__all__ = ["RequestMethods"] 

13 

14_TYPE_ENCODE_URL_FIELDS = typing.Union[ 

15 typing.Sequence[typing.Tuple[str, typing.Union[str, bytes]]], 

16 typing.Mapping[str, typing.Union[str, bytes]], 

17] 

18 

19 

20class RequestMethods: 

21 """ 

22 Convenience mixin for classes who implement a :meth:`urlopen` method, such 

23 as :class:`urllib3.HTTPConnectionPool` and 

24 :class:`urllib3.PoolManager`. 

25 

26 Provides behavior for making common types of HTTP request methods and 

27 decides which type of request field encoding to use. 

28 

29 Specifically, 

30 

31 :meth:`.request_encode_url` is for sending requests whose fields are 

32 encoded in the URL (such as GET, HEAD, DELETE). 

33 

34 :meth:`.request_encode_body` is for sending requests whose fields are 

35 encoded in the *body* of the request using multipart or www-form-urlencoded 

36 (such as for POST, PUT, PATCH). 

37 

38 :meth:`.request` is for making any kind of request, it will look up the 

39 appropriate encoding format and use one of the above two methods to make 

40 the request. 

41 

42 Initializer parameters: 

43 

44 :param headers: 

45 Headers to include with all requests, unless other headers are given 

46 explicitly. 

47 """ 

48 

49 _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} 

50 

51 def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: 

52 self.headers = headers or {} 

53 

54 def urlopen( 

55 self, 

56 method: str, 

57 url: str, 

58 body: _TYPE_BODY | None = None, 

59 headers: typing.Mapping[str, str] | None = None, 

60 encode_multipart: bool = True, 

61 multipart_boundary: str | None = None, 

62 **kw: typing.Any, 

63 ) -> BaseHTTPResponse: # Abstract 

64 raise NotImplementedError( 

65 "Classes extending RequestMethods must implement " 

66 "their own ``urlopen`` method." 

67 ) 

68 

69 def request( 

70 self, 

71 method: str, 

72 url: str, 

73 body: _TYPE_BODY | None = None, 

74 fields: _TYPE_FIELDS | None = None, 

75 headers: typing.Mapping[str, str] | None = None, 

76 json: typing.Any | None = None, 

77 **urlopen_kw: typing.Any, 

78 ) -> BaseHTTPResponse: 

79 """ 

80 Make a request using :meth:`urlopen` with the appropriate encoding of 

81 ``fields`` based on the ``method`` used. 

82 

83 This is a convenience method that requires the least amount of manual 

84 effort. It can be used in most situations, while still having the 

85 option to drop down to more specific methods when necessary, such as 

86 :meth:`request_encode_url`, :meth:`request_encode_body`, 

87 or even the lowest level :meth:`urlopen`. 

88 """ 

89 method = method.upper() 

90 

91 if json is not None and body is not None: 

92 raise TypeError( 

93 "request got values for both 'body' and 'json' parameters which are mutually exclusive" 

94 ) 

95 

96 if json is not None: 

97 if headers is None: 

98 headers = self.headers.copy() # type: ignore 

99 if not ("content-type" in map(str.lower, headers.keys())): 

100 headers["Content-Type"] = "application/json" # type: ignore 

101 

102 body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( 

103 "utf-8" 

104 ) 

105 

106 if body is not None: 

107 urlopen_kw["body"] = body 

108 

109 if method in self._encode_url_methods: 

110 return self.request_encode_url( 

111 method, 

112 url, 

113 fields=fields, # type: ignore[arg-type] 

114 headers=headers, 

115 **urlopen_kw, 

116 ) 

117 else: 

118 return self.request_encode_body( 

119 method, url, fields=fields, headers=headers, **urlopen_kw 

120 ) 

121 

122 def request_encode_url( 

123 self, 

124 method: str, 

125 url: str, 

126 fields: _TYPE_ENCODE_URL_FIELDS | None = None, 

127 headers: typing.Mapping[str, str] | None = None, 

128 **urlopen_kw: str, 

129 ) -> BaseHTTPResponse: 

130 """ 

131 Make a request using :meth:`urlopen` with the ``fields`` encoded in 

132 the url. This is useful for request methods like GET, HEAD, DELETE, etc. 

133 """ 

134 if headers is None: 

135 headers = self.headers 

136 

137 extra_kw: dict[str, typing.Any] = {"headers": headers} 

138 extra_kw.update(urlopen_kw) 

139 

140 if fields: 

141 url += "?" + urlencode(fields) 

142 

143 return self.urlopen(method, url, **extra_kw) 

144 

145 def request_encode_body( 

146 self, 

147 method: str, 

148 url: str, 

149 fields: _TYPE_FIELDS | None = None, 

150 headers: typing.Mapping[str, str] | None = None, 

151 encode_multipart: bool = True, 

152 multipart_boundary: str | None = None, 

153 **urlopen_kw: str, 

154 ) -> BaseHTTPResponse: 

155 """ 

156 Make a request using :meth:`urlopen` with the ``fields`` encoded in 

157 the body. This is useful for request methods like POST, PUT, PATCH, etc. 

158 

159 When ``encode_multipart=True`` (default), then 

160 :func:`urllib3.encode_multipart_formdata` is used to encode 

161 the payload with the appropriate content type. Otherwise 

162 :func:`urllib.parse.urlencode` is used with the 

163 'application/x-www-form-urlencoded' content type. 

164 

165 Multipart encoding must be used when posting files, and it's reasonably 

166 safe to use it in other times too. However, it may break request 

167 signing, such as with OAuth. 

168 

169 Supports an optional ``fields`` parameter of key/value strings AND 

170 key/filetuple. A filetuple is a (filename, data, MIME type) tuple where 

171 the MIME type is optional. For example:: 

172 

173 fields = { 

174 'foo': 'bar', 

175 'fakefile': ('foofile.txt', 'contents of foofile'), 

176 'realfile': ('barfile.txt', open('realfile').read()), 

177 'typedfile': ('bazfile.bin', open('bazfile').read(), 

178 'image/jpeg'), 

179 'nonamefile': 'contents of nonamefile field', 

180 } 

181 

182 When uploading a file, providing a filename (the first parameter of the 

183 tuple) is optional but recommended to best mimic behavior of browsers. 

184 

185 Note that if ``headers`` are supplied, the 'Content-Type' header will 

186 be overwritten because it depends on the dynamic random boundary string 

187 which is used to compose the body of the request. The random boundary 

188 string can be explicitly set with the ``multipart_boundary`` parameter. 

189 """ 

190 if headers is None: 

191 headers = self.headers 

192 

193 extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} 

194 body: bytes | str 

195 

196 if fields: 

197 if "body" in urlopen_kw: 

198 raise TypeError( 

199 "request got values for both 'fields' and 'body', can only specify one." 

200 ) 

201 

202 if encode_multipart: 

203 body, content_type = encode_multipart_formdata( 

204 fields, boundary=multipart_boundary 

205 ) 

206 else: 

207 body, content_type = ( 

208 urlencode(fields), # type: ignore[arg-type] 

209 "application/x-www-form-urlencoded", 

210 ) 

211 

212 extra_kw["body"] = body 

213 extra_kw["headers"].setdefault("Content-Type", content_type) 

214 

215 extra_kw.update(urlopen_kw) 

216 

217 return self.urlopen(method, url, **extra_kw)