docs: def readline(self, size=None):
          """
          Read one entire line from the file.  A trailing newline character is
          kept in the string (but may be absent when a file ends with an
          incomplete line).  If the size argument is present and non-negative, it
          is a maximum byte count (including the trailing newline) and an
          incomplete line may be returned.  An empty string is returned only when
          EOF is encountered immediately.
  
          .. note::
              Unlike stdio's ``fgets``, the returned string contains null
              characters (``'\\0'``) if they occurred in the input.
  
          :param int size: maximum length of returned string.
          :return:
              next line of the file, or an empty string if the end of the
              file has been reached.
  
              If the file was opened in binary (``'b'``) mode: bytes are returned
              Else: the encoding of the file is assumed to be UTF-8 and character
              strings (`str`) are returned
          """
          # it's almost silly how complex this function is.
          if self._closed:
              raise IOError('File is closed')
          if not (self._flags & self.FLAG_READ):
              raise IOError('File not open for reading')
          line = self._rbuffer
          while True:
              if self._at_trailing_cr and (self._flags & self.FLAG_UNIVERSAL_NEWLINE) and (len(line) > 0):
                  # edge case: the newline may be '\r\n' and we may have read
                  # only the first '\r' last time.
                  if line[0] == linefeed_byte_value:
                      line = line[1:]
                      self._record_newline(crlf)
                  else:
                      self._record_newline(cr_byte)
                  self._at_trailing_cr = False
              # check size before looking for a linefeed, in case we already have
              # enough.
              if (size is not None) and (size >= 0):
                  if len(line) >= size:
                      # truncate line and return
                      self._rbuffer = line[size:]
                      line = line[:size]
                      self._pos += len(line)
                      return line if self._flags & self.FLAG_BINARY else u(line)
                  n = size - len(line)
              else:
                  n = self._bufsize
              if (linefeed_byte in line) or ((self._flags & self.FLAG_UNIVERSAL_NEWLINE) and (cr_byte in line)):
                  break
              try:
                  new_data = self._read(n)
              except EOFError:
                  new_data = None
              if (new_data is None) or (len(new_data) == 0):
                  self._rbuffer = bytes()
                  self._pos += len(line)
                  return line if self._flags & self.FLAG_BINARY else u(line)
              line += new_data
              self._realpos += len(new_data)
          # find the newline
          pos = line.find(linefeed_byte)
          if self._flags & self.FLAG_UNIVERSAL_NEWLINE:
              rpos = line.find(cr_byte)
              if (rpos >= 0) and (rpos < pos or pos < 0):
                  pos = rpos
          xpos = pos + 1
          if (line[pos] == cr_byte_value) and (xpos < len(line)) and (line[xpos] == linefeed_byte_value):
              xpos += 1
          self._rbuffer = line[xpos:]
          lf = line[pos:xpos]
          line = line[:pos] + linefeed_byte
          if (len(self._rbuffer) == 0) and (lf == cr_byte):
              # we could read the line up to a '\r' and there could still be a
              # '\n' following that we read next time.  note that and eat it.
              self._at_trailing_cr = True
          else:
              self._record_newline(lf)
          self._pos += len(line)
          return line if self._flags & self.FLAG_BINARY else u(line)