|
1 """ |
|
2 Generic configuration file output |
|
3 """ |
|
4 |
|
5 import os, os.path, tempfile, shutil |
|
6 |
|
7 class Item (object) : |
|
8 """ |
|
9 An object that can be represented as a series of lines of text. |
|
10 |
|
11 The structure of these objects/lines is not defined, but as a convenience, a method for prepending lines of |
|
12 comments is provided. |
|
13 """ |
|
14 |
|
15 # the prefix char for comments, must be defined |
|
16 COMMENT_PREFIX = None |
|
17 |
|
18 @classmethod |
|
19 def filter_comments (self, comments) : |
|
20 """ |
|
21 Return the filtered list of comments |
|
22 """ |
|
23 |
|
24 return [comment for comment in comments if comment] |
|
25 |
|
26 def __init__ (self, comments=()) : |
|
27 """ |
|
28 Initialize with the given list of comments, which will be filtered before use. |
|
29 """ |
|
30 |
|
31 # init the comments list |
|
32 self.comments = self.filter_comments(comments) |
|
33 |
|
34 def add_comment (self, comment) : |
|
35 """ |
|
36 Add the given comment |
|
37 """ |
|
38 |
|
39 self.comments += self.filter_comments([comment]) |
|
40 |
|
41 def iter_comments (self) : |
|
42 """ |
|
43 Formats comments for output |
|
44 """ |
|
45 |
|
46 for comment in self.comments : |
|
47 yield "%s %s" % (self.COMMENT_PREFIX, comment) |
|
48 |
|
49 def iter_lines (self, indent='\t') : |
|
50 """ |
|
51 Yield a series of lines to be output in the file |
|
52 """ |
|
53 |
|
54 abstract |
|
55 |
|
56 def render_lines (self, encoding=None, newline='\n', **opts) : |
|
57 """ |
|
58 Render lines as a series of objects, possibly encoded. |
|
59 """ |
|
60 |
|
61 for line in self.iter_lines(**opts) : |
|
62 if encoding : |
|
63 line = line.encode(encoding) |
|
64 |
|
65 elif encoding is False : |
|
66 line = unicode(line) |
|
67 |
|
68 yield line + newline |
|
69 |
|
70 def render_unicode (self, **opts) : |
|
71 """ |
|
72 Render lines as a single unicode object |
|
73 """ |
|
74 |
|
75 return u''.join(self.render_lines(encoding=False, **opts)) |
|
76 |
|
77 def render_str (self, encoding='utf-8', **opts) : |
|
78 """ |
|
79 Render lines as a single str object |
|
80 """ |
|
81 |
|
82 return ''.join(self.render_lines(encoding=encoding, **opts)) |
|
83 |
|
84 def render_out (self, file, encoding='utf-8', **opts) : |
|
85 """ |
|
86 Render lines and write out to given file object |
|
87 """ |
|
88 |
|
89 for line in self.render_lines(encoding=encoding, **opts) : |
|
90 file.write(line) |
|
91 |
|
92 class Contents (Item) : |
|
93 """ |
|
94 A series of Items |
|
95 """ |
|
96 |
|
97 def __init__ (self, *items) : |
|
98 self.items = items |
|
99 |
|
100 def add_item (self, item) : |
|
101 self.items.append(item) |
|
102 |
|
103 def iter_lines (self, **opts) : |
|
104 for item in self.items : |
|
105 for line in item.iter_lines(**opts) : |
|
106 yield line |
|
107 |
|
108 class File (object) : |
|
109 """ |
|
110 A configuration file on the filesystem. |
|
111 |
|
112 Configuration files can be written out with some given contents. |
|
113 |
|
114 |
|
115 XXX: better logic for backup file |
|
116 """ |
|
117 |
|
118 def __init__ (self, name, path, backup_suffix='.bak', mode=0644) : |
|
119 """ |
|
120 Initialize the config file, but don't open it yet |
|
121 |
|
122 @param name the human-readable friendly name for the config file |
|
123 @param path the full filesystem path for the file |
|
124 @param backup_suffix rename the old file by adding this suffix when replacing it |
|
125 @param mode the permission bits for the new file |
|
126 """ |
|
127 |
|
128 self.name = name |
|
129 self.path = path |
|
130 self.backup_suffix = backup_suffix |
|
131 self.mode = mode |
|
132 |
|
133 def write (self, data) : |
|
134 """ |
|
135 Write out a new config file with the given Item data using the following procedure: |
|
136 |
|
137 * lock the real file |
|
138 * open a new temporary file, and write out the objects into that, closing it |
|
139 * move the real file out of the way |
|
140 * move the temporary file into the real file's place |
|
141 * unlock the real file |
|
142 """ |
|
143 |
|
144 # XXX: how to aquire the lock? |
|
145 # XXX: this needs checking over |
|
146 |
|
147 # open the new temporary file |
|
148 # XXX: ensure OS fd is closed |
|
149 tmp_fd, tmp_path = tempfile.mkstemp(prefix=self.name) |
|
150 |
|
151 # ...as a file |
|
152 tmp_file = os.fdopen(tmp_fd, "w") |
|
153 |
|
154 # fix the permissions |
|
155 os.chmod(tmp_path, self.mode) |
|
156 |
|
157 # write the Item out into the file |
|
158 data.render_out(tmp_file) |
|
159 |
|
160 # close it |
|
161 tmp_file.close() |
|
162 |
|
163 # move the old file out of the way |
|
164 if os.path.exists(self.path) : |
|
165 # XXX: use shutil.move? |
|
166 os.rename(self.path, self.path + self.backup_suffix) |
|
167 |
|
168 # move the new file in |
|
169 shutil.move(tmp_path, self.path) |
|
170 |