Skip to content

Commit f1d8228

Browse files
inline type hints
1 parent 6bc5528 commit f1d8228

7 files changed

Lines changed: 212 additions & 139 deletions

File tree

.pre-commit-config.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/asottile/pyupgrade
3-
rev: v3.2.0
3+
rev: v3.3.1
44
hooks:
55
- id: pyupgrade
66
args: [--py37-plus]
@@ -14,3 +14,9 @@ repos:
1414
hooks:
1515
- id: black
1616
language_version: python3
17+
- repo: https://github.com/pre-commit/mirrors-mypy
18+
rev: 'v0.991'
19+
hooks:
20+
- id: mypy
21+
additional_dependencies:
22+
- "pytest==7.2.0"

MANIFEST.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@ include LICENSE
22
include example.ini
33
include tox.ini
44
include src/iniconfig/py.typed
5-
recursive-include src *.pyi

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ requires = ["setuptools>=41.2.0", "wheel", "setuptools_scm>3"]
33

44
build-backend = "setuptools.build_meta"
55

6-
[tool.setuptools_scm]
6+
[tool.setuptools_scm]
7+
8+
[tool.mypy]
9+
strict = true

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import setuptools_scm # noqa
55

66

7-
def local_scheme(version):
7+
def local_scheme(version: object) -> str:
88
"""Skip the local version (eg. +xyz of 0.6.1.dev4+gdf99fe2)
99
to be able to upload to Test PyPI"""
1010
return ""

src/iniconfig/__init__.py

Lines changed: 146 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,111 @@
11
""" brain-dead simple parser for ini-style files.
22
(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed
33
"""
4+
from __future__ import annotations
5+
from typing import (
6+
Callable,
7+
Iterator,
8+
Mapping,
9+
Optional,
10+
Tuple,
11+
TypeVar,
12+
Union,
13+
TYPE_CHECKING,
14+
NoReturn,
15+
NamedTuple,
16+
overload,
17+
cast,
18+
)
19+
20+
21+
if TYPE_CHECKING:
22+
from typing_extensions import Final
23+
424
__all__ = ["IniConfig", "ParseError"]
525

626
COMMENTCHARS = "#;"
727

28+
_D = TypeVar("_D")
29+
_T = TypeVar("_T")
30+
31+
32+
_str_default = cast(Callable[[str], str], str)
33+
34+
35+
class _ParsedLine(NamedTuple):
36+
lineno: int
37+
section: str | None
38+
name: str | None
39+
value: str | None
40+
841

942
class ParseError(Exception):
10-
def __init__(self, path, lineno, msg):
43+
path: Final[str]
44+
lineno: Final[int]
45+
msg: Final[str]
46+
47+
def __init__(self, path: str, lineno: int, msg: str):
1148
Exception.__init__(self, path, lineno, msg)
1249
self.path = path
1350
self.lineno = lineno
1451
self.msg = msg
1552

16-
def __str__(self):
53+
def __str__(self) -> str:
1754
return f"{self.path}:{self.lineno + 1}: {self.msg}"
1855

1956

2057
class SectionWrapper:
21-
def __init__(self, config, name):
58+
config: Final[IniConfig]
59+
name: Final[str]
60+
61+
def __init__(self, config: IniConfig, name: str):
2262
self.config = config
2363
self.name = name
2464

25-
def lineof(self, name):
65+
def lineof(self, name: str) -> int | None:
2666
return self.config.lineof(self.name, name)
2767

28-
def get(self, key, default=None, convert=str):
68+
def get(
69+
self,
70+
key: str,
71+
default: _D | None = None,
72+
convert: Callable[[str], _T] | None = None,
73+
) -> _D | _T | str | None:
2974
return self.config.get(self.name, key, convert=convert, default=default)
3075

31-
def __getitem__(self, key):
76+
def __getitem__(self, key: str) -> str:
3277
return self.config.sections[self.name][key]
3378

34-
def __iter__(self):
35-
section = self.config.sections.get(self.name, [])
79+
def __iter__(self) -> Iterator[str]:
80+
section: Mapping[str, str] = self.config.sections.get(self.name, {})
3681

37-
def lineof(key):
38-
return self.config.lineof(self.name, key)
82+
def lineof(key: str) -> int:
83+
return self.config.lineof(self.name, key) # type: ignore
3984

4085
yield from sorted(section, key=lineof)
4186

42-
def items(self):
87+
def items(self) -> Iterator[tuple[str, str]]:
4388
for name in self:
4489
yield name, self[name]
4590

4691

4792
class IniConfig:
48-
def __init__(self, path, data=None):
93+
path: Final[str]
94+
sections: Final[Mapping[str, Mapping[str, str]]]
95+
96+
def __init__(
97+
self, path: str, data: str | None = None, encoding: str = "utf-8"
98+
) -> None:
4999
self.path = str(path) # convenience
50100
if data is None:
51-
f = open(self.path)
52-
try:
53-
tokens = self._parse(iter(f))
54-
finally:
55-
f.close()
56-
else:
57-
tokens = self._parse(data.splitlines(True))
101+
with open(self.path, encoding=encoding) as fp:
102+
data = fp.read()
103+
104+
tokens = self._parse(data.splitlines(True))
58105

