Path: blob/main/test/lib/python3.9/site-packages/setuptools/_distutils/text_file.py
4799 views
"""text_file12provides the TextFile class, which gives an interface to text files3that (optionally) takes care of stripping comments, ignoring blank4lines, and joining lines with backslashes."""56import sys, io789class TextFile:10"""Provides a file-like object that takes care of all the things you11commonly want to do when processing a text file that has some12line-by-line syntax: strip comments (as long as "#" is your13comment character), skip blank lines, join adjacent lines by14escaping the newline (ie. backslash at end of line), strip15leading and/or trailing whitespace. All of these are optional16and independently controllable.1718Provides a 'warn()' method so you can generate warning messages that19report physical line number, even if the logical line in question20spans multiple physical lines. Also provides 'unreadline()' for21implementing line-at-a-time lookahead.2223Constructor is called as:2425TextFile (filename=None, file=None, **options)2627It bombs (RuntimeError) if both 'filename' and 'file' are None;28'filename' should be a string, and 'file' a file object (or29something that provides 'readline()' and 'close()' methods). It is30recommended that you supply at least 'filename', so that TextFile31can include it in warning messages. If 'file' is not supplied,32TextFile creates its own using 'io.open()'.3334The options are all boolean, and affect the value returned by35'readline()':36strip_comments [default: true]37strip from "#" to end-of-line, as well as any whitespace38leading up to the "#" -- unless it is escaped by a backslash39lstrip_ws [default: false]40strip leading whitespace from each line before returning it41rstrip_ws [default: true]42strip trailing whitespace (including line terminator!) from43each line before returning it44skip_blanks [default: true}45skip lines that are empty *after* stripping comments and46whitespace. (If both lstrip_ws and rstrip_ws are false,47then some lines may consist of solely whitespace: these will48*not* be skipped, even if 'skip_blanks' is true.)49join_lines [default: false]50if a backslash is the last non-newline character on a line51after stripping comments and whitespace, join the following line52to it to form one "logical line"; if N consecutive lines end53with a backslash, then N+1 physical lines will be joined to54form one logical line.55collapse_join [default: false]56strip leading whitespace from lines that are joined to their57predecessor; only matters if (join_lines and not lstrip_ws)58errors [default: 'strict']59error handler used to decode the file content6061Note that since 'rstrip_ws' can strip the trailing newline, the62semantics of 'readline()' must differ from those of the builtin file63object's 'readline()' method! In particular, 'readline()' returns64None for end-of-file: an empty string might just be a blank line (or65an all-whitespace line), if 'rstrip_ws' is true but 'skip_blanks' is66not."""6768default_options = { 'strip_comments': 1,69'skip_blanks': 1,70'lstrip_ws': 0,71'rstrip_ws': 1,72'join_lines': 0,73'collapse_join': 0,74'errors': 'strict',75}7677def __init__(self, filename=None, file=None, **options):78"""Construct a new TextFile object. At least one of 'filename'79(a string) and 'file' (a file-like object) must be supplied.80They keyword argument options are described above and affect81the values returned by 'readline()'."""82if filename is None and file is None:83raise RuntimeError("you must supply either or both of 'filename' and 'file'")8485# set values for all options -- either from client option hash86# or fallback to default_options87for opt in self.default_options.keys():88if opt in options:89setattr(self, opt, options[opt])90else:91setattr(self, opt, self.default_options[opt])9293# sanity check client option hash94for opt in options.keys():95if opt not in self.default_options:96raise KeyError("invalid TextFile option '%s'" % opt)9798if file is None:99self.open(filename)100else:101self.filename = filename102self.file = file103self.current_line = 0 # assuming that file is at BOF!104105# 'linebuf' is a stack of lines that will be emptied before we106# actually read from the file; it's only populated by an107# 'unreadline()' operation108self.linebuf = []109110def open(self, filename):111"""Open a new file named 'filename'. This overrides both the112'filename' and 'file' arguments to the constructor."""113self.filename = filename114self.file = io.open(self.filename, 'r', errors=self.errors)115self.current_line = 0116117def close(self):118"""Close the current file and forget everything we know about it119(filename, current line number)."""120file = self.file121self.file = None122self.filename = None123self.current_line = None124file.close()125126def gen_error(self, msg, line=None):127outmsg = []128if line is None:129line = self.current_line130outmsg.append(self.filename + ", ")131if isinstance(line, (list, tuple)):132outmsg.append("lines %d-%d: " % tuple(line))133else:134outmsg.append("line %d: " % line)135outmsg.append(str(msg))136return "".join(outmsg)137138def error(self, msg, line=None):139raise ValueError("error: " + self.gen_error(msg, line))140141def warn(self, msg, line=None):142"""Print (to stderr) a warning message tied to the current logical143line in the current file. If the current logical line in the144file spans multiple physical lines, the warning refers to the145whole range, eg. "lines 3-5". If 'line' supplied, it overrides146the current line number; it may be a list or tuple to indicate a147range of physical lines, or an integer for a single physical148line."""149sys.stderr.write("warning: " + self.gen_error(msg, line) + "\n")150151def readline(self):152"""Read and return a single logical line from the current file (or153from an internal buffer if lines have previously been "unread"154with 'unreadline()'). If the 'join_lines' option is true, this155may involve reading multiple physical lines concatenated into a156single string. Updates the current line number, so calling157'warn()' after 'readline()' emits a warning about the physical158line(s) just read. Returns None on end-of-file, since the empty159string can occur if 'rstrip_ws' is true but 'strip_blanks' is160not."""161# If any "unread" lines waiting in 'linebuf', return the top162# one. (We don't actually buffer read-ahead data -- lines only163# get put in 'linebuf' if the client explicitly does an164# 'unreadline()'.165if self.linebuf:166line = self.linebuf[-1]167del self.linebuf[-1]168return line169170buildup_line = ''171172while True:173# read the line, make it None if EOF174line = self.file.readline()175if line == '':176line = None177178if self.strip_comments and line:179180# Look for the first "#" in the line. If none, never181# mind. If we find one and it's the first character, or182# is not preceded by "\", then it starts a comment --183# strip the comment, strip whitespace before it, and184# carry on. Otherwise, it's just an escaped "#", so185# unescape it (and any other escaped "#"'s that might be186# lurking in there) and otherwise leave the line alone.187188pos = line.find("#")189if pos == -1: # no "#" -- no comments190pass191192# It's definitely a comment -- either "#" is the first193# character, or it's elsewhere and unescaped.194elif pos == 0 or line[pos-1] != "\\":195# Have to preserve the trailing newline, because it's196# the job of a later step (rstrip_ws) to remove it --197# and if rstrip_ws is false, we'd better preserve it!198# (NB. this means that if the final line is all comment199# and has no trailing newline, we will think that it's200# EOF; I think that's OK.)201eol = (line[-1] == '\n') and '\n' or ''202line = line[0:pos] + eol203204# If all that's left is whitespace, then skip line205# *now*, before we try to join it to 'buildup_line' --206# that way constructs like207# hello \\208# # comment that should be ignored209# there210# result in "hello there".211if line.strip() == "":212continue213else: # it's an escaped "#"214line = line.replace("\\#", "#")215216# did previous line end with a backslash? then accumulate217if self.join_lines and buildup_line:218# oops: end of file219if line is None:220self.warn("continuation line immediately precedes "221"end-of-file")222return buildup_line223224if self.collapse_join:225line = line.lstrip()226line = buildup_line + line227228# careful: pay attention to line number when incrementing it229if isinstance(self.current_line, list):230self.current_line[1] = self.current_line[1] + 1231else:232self.current_line = [self.current_line,233self.current_line + 1]234# just an ordinary line, read it as usual235else:236if line is None: # eof237return None238239# still have to be careful about incrementing the line number!240if isinstance(self.current_line, list):241self.current_line = self.current_line[1] + 1242else:243self.current_line = self.current_line + 1244245# strip whitespace however the client wants (leading and246# trailing, or one or the other, or neither)247if self.lstrip_ws and self.rstrip_ws:248line = line.strip()249elif self.lstrip_ws:250line = line.lstrip()251elif self.rstrip_ws:252line = line.rstrip()253254# blank line (whether we rstrip'ed or not)? skip to next line255# if appropriate256if (line == '' or line == '\n') and self.skip_blanks:257continue258259if self.join_lines:260if line[-1] == '\\':261buildup_line = line[:-1]262continue263264if line[-2:] == '\\\n':265buildup_line = line[0:-2] + '\n'266continue267268# well, I guess there's some actual content there: return it269return line270271def readlines(self):272"""Read and return the list of all logical lines remaining in the273current file."""274lines = []275while True:276line = self.readline()277if line is None:278return lines279lines.append(line)280281def unreadline(self, line):282"""Push 'line' (a string) onto an internal buffer that will be283checked by future 'readline()' calls. Handy for implementing284a parser with line-at-a-time lookahead."""285self.linebuf.append(line)286287288