Skip to content

Commit 7b600b8

Browse files
committed
Merge branch 'feature-tag-struct'; Version 4.4.0
o Initial support for C*Logix UDT structures o Change to cpppo.server.enip.main API import conventions
2 parents a262223 + daba406 commit 7b600b8

37 files changed

+40242
-642
lines changed

LICENSE

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
Cpppo is distributed under the GNU General Public License version 3 and is also
2-
available under alternative licenses negotiated directly with Hard Consulting
3-
Corporation. If you obtained Cpppo under the GPL, then the GPL applies to all
4-
Cpppo modules used on your system as well, except as defined below. The GPL
5-
(version 3) is included in this source tree in the file COPYING.
2+
available under alternative licenses negotiated directly with Dominion Research
3+
& Development Corp. If you obtained Cpppo under the GPL, then the GPL applies
4+
to all Cpppo modules used on your system as well, except as defined below. The
5+
GPL (version 3) is included in this source tree in the file COPYING.
66

77
Hard Consulting Corporation holds copyright and/or sufficient licenses to all
88
components of the Cpppo package, and therefore can grant, at its sole
@@ -28,9 +28,5 @@ licensed under any license you wish.
2828
If you have any questions regarding our licensing policy, please contact us:
2929

3030
+1.780.970.8148
31-
32-
33-
Hard Consulting Corporation
34-
74 Greystone Crescent
35-
Spruce Grove, AB, T7X 0A7
36-
Canada
31+
32+
Dominion Research & Development Corp.

README.org

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@
315315
from urllib.parse import urlencode
316316

317317
from cpppo.server.enip import device, REAL
318-
from cpppo.server.enip.main import main
318+
from cpppo.server.enip.main import main as enip_main
319319

