Skip to content

Commit 03b7262

Browse files
move parsing to ownfile
1 parent 95fc7f9 commit 03b7262

4 files changed

Lines changed: 110 additions & 100 deletions

File tree

src/iniconfig/__init__.py

Lines changed: 7 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,16 @@
2121
if TYPE_CHECKING:
2222
from typing_extensions import Final
2323

24-
__all__ = ["IniConfig", "ParseError"]
24+
__all__ = ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"]
2525

2626
from .exceptions import ParseError
27-
28-
COMMENTCHARS = "#;"
27+
from . import _parse
28+
from ._parse import COMMENTCHARS, iscommentline
2929

3030
_D = TypeVar("_D")
3131
_T = TypeVar("_T")
3232

3333

34-
_str_default = cast(Callable[[str], str], str)
35-
36-
37-
class _ParsedLine(NamedTuple):
38-
lineno: int
39-
section: str | None
40-
name: str | None
41-
value: str | None
42-
43-
4434
class SectionWrapper:
4535
config: Final[IniConfig]
4636
name: Final[str]
@@ -88,90 +78,26 @@ def __init__(
8878
with open(self.path, encoding=encoding) as fp:
8979
data = fp.read()
9080

91-
tokens = self._parse(data.splitlines(True))
81+
tokens = _parse.parse_lines(path, data.splitlines(True))
9282

9383
self._sources = {}
9484
sections_data: dict[str, dict[str, str]]
9585
self.sections = sections_data = {}
9686

9787
for lineno, section, name, value in tokens:
9888
if section is None:
99-
self._raise(lineno, "no section header defined")
89+
raise ParseError(path, lineno, "no section header defined")
10090
self._sources[section, name] = lineno
10191
if name is None:
10292
if section in self.sections:
103-
self._raise(lineno, f"duplicate section {section!r}")
93+
raise ParseError(path, lineno, f"duplicate section {section!r}")
10494
sections_data[section] = {}
10595
else:
10696
if name in self.sections[section]:
107-
self._raise(lineno, f"duplicate name {name!r}")
97+
raise ParseError(path, lineno, f"duplicate name {name!r}")
10898
assert value is not None
10999
sections_data[section][name] = value
110100

111-
def _raise(self, lineno: int, msg: str) -> NoReturn:
112-
raise ParseError(self.path, lineno, msg)
113-
114-
def _parse(self, line_iter: list[str]) -> list[_ParsedLine]:
115-
result: list[_ParsedLine] = []
116-
section = None
117-
for lineno, line in enumerate(line_iter):
118-
name, data = self._parseline(line, lineno)
119-
# new value
120-
if name is not None and data is not None:
121-
result.append(_ParsedLine(lineno, section, name, data))
122-
# new section
123-
elif name is not None and data is None:
124-
if not name:
125-
self._raise(lineno, "empty section name")
126-
section = name
127-
result.append(_ParsedLine(lineno, section, None, None))
128-
# continuation
129-
elif name is None and data is not None:
130-
if not result:
131-
self._raise(lineno, "unexpected value continuation")
132-
last = result.pop()
133-
if last.name is None:
134-
self._raise(lineno, "unexpected value continuation")
135-
136-
if last.value:
137-
last = last._replace(value=f"{last.value}\n{data}")
138-
else:
139-
last = last._replace(value=data)
140-
result.append(last)
141-
return result
142-
143-
def _parseline(self, line: str, lineno: int) -> tuple[str | None, str | None]:
144-
# blank lines
145-
if iscommentline(line):
146-
line = ""
147-
else:
148-
line = line.rstrip()
149-
if not line:
150-
return None, None
151-
# section
152-
if line[0] == "[":
153-
realline = line
154-
for c in COMMENTCHARS:
155-
line = line.split(c)[0].rstrip()
156-
if line[-1] == "]":
157-
return line[1:-1], None
158-
return None, realline.strip()
159-
# value
160-
elif not line[0].isspace():
161-
try:
162-
name, value = line.split("=", 1)
163-
if ":" in name:
164-
raise ValueError()
165-
except ValueError:
166-
try:
167-
name, value = line.split(":", 1)
168-
except ValueError:
169-
self._raise(lineno, "unexpected line: %r" % line)
170-
return name.strip(), value.strip()
171-
# continuation
172-
else:
173-
return None, line.strip()
174-
175101
def lineof(self, section: str, name: str | None = None) -> int | None:
176102
lineno = self._sources.get((section, name))
177103
return None if lineno is None else lineno + 1
@@ -247,8 +173,3 @@ def __iter__(self) -> Iterator[SectionWrapper]:
247173

248174
def __contains__(self, arg: str) -> bool:
249175
return arg in self.sections
250-
251-
252-
def iscommentline(line: str) -> bool:
253-
c = line.lstrip()[:1]
254-
return c in COMMENTCHARS

src/iniconfig/_parse.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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

src/iniconfig/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
21
from __future__ import annotations
32
from typing import TYPE_CHECKING
43

54
if TYPE_CHECKING:
65
from typing_extensions import Final
76

7+
88
class ParseError(Exception):
99
path: Final[str]
1010
lineno: Final[int]
@@ -17,4 +17,4 @@ def __init__(self, path: str, lineno: int, msg: str):
1717
self.msg = msg
1818

1919
def __str__(self) -> str:
20-
return f"{self.path}:{self.lineno + 1}: {self.msg}"
20+
return f"{self.path}:{self.lineno + 1}: {self.msg}"

testing/test_iniconfig.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22
import pytest
3-
from iniconfig import IniConfig, ParseError, __all__ as ALL, _ParsedLine as PL
3+
from iniconfig import IniConfig, ParseError, __all__ as ALL
4+
from iniconfig._parse import _ParsedLine as PL
45
from iniconfig import iscommentline
56
from textwrap import dedent
67
from pathlib import Path
@@ -58,12 +59,18 @@ def expected(input_expected: tuple[str, list[PL]]) -> list[PL]:
5859

5960

6061
def parse(input: str) -> list[PL]:
61-
ini = IniConfig("sample", data="")
62-
return ini._parse(input.splitlines(True))
62+
from iniconfig._parse import parse_lines
6363

64+
return parse_lines("sample", input.splitlines(True))
6465

65-
def parse_a_error(input: str) -> pytest.ExceptionInfo[ParseError]:
66-
return pytest.raises(ParseError, parse, input)
66+
67+
def parse_a_error(input: str) -> ParseError:
68+
try:
69+
parse(input)
70+
except ParseError as e:
71+
return e
72+
else:
73+
raise ValueError(input)
6774

6875

6976
def test_tokenize(input: str, expected: list[PL]) -> None:
@@ -84,18 +91,18 @@ def test_ParseError() -> None:
8491

8592

8693
def test_continuation_needs_perceeding_token() -> None:
87-
excinfo = parse_a_error(" Foo")
88-
assert excinfo.value.lineno == 0
94+
err = parse_a_error(" Foo")
95+
assert err.lineno == 0
8996

9097

9198
def test_continuation_cant_be_after_section() -> None:
92-
excinfo = parse_a_error("[section]\n Foo")
93-
assert excinfo.value.lineno == 1
99+
err = parse_a_error("[section]\n Foo")
100+
assert err.lineno == 1
94101

95102

96103
def test_section_cant_be_empty() -> None:
97-
excinfo = parse_a_error("[]")
98-
assert excinfo.value.lineno == 0
104+
err = parse_a_error("[]")
105+
assert err.lineno == 0
99106

100107

101108
@pytest.mark.parametrize(
@@ -282,7 +289,7 @@ def test_example_pypirc() -> None:
282289

283290

284291
def test_api_import() -> None:
285-
assert ALL == ["IniConfig", "ParseError"]
292+
assert ALL == ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"]
286293

287294

288295
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)