|
1 """EditorConfig file parser |
|
2 |
|
3 Based on code from ConfigParser.py file distributed with Python 2.6. |
|
4 |
|
5 Licensed under PSF License (see LICENSE.txt file). |
|
6 |
|
7 Changes to original ConfigParser: |
|
8 |
|
9 - Special characters can be used in section names |
|
10 - Octothorpe can be used for comments (not just at beginning of line) |
|
11 - Only track INI options in sections that match target filename |
|
12 - Stop parsing files with when ``root = true`` is found |
|
13 |
|
14 """ |
|
15 |
|
16 import re |
|
17 from codecs import open |
|
18 import posixpath |
|
19 from os import sep |
|
20 from os.path import normpath, dirname |
|
21 |
|
22 from editorconfig.exceptions import ParsingError |
|
23 from editorconfig.fnmatch import fnmatch |
|
24 from editorconfig.odict import OrderedDict |
|
25 from editorconfig.compat import u |
|
26 |
|
27 |
|
28 __all__ = ["ParsingError", "EditorConfigParser"] |
|
29 |
|
30 |
|
31 class EditorConfigParser(object): |
|
32 |
|
33 """Parser for EditorConfig-style configuration files |
|
34 |
|
35 Based on RawConfigParser from ConfigParser.py in Python 2.6. |
|
36 """ |
|
37 |
|
38 # Regular expressions for parsing section headers and options. |
|
39 # Allow ``]`` and escaped ``;`` and ``#`` characters in section headers |
|
40 SECTCRE = re.compile( |
|
41 r""" |
|
42 |
|
43 \s * # Optional whitespace |
|
44 \[ # Opening square brace |
|
45 |
|
46 (?P<header> # One or more characters excluding |
|
47 ( [^\#;] | \\\# | \\; ) + # unescaped # and ; characters |
|
48 ) |
|
49 |
|
50 \] # Closing square brace |
|
51 |
|
52 """, re.VERBOSE |
|
53 ) |
|
54 # Regular expression for parsing option name/values. |
|
55 # Allow any amount of whitespaces, followed by separator |
|
56 # (either ``:`` or ``=``), followed by any amount of whitespace and then |
|
57 # any characters to eol |
|
58 OPTCRE = re.compile( |
|
59 r""" |
|
60 |
|
61 \s * # Optional whitespace |
|
62 (?P<option> # One or more characters excluding |
|
63 [^:=\s] # : a = characters (and first |
|
64 [^:=] * # must not be whitespace) |
|
65 ) |
|
66 \s * # Optional whitespace |
|
67 (?P<vi> |
|
68 [:=] # Single = or : character |
|
69 ) |
|
70 \s * # Optional whitespace |
|
71 (?P<value> |
|
72 . * # One or more characters |
|
73 ) |
|
74 $ |
|
75 |
|
76 """, re.VERBOSE |
|
77 ) |
|
78 |
|
79 def __init__(self, filename): |
|
80 self.filename = filename |
|
81 self.options = OrderedDict() |
|
82 self.root_file = False |
|
83 |
|
84 def matches_filename(self, config_filename, glob): |
|
85 """Return True if section glob matches filename""" |
|
86 config_dirname = normpath(dirname(config_filename)).replace(sep, '/') |
|
87 glob = glob.replace("\\#", "#") |
|
88 glob = glob.replace("\\;", ";") |
|
89 if '/' in glob: |
|
90 if glob.find('/') == 0: |
|
91 glob = glob[1:] |
|
92 glob = posixpath.join(config_dirname, glob) |
|
93 else: |
|
94 glob = posixpath.join('**/', glob) |
|
95 return fnmatch(self.filename, glob) |
|
96 |
|
97 def read(self, filename): |
|
98 """Read and parse single EditorConfig file""" |
|
99 try: |
|
100 fp = open(filename, encoding='utf-8') |
|
101 except IOError: |
|
102 return |
|
103 self._read(fp, filename) |
|
104 fp.close() |
|
105 |
|
106 def _read(self, fp, fpname): |
|
107 """Parse a sectioned setup file. |
|
108 |
|
109 The sections in setup file contains a title line at the top, |
|
110 indicated by a name in square brackets (`[]'), plus key/value |
|
111 options lines, indicated by `name: value' format lines. |
|
112 Continuations are represented by an embedded newline then |
|
113 leading whitespace. Blank lines, lines beginning with a '#', |
|
114 and just about everything else are ignored. |
|
115 """ |
|
116 in_section = False |
|
117 matching_section = False |
|
118 optname = None |
|
119 lineno = 0 |
|
120 e = None # None, or an exception |
|
121 while True: |
|
122 line = fp.readline() |
|
123 if not line: |
|
124 break |
|
125 if lineno == 0 and line.startswith(u('\ufeff')): |
|
126 line = line[1:] # Strip UTF-8 BOM |
|
127 lineno = lineno + 1 |
|
128 # comment or blank line? |
|
129 if line.strip() == '' or line[0] in '#;': |
|
130 continue |
|
131 # a section header or option header? |
|
132 else: |
|
133 # is it a section header? |
|
134 mo = self.SECTCRE.match(line) |
|
135 if mo: |
|
136 sectname = mo.group('header') |
|
137 in_section = True |
|
138 matching_section = self.matches_filename(fpname, sectname) |
|
139 # So sections can't start with a continuation line |
|
140 optname = None |
|
141 # an option line? |
|
142 else: |
|
143 mo = self.OPTCRE.match(line) |
|
144 if mo: |
|
145 optname, vi, optval = mo.group('option', 'vi', 'value') |
|
146 if ';' in optval or '#' in optval: |
|
147 # ';' and '#' are comment delimiters only if |
|
148 # preceeded by a spacing character |
|
149 m = re.search('(.*?) [;#]', optval) |
|
150 if m: |
|
151 optval = m.group(1) |
|
152 optval = optval.strip() |
|
153 # allow empty values |
|
154 if optval == '""': |
|
155 optval = '' |
|
156 optname = self.optionxform(optname.rstrip()) |
|
157 if not in_section and optname == 'root': |
|
158 self.root_file = (optval.lower() == 'true') |
|
159 if matching_section: |
|
160 self.options[optname] = optval |
|
161 else: |
|
162 # a non-fatal parsing error occurred. set up the |
|
163 # exception but keep going. the exception will be |
|
164 # raised at the end of the file and will contain a |
|
165 # list of all bogus lines |
|
166 if not e: |
|
167 e = ParsingError(fpname) |
|
168 e.append(lineno, repr(line)) |
|
169 # if any parsing errors occurred, raise an exception |
|
170 if e: |
|
171 raise e |
|
172 |
|
173 def optionxform(self, optionstr): |
|
174 return optionstr.lower() |