320320
class Attribute_weather( device.Attribute ):
321321
OPT = {
@@ -351,7 +351,7 @@
351351
def __setitem__( self, key, value ):
352352
raise Exception( "Changing the weather isn't that easy..." )
353353

354-
sys.exit( main( attribute_class=Attribute_weather ))
354+
sys.exit( enip_main( attribute_class=Attribute_weather ))
355355
#+END_EXAMPLE
356356

357357
By providing a specialized implementation of device.Attribute's =__getitem__=

automata.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def __init__( self, name, state=None, predicate=None ):
272272
self.name = name
273273
self.state = state
274274
if predicate is None:
275-
predicate = lambda machine=None, source=None, path=None, data=None: True
275+
predicate = lambda **kwds: True
276276
self.predicate = predicate
277277

278278
def __str__( self ):
@@ -288,7 +288,7 @@ def __call__( self, machine=None, source=None, path=None, data=None ):
288288

289289
def execute( self, truth, machine=None, source=None, path=None, data=None ):
290290
target = self.state if truth else None
291-
#log.info( "%s %-13.13s -> %10s w/ data: %r", machine.name_centered(), self, target, data )
291+
#log.debug( "%s %-13.13s -> %10s w/ data: %r", machine.name_centered(), self, target, data )
292292
return target
293293

294294

dotdict.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
#import logging
3+
import copy
34
import threading
45
import sys
56

@@ -291,11 +292,13 @@ def iteritems( self ):
291292
if isinstance( val, dotdict ) and val: # a non-empty sub-dotdict layer
292293
for subkey,subval in val.iteritems():
293294
yield key+'.'+subkey, subval
294-
elif isinstance( val, list ) and all( isinstance( subelm, dotdict ) for subelm in val ):
295+
elif isinstance( val, list ) and val and all( isinstance( subelm, dotdict ) for subelm in val ):
296+
# Non-empty list of dicts
297+
subfmt = "[{subidx:%d}]." % len( str( len( val ) - 1 ))
295298
for subidx,subelm in enumerate( val ):
296299
for subkey,subval in subelm.iteritems():
297-
yield key+'['+str(subidx)+'].'+subkey, subval
298-
else: # non-list elements, empty dotdict layers
300+
yield key+subfmt.format( subidx=subidx )+subkey, subval
301+
else: # non-list elements, empty dotdict layers, empty lists
299302
yield key, val
300303

301304
def itervalues( self ):
@@ -320,6 +323,15 @@ def __listitems( self ):
320323
values = __listvalues if sys.version_info[0] < 3 else itervalues
321324
items = __listitems if sys.version_info[0] < 3 else iteritems
322325

326+
def __deepcopy__( self, memo ):
327+
"""Must copy each layer, to avoid copying keys that reference non-existent members."""
328+
return type( self )( (k,copy.deepcopy( v, memo ))
329+
for k,v in dict.items( self ) )
330+
331+
def __copy__( self ):
332+
return type( self )( (k,copy.copy( v ))
333+
for k,v in dict.items( self ) )
334+
323335

324336
class apidict( dotdict ):
325337
"""A dotdict that ensures that any new values assigned to its attributes are very likely received by

history/times.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class timestamp( object ):
179179

180180
@classmethod
181181
@mutexmethod( '_cls_lock' )
182-
def support_abbreviations( cls, region, exclude=None, at=None, reach=None, reset=False ):
182+
def support_abbreviations( cls, region, exclude=None, at=None, reach=None, reset=False, first=True ):
183183
"""Add all the DST and non-DST abbreviations for the specified region. If a country code
184184
(eg. 'CA') is specified, we'll get all its timezones from pytz.country_timezones.
185185
Otherwise, we'll get all the matching '<region>[/<city>]' zone(s) from pytz's
@@ -201,15 +201,16 @@ def support_abbreviations( cls, region, exclude=None, at=None, reach=None, reset
201201
You cannot load them both at once. If multiple timezones produce the same abbreviation,
202202
they must have the same DST transitions between 'at' +/- 'reach', or AmbiguousTimeZoneError
203203
will be raised -- the timezone abbreviations have ambiguous meaning, and the zones cannot be
204-
identified via abbreviation at the same time.
204+
identified via abbreviation at the same time. If first is True, we'll warn, but just
205+
default to use the first timezone.
205206
206207
Returns all the timezone abbreviations added to the class's _tzabbrev; you may want to check:
207208
208209
region = 'CA'
209210
abbrevs = timestamp.support_abbreviations( region )
210211
assert abbrevs, "Invalid region %r: Matches no timezones" % region
211212
212-
Timezone definitions change over time. A 'reach' timedelta (default: 1 year) on either side
213+
Timezone definitions change over time. A 'reach' timedelta (default: 1/2 year) on either side
213214
of the 'at' (a naive UTC datetime, default: current time) is required, in order for multiple
214215
zones to use the same abbreviation with guaranteed consistent definitions.
215216
@@ -317,8 +318,8 @@ def format_dst( dst ):
317318
dup = abb in abbrev
318319
if dup and not dst:
319320
# A duplicate; non-DST or ambiguous, must have consistent UTC offset and DST
320-
# designation. We'll allow replacement of a dst=None (still ambiguous) zone with a dst=False zone
321-
321+
# designation. We'll allow replacement of a dst=None (still ambiguous) zone
322+
# with a dst=False zone
322323
abbtzi,abbdst,abboff= abbrev[abb]
323324
if abboff != off:
324325
msg += " x %-5s %s %s in %s; incompatible" % (
@@ -340,7 +341,7 @@ def format_dst( dst ):
340341
msg = "%s has %d time changes vs. %d in %s" % (
341342
abb, lst-nxt, abbtzilst-abbtzinxt, abbtzi )
342343
incompatible.append( "%s: %s" % ( tzinfo, msg ))
343-
log.warning( "%-30s: %s", tzinfo, msg )
344+
log.warning( "%-30s: inconsistency: %s", tzinfo, msg )
344345
continue
345346
chg = zip( tzinfo._utc_transition_times[nxt:lst], tzinfo._transition_info[nxt:lst] )
346347
abbchg = zip( abbtzi._utc_transition_times[abbtzinxt:abbtzilst], abbtzi._transition_info[abbtzinxt:abbtzilst] )
@@ -364,7 +365,7 @@ def transition_consistent( zt1, zt2 ):
364365
dt.strftime( cls._fmt ), "; Ignoring duplicate" if dup else "" )
365366
if not dup:
366367
abbrev[abb] = tzinfo,dst,off
367-
if incompatible:
368+
if incompatible and not first:
368369
raise AmbiguousTimeZoneError( "%-30s region(s) incompatible: %s" % ( region, ", ".join( incompatible )))
369370
added = list( set( abbrev ) - set( cls._tzabbrev ))
370371
cls._tzabbrev = abbrev

history_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_history_timestamp_abbreviations():
6060
# Try to add all of the Americas to the CA abbreviations already supported; can't be done (too
6161
# many inconsistencies)
6262
try:
63-
abbrev = timestamp.support_abbreviations( 'America' )
63+
abbrev = timestamp.support_abbreviations( 'America', first=False )
6464
assert False, "Many zones should have been ambiguously abbreviated"
6565
except AmbiguousTimeZoneError as exc:
6666
assert "America/Mazatlan" in str( exc )
@@ -120,7 +120,7 @@ def test_history_timestamp_abbreviations():
120120

121121
assert 'EEST' in timestamp._tzabbrev
122122
try:
123-
timestamp.support_abbreviations( 'Asia' )
123+
timestamp.support_abbreviations( 'Asia', first=False )
124124
assert False, "Asia/Jerusalem IST should have mismatched Europe/Dublin IST"
125125
except AmbiguousTimeZoneError as exc:
126126
assert "Asia/Jerusalem" in str( exc )

misc.py

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import sys
2727
import time
2828
import types
29+
import re
2930

3031
# Import ip_address/network and urlparse into the cpppo namespace. ip_address requires unicode, so
3132
# we also provide a Python2 shim to ensure a str is interpreted as unicode, as well as provide
@@ -41,6 +42,11 @@
4142
except ImportError:
4243
import repr as reprlib
4344

45+
try:
46+
xrange(0,1)
47+
except NameError:
48+
xrange = range
49+
4450
__author__ = "Perry Kundert"
4551
__email__ = "[email protected]"
4652
__copyright__ = "Copyright (c) 2013 Hard Consulting Corporation"
@@ -524,7 +530,8 @@ def wrapper( *args, **kwds ):
524530
return wrapper
525531
return decorator
526532

527-
def hexdump( src, length=16, sep='.' ):
533+
534+
def hexdumper( src, offset=0, length=16, sep='.', quote='|' ):
528535
'''
529536
@brief Return {src} in hex dump.
530537
@param[in] length {Int} Nb Bytes by row.
@@ -535,12 +542,6 @@ def hexdump( src, length=16, sep='.' ):
535542
'''
536543
result = []
537544

538-
# Python3 support
539-
try:
540-
xrange(0,1);
541-
except NameError:
542-
xrange = range;
543-
544545
for i in xrange(0, len(src), length):
545546
subSrc = src[i:i+length];
546547
hexa = '';
@@ -564,9 +565,105 @@ def hexdump( src, length=16, sep='.' ):
564565
text += chr(c);
565566
else:
566567
text += sep;
567-
result.append(('%08X: %-'+str(length*(2+1)+1)+'s |%s|') % (i, hexa, text));
568+
yield "{addr:08X}: {hexa:<{hexawidth}s} {quote}{text}{quote}".format(
569+
addr=i+offset, hexa=hexa, hexawidth=length*(2+1)+1, text=text, quote=quote or '' )
570+
571+
572+
def hexdump( src, offset=0, length=16, sep='.', quote='|' ):
573+
return '\n'.join( hexdumper( src, offset=offset, length=length, sep=sep, quote=quote ))
574+
575+
576+
def hexdump_differs( *dumps, **kwds ): # Python3 version: ', inclusive=False ):'
577+
"""Compare a number of hexdump outputs side by side, returning differing lines."""
578+
inclusive = kwds.get( 'inclusive', False ) # for Python2 compatibility
579+
lines = [ d.split( '\n' ) for d in dumps ]
580+
differs = []
581+
for cols in zip( *lines ):
582+
same = all( c == cols[0] for c in cols[1:] )
583+
if not same or inclusive:
584+
differs.append(( ' == ' if same else ' != ' ).join( cols ))
585+
return '\n'.join( differs )
586+
587+
588+
def hexdecode( enc, offset=0, sep=':' ):
589+
"""Decode hex octets "ab:cd:ef:01..." (starting at off bytes in) into b"\xab\xcd\xef\x01..." """
590+
return bytes(bytearray.fromhex( ''.join( enc.split( sep ))))[offset:]
591+
592+
593+
def hexloader( dump, offset=0, fill=False, skip=False ):
594+
"""Load data from a iterable hex dump, eg, either as a sequence of rows or a string:
595+
596+
00003FD0: 3F D0 00 00 00 00 00 00 00 00 00 00 12 00 00 00 |................|
597+
598+
00003FF0: 3F F0 00 00 00 00 00 00 00 00 00 00 12 00 00 00 |................|
599+
00004000: 40 00 30 31 20 53 45 34 20 45 20 32 33 2e 35 63 |@.01 SE4 E 23.5c|
600+
601+
Yields a corresponding sequence of address,bytes. To ignore the address
602+
and get the data:
603+
604+
b''.join( data for addr,data in hexload( ... )
605+
606+
If fill may be False/b'', or a single-byte value used to in-fill any missing
607+
address ranges.
608+
609+
If skip is Truthy, we allow and skip empty/non-matching lines.
610+
If gaps is Truthy, allow gaps in addresses.
611+
"""
612+
if fill:
613+
assert isinstance( fill, bytes ) and len( fill ) == 1, \
614+
"fill must be a bytes singleton, not {fill!r}".format( fill=fill )
615+
if isinstance( dump, basestring if sys.version_info[0] < 3 else str ):
616+
dump = dump.split( '\n' )
617+
for row in dump:
618+
if not row.strip():
619+
continue # all whitespace; ignore
620+
match = hexloader.parser.match( row )
621+
if not match:
622+
assert skip, \
623+
"Failed to match a hex dump on row: {row!r}".format( row=row )
624+
continue
625+
addr = int( match.group( 'address' ), 16 )
626+
data = hexdecode( match.group( 'values' ), sep=' ' )
627+
628+
if addr > offset:
629+
# row address is beyond current offset; fill, or skip offset ahead
630+
if fill:
631+
yield offset,(fill * ( addr - offset ))
632+
offset = addr
633+
if addr < offset:
634+
# Row starts before desired offset; skip or clip
635+
if addr + len( data ) <= offset:
636+
continue
637+
data = data[offset-addr:]
638+
addr = offset
639+
yield addr,data
640+
offset = addr + len( data )
641+
642+
hexloader.parser = re.compile(
643+
r"""^
644+
\s*
645+
(?P<address>
646+
{hexclass}{{1,16}} # address
647+
)
648+
[:]\s* # : whitespace
649+
(?P<values>
650+
(?:\s{{0,2}}{hexclass}{{2}})+ # hex pairs separated by 0-2 whitespace
651+
)
652+
(?:
653+
\s+ # whitespace at end
654+
(?P<quote>\|?) # | (optional ..print.. quote)
655+
(?P<print>
656+
.* # |..print..|
657+
)
658+
(?P=quote) # | (optional ..print.. quote)
659+
)? # entire ..print.. section optional
660+
$""".format( hexclass='[0-9A-Fa-f]' ), re.VERBOSE )
661+
662+
663+
def hexload( dump, offset=0, fill=False, skip=False ):
664+
"""Return bytes data specified from dump"""
665+
return b''.join( d for a,d in hexloader( dump, offset=offset, fill=fill, skip=skip ))
568666

569-
return '\n'.join(result);
570667

571668
#
572669
# unicode, ip/network, parse_ip_port -- handle unicode/str IP addresses

0 commit comments

Comments
 (0)