python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
setuptools/
_distutils/
text_file.py
       1  """text_file
       2  
       3  provides the TextFile class, which gives an interface to text files
       4  that (optionally) takes care of stripping comments, ignoring blank
       5  lines, and joining lines with backslashes."""
       6  
       7  import sys
       8  
       9  
      10  class ESC[4;38;5;81mTextFile:
      11      """Provides a file-like object that takes care of all the things you
      12      commonly want to do when processing a text file that has some
      13      line-by-line syntax: strip comments (as long as "#" is your
      14      comment character), skip blank lines, join adjacent lines by
      15      escaping the newline (ie. backslash at end of line), strip
      16      leading and/or trailing whitespace.  All of these are optional
      17      and independently controllable.
      18  
      19      Provides a 'warn()' method so you can generate warning messages that
      20      report physical line number, even if the logical line in question
      21      spans multiple physical lines.  Also provides 'unreadline()' for
      22      implementing line-at-a-time lookahead.
      23  
      24      Constructor is called as:
      25  
      26          TextFile (filename=None, file=None, **options)
      27  
      28      It bombs (RuntimeError) if both 'filename' and 'file' are None;
      29      'filename' should be a string, and 'file' a file object (or
      30      something that provides 'readline()' and 'close()' methods).  It is
      31      recommended that you supply at least 'filename', so that TextFile
      32      can include it in warning messages.  If 'file' is not supplied,
      33      TextFile creates its own using 'io.open()'.
      34  
      35      The options are all boolean, and affect the value returned by
      36      'readline()':
      37        strip_comments [default: true]
      38          strip from "#" to end-of-line, as well as any whitespace
      39          leading up to the "#" -- unless it is escaped by a backslash
      40        lstrip_ws [default: false]
      41          strip leading whitespace from each line before returning it
      42        rstrip_ws [default: true]
      43          strip trailing whitespace (including line terminator!) from
      44          each line before returning it
      45        skip_blanks [default: true}
      46          skip lines that are empty *after* stripping comments and
      47          whitespace.  (If both lstrip_ws and rstrip_ws are false,
      48          then some lines may consist of solely whitespace: these will
      49          *not* be skipped, even if 'skip_blanks' is true.)
      50        join_lines [default: false]
      51          if a backslash is the last non-newline character on a line
      52          after stripping comments and whitespace, join the following line
      53          to it to form one "logical line"; if N consecutive lines end
      54          with a backslash, then N+1 physical lines will be joined to
      55          form one logical line.
      56        collapse_join [default: false]
      57          strip leading whitespace from lines that are joined to their
      58          predecessor; only matters if (join_lines and not lstrip_ws)
      59        errors [default: 'strict']
      60          error handler used to decode the file content
      61  
      62      Note that since 'rstrip_ws' can strip the trailing newline, the
      63      semantics of 'readline()' must differ from those of the builtin file
      64      object's 'readline()' method!  In particular, 'readline()' returns
      65      None for end-of-file: an empty string might just be a blank line (or
      66      an all-whitespace line), if 'rstrip_ws' is true but 'skip_blanks' is
      67      not."""
      68  
      69      default_options = {
      70          'strip_comments': 1,
      71          'skip_blanks': 1,
      72          'lstrip_ws': 0,
      73          'rstrip_ws': 1,
      74          'join_lines': 0,
      75          'collapse_join': 0,
      76          'errors': 'strict',
      77      }
      78  
      79      def __init__(self, filename=None, file=None, **options):
      80          """Construct a new TextFile object.  At least one of 'filename'
      81          (a string) and 'file' (a file-like object) must be supplied.
      82          They keyword argument options are described above and affect
      83          the values returned by 'readline()'."""
      84          if filename is None and file is None:
      85              raise RuntimeError(
      86                  "you must supply either or both of 'filename' and 'file'"
      87              )
      88  
      89          # set values for all options -- either from client option hash
      90          # or fallback to default_options
      91          for opt in self.default_options.keys():
      92              if opt in options:
      93                  setattr(self, opt, options[opt])
      94              else:
      95                  setattr(self, opt, self.default_options[opt])
      96  
      97          # sanity check client option hash
      98          for opt in options.keys():
      99              if opt not in self.default_options:
     100                  raise KeyError("invalid TextFile option '%s'" % opt)
     101  
     102          if file is None:
     103              self.open(filename)
     104          else:
     105              self.filename = filename
     106              self.file = file
     107              self.current_line = 0  # assuming that file is at BOF!
     108  
     109          # 'linebuf' is a stack of lines that will be emptied before we
     110          # actually read from the file; it's only populated by an
     111          # 'unreadline()' operation
     112          self.linebuf = []
     113  
     114      def open(self, filename):
     115          """Open a new file named 'filename'.  This overrides both the
     116          'filename' and 'file' arguments to the constructor."""
     117          self.filename = filename
     118          self.file = open(self.filename, errors=self.errors)
     119          self.current_line = 0
     120  
     121      def close(self):
     122          """Close the current file and forget everything we know about it
     123          (filename, current line number)."""
     124          file = self.file
     125          self.file = None
     126          self.filename = None
     127          self.current_line = None
     128          file.close()
     129  
     130      def gen_error(self, msg, line=None):
     131          outmsg = []
     132          if line is None:
     133              line = self.current_line
     134          outmsg.append(self.filename + ", ")
     135          if isinstance(line, (list, tuple)):
     136              outmsg.append("lines %d-%d: " % tuple(line))
     137          else:
     138              outmsg.append("line %d: " % line)
     139          outmsg.append(str(msg))
     140          return "".join(outmsg)
     141  
     142      def error(self, msg, line=None):
     143          raise ValueError("error: " + self.gen_error(msg, line))
     144  
     145      def warn(self, msg, line=None):
     146          """Print (to stderr) a warning message tied to the current logical
     147          line in the current file.  If the current logical line in the
     148          file spans multiple physical lines, the warning refers to the
     149          whole range, eg. "lines 3-5".  If 'line' supplied, it overrides
     150          the current line number; it may be a list or tuple to indicate a
     151          range of physical lines, or an integer for a single physical
     152          line."""
     153          sys.stderr.write("warning: " + self.gen_error(msg, line) + "\n")
     154  
     155      def readline(self):  # noqa: C901
     156          """Read and return a single logical line from the current file (or
     157          from an internal buffer if lines have previously been "unread"
     158          with 'unreadline()').  If the 'join_lines' option is true, this
     159          may involve reading multiple physical lines concatenated into a
     160          single string.  Updates the current line number, so calling
     161          'warn()' after 'readline()' emits a warning about the physical
     162          line(s) just read.  Returns None on end-of-file, since the empty
     163          string can occur if 'rstrip_ws' is true but 'strip_blanks' is
     164          not."""
     165          # If any "unread" lines waiting in 'linebuf', return the top
     166          # one.  (We don't actually buffer read-ahead data -- lines only
     167          # get put in 'linebuf' if the client explicitly does an
     168          # 'unreadline()'.
     169          if self.linebuf:
     170              line = self.linebuf[-1]
     171              del self.linebuf[-1]
     172              return line
     173  
     174          buildup_line = ''
     175  
     176          while True:
     177              # read the line, make it None if EOF
     178              line = self.file.readline()
     179              if line == '':
     180                  line = None
     181  
     182              if self.strip_comments and line:
     183  
     184                  # Look for the first "#" in the line.  If none, never
     185                  # mind.  If we find one and it's the first character, or
     186                  # is not preceded by "\", then it starts a comment --
     187                  # strip the comment, strip whitespace before it, and
     188                  # carry on.  Otherwise, it's just an escaped "#", so
     189                  # unescape it (and any other escaped "#"'s that might be
     190                  # lurking in there) and otherwise leave the line alone.
     191  
     192                  pos = line.find("#")
     193                  if pos == -1:  # no "#" -- no comments
     194                      pass
     195  
     196                  # It's definitely a comment -- either "#" is the first
     197                  # character, or it's elsewhere and unescaped.
     198                  elif pos == 0 or line[pos - 1] != "\\":
     199                      # Have to preserve the trailing newline, because it's
     200                      # the job of a later step (rstrip_ws) to remove it --
     201                      # and if rstrip_ws is false, we'd better preserve it!
     202                      # (NB. this means that if the final line is all comment
     203                      # and has no trailing newline, we will think that it's
     204                      # EOF; I think that's OK.)
     205                      eol = (line[-1] == '\n') and '\n' or ''
     206                      line = line[0:pos] + eol
     207  
     208                      # If all that's left is whitespace, then skip line
     209                      # *now*, before we try to join it to 'buildup_line' --
     210                      # that way constructs like
     211                      #   hello \\
     212                      #   # comment that should be ignored
     213                      #   there
     214                      # result in "hello there".
     215                      if line.strip() == "":
     216                          continue
     217                  else:  # it's an escaped "#"
     218                      line = line.replace("\\#", "#")
     219  
     220              # did previous line end with a backslash? then accumulate
     221              if self.join_lines and buildup_line:
     222                  # oops: end of file
     223                  if line is None:
     224                      self.warn("continuation line immediately precedes " "end-of-file")
     225                      return buildup_line
     226  
     227                  if self.collapse_join:
     228                      line = line.lstrip()
     229                  line = buildup_line + line
     230  
     231                  # careful: pay attention to line number when incrementing it
     232                  if isinstance(self.current_line, list):
     233                      self.current_line[1] = self.current_line[1] + 1
     234                  else:
     235                      self.current_line = [self.current_line, self.current_line + 1]
     236              # just an ordinary line, read it as usual
     237              else:
     238                  if line is None:  # eof
     239                      return None
     240  
     241                  # still have to be careful about incrementing the line number!
     242                  if isinstance(self.current_line, list):
     243                      self.current_line = self.current_line[1] + 1
     244                  else:
     245                      self.current_line = self.current_line + 1
     246  
     247              # strip whitespace however the client wants (leading and
     248              # trailing, or one or the other, or neither)
     249              if self.lstrip_ws and self.rstrip_ws:
     250                  line = line.strip()
     251              elif self.lstrip_ws:
     252                  line = line.lstrip()
     253              elif self.rstrip_ws:
     254                  line = line.rstrip()
     255  
     256              # blank line (whether we rstrip'ed or not)? skip to next line
     257              # if appropriate
     258              if (line == '' or line == '\n') and self.skip_blanks:
     259                  continue
     260  
     261              if self.join_lines:
     262                  if line[-1] == '\\':
     263                      buildup_line = line[:-1]
     264                      continue
     265  
     266                  if line[-2:] == '\\\n':
     267                      buildup_line = line[0:-2] + '\n'
     268                      continue
     269  
     270              # well, I guess there's some actual content there: return it
     271              return line
     272  
     273      def readlines(self):
     274          """Read and return the list of all logical lines remaining in the
     275          current file."""
     276          lines = []
     277          while True:
     278              line = self.readline()
     279              if line is None:
     280                  return lines
     281              lines.append(line)
     282  
     283      def unreadline(self, line):
     284          """Push 'line' (a string) onto an internal buffer that will be
     285          checked by future 'readline()' calls.  Handy for implementing
     286          a parser with line-at-a-time lookahead."""
     287          self.linebuf.append(line)