3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 dblite.py module contributed by Ralf W. Grosse-Kunstleve.
26 Extended for Unicode by Steven Knight.
28 This is a very simple-minded "database" used for saved signature
29 information, with an interface modeled on the Python dbm database
39 from SCons
.compat
import PICKLE_PROTOCOL
41 KEEP_ALL_FILES
= False
42 IGNORE_CORRUPT_DBFILES
= False
45 def corruption_warning(filename
) -> None:
46 """Local warning for corrupt db.
48 Used for self-tests. SCons overwrites this with a
49 different warning function in SConsign.py.
51 print("Warning: Discarding corrupt database:", filename
)
54 DBLITE_SUFFIX
= ".dblite"
59 """Lightweight signature database class.
61 Behaves like a dict when in memory, loads from a pickled disk
62 file on open and writes back out to it on close.
64 Open the database file using a path derived from *file_base_name*.
65 The optional *flag* argument can be:
67 +---------+---------------------------------------------------+
69 +=========+===================================================+
70 | ``'r'`` | Open existing database for reading only (default) |
71 +---------+---------------------------------------------------+
72 | ``'w'`` | Open existing database for reading and writing |
73 +---------+---------------------------------------------------+
74 | ``'c'`` | Open database for reading and writing, creating |
75 | | it if it doesn't exist |
76 +---------+---------------------------------------------------+
77 | ``'n'`` | Always create a new, empty database, open for |
78 | | reading and writing |
79 +---------+---------------------------------------------------+
81 The optional *mode* argument is the POSIX mode of the file, used only
82 when the database has to be created. It defaults to octal ``0o666``.
85 # Because open() is defined at module level, overwriting builtin open
86 # in the scope of this module, we use io.open to avoid ambiguity.
87 _open
= staticmethod(io
.open)
89 # we need to squirrel away references to functions from various modules
90 # that we'll use when sync() is called: this may happen at Python
91 # teardown time (we call it from our __del__), and the global module
92 # references themselves may already have been rebound to None.
93 _pickle_dump
= staticmethod(pickle
.dump
)
94 _pickle_protocol
= PICKLE_PROTOCOL
96 _os_chown
= staticmethod(os
.chown
)
97 except AttributeError:
99 _os_replace
= staticmethod(os
.replace
)
100 _os_chmod
= staticmethod(os
.chmod
)
101 _shutil_copyfile
= staticmethod(shutil
.copyfile
)
102 _time_time
= staticmethod(time
.time
)
104 def __init__(self
, file_base_name
, flag
='r', mode
=0o666) -> None:
105 assert flag
in ("r", "w", "c", "n")
107 base
, ext
= os
.path
.splitext(file_base_name
)
108 if ext
== DBLITE_SUFFIX
:
109 # There's already a suffix on the file name, don't add one.
110 self
._file
_name
= file_base_name
111 self
._tmp
_name
= base
+ TMP_SUFFIX
113 self
._file
_name
= file_base_name
+ DBLITE_SUFFIX
114 self
._tmp
_name
= file_base_name
+ TMP_SUFFIX
119 self
._needs
_sync
= False
121 if self
._os
_chown
is not None and 0 in (os
.geteuid(), os
.getegid()):
122 # running as root; chown back to current owner/group when done
124 statinfo
= os
.stat(self
._file
_name
)
125 self
._chown
_to
= statinfo
.st_uid
126 self
._chgrp
_to
= statinfo
.st_gid
128 # db file doesn't exist yet.
129 # Check os.environ for SUDO_UID, use if set
130 self
._chown
_to
= int(os
.environ
.get('SUDO_UID', -1))
131 self
._chgrp
_to
= int(os
.environ
.get('SUDO_GID', -1))
133 self
._chown
_to
= -1 # don't chown
134 self
._chgrp
_to
= -1 # don't chgrp
136 if self
._flag
== "n":
137 with io
.open(self
._file
_name
, "wb", opener
=self
.opener
):
138 return # just make sure it exists
140 # We only need the disk file to slurp in the data. Updates are
141 # handled on close, db is mainained only in memory until then.
143 with io
.open(self
._file
_name
, "rb") as f
:
146 # an error for file not to exist, unless flag is create
147 if self
._flag
!= "c":
149 with io
.open(self
._file
_name
, "wb", opener
=self
.opener
):
150 return # just make sure it exists
153 self
._dict
= pickle
.loads(p
, encoding
='bytes')
155 pickle
.UnpicklingError
,
157 # Note that other exceptions may also be raised during
158 # unpickling, including (but not necessarily limited to)
159 # AttributeError, EOFError, ImportError, and IndexError.
165 if IGNORE_CORRUPT_DBFILES
:
166 corruption_warning(self
._file
_name
)
170 def opener(self
, path
, flags
):
171 """Database open helper when creation may be needed.
173 The high-level Python open() function cannot specify a file mode
174 for creation. Using this as the opener with the saved mode lets
177 return os
.open(path
, flags
, mode
=self
._mode
)
179 def close(self
) -> None:
183 def __del__(self
) -> None:
186 def sync(self
) -> None:
187 """Flush the database to disk.
189 This routine *must* succeed, since the in-memory and on-disk
190 copies are out of sync as soon as we do anything that changes
191 the in-memory version. Thus, to be cautious, flush to a
192 temporary file and then move it over with some error handling.
194 self
._check
_writable
()
195 with self
._open
(self
._tmp
_name
, "wb", opener
=self
.opener
) as f
:
196 self
._pickle
_dump
(self
._dict
, f
, self
._pickle
_protocol
)
199 self
._os
_replace
(self
._tmp
_name
, self
._file
_name
)
200 except PermissionError
:
201 # If we couldn't replace due to perms, try to change and retry.
202 # This is mainly for Windows - on POSIX the file permissions
203 # don't matter, the os.replace would have worked anyway.
204 # We're giving up if the retry fails, just let the Python
205 # exception abort us.
207 self
._os
_chmod
(self
._file
_name
, 0o777)
208 except PermissionError
:
210 self
._os
_replace
(self
._tmp
_name
, self
._file
_name
)
213 self
._os
_chown
is not None and self
._chown
_to
> 0
214 ): # don't chown to root or -1
216 self
._os
_chown
(self
._file
_name
, self
._chown
_to
, self
._chgrp
_to
)
220 self
._needs
_sync
= False
222 self
._shutil
_copyfile
(
223 self
._file
_name
, f
"{self._file_name}_{int(self._time_time())}"
226 def _check_writable(self
):
227 if self
._flag
== "r":
228 raise OSError(f
"Read-only database: {self._file_name}")
230 def __getitem__(self
, key
):
231 return self
._dict
[key
]
233 def __setitem__(self
, key
, value
):
234 self
._check
_writable
()
236 if not isinstance(key
, str):
237 raise TypeError(f
"key `{key}' must be a string but is {type(key)}")
239 if not isinstance(value
, bytes
):
240 raise TypeError(f
"value `{value}' must be bytes but is {type(value)}")
242 self
._dict
[key
] = value
243 self
._needs
_sync
= True
245 def __delitem__(self
, key
):
249 return self
._dict
.keys()
252 return self
._dict
.items()
255 return self
._dict
.values()
259 def __contains__(self
, key
) -> bool:
260 return key
in self
._dict
262 def __len__(self
) -> int:
263 return len(self
._dict
)
266 def open(file, flag
="r", mode
: int = 0o666): # pylint: disable=redefined-builtin
267 return _Dblite(file, flag
, mode
)
271 db
= open("tmp", "n")
274 assert db
["foo"] == b
"bar"
277 db
= open("tmp", "c")
278 assert len(db
) == 1, len(db
)
279 assert db
["foo"] == b
"bar"
281 assert db
["bar"] == b
"foo"
285 assert len(db
) == 2, len(db
)
286 assert db
["foo"] == b
"bar"
287 assert db
["bar"] == b
"foo"
291 assert str(e
) == "Read-only database: tmp.dblite"
293 raise RuntimeError("IOError expected.")
294 db
= open("tmp", "w")
295 assert len(db
) == 2, len(db
)
301 except TypeError as e
:
302 assert str(e
) == "key `(1, 2)' must be a string but is <class 'tuple'>", str(e
)
304 raise RuntimeError("TypeError exception expected")
308 except TypeError as e
:
309 assert str(e
) == "value `[1, 2]' must be bytes but is <class 'list'>", str(e
)
311 raise RuntimeError("TypeError exception expected")
314 assert len(db
) == 3, len(db
)
316 db
= open("tmp", "n")
317 assert len(db
) == 0, len(db
)
318 _Dblite
._open
("tmp.dblite", "w")
321 _Dblite
._open
("tmp.dblite", "w").write("x")
324 except pickle
.UnpicklingError
:
327 raise RuntimeError("pickle exception expected.")
329 global IGNORE_CORRUPT_DBFILES
330 IGNORE_CORRUPT_DBFILES
= True
332 assert len(db
) == 0, len(db
)
333 os
.unlink("tmp.dblite")
335 db
= open("tmp", "w")
337 assert str(e
) == "[Errno 2] No such file or directory: 'tmp.dblite'", str(e
)
339 raise RuntimeError("IOError expected.")
341 print("Completed _exercise()")
344 if __name__
== "__main__":
349 # indent-tabs-mode:nil
351 # vim: set expandtab tabstop=4 shiftwidth=4: