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

87 statements  

1from collections.abc import Mapping 

2from typing import Final 

3from typing import NamedTuple 

4 

5from .exceptions import ParseError 

6 

7COMMENTCHARS = "#;" 

8UTF8_BOM: Final = "\N{BYTE ORDER MARK}" 

9 

10 

11class ParsedLine(NamedTuple): 

12 lineno: int 

13 section: str | None 

14 name: str | None 

15 value: list[str] | None 

16 

17 

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. 

26 

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. 

33 

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) 

40 

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 ) 

47 

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

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

50 

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) 

64 

65 return sections_data, sources 

66 

67 

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 

97 

98 

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 

136 

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

138 key_name = name.strip() 

139 

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() 

146 

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 

156 

157 

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

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

160 return c in COMMENTCHARS