Skip to content

Commit a55d938

Browse files
authored
DB usability improvements (#385)
* add support to the util.DB class to read and write an entire table util.DB is a wrapper class to manipulate data in tabular format. before this commit, it was not possible to read or write the entire table in one go. this commit introduces support for such operations. the rationale of these operations is to have the ability to operate on the entire table in the same way it is possible to operate on the single rows, without requiring external iteration. * implement more dict-like semantics for util.DB implement keys(), items(), __contains__() methods for the DB class with dict-like semantics. implement an export() method, flattening the entire structure (DB and DB_Row's), with semantics akin to DB_Row.export(). document in more detail the existing dict-like semantics of the DB class, detailing in particular the instances where the existing behaviour diverges from Python dicts. introduce an unit test for the new DB.export() method. * use correct type hints for util.DB constructor the existing type hints make some attributes Optional, while a None value for any of those attribute would be in fact an error. there might be more esuch cases in the DB class: this commit is not meant to be exhaustive.
1 parent 38d6576 commit a55d938

File tree

2 files changed

+146
-10
lines changed

2 files changed

+146
-10
lines changed

snap7/util.py

Lines changed: 125 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,12 @@ class DB:
13541354
13551355
Probably most usecases there is just one row
13561356
1357+
Note:
1358+
This class has some of the semantics of a dict. In particular, the membership operators
1359+
(``in``, ``not it``), the access operator (``[]``), as well as the :func:`~DB.keys()` and
1360+
:func:`~DB.items()` methods work as usual. Iteration, on the other hand, happens on items
1361+
instead of keys (much like :func:`~DB.items()` method).
1362+
13571363
Attributes:
13581364
bytearray_: buffer data from the PLC.
13591365
specification: layout of the DB Rows.
@@ -1364,14 +1370,14 @@ class DB:
13641370
13651371
Examples:
13661372
>>> db1[0]['testbool1'] = test
1367-
>>> db1.write() # puts data in plc
1373+
>>> db1.write(client) # puts data in plc
13681374
"""
13691375
bytearray_: Optional[bytearray] = None # data from plc
1370-
specification: Optional[str] = None # layout of db rows
1371-
row_size: Optional[int] = None # bytes size of a db row
1372-
layout_offset: Optional[int] = None # at which byte in row specification should
1373-
# we start reading the data
1374-
db_offset: Optional[int] = None # at which byte in db should we start reading?
1376+
specification: Optional[str] = None # layout of db rows
1377+
id_field: Optional[str] = None # ID field of the rows
1378+
row_size: int = 0 # bytes size of a db row
1379+
layout_offset: int = 0 # at which byte in row specification should
1380+
db_offset: int = 0 # at which byte in db should we start reading?
13751381

13761382
# first fields could be be status data.
13771383
# and only the last part could be control data
@@ -1380,8 +1386,8 @@ class DB:
13801386

13811387
def __init__(self, db_number: int, bytearray_: bytearray,
13821388
specification: str, row_size: int, size: int, id_field: Optional[str] = None,
1383-
db_offset: Optional[int] = 0, layout_offset: Optional[int] = 0, row_offset: Optional[int] = 0,
1384-
area: Optional[Areas] = Areas.DB):
1389+
db_offset: int = 0, layout_offset: int = 0, row_offset: int = 0,
1390+
area: Areas = Areas.DB):
13851391
""" Creates a new instance of the `Row` class.
13861392
13871393
Args:
@@ -1415,7 +1421,7 @@ def __init__(self, db_number: int, bytearray_: bytearray,
14151421
self.make_rows()
14161422

14171423
def make_rows(self):
1418-
""" Make each row for the DB. """
1424+
""" Make each row for the DB."""
14191425
id_field = self.id_field
14201426
row_size = self.row_size
14211427
specification = self.specification
@@ -1442,14 +1448,66 @@ def make_rows(self):
14421448
self.index[key] = row
14431449

14441450
def __getitem__(self, key: str, default: Optional[None] = None) -> Union[None, int, float, str, bool, datetime]:
1451+
"""Access a row of the table through its index.
1452+
1453+
Rows (values) are of type :class:`DB_Row`.
1454+
1455+
Notes:
1456+
This method has the same semantics as :class:`dict` access.
1457+
"""
14451458
return self.index.get(key, default)
14461459

14471460
def __iter__(self):
1461+
"""Iterate over the items contained in the table, in the physical order they are contained
1462+
in memory.
1463+
1464+
Notes:
1465+
This method does not have the same semantics as :class:`dict` iteration. Instead, it
1466+
has the same semantics as the :func:`~DB.items` method, yielding ``(index, row)``
1467+
tuples.
1468+
"""
14481469
yield from self.index.items()
14491470

14501471
def __len__(self):
1472+
"""Return the number of rows contained in the DB.
1473+
1474+
Notes:
1475+
If more than one row has the same index value, it is only counted once.
1476+
"""
14511477
return len(self.index)
14521478

1479+
def __contains__(self, key):
1480+
"""Return whether the given key is the index of a row in the DB."""
1481+
return key in self.index
1482+
1483+
def keys(self):
1484+
"""Return a *view object* of the keys that are used as indices for the rows in the
1485+
DB.
1486+
"""
1487+
yield from self.index.keys()
1488+
1489+
def items(self):
1490+
"""Return a *view object* of the items (``(index, row)`` pairs) that are used as indices
1491+
for the rows in the DB.
1492+
"""
1493+
yield from self.index.items()
1494+
1495+
def export(self):
1496+
"""Export the object to an :class:`OrderedDict`, where each item in the dictionary
1497+
has an index as the key, and the value of the DB row associated with that index
1498+
as a value, represented itself as a :class:`dict` (as returned by :func:`DB_Row.export`).
1499+
1500+
The outer dictionary contains the rows in the physical order they are contained in
1501+
memory.
1502+
1503+
Notes:
1504+
This function effectively returns a snapshot of the DB.
1505+
"""
1506+
ret = OrderedDict()
1507+
for (k, v) in self.items():
1508+
ret[k] = v.export()
1509+
return ret
1510+
14531511
def set_data(self, bytearray_: bytearray):
14541512
"""Set the new buffer data from the PLC to the current instance.
14551513
@@ -1463,6 +1521,63 @@ def set_data(self, bytearray_: bytearray):
14631521
raise TypeError(f"Value bytearray_: {bytearray_} is not from type bytearray")
14641522
self._bytearray = bytearray_
14651523

1524+
def read(self, client: Client):
1525+
"""Reads all the rows from the PLC to the :obj:`bytearray` of this instance.
1526+
1527+
Args:
1528+
client: :obj:`Client` snap7 instance.
1529+
1530+
Raises:
1531+
:obj:`ValueError`: if the `row_size` is less than 0.
1532+
"""
1533+
if self.row_size < 0:
1534+
raise ValueError("row_size must be greater equal zero.")
1535+
1536+
total_size = self.size * (self.row_size + self.row_offset)
1537+
if self.area == Areas.DB: # note: is it worth using the upload method?
1538+
bytearray_ = client.db_read(self.db_number, self.db_offset, total_size)
1539+
else:
1540+
bytearray_ = client.read_area(self.area, 0, self.db_offset, total_size)
1541+
1542+
# replace data in bytearray
1543+
for i, b in enumerate(bytearray_):
1544+
self._bytearray[i + self.db_offset] = b
1545+
1546+
# todo: optimize by only rebuilding the index instead of all the DB_Row objects
1547+
self.index.clear()
1548+
self.make_rows()
1549+
1550+
def write(self, client):
1551+
"""Writes all the rows from the :obj:`bytearray` of this instance to the PLC
1552+
1553+
Notes:
1554+
When the row_offset property has been set to something other than None while
1555+
constructing this object, this operation is not guaranteed to be atomic.
1556+
1557+
Args:
1558+
client: :obj:`Client` snap7 instance.
1559+
1560+
Raises:
1561+
:obj:`ValueError`: if the `row_size` is less than 0.
1562+
"""
1563+
if self.row_size < 0:
1564+
raise ValueError("row_size must be greater equal zero.")
1565+
1566+
# special case: we have a row offset, so we must write each row individually
1567+
# this is because we don't want to change the data before the offset
1568+
if self.row_offset:
1569+
for _, v in self.index.items():
1570+
v.write(client)
1571+
return
1572+
1573+
total_size = self.size * (self.row_size + self.row_offset)
1574+
data = self._bytearray[self.db_offset:self.db_offset + total_size]
1575+
1576+
if self.area == Areas.DB:
1577+
client.db_write(self.db_number, self.db_offset, data)
1578+
else:
1579+
client.write_area(self.area, 0, self.db_offset, data)
1580+
14661581

14671582
class DB_Row:
14681583
"""
@@ -1755,7 +1870,7 @@ def read(self, client: Client) -> None:
17551870
if self.area == Areas.DB:
17561871
bytearray_ = client.db_read(db_nr, self.db_offset, self.row_size)
17571872
else:
1758-
bytearray_ = client.read_area(self.area, 0, 0, self.row_size)
1873+
bytearray_ = client.read_area(self.area, 0, self.db_offset, self.row_size)
17591874

17601875
data = self.get_bytearray()
17611876
# replace data in bytearray

test/test_util.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,27 @@ def test_db_creation(self):
373373
self.assertEqual(row['testbool8'], 0)
374374
self.assertEqual(row['NAME'], 'test')
375375

376+
def test_db_export(self):
377+
test_array = bytearray(_bytearray * 10)
378+
test_db = util.DB(1, test_array, test_spec,
379+
row_size=len(_bytearray),
380+
size=10,
381+
layout_offset=4,
382+
db_offset=0)
383+
384+
db_export = test_db.export()
385+
for i in db_export:
386+
self.assertEqual(db_export[i]['testbool1'], 1)
387+
self.assertEqual(db_export[i]['testbool2'], 1)
388+
self.assertEqual(db_export[i]['testbool3'], 1)
389+
self.assertEqual(db_export[i]['testbool4'], 1)
390+
391+
self.assertEqual(db_export[i]['testbool5'], 0)
392+
self.assertEqual(db_export[i]['testbool6'], 0)
393+
self.assertEqual(db_export[i]['testbool7'], 0)
394+
self.assertEqual(db_export[i]['testbool8'], 0)
395+
self.assertEqual(db_export[i]['NAME'], 'test')
396+
376397
def test_get_real(self):
377398
test_array = bytearray(_bytearray)
378399
row = util.DB_Row(test_array, test_spec, layout_offset=4)

0 commit comments

Comments
 (0)