Version 1.0
Copyright © 2023 Lowell D. Thomas
python-ini
 … powered by Python APG
ini_writer.py
Go to the documentation of this file.
1 ''' @file python_ini/ini_writer.py
2 @brief The IniWriter class for creating and writing an INI file.
3 
4 The writer is configurable to specify optional delimiters, boolean values
5 and tab space for inline comments.
6 '''
7 
8 
9 class IniWriter:
10 
11  def __init__(self):
12  '''Ini file writer constructor.
13  Note that configurable values are set to defaults.
14  - True = 'true'
15  - False = 'false'
16  - None = 'none'
17  - ; for comment delimiter
18  - = for key/value delimiter
19  - , for value list delimiter
20  - column 40 for tab to inline comments
21  '''
22 
23  self.__SECTION__SECTION = 0
24  self.__KEY__KEY = 1
25  self.__COMMENT__COMMENT = 2
26  self.__TRUE__TRUE = 'true'
27  self.__FALSE__FALSE = 'false'
28  self.__NONE__NONE = 'none'
29  self.__COMMENT_DELIM_SEMI__COMMENT_DELIM_SEMI = '; '
30  self.__COMMENT_DELIM_HASH__COMMENT_DELIM_HASH = '# '
31  self.__COMMENT_TAB__COMMENT_TAB = 40
32  self.__LINE_END__LINE_END = '\n'
33  self.__KEY_DELIM_EQUALS__KEY_DELIM_EQUALS = ' = '
34  self.__KEY_DELIM_COLON__KEY_DELIM_COLON = ': '
35  self.__KEY_DELIM_SPACE__KEY_DELIM_SPACE = ' '
36  self.__VALUE_DELIM_COMMA__VALUE_DELIM_COMMA = ', '
37  self.__VALUE_DELIM_SPACE__VALUE_DELIM_SPACE = ' '
38  self.__NAME_CHARS__NAME_CHARS = [33, 36, 37, 38, 40, 41, 42, 43, 45,
39  46, 60, 62, 63, 64, 94, 95, 123, 124, 125, 126]
40  self.clearclear()
41 
42  def clear(self):
43  '''Initialize or reset the writer to its constructor defaults.'''
44  self.errorserrors = []
45  self.__lines__lines = []
46  self.__comment_delim__comment_delim = self.__COMMENT_DELIM_SEMI__COMMENT_DELIM_SEMI
47  self.__key_delim__key_delim = self.__KEY_DELIM_EQUALS__KEY_DELIM_EQUALS
48  self.__value_delim__value_delim = self.__VALUE_DELIM_COMMA__VALUE_DELIM_COMMA
49  self.__comment_tab__comment_tab = self.__COMMENT_TAB__COMMENT_TAB
50  self.__true__true = self.__TRUE__TRUE
51  self.__false__false = self.__FALSE__FALSE
52  self.__none__none = self.__NONE__NONE
53 
54  def __validate_name(self, name):
55  if(not isinstance(name, str)):
56  return False
57  for s in name:
58  c = ord(s)
59  if(c >= 48 and c <= 57):
60  continue
61  if(c >= 65 and c <= 90):
62  continue
63  if(c >= 97 and c <= 122):
64  continue
65  if(c in self.__NAME_CHARS__NAME_CHARS):
66  continue
67 
68  # invalid character in name
69  return False
70  return True
71 
72  def __normalize_comment(self, comment):
73  if(comment is None):
74  return None
75  if(not isinstance(comment, str)):
76  raise Exception(
77  'IniWriter: invalid comment, must be string of ASCII characters 32-126', comment)
78  elif(comment == '' or comment.isspace()):
79  return ''
80  for s in comment:
81  c = ord(s)
82  if(c >= 32 and c <= 126):
83  continue
84  raise Exception(
85  'IniWriter: invalid character in comment - char code(' + str(c) + ')')
86  return comment
87 
88  def __normalize_string(self, string):
89  v = ''
90  for s in string:
91  c = ord(s)
92  if(c >= 32 and c <= 126):
93  v += s
94  elif(c == 9):
95  v += '\\t'
96  elif(c == 10):
97  v += '\\n'
98  elif(c == 13):
99  v += '\\r'
100  elif(c == 39):
101  v += "\\'"
102  elif(c <= 0xff):
103  h = str(hex(c))
104  h = h[2:]
105  if(len(h) == 1):
106  h = '0' + h
107  v += '\\x' + h
108  elif(c < 0xd800 or (c >= 0xe000 and c <= 0xffff)):
109  h = str(hex(c))
110  h = h[2:]
111  while(len(h) < 4):
112  h = '0' + h
113  v += '\\u' + h
114  elif(c >= 0xd800 and c < 0xe000):
115  raise Exception(
116  'IniWriter: string has Unicode surrogate value', string)
117  elif(c <= 0x10ffff):
118  h = str(hex(c))
119  h = h[2:]
120  while(len(h) < 8):
121  h = '0' + h
122  v += '\\U' + h
123  else:
124  # Note: will probably never get here
125  raise Exception(
126  'IniWriter: string has Unicode value out of range (>0x10FFFF)', string)
127  return "'" + v + "'"
128 
129  def booleans(self, true=False, false=False, none=False):
130  '''Sets the values to specify for boolean (and null) values.
131  @param true If specified must be one of 'true', 'yes' or 'on', all case insensitive.
132  @param false If specified must be one of 'false', 'no' or 'off', all case insensitive.
133  @param none If specified must be one of 'none', 'null' or 'void', all case insensitive.
134  @return Raises Exception on any parameter error.
135  '''
136  if(true):
137  l = true.lower()
138  if(l == 'true' or l == 'yes' or l == 'on'):
139  self.__true__true = true
140  else:
141  raise Exception(
142  'IniWriter.booleans(): true argument must be one of "true", "yes" or "on", case insensitive',
143  true)
144  if(false):
145  l = false.lower()
146  if(l == 'false' or l == 'no' or l == 'off'):
147  self.__false__false = false
148  else:
149  raise Exception(
150  'IniWriter.booleans(): false argument must be one of "false", "no" or "off", case insensitive',
151  false)
152  if(none):
153  l = none.lower()
154  if(l == 'none' or l == 'null' or l == 'void'):
155  self.__none__none = none
156  else:
157  raise Exception(
158  'IniWriter.booleans(): none argument must be one of "none", "null" or "void", case insensitive',
159  none)
160 
161  def comment_tab(self, tab=None):
162  '''Sets the column value for the tab to inline comments.
163  @param tab The column value to begin inline comments.
164  Must be an integer tab >= 0.
165  @return Raises Exception on invalid tab value.
166  '''
167  if(tab is None):
168  self.__comment_tab__comment_tab = self.__COMMENT_TAB__COMMENT_TAB
169  elif(isinstance(tab, int) and (tab >= 0)):
170  self.__comment_tab__comment_tab = tab
171  else:
172  raise Exception(
173  'IniWriter.comment_tab(): argument must be positive integer', tab)
174 
175  def delimiters(self, comment=False, key=False, value=False):
176  '''Sets the delimeter values.
177  @param comment The comment delimiter. Must be semicolon(";") or hash("#").
178  @param key The key/value delimiter. Must be equals("="), colon(":") or space.
179  @param value The delimiter between multiple key values. Must be comma(",") or space.
180  @return Raises Exception on invalid argument values.
181  '''
182  if(comment):
183  if(comment == ';'):
184  self.__comment_delim__comment_delim = self.__COMMENT_DELIM_SEMI__COMMENT_DELIM_SEMI
185  elif(comment == '#'):
186  self.__comment_delim__comment_delim = self.__COMMENT_DELIM_HASH__COMMENT_DELIM_HASH
187  else:
188  raise Exception(
189  'IniWriter.comment_delimiters(): comment argument must be semicolon(":") or hash("#")', comment)
190  if(key):
191  if(key == '='):
192  self.__key_delim__key_delim = self.__KEY_DELIM_EQUALS__KEY_DELIM_EQUALS
193  elif(key == ':'):
194  self.__key_delim__key_delim = self.__KEY_DELIM_COLON__KEY_DELIM_COLON
195  elif(key.isspace()):
196  self.__key_delim__key_delim = self.__KEY_DELIM_SPACE__KEY_DELIM_SPACE
197  else:
198  raise Exception(
199  'IniWriter.key_delimiters(): key argument must be equals(=), comma(,) or space', key)
200  if(value):
201  if(value == ','):
202  self.__value_delim__value_delim = self.__VALUE_DELIM_COMMA__VALUE_DELIM_COMMA
203  elif(value.isspace()):
204  self.__value_delim__value_delim = self.__VALUE_DELIM_SPACE__VALUE_DELIM_SPACE
205  else:
206  raise Exception(
207  'IniWriter.value_delimiters(): value argument must be comma(,) or space', value)
208 
209  def section(self, name, comment=None):
210  '''Add a section line to the INI file.
211  @param name The section name. Must be one or more characters from the set
212  - a-zA-Z0-9!$%()*+-.<>?@^_{|}~
213  @param comment An inline comment to add to the section name line.
214  @returns Raises Exception on invalid name or comment.
215  '''
216  self.__validate_name__validate_name(name)
217  self.__lines__lines.append(
218  [self.__SECTION__SECTION, name, self.__normalize_comment__normalize_comment(comment)])
219 
220  def key(self, name, varg, comment=None):
221  '''Add a key/value line to the INI file.
222  @param name The key name. Must be one or more characters from the set
223  - a-zA-Z0-9!$%()*+-.<>?@^_{|}~
224  @param varg The value for the key. May be a single value or a list of values.
225  Valid values are:
226  - True
227  - False
228  - None
229  - integer, positive or negative
230  - floating point number, positive or negative
231  - string
232  @param comment An inline comment to add to the key/value line.
233  @returns Raises Exception on invalid name, value or comment.
234  '''
235  self.__validate_name__validate_name(name)
236  if(isinstance(varg, list)):
237  values = varg
238  else:
239  values = [varg]
240  vlist = []
241  for value in values:
242  if(value is True):
243  v = self.__true__true
244  elif(value is False):
245  v = self.__false__false
246  elif(value is None):
247  v = self.__none__none
248  elif(isinstance(value, int) or isinstance(value, float)):
249  v = str(value)
250  elif(isinstance(value, str)):
251  v = self.__normalize_string__normalize_string(value)
252  else:
253  raise Exception('IniWriter.key(): invalid value', value)
254  vlist.append(v)
255  self.__lines__lines.append(
256  [self.__KEY__KEY, name, vlist, self.__normalize_comment__normalize_comment(comment)])
257 
258  def comment(self, comment=None):
259  '''Add a comment line to the INI file.
260  @param comment A comment string. Valid comments are:
261  - None (default) adds a blank line
262  - ' ' empty or all white space string adds a blank line
263  - string of printing ASCII characters only, char codes 32-126
264  @returns Raises Exception on invalid ncomment.
265  '''
266  self.__lines__lines.append(
267  [self.__COMMENT__COMMENT, self.__normalize_comment__normalize_comment(comment)])
268 
269  def to_string(self):
270  '''Convert all lines added with the section(), key() and comment() functions
271  to a valid, formatted INI file.
272  @returns The formatted INI file as a string.
273  '''
274 
275  def indent(n):
276  out = ''
277  while(n):
278  out += ' '
279  n -= 1
280  return out
281 
282  def add_comment(out, comment):
283  lout = len(out)
284  if(not comment):
285  add = self.__LINE_END__LINE_END
286  elif(lout >= self.__comment_tab__comment_tab):
287  add = self.__LINE_END__LINE_END
288  add += indent(self.__comment_tab__comment_tab)
289  add += self.__comment_delim__comment_delim + comment
290  add += self.__LINE_END__LINE_END
291  else:
292  add = indent(self.__comment_tab__comment_tab - lout)
293  add += self.__comment_delim__comment_delim + comment
294  add += self.__LINE_END__LINE_END
295  return add
296 
297  # key line = [id, name, value, comment]
298  # section line = [id, name, comment]
299  # comment line = [id, comment]
300  out = ''
301  for line in self.__lines__lines:
302  if(line[0] == self.__COMMENT__COMMENT):
303  if(line[1] == ''):
304  out += self.__LINE_END__LINE_END
305  else:
306  if(line[1] is None):
307  out += self.__LINE_END__LINE_END
308  else:
309  out += self.__comment_delim__comment_delim + line[1] + self.__LINE_END__LINE_END
310  elif(line[0] == self.__KEY__KEY):
311  out_line = line[1] + self.__key_delim__key_delim
312  count = 0
313  for v in line[2]:
314  if(count > 0):
315  out_line += self.__value_delim__value_delim
316  out_line += v
317  count += 1
318  out += out_line + add_comment(out_line, line[3])
319  else:
320  out_line = '[' + line[1] + ']'
321  out += out_line + add_comment(out_line, line[2])
322  return out
323 
324  def write(self, fname):
325  '''Write the formatted INI file to a file. Calls to_string().
326  @param fname The file name to write the INI file to.
327  @return Raises exceptions on file open or write errors.
328  '''
329  with open(fname, 'w') as fd:
330  fd.write(self.to_stringto_string())
def key(self, name, varg, comment=None)
Add a key/value line to the INI file.
Definition: ini_writer.py:220
def to_string(self)
Convert all lines added with the section(), key() and comment() functions to a valid,...
Definition: ini_writer.py:269
def __normalize_comment(self, comment)
Definition: ini_writer.py:72
def __validate_name(self, name)
Definition: ini_writer.py:54
def delimiters(self, comment=False, key=False, value=False)
Sets the delimeter values.
Definition: ini_writer.py:175
def booleans(self, true=False, false=False, none=False)
Sets the values to specify for boolean (and null) values.
Definition: ini_writer.py:129
def __normalize_string(self, string)
Definition: ini_writer.py:88
def section(self, name, comment=None)
Add a section line to the INI file.
Definition: ini_writer.py:209
def comment(self, comment=None)
Add a comment line to the INI file.
Definition: ini_writer.py:258
def clear(self)
Initialize or reset the writer to its constructor defaults.
Definition: ini_writer.py:42
def comment_tab(self, tab=None)
Sets the column value for the tab to inline comments.
Definition: ini_writer.py:161
def write(self, fname)
Write the formatted INI file to a file.
Definition: ini_writer.py:324
def __init__(self)
Ini file writer constructor.
Definition: ini_writer.py:11
Python APG, Version 1.0, is licensed under the 2-Clause BSD License,
an Open Source Initiative Approved License.