1 from __future__ import annotations

2 from .exceptions import ParseError

3

4 from typing import NamedTuple

5

6

7 COMMENTCHARS = "#;"

8

9

10 class _ParsedLine(NamedTuple):

11 lineno: int

12 section: str | None

13 name: str | None

14 value: str | None

15

16

17 def parse_lines(path: str, line_iter: list[str]) -> list[_ParsedLine]:

18 result: list[_ParsedLine] = []

19 section = None

20 for lineno, line in enumerate(line_iter):

21 name, data = _parseline(path, line, lineno)

22 # new value

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

24 result.append(_ParsedLine(lineno, section, name, data))

25 # new section

26 elif name is not None and data is None:

27 if not name:

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

29 section = name

30 result.append(_ParsedLine(lineno, section, None, None))

31 # continuation

32 elif name is None and data is not None:

33 if not result:

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

35 last = result.pop()

36 if last.name is None:

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

38

39 if last.value:

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

41 else:

42 last = last._replace(value=data)

43 result.append(last)

44 return result

45

46

47 def _parseline(path: str, line: str, lineno: int) -> tuple[str | None, str | None]:

48 # blank lines

49 if iscommentline(line):

50 line = ""

51 else:

52 line = line.rstrip()

53 if not line:

54 return None, None

55 # section

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

57 realline = line

58 for c in COMMENTCHARS:

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

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

61 return line[1:-1], None

62 return None, realline.strip()

63 # value

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

65 try:

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

67 if ":" in name:

68 raise ValueError()

69 except ValueError:

70 try:

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

72 except ValueError:

73 raise ParseError(path, lineno, "unexpected line: %r" % line)

74 return name.strip(), value.strip()

75 # continuation

76 else:

77 return None, line.strip()

78

79

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

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

82 return c in COMMENTCHARS