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
626COMMENTCHARS = "#;"
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
942class 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
2057class 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
4792class 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
0 commit comments