1
- import io
2
- import os
3
1
import sys
4
2
from configparser import Error
5
- from typing import Optional , TextIO , Tuple , Union , cast
3
+ from typing import Optional , TextIO , Tuple , TypeVar
6
4
7
5
if sys .version_info [:2 ] >= (3 , 9 ):
8
- from collections .abc import Iterable , MutableMapping
6
+ from collections .abc import Iterable
9
7
10
8
List = list
11
9
Dict = dict
12
10
else :
13
- from typing import Iterable , MutableMapping
11
+ from typing import Iterable
12
+
13
+ from .block import Comment , Space
14
+ from .document import Document
15
+ from .option import Option
16
+ from .parser import Parser
17
+ from .section import Section
18
+
19
+ __all__ = [
20
+ "ConfigUpdater" ,
21
+ "Section" ,
22
+ "Option" ,
23
+ "Comment" ,
24
+ "Space" ,
25
+ "Parser" ,
26
+ "NoConfigFileReadError" ,
27
+ ]
28
+
29
+ T = TypeVar ("T" , bound = "ConfigUpdater" )
14
30
15
31
16
32
class NoConfigFileReadError (Error ):
@@ -20,11 +36,8 @@ def __init__(self):
20
36
super ().__init__ ("No configuration file was yet read! Use .read(...) first." )
21
37
22
38
23
- ConfigContent = Union ["Section" , "Comment" , "Space" ]
24
-
25
-
26
- class ConfigUpdater (Container [ConfigContent ], MutableMapping [str , Section ]):
27
- """Parser for updating configuration files.
39
+ class ConfigUpdater (Document ):
40
+ """Tool to parse and modify existing ``cfg`` files.
28
41
29
42
ConfigUpdater follows the API of ConfigParser with some differences:
30
43
* inline comments are treated as part of a key's value,
@@ -52,20 +65,34 @@ def __init__(
52
65
empty_lines_in_values : bool = True ,
53
66
space_around_delimiters : bool = True ,
54
67
):
55
- pass
56
-
57
- def read (self , filename : str , encoding : Optional [str ] = None ):
68
+ self ._parser_opts = {
69
+ "allow_no_value" : allow_no_value ,
70
+ "delimiters" : delimiters ,
71
+ "comment_prefixes" : comment_prefixes ,
72
+ "inline_comment_prefixes" : inline_comment_prefixes ,
73
+ "strict" : strict ,
74
+ "empty_lines_in_values" : empty_lines_in_values ,
75
+ "space_around_delimiters" : space_around_delimiters ,
76
+ }
77
+ self ._filename : Optional [str ] = None
78
+ super ().__init__ ()
79
+
80
+ def _parser (self , ** kwargs ):
81
+ opts = {"optionxform" : self .optionxform , ** self ._parser_opts , ** kwargs }
82
+ return Parser (** opts )
83
+
84
+ def read (self : T , filename : str , encoding : Optional [str ] = None ) -> T :
58
85
"""Read and parse a filename.
59
86
60
87
Args:
61
88
filename (str): path to file
62
89
encoding (str): encoding of file, default None
63
90
"""
64
- with open ( filename , encoding = encoding ) as fp :
65
- self ._read ( fp , filename )
66
- self ._filename = os . path . abspath (filename )
91
+ self . remove_all ()
92
+ self ._filename = filename
93
+ return self ._parser (). read (filename , encoding , self )
67
94
68
- def read_file (self , f : Iterable [str ], source : Optional [str ] = None ):
95
+ def read_file (self : T , f : Iterable [str ], source : Optional [str ] = None ) -> T :
69
96
"""Like read() but the argument must be a file-like object.
70
97
71
98
The ``f`` argument must be iterable, returning one line at a time.
@@ -77,24 +104,20 @@ def read_file(self, f: Iterable[str], source: Optional[str] = None):
77
104
f: file like object
78
105
source (str): reference name for file object, default None
79
106
"""
80
- if isinstance (f , str ):
81
- raise RuntimeError ("f must be a file-like object, not string!" )
82
- if source is None :
83
- try :
84
- source = cast (str , cast (io .FileIO , f ).name )
85
- except AttributeError :
86
- source = "<???>"
87
- self ._read (f , source )
88
-
89
- def read_string (self , string : str , source = "<string>" ):
107
+ self .remove_all ()
108
+ if hasattr (f , "name" ):
109
+ self ._filename = f .name # type: ignore[attr-defined]
110
+ return self ._parser ().read_file (f , source , self )
111
+
112
+ def read_string (self : T , string : str , source = "<string>" ) -> T :
90
113
"""Read configuration from a given string.
91
114
92
115
Args:
93
116
string (str): string containing a configuration
94
117
source (str): reference name for file object, default '<string>'
95
118
"""
96
- sfile = io . StringIO ( string )
97
- self .read_file ( sfile , source )
119
+ self . remove_all ( )
120
+ return self ._parser (). read_string ( string , source , self )
98
121
99
122
def optionxform (self , optionstr ) -> str :
100
123
"""Converts an option key to lower case for unification
@@ -116,10 +139,10 @@ def write(self, fp: TextIO, validate: bool = True):
116
139
validate (Boolean): validate format before writing
117
140
"""
118
141
if validate :
119
- self .validate_format ()
142
+ self .validate_format (** self . _parser_opts )
120
143
fp .write (str (self ))
121
144
122
- def update_file (self , validate : bool = True ):
145
+ def update_file (self : T , validate : bool = True ) -> T :
123
146
"""Update the read-in configuration file.
124
147
125
148
Args:
@@ -128,6 +151,7 @@ def update_file(self, validate: bool = True):
128
151
if self ._filename is None :
129
152
raise NoConfigFileReadError ()
130
153
if validate : # validate BEFORE opening the file!
131
- self .validate_format ()
154
+ self .validate_format (** self . _parser_opts )
132
155
with open (self ._filename , "w" ) as fb :
133
156
self .write (fb , validate = False )
157
+ return self
0 commit comments