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

89 statements  

1from collections.abc import Mapping 

2from typing import NamedTuple 

3 

4from .exceptions import ParseError 

5 

6COMMENTCHARS = "#;" 

7 

8 

9class ParsedLine(NamedTuple): 

10 lineno: int 

11 section: str | None 

12 name: str | None 

13 value: str | None 

14 

15 

16def parse_ini_data( 

17 path: str, 

18 data: str, 

19 *, 

20 strip_inline_comments: bool, 

21 strip_section_whitespace: bool = False, 

22) -> tuple[Mapping[str, Mapping[str, str]], Mapping[tuple[str, str | None], int]]: 

23 """Parse INI data and return sections and sources mappings. 

24 

25 Args: 

26 path: Path for error messages 

27 data: INI content as string 

28 strip_inline_comments: Whether to strip inline comments from values 

29 strip_section_whitespace: Whether to strip whitespace from section and key names 

30 (default: False). When True, addresses issue #4 by stripping Unicode whitespace. 

31 

32 Returns: 

33 Tuple of (sections_data, sources) where: 

34 - sections_data: mapping of section -> {name -> value} 

35 - sources: mapping of (section, name) -> line number 

36 """ 

37 tokens = parse_lines( 

38 path, 

39 data.splitlines(True), 

40 strip_inline_comments=strip_inline_comments, 

41 strip_section_whitespace=strip_section_whitespace, 

42 ) 

43 

44 sources: dict[tuple[str, str | None], int] = {} 

45 sections_data: dict[str, dict[str, str]] = {} 

46 

47 for lineno, section, name, value in tokens: 

48 if section is None: 

49 raise ParseError(path, lineno, "no section header defined") 

50 sources[section, name] = lineno 

51 if name is None: 

52 if section in sections_data: 

53 raise ParseError(path, lineno, f"duplicate section {section!r}") 

54 sections_data[section] = {} 

55 else: 

56 if name in sections_data[section]: 

57 raise ParseError(path, lineno, f"duplicate name {name!r}") 

58 assert value is not None 

59 sections_data[section][name] = value 

60 

61 return sections_data, sources 

62 

63 

64def parse_lines( 

65 path: str, 

66 line_iter: list[str], 

67 *, 

68 strip_inline_comments: bool = False, 

69 strip_section_whitespace: bool = False, 

70) -> list[ParsedLine]: 

71 result: list[ParsedLine] = [] 

72 section = None 

73 for lineno, line in enumerate(line_iter): 

74 name, data = _parseline( 

75 path, line, lineno, strip_inline_comments, strip_section_whitespace 

76 ) 

77 # new value 

78 if name is not None and data is not None: 

79 result.append(ParsedLine(lineno, section, name, data)) 

80 # new section 

81 elif name is not None and data is None: 

82 if not name: 

83 raise ParseError(path, lineno, "empty section name") 

84 section = name 

85 result.append(ParsedLine(lineno, section, None, None)) 

86 # continuation 

87 elif name is None and data is not None: 

88 if not result: 

89 raise ParseError(path, lineno, "unexpected value continuation") 

90 last = result.pop() 

91 if last.name is None: 

92 raise ParseError(path, lineno, "unexpected value continuation") 

93 

94 if last.value: 

95 last = last._replace(value=f"{last.value}\n{data}") 

96 else: 

97 last = last._replace(value=data) 

98 result.append(last) 

99 return result 

100 

101 

102def _parseline( 

103 path: str, 

104 line: str, 

105 lineno: int, 

106 strip_inline_comments: bool, 

107 strip_section_whitespace: bool, 

108) -> tuple[str | None, str | None]: 

109 # blank lines 

110 if iscommentline(line): 

111 line = "" 

112 else: 

113 line = line.rstrip() 

114 if not line: 

115 return None, None 

116 # section 

117 if line[0] == "[": 

118 realline = line 

119 for c in COMMENTCHARS: 

120 line = line.split(c)[0].rstrip() 

121 if line[-1] == "]": 

122 section_name = line[1:-1] 

123 # Optionally strip whitespace from section name (issue #4) 

124 if strip_section_whitespace: 

125 section_name = section_name.strip() 

126 return section_name, None 

127 return None, realline.strip() 

128 # value 

129 elif not line[0].isspace(): 

130 try: 

131 name, value = line.split("=", 1) 

132 if ":" in name: 

133 raise ValueError() 

134 except ValueError: 

135 try: 

136 name, value = line.split(":", 1) 

137 except ValueError: 

138 raise ParseError(path, lineno, f"unexpected line: {line!r}") from None 

139 

140 # Strip key name (always for backward compatibility, optionally with unicode awareness) 

141 key_name = name.strip() 

142 

143 # Strip value 

144 value = value.strip() 

145 # Strip inline comments from values if requested (issue #55) 

146 if strip_inline_comments: 

147 for c in COMMENTCHARS: 

148 value = value.split(c)[0].rstrip() 

149 

150 return key_name, value 

151 # continuation 

152 else: 

153 line = line.strip() 

154 # Strip inline comments from continuations if requested (issue #55) 

155 if strip_inline_comments: 

156 for c in COMMENTCHARS: 

157 line = line.split(c)[0].rstrip() 

158 return None, line 

159 

160 

161def iscommentline(line: str) -> bool: 

162 c = line.lstrip()[:1] 

163 return c in COMMENTCHARS