59106
self._sources = {}
60-
self.sections = {}
107+
sections_data: dict[str, dict[str, str]]
108+
self.sections = sections_data = {}
61109

62110
for lineno, section, name, value in tokens:
63111
if section is None:
@@ -66,44 +114,46 @@ def __init__(self, path, data=None):
66114
if name is None:
67115
if section in self.sections:
68116
self._raise(lineno, f"duplicate section {section!r}")
69-
self.sections[section] = {}
117+
sections_data[section] = {}
70118
else:
71119
if name in self.sections[section]:
72120
self._raise(lineno, f"duplicate name {name!r}")
73-
self.sections[section][name] = value
121+
assert value is not None
122+
sections_data[section][name] = value
74123

75-
def _raise(self, lineno, msg):
124+
def _raise(self, lineno: int, msg: str) -> NoReturn:
76125
raise ParseError(self.path, lineno, msg)
77126

78-
def _parse(self, line_iter):
79-
result = []
127+
def _parse(self, line_iter: list[str]) -> list[_ParsedLine]:
128+
result: list[_ParsedLine] = []
80129
section = None
81130
for lineno, line in enumerate(line_iter):
82131
name, data = self._parseline(line, lineno)
83132
# new value
84133
if name is not None and data is not None:
85-
result.append((lineno, section, name, data))
134+
result.append(_ParsedLine(lineno, section, name, data))
86135
# new section
87136
elif name is not None and data is None:
88137
if not name:
89138
self._raise(lineno, "empty section name")
90139
section = name
91-
result.append((lineno, section, None, None))
140+
result.append(_ParsedLine(lineno, section, None, None))
92141
# continuation
93142
elif name is None and data is not None:
94143
if not result:
95144
self._raise(lineno, "unexpected value continuation")
96145
last = result.pop()
97-
last_name, last_data = last[-2:]
98-
if last_name is None:
146+
if last.name is None:
99147
self._raise(lineno, "unexpected value continuation")
100148

101-
if last_data:
102-
data = f"{last_data}\n{data}"
103-
result.append(last[:-1] + (data,))
149+
if last.value:
150+
last = last._replace(value=f"{last.value}\n{data}")
151+
else:
152+
last = last._replace(value=data)
153+
result.append(last)
104154
return result
105155

106-
def _parseline(self, line, lineno):
156+
def _parseline(self, line: str, lineno: int) -> tuple[str | None, str | None]:
107157
# blank lines
108158
if iscommentline(line):
109159
line = ""
@@ -135,30 +185,83 @@ def _parseline(self, line, lineno):
135185
else:
136186
return None, line.strip()
137187

138-
def lineof(self, section, name=None):
188+
def lineof(self, section: str, name: str | None = None) -> int | None:
139189
lineno = self._sources.get((section, name))
140-
if lineno is not None:
141-
return lineno + 1
190+
return None if lineno is None else lineno + 1
191+
192+
@overload
193+
def get(
194+
self,
195+
section: str,
196+
name: str,
197+
) -> str | None:
198+
...
199+
200+
@overload
201+
def get(
202+
self,
203+
section: str,
204+
name: str,
205+
convert: Callable[[str], _T],
206+
) -> _T | None:
207+
...
142208

143-
def get(self, section, name, default=None, convert=str):
209+
@overload
210+
def get(
211+
self,
212+
section: str,
213+
name: str,
214+
default: None,
215+
convert: Callable[[str], _T],
216+
) -> _T | None:
217+
...
218+
219+
@overload
220+
def get(
221+
self, section: str, name: str, default: _D, convert: None = None
222+
) -> str | _D:
223+
...
224+
225+
@overload
226+
def get(
227+
self,
228+
section: str,
229+
name: str,
230+
default: _D,
231+
convert: Callable[[str], _T],
232+
) -> _T | _D:
233+
...
234+
235+
def get( # type: ignore
236+
self,
237+
section: str,
238+
name: str,
239+
default: _D | None = None,
240+
convert: Callable[[str], _T] | None = None,
241+
) -> _D | _T | str | None:
144242
try:
145-
return convert(self.sections[section][name])
243+
value: str = self.sections[section][name]
146244
except KeyError:
147245
return default
246+
else:
247+
if convert is not None:
248+
return convert(value)
249+
else:
250+
return value
148251

149-
def __getitem__(self, name):
252+
def __getitem__(self, name: str) -> SectionWrapper:
150253
if name not in self.sections:
151254
raise KeyError(name)
152255
return SectionWrapper(self, name)
153256

154-
def __iter__(self):
155-
for name in sorted(self.sections, key=self.lineof):
257+
def __iter__(self) -> Iterator[SectionWrapper]:
258+
for name in sorted(self.sections, key=self.lineof): # type: ignore
156259
yield SectionWrapper(self, name)
157260

158-
def __contains__(self, arg):
261+
def __contains__(self, arg: str) -> bool:
159262
return arg in self.sections
160263

161264

162-
def iscommentline(line):
265+
def iscommentline(line: str) -> bool:
163266
c = line.lstrip()[:1]
164267
return c in COMMENTCHARS

src/iniconfig/__init__.pyi

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)