Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/iniconfig/_parse.py: 94%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from collections.abc import Mapping
2from typing import Final
3from typing import NamedTuple
5from .exceptions import ParseError
7COMMENTCHARS = "#;"
8UTF8_BOM: Final = "\N{BYTE ORDER MARK}"
11class ParsedLine(NamedTuple):
12 lineno: int
13 section: str | None
14 name: str | None
15 value: list[str] | None
18def parse_ini_data(
19 path: str,
20 data: str,
21 *,
22 strip_inline_comments: bool,
23 strip_section_whitespace: bool = False,
24) -> tuple[Mapping[str, Mapping[str, str]], Mapping[tuple[str, str | None], int]]:
25 """Parse INI data and return sections and sources mappings.
27 Args:
28 path: Path for error messages
29 data: INI content as string
30 strip_inline_comments: Whether to strip inline comments from values
31 strip_section_whitespace: Whether to strip whitespace from section and key names
32 (default: False). When True, addresses issue #4 by stripping Unicode whitespace.
34 Returns:
35 Tuple of (sections_data, sources) where:
36 - sections_data: mapping of section -> {name -> value}
37 - sources: mapping of (section, name) -> line number
38 """
39 data = data.removeprefix(UTF8_BOM)
41 tokens = parse_lines(
42 path,
43 data.splitlines(True),
44 strip_inline_comments=strip_inline_comments,
45 strip_section_whitespace=strip_section_whitespace,
46 )
48 sources: dict[tuple[str, str | None], int] = {}
49 sections_data: dict[str, dict[str, str]] = {}
51 for lineno, section, name, value in tokens:
52 if section is None:
53 raise ParseError(path, lineno, "no section header defined")
54 sources[section, name] = lineno
55 if name is None:
56 if section in sections_data:
57 raise ParseError(path, lineno, f"duplicate section {section!r}")
58 sections_data[section] = {}
59 else:
60 if name in sections_data[section]:
61 raise ParseError(path, lineno, f"duplicate name {name!r}")
62 assert value is not None
63 sections_data[section][name] = "\n".join(value)
65 return sections_data, sources
68def parse_lines(
69 path: str,
70 line_iter: list[str],
71 *,
72 strip_inline_comments: bool = False,
73 strip_section_whitespace: bool = False,
74) -> list[ParsedLine]:
75 result: list[ParsedLine] = []
76 section = None
77 for lineno, line in enumerate(line_iter):
78 name, data = _parseline(
79 path, line, lineno, strip_inline_comments, strip_section_whitespace
80 )
81 # new value
82 if name is not None and data is not None:
83 result.append(ParsedLine(lineno, section, name, [data] if data else []))
84 # new section
85 elif name is not None and data is None:
86 if not name:
87 raise ParseError(path, lineno, "empty section name")
88 section = name
89 result.append(ParsedLine(lineno, section, None, None))
90 # continuation
91 elif name is None and data is not None:
92 value = result[-1].value if result else None
93 if value is None:
94 raise ParseError(path, lineno, "unexpected value continuation")
95 value.append(data)
96 return result
99def _parseline(
100 path: str,
101 line: str,
102 lineno: int,
103 strip_inline_comments: bool,
104 strip_section_whitespace: bool,
105) -> tuple[str | None, str | None]:
106 # blank lines
107 if iscommentline(line):
108 line = ""
109 else:
110 line = line.rstrip()
111 if not line:
112 return None, None
113 # section
114 if line[0] == "[":
115 realline = line
116 for c in COMMENTCHARS:
117 line = line.split(c)[0].rstrip()
118 if line[-1] == "]":
119 section_name = line[1:-1]
120 # Optionally strip whitespace from section name (issue #4)
121 if strip_section_whitespace:
122 section_name = section_name.strip()
123 return section_name, None
124 return None, realline.strip()
125 # value
126 elif not line[0].isspace():
127 try:
128 name, value = line.split("=", 1)
129 if ":" in name:
130 raise ValueError()
131 except ValueError:
132 try:
133 name, value = line.split(":", 1)
134 except ValueError:
135 raise ParseError(path, lineno, f"unexpected line: {line!r}") from None
137 # Strip key name (always for backward compatibility, optionally with unicode awareness)
138 key_name = name.strip()
140 # Strip value
141 value = value.strip()
142 # Strip inline comments from values if requested (issue #55)
143 if strip_inline_comments:
144 for c in COMMENTCHARS:
145 value = value.split(c)[0].rstrip()
147 return key_name, value
148 # continuation
149 else:
150 line = line.strip()
151 # Strip inline comments from continuations if requested (issue #55)
152 if strip_inline_comments:
153 for c in COMMENTCHARS:
154 line = line.split(c)[0].rstrip()
155 return None, line
158def iscommentline(line: str) -> bool:
159 c = line.lstrip()[:1]
160 return c in COMMENTCHARS