Skip to content

Commit 8d88fd1

Browse files
committed
add support for using a btree database (DefinesDB) to store defines for preprocessing
The btree module, which ships with MicroPython, can efficiency manage a large number of key-value pairs with minimal memory. It automatically initialises to appropriate memory and cache limits, based on the device it's running on, but if needed those parameters can be tuned too, e.g. to restrict memory usage further. The database is optional and must be supplied to the Preprocessor via the use_db() method. It's safe however to always supply it, because a non-existing database will behave like an empty database. Care is taken not to unnecessarily create an empty db, when only reading from it and not to unnecessarily check the file-system whether the database exists. Inside the Preprocessor the database is opened and closed with a context manager. This ensures the database will be closed properly again. While DefinesDB opens the underlying database automatically, it cannot automatically close the database again (using a destructor __del__ does not work, and MicroPython does not have the "atexit" exit handler on the esp32). By using a context manager, the code becomes cleaner, while still ensuring the database is closed at the end.
1 parent ac1de99 commit 8d88fd1

File tree

7 files changed

+299
-18
lines changed

7 files changed

+299
-18
lines changed

esp32_ulp/definesdb.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import os
2+
import btree
3+
from .util import file_exists
4+
5+
DBNAME = 'defines.db'
6+
7+
8+
class DefinesDB:
9+
def __init__(self):
10+
self._file = None
11+
self._db = None
12+
self._db_exists = None
13+
14+
def clear(self):
15+
self.close()
16+
try:
17+
os.remove(DBNAME)
18+
self._db_exists = False
19+
except OSError:
20+
pass
21+
22+
def open(self):
23+
if self._db:
24+
return
25+
try:
26+
self._file = open(DBNAME, 'r+b')
27+
except OSError:
28+
self._file = open(DBNAME, 'w+b')
29+
self._db = btree.open(self._file)
30+
self._db_exists = True
31+
32+
def close(self):
33+
if not self._db:
34+
return
35+
self._db.close()
36+
self._db = None
37+
self._file.close()
38+
self._file = None
39+
40+
def db_exists(self):
41+
if self._db_exists is None:
42+
self._db_exists = file_exists(DBNAME)
43+
return self._db_exists
44+
45+
def update(self, dictionary):
46+
for k, v in dictionary.items():
47+
self.__setitem__(k, v)
48+
49+
def get(self, key, default):
50+
try:
51+
result = self.__getitem__(key)
52+
except KeyError:
53+
result = default
54+
return result
55+
56+
def keys(self):
57+
if not self.db_exists():
58+
return []
59+
60+
self.open()
61+
return [k.decode() for k in self._db.keys()]
62+
63+
def __getitem__(self, key):
64+
if not self.db_exists():
65+
raise KeyError
66+
67+
self.open()
68+
return self._db[key.encode()].decode()
69+
70+
def __setitem__(self, key, value):
71+
self.open()
72+
self._db[key.encode()] = str(value).encode()
73+
74+
def __iter__(self):
75+
return iter(self.keys())

esp32_ulp/preprocess.py

+45-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from . import nocomment
22
from .util import split_tokens
3+
from .definesdb import DefinesDB
34

45

56
class RTC_Macros:
@@ -56,6 +57,7 @@ def parse_define_line(self, line):
5657
def parse_defines(self, content):
5758
for line in content.splitlines():
5859
self._defines.update(self.parse_define_line(line))
60+
5961
return self._defines
6062

6163
def expand_defines(self, line):
@@ -66,21 +68,22 @@ def expand_defines(self, line):
6668
line = ""
6769
for t in tokens:
6870
lu = self._defines.get(t, t)
71+
if lu == t and self._defines_db:
72+
lu = self._defines_db.get(t, t)
6973
if lu != t:
7074
found = True
7175
line += lu
7276

7377
return line
7478

7579
def process_include_file(self, filename):
76-
defines = self._defines
77-
78-
with open(filename, 'r') as f:
79-
for line in f:
80-
result = self.parse_defines(line)
81-
defines.update(result)
80+
with self.open_db() as db:
81+
with open(filename, 'r') as f:
82+
for line in f:
83+
result = self.parse_define_line(line)
84+
db.update(result)
8285

83-
return defines
86+
return db
8487

8588
def expand_rtc_macros(self, line):
8689
clean_line = line.strip()
@@ -103,17 +106,43 @@ def expand_rtc_macros(self, line):
103106

104107
return macro_fn(*macro_args)
105108

109+
def use_db(self, defines_db):
110+
self._defines_db = defines_db
111+
112+
def open_db(self):
113+
class ctx:
114+
def __init__(self, db):
115+
self._db = db
116+
117+
def __enter__(self):
118+
# not opening DefinesDB - it opens itself when needed
119+
return self._db
120+
121+
def __exit__(self, type, value, traceback):
122+
if isinstance(self._db, DefinesDB):
123+
self._db.close()
124+
125+
if self._defines_db:
126+
return ctx(self._defines_db)
127+
128+
return ctx(self._defines)
129+
106130
def preprocess(self, content):
107131
self.parse_defines(content)
108-
lines = nocomment.remove_comments(content)
109-
result = []
110-
for line in lines:
111-
line = self.expand_defines(line)
112-
line = self.expand_rtc_macros(line)
113-
result.append(line)
114-
result = "\n".join(result)
132+
133+
with self.open_db():
134+
lines = nocomment.remove_comments(content)
135+
result = []
136+
for line in lines:
137+
line = self.expand_defines(line)
138+
line = self.expand_rtc_macros(line)
139+
result.append(line)
140+
result = "\n".join(result)
141+
115142
return result
116143

117144

