roslib.message: moving most routines to genpy

This commit is contained in:
Ken Conley 2012-01-05 02:39:59 +00:00
parent 64b78ea3b1
commit 010df83e1e
1 changed files with 21 additions and 513 deletions

View File

@ -34,68 +34,42 @@
"""
Support library for Python autogenerated message files. This defines
the L{Message} base class used by genmsg_py as well as support
the Message base class used by genmsg_py as well as support
libraries for type checking and retrieving message classes by type
name.
"""
import math
import itertools
import traceback
import struct
import os
import sys
import roslib.names
import genpy
from genpy import Time, Duration, TVal
# common struct pattern singletons for msgs to use. Although this
# would better placed in a generator-specific module, we don't want to
# add another import to messages (which incurs higher import cost)
if sys.version > '3':
long = int
struct_I = struct.Struct('<I')
def isstring(s):
"""Small helper version to check an object is a string in a way that works
for both Python 2 and 3
"""
try:
return isinstance(s, basestring)
except NameError:
return isinstance(s, str)
class ROSMessageException(Exception):
"""
Exception type for errors in roslib.message routines
"""
pass
# forward a bunch of old symbols from genmsg for backwards compat
from genmsg.message imoprt check_type, strify_message
from genpy import Message, DeserializationError, SerializationError, \
Time, Duration, TVal
from genpy.message import get_printable_message_args, fill_message_args
def _get_message_or_service_class(type_str, message_type, reload_on_error=False):
"""
Utility for retrieving message/service class instances. Used by
get_message_class and get_service_class.
@param type_str: 'msg' or 'srv'
@type type_str: str
@param message_type: type name of message/service
@type message_type: str
@return: Message/Service for message/service type or None
@rtype: class
@raise ValueError: if message_type is invalidly specified
:param type_str: 'msg' or 'srv', ``str``
:param message_type: type name of message/service, ``str``
:returns: Message/Service for message/service type or None, ``class``
:raises: :exc:`ValueError` If message_type is invalidly specified
"""
## parse package and local type name for import
package, base_type = roslib.names.package_resource_name(message_type)
if not package:
if base_type == roslib.msgs.HEADER:
if base_type == 'Header':
package = 'std_msgs'
else:
raise ValueError("message type is missing package name: %s"%str(message_type))
pypkg = val = None
try:
# bootstrap our sys.path
roslib.launcher.load_manifest(package)
# import the package and return the class
pypkg = __import__('%s.%s'%(package, type_str))
val = getattr(getattr(pypkg, type_str), base_type)
@ -124,14 +98,12 @@ def get_message_class(message_type, reload_on_error=False):
"""
Get the message class. NOTE: this function maintains a
local cache of results to improve performance.
@param message_type: type name of message
@type message_type: str
@param reload_on_error: (optional). Attempt to reload the Python
:param message_type: type name of message, ``str``
:param reload_on_error: (optional). Attempt to reload the Python
module if unable to load message the first time. Defaults to
False. This is necessary if messages are built after the first load.
@return: Message class for message/service type
@rtype: Message class
@raise ValueError: if message_type is invalidly specified
:returns: Message class for message/service type, ``Message class``
:raises :exc:`ValueError`: if message_type is invalidly specified
"""
if message_type in _message_class_cache:
return _message_class_cache[message_type]
@ -147,14 +119,12 @@ def get_service_class(service_type, reload_on_error=False):
"""
Get the service class. NOTE: this function maintains a
local cache of results to improve performance.
@param service_type: type name of service
@type service_type: str
@param reload_on_error: (optional). Attempt to reload the Python
:param service_type: type name of service, ``str``
:param reload_on_error: (optional). Attempt to reload the Python
module if unable to load message the first time. Defaults to
False. This is necessary if messages are built after the first load.
@return: Service class for service type
@rtype: Service class
@raise Exception: if service_type is invalidly specified
:returns: Service class for service type, ``Service class``
:raises :exc:`Exception` If service_type is invalidly specified
"""
if service_type in _service_class_cache:
return _service_class_cache[service_type]
@ -162,465 +132,3 @@ def get_service_class(service_type, reload_on_error=False):
_service_class_cache[service_type] = cls
return cls
# we expose the generic message-strify routine for fn-oriented code like rostopic
def strify_message(val, indent='', time_offset=None, current_time=None, field_filter=None):
"""
Convert value to string representation
@param val: to convert to string representation. Most likely a Message.
@type val: Value
@param indent: indentation. If indent is set, then the return value will have a leading \n
@type indent: str
@param time_offset: if not None, time fields will be displayed
as deltas from time_offset
@type time_offset: Time
@param current_time: currently not used. Only provided for API compatibility. current_time passes in the current time with respect to the message.
@type current_time: Time
@param field_filter: filter the fields that are strified for Messages.
@type field_filter: fn(Message)->iter(str)
@return: string (YAML) representation of message
@rtype: str
"""
type_ = type(val)
if type_ in (int, long, float, bool):
return str(val)
elif isstring(val):
#TODO: need to escape strings correctly
if not val:
return "''"
return val
elif isinstance(val, TVal):
if time_offset is not None and isinstance(val, Time):
val = val-time_offset
return '\n%ssecs: %s\n%snsecs: %s'%(indent, val.secs, indent, val.nsecs)
elif type_ in (list, tuple):
if len(val) == 0:
return "[]"
val0 = val[0]
if type(val0) in (int, float, str, bool):
# TODO: escape strings properly
return str(list(val))
else:
pref = indent + '- '
indent = indent + ' '
return '\n'+'\n'.join([pref+strify_message(v, indent, time_offset, current_time, field_filter) for v in val])
elif isinstance(val, Message):
# allow caller to select which fields of message are strified
if field_filter is not None:
fields = list(field_filter(val))
else:
fields = val.__slots__
p = '%s%%s: %%s'%(indent)
ni = ' '+indent
if sys.hexversion > 0x03000000: #Python3
vals = '\n'.join([p%(f,
strify_message(_convert_getattr(val, f, t), ni, time_offset, current_time, field_filter)) for f,t in zip(val.__slots__, val._slot_types) if f in fields])
else: #Python2
vals = '\n'.join([p%(f,
strify_message(_convert_getattr(val, f, t), ni, time_offset, current_time, field_filter)) for f,t in itertools.izip(val.__slots__, val._slot_types) if f in fields])
if indent:
return '\n'+vals
else:
return vals
else:
return str(val) #punt
def _convert_getattr(val, f, t):
"""
Convert atttribute types on the fly, if necessary. This is mainly
to convert uint8[] fields back to an array type.
"""
attr = getattr(val, f)
if isstring(attr) and 'uint8[' in t:
return [ord(x) for x in attr]
else:
return attr
# check_type mildly violates some abstraction boundaries between .msg
# representation and the python Message representation. The
# alternative is to have the message generator map .msg types to
# python types beforehand, but that would make it harder to do
# width/signed checks.
_widths = {
'byte': 8, 'char': 8, 'int8': 8, 'uint8': 8,
'int16': 16, 'uint16': 16,
'int32': 32, 'uint32': 32,
'int64': 64, 'uint64': 64,
}
def check_type(field_name, field_type, field_val):
"""
Dynamic type checker that maps ROS .msg types to python types and
verifies the python value. check_type() is not designed to be
fast and is targeted at error diagnosis. This type checker is not
designed to run fast and is meant only for error diagnosis.
@param field_name: ROS .msg field name
@type field_name: str
@param field_type: ROS .msg field type
@type field_type: str
@param field_val: field value
@type field_val: Any
@raise SerializationError: if typecheck fails
"""
# lazy-import as roslib.genpy has lots of extra imports. Would
# prefer to do lazy-init in a different manner
import roslib.genpy_electric
if roslib.genpy_electric.is_simple(field_type):
# check sign and width
if field_type in ['byte', 'int8', 'int16', 'int32', 'int64']:
if type(field_val) not in [long, int]:
raise SerializationError('field %s must be an integer type'%field_name)
maxval = int(math.pow(2, _widths[field_type]-1))
if field_val >= maxval or field_val <= -maxval:
raise SerializationError('field %s exceeds specified width [%s]'%(field_name, field_type))
elif field_type in ['char', 'uint8', 'uint16', 'uint32', 'uint64']:
if type(field_val) not in [long, int] or field_val < 0:
raise SerializationError('field %s must be unsigned integer type'%field_name)
maxval = int(math.pow(2, _widths[field_type]))
if field_val >= maxval:
raise SerializationError('field %s exceeds specified width [%s]'%(field_name, field_type))
elif field_type == 'bool':
if field_val not in [True, False, 0, 1]:
raise SerializationError('field %s is not a bool'%(field_name))
elif field_type == 'string':
if sys.hexversion > 0x03000000:
if type(field_val) == str:
raise SerializationError('field %s is a unicode string instead of an ascii string'%field_name)
else:
if type(field_val) == unicode:
raise SerializationError('field %s is a unicode string instead of an ascii string'%field_name)
elif not isstring(field_val):
raise SerializationError('field %s must be of type str'%field_name)
elif field_type == 'time':
if not isinstance(field_val, Time):
raise SerializationError('field %s must be of type Time'%field_name)
elif field_type == 'duration':
if not isinstance(field_val, Duration):
raise SerializationError('field %s must be of type Duration'%field_name)
elif field_type.endswith(']'): # array type
# use index to generate error if '[' not present
base_type = field_type[:field_type.index('[')]
if type(field_val) == str:
if not base_type in ['char', 'uint8']:
raise SerializationError('field %s must be a list or tuple type. Only uint8[] can be a string' % field_name);
else:
#It's a string so its already in byte format and we
#don't need to check the individual bytes in the
#string.
return
if not type(field_val) in [list, tuple]:
raise SerializationError('field %s must be a list or tuple type'%field_name)
for v in field_val:
check_type(field_name+"[]", base_type, v)
else:
if isinstance(field_val, Message):
# roslib/Header is the old location of Header. We check it for backwards compat
if field_val._type in ['std_msgs/Header', 'roslib/Header']:
if field_type not in ['Header', 'std_msgs/Header', 'roslib/Header']:
raise SerializationError("field %s must be a Header instead of a %s"%(field_name, field_val._type))
elif field_val._type != field_type:
raise SerializationError("field %s must be of type %s instead of %s"%(field_name, field_type, field_val._type))
for n, t in zip(field_val.__slots__, field_val._get_types()):
check_type("%s.%s"%(field_name,n), t, getattr(field_val, n))
else:
raise SerializationError("field %s must be of type [%s]"%(field_name, field_type))
#TODO: dynamically load message class and do instance compare
class Message(object):
"""Base class of Message data classes auto-generated from msg files. """
# slots is explicitly both for data representation and
# performance. Higher-level code assumes that there is a 1-to-1
# mapping between __slots__ and message fields. In terms of
# performance, explicitly settings slots eliminates dictionary for
# new-style object.
__slots__ = ['_connection_header']
def __init__(self, *args, **kwds):
"""
Create a new Message instance. There are multiple ways of
initializing Message instances, either using a 1-to-1
correspondence between constructor arguments and message
fields (*args), or using Python "keyword" arguments (**kwds) to initialize named field
and leave the rest with default values.
"""
if args and kwds:
raise TypeError("Message constructor may only use args OR keywords, not both")
if args:
if len(args) != len(self.__slots__):
raise TypeError("Invalid number of arguments, args should be %s"%str(self.__slots__)+" args are"+str(args))
for i, k in enumerate(self.__slots__):
setattr(self, k, args[i])
else:
# validate kwds
for k,v in kwds.items():
if not k in self.__slots__:
raise AttributeError("%s is not an attribute of %s"%(k, self.__class__.__name__))
# iterate through slots so all fields are initialized.
# this is important so that subclasses don't reference an
# uninitialized field and raise an AttributeError.
for k in self.__slots__:
if k in kwds:
setattr(self, k, kwds[k])
else:
setattr(self, k, None)
def __getstate__(self):
"""
support for Python pickling
"""
return [getattr(self, x) for x in self.__slots__]
def __setstate__(self, state):
"""
support for Python pickling
"""
for x, val in zip(self.__slots__, state):
setattr(self, x, val)
def _get_types(self):
raise Exception("must be overriden")
def _check_types(self, exc=None):
"""
Perform dynamic type-checking of Message fields. This is performance intensive
and is meant for post-error diagnosis
@param exc: underlying exception that gave cause for type check.
@type exc: Exception
@raise roslib.messages.SerializationError: if typecheck fails
"""
for n, t in zip(self.__slots__, self._get_types()):
check_type(n, t, getattr(self, n))
if exc: # if exc is set and check_type could not diagnose, raise wrapped error
raise SerializationError(str(exc))
def serialize(self, buff):
"""
Serialize data into buffer
@param buff: buffer
@type buff: StringIO
"""
pass
def deserialize(self, str):
"""
Deserialize data in str into this instance
@param str: serialized data
@type str: str
"""
pass
def __repr__(self):
return strify_message(self)
def __str__(self):
return strify_message(self)
# TODO: unit test
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
for f in self.__slots__:
try:
v1 = getattr(self, f)
v2 = getattr(other, f)
if type(v1) in (list, tuple) and type(v2) in (list, tuple):
# we treat tuples and lists as equivalent
if tuple(v1) != tuple(v2):
return False
elif not v1 == v2:
return False
except AttributeError:
return False
return True
class DeserializationError(ROSMessageException):
"""Message deserialization error"""
pass
class SerializationError(ROSMessageException):
"""Message serialization error"""
pass
# Utilities for rostopic/rosservice
def get_printable_message_args(msg, buff=None, prefix=''):
"""
Get string representation of msg arguments
@param msg: msg message to fill
@type msg: Message
@param prefix: field name prefix (for verbose printing)
@type prefix: str
@return: printable representation of msg args
@rtype: str
"""
try:
from cStringIO import StringIO # Python 2.x
python3 = 0
except ImportError:
from io import BytesIO # Python 3.x
python3 = 1
if buff is None:
if python3 == 1:
buff = BytesIO()
else:
buff = StringIO()
for f in msg.__slots__:
if isinstance(getattr(msg, f), Message):
get_printable_message_args(getattr(msg, f), buff=buff, prefix=(prefix+f+'.'))
else:
buff.write(prefix+f+' ')
return buff.getvalue().rstrip()
def _fill_val(msg, f, v, keys, prefix):
"""
Subroutine of L{_fill_message_args()}. Sets a particular field on a message
@param f: field name
@type f: str
@param v: field value
@param keys: keys to use as substitute values for messages and timestamps.
@type keys: dict
"""
if not f in msg.__slots__:
raise ROSMessageException("No field name [%s%s]"%(prefix, f))
def_val = getattr(msg, f)
if isinstance(def_val, Message) or isinstance(def_val, genpy.TVal):
# check for substitution key, e.g. 'now'
if type(v) == str:
if v in keys:
setattr(msg, f, keys[v])
else:
raise ROSMessageException("No key named [%s]"%(v))
elif isinstance(def_val, genpy.TVal) and type(v) in (int, long):
#special case to handle time value represented as a single number
#TODO: this is a lossy conversion
if isinstance(def_val, genpy.Time):
setattr(msg, f, genpy.Time.from_sec(v/1e9))
elif isinstance(def_val, genpy.Duration):
setattr(msg, f, genpy.Duration.from_sec(v/1e9))
else:
raise ROSMessageException("Cannot create time values of type [%s]"%(type(def_val)))
else:
_fill_message_args(def_val, v, keys, prefix=(prefix+f+'.'))
elif type(def_val) == list:
if not type(v) in [list, tuple]:
raise ROSMessageException("Field [%s%s] must be a list or tuple instead of: %s"%(prefix, f, type(v).__name__))
# determine base_type of field by looking at _slot_types
idx = msg.__slots__.index(f)
t = msg._slot_types[idx]
base_type = roslib.msgs.base_msg_type(t)
# - for primitives, we just directly set (we don't
# type-check. we rely on serialization type checker)
if base_type in roslib.msgs.PRIMITIVE_TYPES:
setattr(msg, f, v)
# - for complex types, we have to iteratively append to def_val
else:
list_msg_class = get_message_class(base_type)
for el in v:
inner_msg = list_msg_class()
_fill_message_args(inner_msg, el, prefix)
def_val.append(inner_msg)
else:
#print "SET2", f, v
setattr(msg, f, v)
def _fill_message_args(msg, msg_args, keys, prefix=''):
"""
Populate message with specified args.
@param msg: message to fill
@type msg: Message
@param msg_args: list of arguments to set fields to
@type msg_args: [args]
@param keys: keys to use as substitute values for messages and timestamps.
@type keys: dict
@param prefix: field name prefix (for verbose printing)
@type prefix: str
@return: unused/leftover message arguments.
@rtype: [args]
@raise ROSMessageException: if not enough message arguments to fill message
@raise ValueError: if msg or msg_args is not of correct type
"""
if not isinstance(msg, (Message, genpy.TVal)):
raise ValueError("msg must be a Message instance: %s"%msg)
if type(msg_args) == dict:
#print "DICT ARGS", msg_args
#print "ACTIVE SLOTS",msg.__slots__
for f, v in msg_args.items():
# assume that an empty key is actually an empty string
if v == None:
v = ''
_fill_val(msg, f, v, keys, prefix)
elif type(msg_args) == list:
#print "LIST ARGS", msg_args
#print "ACTIVE SLOTS",msg.__slots__
if len(msg_args) > len(msg.__slots__):
raise ROSMessageException("Too many arguments:\n * Given: %s\n * Expected: %s"%(msg_args, msg.__slots__))
elif len(msg_args) < len(msg.__slots__):
raise ROSMessageException("Not enough arguments:\n * Given: %s\n * Expected: %s"%(msg_args, msg.__slots__))
for f, v in zip(msg.__slots__, msg_args):
_fill_val(msg, f, v, keys, prefix)
else:
raise ValueError("invalid msg_args type: %s"%str(msg_args))
def fill_message_args(msg, msg_args, keys={}):
"""
Populate message with specified args. Args are assumed to be a
list of arguments from a command-line YAML parser. See
http://www.ros.org/wiki/ROS/YAMLCommandLine for specification on
how messages are filled.
fill_message_args also takes in an optional 'keys' dictionary
which contain substitute values for message and time types. These
values must be of the correct instance type, i.e. a Message, Time,
or Duration. In a string key is encountered with these types, the
value from the keys dictionary will be used instead. This is
mainly used to provide values for the 'now' timestamp.
@param msg: message to fill
@type msg: Message
@param msg_args: list of arguments to set fields to, or
If None, msg_args will be made an empty list.
@type msg_args: [args]
@param keys: keys to use as substitute values for messages and timestamps.
@type keys: dict
@raise ROSMessageException: if not enough/too many message arguments to fill message
"""
# a list of arguments is similar to python's
# *args, whereas dictionaries are like **kwds.
# empty messages serialize as a None, which we make equivalent to
# an empty message
if msg_args is None:
msg_args = []
# msg_args is always a list, due to the fact it is parsed from a
# command-line argument list. We have to special-case handle a
# list with a single dictionary, which has precedence over the
# general list representation. We offer this precedence as there
# is no other way to do kwd assignments into the outer message.
if len(msg_args) == 1 and type(msg_args[0]) == dict:
# according to spec, if we only get one msg_arg and it's a dictionary, we
# use it directly
_fill_message_args(msg, msg_args[0], keys, '')
else:
_fill_message_args(msg, msg_args, keys, '')