diff --git a/.gitignore b/.gitignore index 85c18d3..02517cd 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +.cache # sqlite databases *.sqlite diff --git a/sqlitedict.py b/sqlitedict.py index f6b47d7..07343c6 100755 --- a/sqlitedict.py +++ b/sqlitedict.py @@ -168,7 +168,7 @@ def __exit__(self, *exc_info): self.close() def __str__(self): - return "SqliteDict(%s)" % (self.conn.filename) + return "SqliteDict(%s)" % (self.filename) def __repr__(self): return str(self) # no need of something complex @@ -233,12 +233,18 @@ def __setitem__(self, key, value): self.conn.execute(ADD_ITEM, (key, encode(value))) def __delitem__(self, key): + if self.flag == 'r': + raise RuntimeError('Refusing to delete from read-only SqliteDict') + if key not in self: raise KeyError(key) DEL_ITEM = 'DELETE FROM %s WHERE key = ?' % self.tablename self.conn.execute(DEL_ITEM, (key,)) def update(self, items=(), **kwds): + if self.flag == 'r': + raise RuntimeError('Refusing to update read-only SqliteDict') + try: items = [(k, encode(v)) for k, v in items.items()] except AttributeError: @@ -253,6 +259,9 @@ def __iter__(self): return self.iterkeys() def clear(self): + if self.flag == 'r': + raise RuntimeError('Refusing to clear read-only SqliteDict') + CLEAR_ALL = 'DELETE FROM %s;' % self.tablename # avoid VACUUM, as it gives "OperationalError: database schema has changed" self.conn.commit() self.conn.execute(CLEAR_ALL) @@ -289,6 +298,9 @@ def close(self, do_log=True): def terminate(self): """Delete the underlying database file. Use with care.""" + if self.flag == 'r': + raise RuntimeError('Refusing to terminate read-only SqliteDict') + self.close() if self.filename == ':memory:': @@ -303,7 +315,13 @@ def terminate(self): def __del__(self): # like close(), but assume globals are gone by now (do not log!) - self.close(do_log=False) + try: + self.close(do_log=False) + except Exception: + # prevent error log flood in case of multiple SqliteDicts + # closed after connection lost (exceptions are always ignored + # in __del__ method. + pass # Adding extra methods for python 2 compatibility (at import time) if major_version == 2: diff --git a/tests/test_core.py b/tests/test_core.py index b3476ae..78a4419 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -26,6 +26,9 @@ def test_as_str(self): db = sqlitedict.SqliteDict() # exercise db.__str__() + # test when db closed + db.close() + db.__str__() def test_as_repr(self): """Verify SqliteDict.__repr__().""" @@ -94,17 +97,38 @@ def test_readonly(self): fname = norm_file('tests/db/sqlitedict-override-test.sqlite') orig_db = sqlitedict.SqliteDict(filename=fname) orig_db['key'] = 'value' + orig_db['key_two'] = 2 orig_db.commit() orig_db.close() readonly_db = sqlitedict.SqliteDict(filename=fname, flag = 'r') self.assertTrue(readonly_db['key'] == 'value') + self.assertTrue(readonly_db['key_two'] == 2) def attempt_write(): readonly_db['key'] = ['new_value'] - with self.assertRaises(RuntimeError): - attempt_write() + def attempt_update(): + readonly_db.update(key = 'value2', key_two = 2.1) + + def attempt_delete(): + del readonly_db['key'] + + def attempt_clear(): + readonly_db.clear() + + def attempt_terminate(): + readonly_db.terminate() + + attempt_funcs = [attempt_write, + attempt_update, + attempt_delete, + attempt_clear, + attempt_terminate] + + for func in attempt_funcs: + with self.assertRaises(RuntimeError): + func() def test_overwrite_using_flag_w(self): """Re-opening of a database with flag='w' destroys only the target table."""