118-
def preprocess(content):
119-
return Preprocessor().preprocess(content)
145+
def preprocess(content, use_defines_db=True):
146+
preprocessor = Preprocessor()
147+
preprocessor.use_db(DefinesDB())
148+
return preprocessor.preprocess(content)

esp32_ulp/util.py

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
DEBUG = False
22

33
import gc
4+
import os
45

56
NORMAL, WHITESPACE = 0, 1
67

@@ -67,3 +68,12 @@ def validate_expression(param):
6768
if c not in '0123456789abcdef':
6869
state = 0
6970
return True
71+
72+
73+
def file_exists(filename):
74+
try:
75+
os.stat(filename)
76+
return True
77+
except OSError:
78+
pass
79+
return False

tests/00_unit_tests.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
set -e
66

7-
for file in opcodes assemble link util preprocess; do
7+
for file in opcodes assemble link util preprocess definesdb; do
88
echo testing $file...
99
micropython $file.py
1010
done

tests/definesdb.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
3+
from esp32_ulp.definesdb import DefinesDB, DBNAME
4+
from esp32_ulp.util import file_exists
5+
6+
tests = []
7+
8+
9+
def test(param):
10+
tests.append(param)
11+
12+
13+
@test
14+
def test_definesdb_clear_removes_all_keys():
15+
db = DefinesDB()
16+
db.open()
17+
db.update({'KEY1': 'VALUE1'})
18+
19+
db.clear()
20+
21+
assert 'KEY1' not in db
22+
23+
db.close()
24+
25+
26+
@test
27+
def test_definesdb_persists_data_across_instantiations():
28+
db = DefinesDB()
29+
db.open()
30+
db.clear()
31+
32+
db.update({'KEY1': 'VALUE1'})
33+
34+
assert 'KEY1' in db
35+
36+
db.close()
37+
del db
38+
db = DefinesDB()
39+
db.open()
40+
41+
assert db.get('KEY1', None) == 'VALUE1'
42+
43+
db.close()
44+
45+
46+
@test
47+
def test_definesdb_should_not_create_a_db_file_when_only_reading():
48+
db = DefinesDB()
49+
50+
db.clear()
51+
assert not file_exists(DBNAME)
52+
53+
assert db.get('some-key', None) is None
54+
assert not file_exists(DBNAME)
55+
56+
57+
if __name__ == '__main__':
58+
# run all methods marked with @test
59+
for t in tests:
60+
t()

tests/preprocess.py

+93
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import os
2+
13
from esp32_ulp.preprocess import Preprocessor
4+
from esp32_ulp.definesdb import DefinesDB, DBNAME
5+
from esp32_ulp.util import file_exists
26

37
tests = []
48

@@ -186,6 +190,7 @@ def test_process_include_file():
186190
p = Preprocessor()
187191

188192
defines = p.process_include_file('fixtures/incl.h')
193+
189194
assert defines['CONST1'] == '42'
190195
assert defines['CONST2'] == '99'
191196
assert defines.get('MULTI_LINE', None) == 'abc \\' # correct. line continuations not supported
@@ -204,6 +209,94 @@ def test_process_include_file_with_multiple_files():
204209
assert defines['CONST3'] == '777', "constant from incl2.h"
205210

206211

212+
@test
213+
def test_process_include_file_using_database():
214+
db = DefinesDB()
215+
db.clear()
216+
217+
p = Preprocessor()
218+
p.use_db(db)
219+
220+
p.process_include_file('fixtures/incl.h')
221+
p.process_include_file('fixtures/incl2.h')
222+
223+
assert db['CONST1'] == '42', "constant from incl.h"
224+
assert db['CONST2'] == '123', "constant overridden by incl2.h"
225+
assert db['CONST3'] == '777', "constant from incl2.h"
226+
227+
db.close()
228+
229+
230+
@test
231+
def test_process_include_file_should_not_load_database_keys_into_instance_defines_dictionary():
232+
db = DefinesDB()
233+
db.clear()
234+
235+
p = Preprocessor()
236+
p.use_db(db)
237+
238+
p.process_include_file('fixtures/incl.h')
239+
240+
# a bit hackish to reference instance-internal state
241+
# but it's important to verify this, as we otherwise run out of memory on device
242+
assert 'CONST2' not in p._defines
243+
244+
245+
246+
@test
247+
def test_preprocess_should_use_definesdb_when_provided():
248+
p = Preprocessor()
249+
250+
content = """\
251+
#define LOCALCONST 42
252+
253+
entry:
254+
move r1, LOCALCONST
255+
move r2, DBKEY
256+
"""
257+
258+
# first try without db
259+
result = p.preprocess(content)
260+
261+
assert "move r1, 42" in result
262+
assert "move r2, DBKEY" in result
263+
assert "move r2, 99" not in result
264+
265+
# now try with db
266+
db = DefinesDB()
267+
db.clear()
268+
db.update({'DBKEY': '99'})
269+
p.use_db(db)
270+
271+
result = p.preprocess(content)
272+
273+
assert "move r1, 42" in result
274+
assert "move r2, 99" in result
275+
assert "move r2, DBKEY" not in result
276+
277+
278+
@test
279+
def test_preprocess_should_ensure_no_definesdb_is_created_when_only_reading_from_it():
280+
content = """\
281+
#define CONST 42
282+
move r1, CONST"""
283+
284+
# remove any existing db
285+
db = DefinesDB()
286+
db.clear()
287+
assert not file_exists(DBNAME)
288+
289+
# now preprocess using db
290+
p = Preprocessor()
291+
p.use_db(db)
292+
293+
result = p.preprocess(content)
294+
295+
assert "move r1, 42" in result
296+
297+
assert not file_exists(DBNAME)
298+
299+
207300
if __name__ == '__main__':
208301
# run all methods marked with @test
209302
for t in tests:

0 commit comments

Comments
 (0)