root/masterdriverz/snakeoil-formatters/snakeoil/fileutils.py @ ferringb%2540gmail.com-20070509161011-o1j6jndkyc1lv9di

Revision ferringb%2540gmail.com-20070509161011-o1j6jndkyc1lv9di, 8.6 kB (checked in by Brian Harring <ferringb@…>, 20 months ago)

tweak the setattr slightly to do less work

Line 
1# Copyright: 2005-2006 Brian Harring <ferringb@gmail.com>
2# License: GPL2
3
4"""
5file related operations, mainly reading
6"""
7
8import re, os
9from shlex import shlex
10from snakeoil.mappings import ProtectedDict
11from snakeoil.osutils import readlines
12
13class AtomicWriteFile(file):
14
15    """File class that stores the changes in a tempfile.
16
17    Upon close call, uses rename to replace the destination.
18
19    Similar to file protocol behaviour, except for the C{__init__}, and
20    that close *must* be called for the changes to be made live,
21
22    if C{__del__} is triggered it's assumed that an exception occured,
23    thus the changes shouldn't be made live.
24    """
25    def __init__(self, fp, binary=False, **kwds):
26        self.is_finalized = False
27        if binary:
28            mode = "wb"
29        else:
30            mode = "w"
31        fp = os.path.realpath(fp)
32        self.original_fp = fp
33        self.temp_fp = os.path.join(
34            os.path.dirname(fp), ".update.%s" % os.path.basename(fp))
35        file.__init__(self, self.temp_fp, mode=mode, **kwds)
36
37    def close(self):
38        file.close(self)
39        os.rename(self.temp_fp, self.original_fp)
40        self.is_finalized = True
41
42    def __del__(self):
43        file.close(self)
44        if not self.is_finalized:
45            os.unlink(self.temp_fp)
46
47
48def iter_read_bash(bash_source):
49    """
50    Read file honoring bash commenting rules.
51
52    Note that it's considered good behaviour to close filehandles, as
53    such, either iterate fully through this, or use read_bash instead.
54    Once the file object is no longer referenced the handle will be
55    closed, but be proactive instead of relying on the garbage
56    collector.
57
58    @param bash_source: either a file to read from
59        or a string holding the filename to open.
60    """
61    if isinstance(bash_source, basestring):
62        bash_source = readlines(bash_source, True)
63    for s in bash_source:
64        s = s.strip()
65        if s and s[0] != "#":
66            yield s
67
68
69def read_bash(bash_source):
70    return list(iter_read_bash(bash_source))
71read_bash.__doc__ = iter_read_bash.__doc__
72
73
74def read_dict(bash_source, splitter="=", source_isiter=False):
75    """
76    read key value pairs, ignoring bash-style comments.
77
78    @param splitter: the string to split on.  Can be None to
79        default to str.split's default
80    @param bash_source: either a file to read from,
81        or a string holding the filename to open.
82    """
83    d = {}
84    if not source_isiter:
85        filename = bash_source
86        i = iter_read_bash(bash_source)
87    else:
88        # XXX what to do?
89        filename = '<unknown>'
90        i = bash_source
91    line_count = 1
92    try:
93        for k in i:
94            line_count += 1
95            try:
96                k, v = k.split(splitter, 1)
97            except ValueError:
98                raise ParseError(filename, line_count)
99            if len(v) > 2 and v[0] == v[-1] and v[0] in ("'", '"'):
100                v = v[1:-1]
101            d[k] = v
102    finally:
103        del i
104    return d
105
106def read_bash_dict(bash_source, vars_dict=None, sourcing_command=None):
107    """
108    read bash source, yielding a dict of vars
109
110    @param bash_source: either a file to read from
111        or a string holding the filename to open
112    @param vars_dict: initial 'env' for the sourcing.
113        Is protected from modification.
114    @type vars_dict: dict or None
115    @param sourcing_command: controls whether a source command exists.
116        If one does and is encountered, then this func is called.
117    @type sourcing_command: callable
118    @raise ParseError: thrown if invalid syntax is encountered.
119    @return: dict representing the resultant env if bash executed the source.
120    """
121
122    # quite possibly I'm missing something here, but the original
123    # portage_util getconfig/varexpand seemed like it only went
124    # halfway. The shlex posix mode *should* cover everything.
125
126    if vars_dict is not None:
127        d, protected = ProtectedDict(vars_dict), True
128    else:
129        d, protected = {}, False
130    if isinstance(bash_source, basestring):
131        f = open(bash_source, "r")
132    else:
133        f = bash_source
134    s = bash_parser(f, sourcing_command=sourcing_command, env=d)
135
136    try:
137        tok = ""
138        try:
139            while tok is not None:
140                key = s.get_token()
141                if key is None:
142                    break
143                elif key.isspace():
144                    # we specifically have to check this, since we're
145                    # screwing with the whitespace filters below to
146                    # detect empty assigns
147                    continue
148                eq = s.get_token()
149                if eq != '=':
150                    raise ParseError(bash_source, s.lineno)
151                val = s.get_token()
152                if val is None:
153                    val = ''
154                # look ahead to see if we just got an empty assign.
155                next_tok = s.get_token()
156                if next_tok == '=':
157                    # ... we did.
158                    # leftmost insertions, thus reversed ordering
159                    s.push_token(next_tok)
160                    s.push_token(val)
161                    val = ''
162                else:
163                    s.push_token(next_tok)
164                d[key] = val
165        except ValueError:
166            raise ParseError(bash_source, s.lineno)
167    finally:
168        del f
169    if protected:
170        d = d.new
171    return d
172
173
174var_find = re.compile(r'\\?(\${\w+}|\$\w+)')
175backslash_find = re.compile(r'\\.')
176def nuke_backslash(s):
177    s = s.group()
178    if s == "\\\n":
179        return "\n"
180    try:
181        return chr(ord(s))
182    except TypeError:
183        return s[1]
184
185class bash_parser(shlex):
186    def __init__(self, source, sourcing_command=None, env=None):
187        self.__dict__['state'] = ' '
188        shlex.__init__(self, source, posix=True)
189        self.wordchars += "@${}/.-+/:~^"
190        self.wordchars = frozenset(self.wordchars)
191        if sourcing_command is not None:
192            self.source = sourcing_command
193        if env is None:
194            env = {}
195        self.env = env
196        self.__pos = 0
197
198    def __setattr__(self, attr, val):
199        if attr == "state":
200            if (self.state, val) in (
201                ('"', 'a'), ('a', '"'), ('a', ' '), ("'", 'a')):
202                strl = len(self.token)
203                if self.__pos != strl:
204                    self.changed_state.append(
205                        (self.state, self.token[self.__pos:]))
206                self.__pos = strl
207        self.__dict__[attr] = val
208
209    def sourcehook(self, newfile):
210        try:
211            return shlex.sourcehook(self, newfile)
212        except IOError, ie:
213            raise ParseError(newfile, 0, str(ie))
214
215    def read_token(self):
216        self.changed_state = []
217        self.__pos = 0
218        tok = shlex.read_token(self)
219        if tok is None:
220            return tok
221        self.changed_state.append((self.state, self.token[self.__pos:]))
222        tok = ''
223        for s, t in self.changed_state:
224            if s in ('"', "a"):
225                tok += self.var_expand(t).replace("\\\n", '')
226            else:
227                tok += t
228        return tok
229
230    def var_expand(self, val):
231        prev, pos = 0, 0
232        l = []
233        match = var_find.search(val)
234        while match is not None:
235            pos = match.start()
236            if val[pos] == '\\':
237                # it's escaped. either it's \\$ or \\${ , either way,
238                # skipping two ahead handles it.
239                pos += 2
240            else:
241                var = val[match.start():match.end()].strip("${}")
242                if prev != pos:
243                    l.append(val[prev:pos])
244                if var in self.env:
245                    if not isinstance(self.env[var], basestring):
246                        raise ValueError(
247                            "env key %r must be a string, not %s: %r" % (
248                                var, type(self.env[var]), self.env[var]))
249                    l.append(self.env[var])
250                else:
251                    l.append("")
252                prev = pos = match.end()
253            match = var_find.search(val, pos)
254
255        # do \\ cleansing, collapsing val down also.
256        val = backslash_find.sub(nuke_backslash, ''.join(l) + val[prev:])
257        return val
258
259
260class ParseError(Exception):
261
262    def __init__(self, filename, line, errmsg=None):
263        if errmsg is not None:
264            Exception.__init__(self,
265                               "error parsing '%s' on or before %i: err %s" %
266                               (filename, line, errmsg))
267        else:
268            Exception.__init__(self,
269                               "error parsing '%s' on or before %i" %
270                               (filename, line))
271        self.file, self.line, self.errmsg = filename, line, errmsg
Note: See TracBrowser for help on using the browser.