3939import errno
4040import sys
4141
42+ import json as json_module
43+
4244if sys .implementation .name == "circuitpython" :
4345
4446 def cast (_t , value ):
4547 """No-op shim for the typing.cast() function which is not available in CircuitPython."""
4648 return value
4749
48-
4950else :
5051 from ssl import SSLContext
5152 from types import ModuleType , TracebackType
@@ -148,16 +149,6 @@ def TLS_MODE(self) -> int: # pylint: disable=invalid-name
148149 SSLContextType = Union [SSLContext , "_FakeSSLContext" ]
149150
150151
151- # CircuitPython 6.0 does not have the bytearray.split method.
152- # This function emulates buf.split(needle)[0], which is the functionality
153- # required.
154- def _buffer_split0 (buf : Union [bytes , bytearray ], needle : Union [bytes , bytearray ]):
155- index = buf .find (needle )
156- if index == - 1 :
157- return buf
158- return buf [:index ]
159-
160-
161152class _RawResponse :
162153 def __init__ (self , response : "Response" ) -> None :
163154 self ._response = response
@@ -177,10 +168,6 @@ def readinto(self, buf: bytearray) -> int:
177168 return self ._response ._readinto (buf ) # pylint: disable=protected-access
178169
179170
180- class _SendFailed (Exception ):
181- """Custom exception to abort sending a request."""
182-
183-
184171class OutOfRetries (Exception ):
185172 """Raised when requests has retried to make a request unsuccessfully."""
186173
@@ -240,56 +227,25 @@ def _recv_into(self, buf: bytearray, size: int = 0) -> int:
240227 return read_size
241228 return cast ("SupportsRecvInto" , self .socket ).recv_into (buf , size )
242229
243- @staticmethod
244- def _find (buf : bytes , needle : bytes , start : int , end : int ) -> int :
245- if hasattr (buf , "find" ):
246- return buf .find (needle , start , end )
247- result = - 1
248- i = start
249- while i < end :
250- j = 0
251- while j < len (needle ) and i + j < end and buf [i + j ] == needle [j ]:
252- j += 1
253- if j == len (needle ):
254- result = i
255- break
256- i += 1
257-
258- return result
259-
260- def _readto (self , first : bytes , second : bytes = b"" ) -> bytes :
230+ def _readto (self , stop : bytes ) -> bytearray :
261231 buf = self ._receive_buffer
262232 end = self ._received_length
263233 while True :
264- firsti = self ._find (buf , first , 0 , end )
265- secondi = - 1
266- if second :
267- secondi = self ._find (buf , second , 0 , end )
268-
269- i = - 1
270- needle_len = 0
271- if firsti >= 0 :
272- i = firsti
273- needle_len = len (first )
274- if secondi >= 0 and (firsti < 0 or secondi < firsti ):
275- i = secondi
276- needle_len = len (second )
234+ i = buf .find (stop , 0 , end )
277235 if i >= 0 :
236+ # Stop was found. Return everything up to but not including stop.
278237 result = buf [:i ]
279- new_start = i + needle_len
280-
281- if i + needle_len <= end :
282- new_end = end - new_start
283- buf [:new_end ] = buf [new_start :end ]
284- self ._received_length = new_end
238+ new_start = i + len (stop )
239+ # Remove everything up to and including stop from the buffer.
240+ new_end = end - new_start
241+ buf [:new_end ] = buf [new_start :end ]
242+ self ._received_length = new_end
285243 return result
286244
287- # Not found so load more.
288-
245+ # Not found so load more bytes.
289246 # If our buffer is full, then make it bigger to load more.
290247 if end == len (buf ):
291- new_size = len (buf ) + 32
292- new_buf = bytearray (new_size )
248+ new_buf = bytearray (len (buf ) + 32 )
293249 new_buf [: len (buf )] = buf
294250 buf = new_buf
295251 self ._receive_buffer = buf
@@ -300,8 +256,6 @@ def _readto(self, first: bytes, second: bytes = b"") -> bytes:
300256 return buf [:end ]
301257 end += read
302258
303- return b""
304-
305259 def _read_from_buffer (
306260 self , buf : Optional [bytearray ] = None , nbytes : Optional [int ] = None
307261 ) -> int :
@@ -333,7 +287,7 @@ def _readinto(self, buf: bytearray) -> int:
333287 # Consume trailing \r\n for chunks 2+
334288 if self ._remaining == 0 :
335289 self ._throw_away (2 )
336- chunk_header = _buffer_split0 (self ._readto (b"\r \n " ), b";" )
290+ chunk_header = bytes (self ._readto (b"\r \n " )). split ( b";" , 1 )[ 0 ]
337291 http_chunk_size = int (bytes (chunk_header ), 16 )
338292 if http_chunk_size == 0 :
339293 self ._chunked = False
@@ -374,7 +328,7 @@ def close(self) -> None:
374328 self ._throw_away (self ._remaining )
375329 elif self ._chunked :
376330 while True :
377- chunk_header = _buffer_split0 (self ._readto (b"\r \n " ), b";" )
331+ chunk_header = bytes (self ._readto (b"\r \n " )). split ( b";" , 1 )[ 0 ]
378332 chunk_size = int (bytes (chunk_header ), 16 )
379333 if chunk_size == 0 :
380334 break
@@ -392,11 +346,10 @@ def _parse_headers(self) -> None:
392346 Expects first line of HTTP request/response to have been read already.
393347 """
394348 while True :
395- title = self ._readto (b": " , b"\r \n " )
396- if not title :
349+ header = self ._readto (b"\r \n " )
350+ if not header :
397351 break
398-
399- content = self ._readto (b"\r \n " )
352+ title , content = bytes (header ).split (b": " , 1 )
400353 if title and content :
401354 # enforce that all headers are lowercase
402355 title = str (title , "utf-8" ).lower ()
@@ -407,6 +360,17 @@ def _parse_headers(self) -> None:
407360 self ._chunked = content .strip ().lower () == "chunked"
408361 self ._headers [title ] = content
409362
363+ def _validate_not_gzip (self ) -> None :
364+ """gzip encoding is not supported. Raise an exception if found."""
365+ if (
366+ "content-encoding" in self .headers
367+ and self .headers ["content-encoding" ] == "gzip"
368+ ):
369+ raise ValueError (
370+ "Content-encoding is gzip, data cannot be accessed as json or text. "
371+ "Use content property to access raw bytes."
372+ )
373+
410374 @property
411375 def headers (self ) -> Dict [str , str ]:
412376 """
@@ -435,22 +399,13 @@ def text(self) -> str:
435399 return self ._cached
436400 raise RuntimeError ("Cannot access text after getting content or json" )
437401
438- if (
439- "content-encoding" in self .headers
440- and self .headers ["content-encoding" ] == "gzip"
441- ):
442- raise ValueError (
443- "Content-encoding is gzip, data cannot be accessed as json or text. "
444- "Use content property to access raw bytes."
445- )
402+ self ._validate_not_gzip ()
403+
446404 self ._cached = str (self .content , self .encoding )
447405 return self ._cached
448406
449407 def json (self ) -> Any :
450408 """The HTTP content, parsed into a json dictionary"""
451- # pylint: disable=import-outside-toplevel
452- import json
453-
454409 # The cached JSON will be a list or dictionary.
455410 if self ._cached :
456411 if isinstance (self ._cached , (list , dict )):
@@ -459,20 +414,9 @@ def json(self) -> Any:
459414 if not self ._raw :
460415 self ._raw = _RawResponse (self )
461416
462- if (
463- "content-encoding" in self .headers
464- and self .headers ["content-encoding" ] == "gzip"
465- ):
466- raise ValueError (
467- "Content-encoding is gzip, data cannot be accessed as json or text. "
468- "Use content property to access raw bytes."
469- )
470- try :
471- obj = json .load (self ._raw )
472- except OSError :
473- # <5.3.1 doesn't piecemeal load json from any object with readinto so load the whole
474- # string.
475- obj = json .loads (self ._raw .read ())
417+ self ._validate_not_gzip ()
418+
419+ obj = json_module .load (self ._raw )
476420 if not self ._cached :
477421 self ._cached = obj
478422 self .close ()
@@ -599,12 +543,19 @@ def _send(socket: SocketType, data: bytes):
599543 # ESP32SPI sockets raise a RuntimeError when unable to send.
600544 try :
601545 sent = socket .send (data [total_sent :])
602- except RuntimeError :
603- sent = 0
546+ except OSError as exc :
547+ if exc .errno == errno .EAGAIN :
548+ # Can't send right now (e.g., no buffer space), try again.
549+ continue
550+ # Some worse error.
551+ raise
552+ except RuntimeError as exc :
553+ raise OSError (errno .EIO ) from exc
604554 if sent is None :
605555 sent = len (data )
606556 if sent == 0 :
607- raise _SendFailed ()
557+ # Not EAGAIN; that was already handled.
558+ raise OSError (errno .EIO )
608559 total_sent += sent
609560
610561 def _send_request (
@@ -636,11 +587,6 @@ def _send_request(
636587 self ._send (socket , b"\r \n " )
637588 if json is not None :
638589 assert data is None
639- # pylint: disable=import-outside-toplevel
640- try :
641- import json as json_module
642- except ImportError :
643- import ujson as json_module
644590 data = json_module .dumps (json )
645591 self ._send (socket , b"Content-Type: application/json\r \n " )
646592 if data :
@@ -711,7 +657,7 @@ def request(
711657 ok = True
712658 try :
713659 self ._send_request (socket , host , method , path , headers , data , json )
714- except ( _SendFailed , OSError ) :
660+ except OSError :
715661 ok = False
716662 if ok :
717663 # Read the H of "HTTP/1.1" to make sure the socket is alive. send can appear to work
0 commit comments