1
1
"""Provides extensions for containers of Drake-related objects."""
2
2
3
3
import numpy as np
4
+ import re
4
5
5
6
6
7
class _EqualityProxyBase :
@@ -71,7 +72,6 @@ class EqualToDict(_DictKeyWrap):
71
72
`lhs.EqualTo(rhs)`.
72
73
"""
73
74
def __init__ (self , * args , ** kwargs ):
74
-
75
75
class Proxy (_EqualityProxyBase ):
76
76
def __eq__ (self , other ):
77
77
T = type (self .value )
@@ -118,8 +118,7 @@ def __setattr__(self, name, value):
118
118
if not hasattr (self , name ):
119
119
raise AttributeError (
120
120
"Cannot add attributes! The fields in this named view are"
121
- f"{ self .get_fields ()} , but you tried to set '{ name } '."
122
- )
121
+ f"{ self .get_fields ()} , but you tried to set '{ name } '." )
123
122
object .__setattr__ (self , name , value )
124
123
125
124
def __len__ (self ):
@@ -142,22 +141,41 @@ def __repr__(self):
142
141
@staticmethod
143
142
def _item_property (i ):
144
143
# Maps an item (at a given index) to a property.
145
- return property (
146
- fget = lambda self : self [i ],
147
- fset = lambda self , value : self .__setitem__ (i , value ))
144
+ return property (fget = lambda self : self [i ],
145
+ fset = lambda self , value : self .__setitem__ (i , value ))
148
146
149
147
@classmethod
150
148
def Zero (cls ):
151
149
"""Constructs a view onto values set to all zeros."""
152
- return cls ([0 ]* len (cls ._fields ))
150
+ return cls ([0 ] * len (cls ._fields ))
151
+
153
152
153
+ def _sanitize_field_name (name : str ):
154
+ result = name
155
+ # Ensure the first character is a valid opener (e.g., no numbers allowed).
156
+ if not result [0 ].isidentifier ():
157
+ result = "_" + result
158
+ # Ensure that each additional character is valid in turn, avoiding the
159
+ # special case for opening characters by prepending "_" during the check.
160
+ for i in range (1 , len (result )):
161
+ if not ("_" + result [i ]).isidentifier ():
162
+ result = result [:i ] + "_" + result [i + 1 :]
163
+ result = re .sub ("__+" , "_" , result )
164
+ assert result .isidentifier (), f"Sanitization failed on { name } => { result } "
165
+ return result
154
166
155
- def namedview (name , fields ):
167
+
168
+ def namedview (name , fields , * , sanitize_field_names = True ):
156
169
"""
157
170
Creates a class that is a named view with given ``fields``. When the class
158
171
is instantiated, it must be given the object that it will be a proxy for.
159
172
Similar to ``namedtuple``.
160
173
174
+ If ``sanitize_field_names`` is True (the default), then any characters in
175
+ ``fields`` which are not valid in Python identifiers will be automatically
176
+ replaced with `_`. Leading numbers will have `_` inserted, and duplicate
177
+ `_` will be replaced by a single `_`.
178
+
161
179
Example:
162
180
::
163
181
@@ -186,7 +204,10 @@ def namedview(name, fields):
186
204
187
205
For more details, see ``NamedViewBase``.
188
206
"""
189
- base_cls = (NamedViewBase ,)
207
+ base_cls = (NamedViewBase , )
208
+ if sanitize_field_names :
209
+ fields = [_sanitize_field_name (f ) for f in fields ]
210
+ assert len (set (fields )) == len (fields ), "Field names must be unique"
190
211
type_dict = dict (_fields = tuple (fields ))
191
212
for i , field in enumerate (fields ):
192
213
type_dict [field ] = NamedViewBase ._item_property (i )
0 commit comments