Lichen

Annotated access_plan.py

1021:06335a0e4113
14 months ago Paul Boddie Prevent inadvertent object copying during __data__ attribute assignment. value-replacement
paul@1007 1
#!/usr/bin/env python
paul@1007 2
paul@1007 3
"""
paul@1007 4
Attribute access plan translation.
paul@1007 5
paul@1007 6
Copyright (C) 2014-2018, 2023 Paul Boddie <paul@boddie.org.uk>
paul@1007 7
paul@1007 8
This program is free software; you can redistribute it and/or modify it under
paul@1007 9
the terms of the GNU General Public License as published by the Free Software
paul@1007 10
Foundation; either version 3 of the License, or (at your option) any later
paul@1007 11
version.
paul@1007 12
paul@1007 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@1007 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@1007 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@1007 16
details.
paul@1007 17
paul@1007 18
You should have received a copy of the GNU General Public License along with
paul@1007 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@1007 20
"""
paul@1007 21
paul@1007 22
from encoders import encode_access_location
paul@1007 23
paul@1007 24
class AccessPlan:
paul@1007 25
paul@1007 26
    "An attribute access plan."
paul@1007 27
paul@1007 28
    def __init__(self, name, test, test_type, base, traversed, traversal_modes,
paul@1007 29
                 remaining, context, context_test, first_method, final_method,
paul@1007 30
                 origin, accessor_kinds):
paul@1007 31
paul@1007 32
        "Initialise the plan."
paul@1007 33
paul@1007 34
        # With instance attribute initialisers, the assignment below would be
paul@1007 35
        # generated automatically.
paul@1007 36
paul@1007 37
        (
paul@1007 38
            self.name, self.test, self.test_type, self.base,
paul@1007 39
            self.traversed, self.traversal_modes, self.remaining,
paul@1007 40
            self.context, self.context_test,
paul@1007 41
            self.first_method, self.final_method,
paul@1007 42
            self.origin, self.accessor_kinds) = (
paul@1007 43
paul@1007 44
            name, test, test_type, base,
paul@1007 45
            traversed, traversal_modes, remaining,
paul@1007 46
            context, context_test,
paul@1007 47
            first_method, final_method,
paul@1007 48
            origin, accessor_kinds)
paul@1007 49
paul@1007 50
        # Define the first attribute access and subsequent accesses.
paul@1007 51
paul@1007 52
        self.first_attrname = None
paul@1007 53
        self.traversed_attrnames = traversed
paul@1007 54
        self.traversed_attrname_modes = traversal_modes
paul@1007 55
        self.remaining_attrnames = remaining
paul@1007 56
paul@1007 57
        if traversed:
paul@1007 58
            self.first_attrname = traversed[0]
paul@1007 59
            self.traversed_attrnames = traversed[1:]
paul@1007 60
            self.traversed_attrname_modes = traversal_modes[1:]
paul@1007 61
        elif remaining:
paul@1007 62
            self.first_attrname = remaining[0]
paul@1007 63
            self.remaining_attrnames = remaining[1:]
paul@1007 64
paul@1007 65
    def access_first_attribute(self):
paul@1007 66
paul@1007 67
        "Return whether the first attribute is to be accessed."
paul@1007 68
paul@1007 69
        return self.final_method in ("access", "access-invoke", "assign") or \
paul@1007 70
            self.all_subsequent_attributes()
paul@1007 71
paul@1007 72
    def assigning_first_attribute(self):
paul@1007 73
paul@1007 74
        "Return whether the first attribute access involves assignment."
paul@1007 75
paul@1007 76
        return not self.all_subsequent_attributes() and self.final_method == "assign"
paul@1007 77
paul@1007 78
    def get_first_attribute_name(self):
paul@1007 79
paul@1007 80
        "Return any first attribute name to be used in an initial access."
paul@1007 81
paul@1007 82
        return self.first_attrname
paul@1007 83
paul@1007 84
    def all_subsequent_attributes(self):
paul@1007 85
paul@1007 86
        "Return all subsequent attribute names involved in accesses."
paul@1007 87
paul@1007 88
        return self.traversed_attrnames + self.remaining_attrnames
paul@1007 89
paul@1007 90
    def attribute_traversals(self):
paul@1007 91
paul@1007 92
        "Return a collection of (attribute name, traversal mode) tuples."
paul@1007 93
paul@1007 94
        return zip(self.traversed_attrnames, self.traversed_attrname_modes)
paul@1007 95
paul@1007 96
    def stored_accessor(self):
paul@1007 97
paul@1007 98
        "Return the variable used to obtain the accessor."
paul@1007 99
paul@1007 100
        return self.assigning_first_attribute() and "<target_accessor>" or "<accessor>"
paul@1007 101
paul@1007 102
    def stored_accessor_modifier(self):
paul@1007 103
paul@1007 104
        "Return the variable used to set the accessor."
paul@1007 105
paul@1007 106
        return self.assigning_first_attribute() and "<set_target_accessor>" or "<set_accessor>"
paul@1007 107
paul@1007 108
    def get_original_accessor(self):
paul@1007 109
paul@1007 110
        "Return the original accessor details."
paul@1007 111
paul@1007 112
        # Identify any static original accessor.
paul@1007 113
paul@1007 114
        if self.base:
paul@1007 115
            return self.base
paul@1007 116
paul@1007 117
        # Employ names as contexts unless the context needs testing and
paul@1007 118
        # potentially updating. In such cases, temporary context storage is
paul@1007 119
        # used instead.
paul@1007 120
paul@1007 121
        elif self.name and not (self.context_test == "test" and
paul@1007 122
            self.final_method in ("access-invoke", "static-invoke")):
paul@1007 123
paul@1007 124
            return "<name>"
paul@1007 125
paul@1007 126
        # Use a generic placeholder representing the access expression in
paul@1007 127
        # the general case.
paul@1007 128
paul@1007 129
        else:
paul@1007 130
            return "<expr>"
paul@1007 131
paul@1007 132
    def get_instructions(self):
paul@1007 133
paul@1007 134
        "Return a list of instructions corresponding to the plan."
paul@1007 135
paul@1007 136
        # Emit instructions by appending them to a list.
paul@1007 137
paul@1007 138
        instructions = []
paul@1007 139
        emit = instructions.append
paul@1007 140
paul@1007 141
        # Set up any initial instructions.
paul@1007 142
paul@1007 143
        accessor, context = self.process_initialisation(emit)
paul@1007 144
paul@1007 145
        # Apply any test.
paul@1007 146
paul@1007 147
        if self.test[0] == "test":
paul@1007 148
            test_accessor = accessor = ("__%s_%s_%s" % self.test, accessor, self.test_type)
paul@1007 149
        else:
paul@1007 150
            test_accessor = None
paul@1007 151
paul@1007 152
        # Perform the first or final access.
paul@1007 153
        # The access only needs performing if the resulting accessor is used.
paul@1007 154
paul@1007 155
        accessor = self.process_first_attribute(accessor, emit)
paul@1007 156
paul@1007 157
        # Perform accesses for the traversed and remaining attributes.
paul@1007 158
paul@1007 159
        accessor, context = self.process_traversed_attributes(accessor, context, emit)
paul@1007 160
        accessor, context = self.process_remaining_attributes(accessor, context, emit)
paul@1007 161
paul@1007 162
        # Make any accessor test available if not emitted.
paul@1007 163
paul@1007 164
        test_accessor = not instructions and test_accessor or None
paul@1007 165
paul@1007 166
        # Perform the access on the actual target.
paul@1007 167
paul@1007 168
        accessor = self.process_attribute_access(accessor, context, test_accessor, emit)
paul@1007 169
paul@1007 170
        # Produce an advisory instruction regarding the context.
paul@1007 171
paul@1007 172
        self.process_context_identity(context, emit)
paul@1007 173
paul@1007 174
        # Produce an advisory instruction regarding the final attribute.
paul@1007 175
paul@1007 176
        if self.origin:
paul@1007 177
            emit(("<final_identity>", self.origin))
paul@1007 178
paul@1007 179
        return instructions
paul@1007 180
paul@1007 181
    def process_initialisation(self, emit):
paul@1007 182
paul@1007 183
        """
paul@1007 184
        Use 'emit' to generate instructions for any initialisation of attribute
paul@1007 185
        access. Return the potentially revised accessor and context indicators.
paul@1007 186
        """
paul@1007 187
paul@1007 188
        # Identify any static original accessor.
paul@1007 189
paul@1007 190
        original_accessor = self.get_original_accessor()
paul@1007 191
paul@1007 192
        # Determine whether the first access involves assignment.
paul@1007 193
paul@1007 194
        set_accessor = self.stored_accessor_modifier()
paul@1007 195
        stored_accessor = self.stored_accessor()
paul@1007 196
paul@1007 197
        # Set the context if already available.
paul@1007 198
paul@1007 199
        context = None
paul@1007 200
paul@1007 201
        if self.context == "base":
paul@1007 202
            accessor = context = (self.base,)
paul@1007 203
        elif self.context == "original-accessor":
paul@1007 204
paul@1007 205
            # Prevent re-evaluation of any dynamic expression by storing it.
paul@1007 206
paul@1007 207
            if original_accessor == "<expr>":
paul@1007 208
                if self.final_method in ("access-invoke", "static-invoke"):
paul@1007 209
                    emit(("<set_context>", original_accessor))
paul@1007 210
                    accessor = context = ("<context>",)
paul@1007 211
                else:
paul@1007 212
                    emit((set_accessor, original_accessor))
paul@1007 213
                    accessor = context = (stored_accessor,)
paul@1007 214
            else:
paul@1007 215
                accessor = context = (original_accessor,)
paul@1007 216
paul@1007 217
        # Assigning does not set the context.
paul@1007 218
paul@1007 219
        elif self.context in ("final-accessor", "unset") and self.access_first_attribute():
paul@1007 220
paul@1007 221
            # Prevent re-evaluation of any dynamic expression by storing it.
paul@1007 222
paul@1007 223
            if original_accessor == "<expr>":
paul@1007 224
                emit((set_accessor, original_accessor))
paul@1007 225
                accessor = (stored_accessor,)
paul@1007 226
            else:
paul@1007 227
                accessor = (original_accessor,)
paul@1007 228
        else:
paul@1007 229
            accessor = None
paul@1007 230
paul@1007 231
        return accessor, context
paul@1007 232
paul@1007 233
    def process_first_attribute(self, accessor, emit):
paul@1007 234
paul@1007 235
        """
paul@1007 236
        Using 'accessor', use 'emit' to generate instructions for any first
paul@1007 237
        attribute access. Return the potentially revised accessor.
paul@1007 238
        """
paul@1007 239
paul@1007 240
        if self.access_first_attribute():
paul@1007 241
            attrname = self.get_first_attribute_name()
paul@1007 242
            assigning = self.assigning_first_attribute()
paul@1007 243
paul@1021 244
            store = attrname != "__data__" and "__store_via_attr_ref" or "__store_via_attr_ref__"
paul@1021 245
paul@1009 246
            # Access via the accessor's class.
paul@1009 247
paul@1007 248
            if self.first_method == "relative-class":
paul@1007 249
                if assigning:
paul@1009 250
                    emit(("<set_attr_ref>", ("__get_class_attr_ref", accessor, attrname)))
paul@1021 251
                    emit((store, "<attr_ref>", "<assexpr>"))
paul@1007 252
                else:
paul@1007 253
                    accessor = ("__load_via_class", accessor, attrname)
paul@1007 254
paul@1009 255
            # Access via the accessor itself.
paul@1009 256
paul@1007 257
            elif self.first_method == "relative-object":
paul@1007 258
                if assigning:
paul@1009 259
                    emit(("<set_attr_ref>", ("__get_object_attr_ref", accessor, attrname)))
paul@1021 260
                    emit((store, "<attr_ref>", "<assexpr>"))
paul@1007 261
                else:
paul@1007 262
                    accessor = ("__load_via_object", accessor, attrname)
paul@1007 263
paul@1009 264
            # Access via a class accessor or the accessor's class.
paul@1009 265
paul@1007 266
            elif self.first_method == "relative-object-class":
paul@1007 267
                if assigning:
paul@1009 268
                    emit(("__raise_type_error",))
paul@1007 269
                else:
paul@1007 270
                    accessor = ("__get_class_and_load", accessor, attrname)
paul@1007 271
paul@1009 272
            # Access via the accessor's class.
paul@1009 273
paul@1007 274
            elif self.first_method == "check-class":
paul@1007 275
                if assigning:
paul@1009 276
                    emit(("__raise_type_error",))
paul@1007 277
                else:
paul@1007 278
                    accessor = ("__check_and_load_via_class", accessor, attrname)
paul@1007 279
paul@1009 280
            # Access via the accessor itself.
paul@1009 281
paul@1007 282
            elif self.first_method == "check-object":
paul@1007 283
                if assigning:
paul@1009 284
                    emit(("<set_attr_ref>", ("__check_and_get_object_attr_ref", accessor, attrname)))
paul@1021 285
                    emit((store, "<attr_ref>", "<assexpr>"))
paul@1007 286
                else:
paul@1007 287
                    accessor = ("__check_and_load_via_object", accessor, attrname)
paul@1007 288
paul@1009 289
            # Access via a class accessor or the accessor's class.
paul@1009 290
            # Here, only access via the accessor is supported.
paul@1009 291
paul@1007 292
            elif self.first_method == "check-object-class":
paul@1007 293
                if assigning:
paul@1009 294
                    emit(("<set_attr_ref>", ("__check_and_get_object_attr_ref", accessor, attrname)))
paul@1021 295
                    emit((store, "<attr_ref>", "<assexpr>"))
paul@1007 296
                else:
paul@1007 297
                    accessor = ("__check_and_load_via_any", accessor, attrname)
paul@1007 298
paul@1007 299
        return accessor
paul@1007 300
paul@1007 301
    def process_traversed_attributes(self, accessor, context, emit):
paul@1007 302
paul@1007 303
        """
paul@1007 304
        Using 'accessor' and 'context', use 'emit' to generate instructions
paul@1007 305
        for the traversed attribute accesses. Return the potentially revised
paul@1007 306
        accessor and context indicators.
paul@1007 307
        """
paul@1007 308
paul@1007 309
        # Traverse attributes using the accessor.
paul@1007 310
paul@1007 311
        num_remaining = len(self.all_subsequent_attributes())
paul@1007 312
paul@1007 313
        if self.traversed_attrnames:
paul@1007 314
            for attrname, traversal_mode in self.attribute_traversals():
paul@1007 315
                assigning = num_remaining == 1 and self.final_method == "assign"
paul@1007 316
paul@1007 317
                # Set the context, if appropriate.
paul@1007 318
paul@1007 319
                if num_remaining == 1 and self.final_method != "assign" and self.context == "final-accessor":
paul@1007 320
paul@1007 321
                    # Invoked attributes employ a separate context accessed
paul@1007 322
                    # during invocation.
paul@1007 323
paul@1007 324
                    if self.final_method in ("access-invoke", "static-invoke"):
paul@1007 325
                        emit(("<set_context>", accessor))
paul@1007 326
                        accessor = context = "<context>"
paul@1007 327
paul@1007 328
                    # A private context within the access is otherwise
paul@1007 329
                    # retained.
paul@1007 330
paul@1007 331
                    else:
paul@1007 332
                        emit(("<set_private_context>", accessor))
paul@1007 333
                        accessor = context = "<private_context>"
paul@1007 334
paul@1007 335
                # Perform the access only if not achieved directly.
paul@1007 336
paul@1007 337
                if num_remaining > 1 or self.final_method in ("access", "access-invoke", "assign"):
paul@1007 338
paul@1021 339
                    store = attrname != "__data__" and "__store_via_attr_ref" or "__store_via_attr_ref__"
paul@1021 340
paul@1007 341
                    if traversal_mode == "class":
paul@1007 342
                        if assigning:
paul@1009 343
                            emit(("<set_attr_ref>", ("__get_class_attr_ref", accessor, attrname)))
paul@1021 344
                            emit((store, "<attr_ref>", "<assexpr>"))
paul@1007 345
                        else:
paul@1007 346
                            accessor = ("__load_via_class", accessor, attrname)
paul@1007 347
                    else:
paul@1007 348
                        if assigning:
paul@1009 349
                            emit(("<set_attr_ref>", ("__get_object_attr_ref", accessor, attrname)))
paul@1021 350
                            emit((store, "<attr_ref>", "<assexpr>"))
paul@1007 351
                        else:
paul@1007 352
                            accessor = ("__load_via_object", accessor, attrname)
paul@1007 353
paul@1007 354
                num_remaining -= 1
paul@1007 355
paul@1007 356
        return accessor, context
paul@1007 357
paul@1007 358
    def process_remaining_attributes(self, accessor, context, emit):
paul@1007 359
paul@1007 360
        """
paul@1007 361
        Using 'accessor' and 'context', use 'emit' to generate instructions
paul@1007 362
        for the remaining attribute accesses. Return the potentially revised
paul@1007 363
        accessor and context indicators.
paul@1007 364
        """
paul@1007 365
paul@1007 366
        remaining = self.remaining_attrnames
paul@1007 367
paul@1007 368
        if remaining:
paul@1007 369
            num_remaining = len(remaining)
paul@1007 370
paul@1007 371
            for attrname in remaining:
paul@1007 372
                assigning = num_remaining == 1 and self.final_method == "assign"
paul@1007 373
paul@1007 374
                # Set the context, if appropriate.
paul@1007 375
paul@1007 376
                if num_remaining == 1 and self.final_method != "assign" and self.context == "final-accessor":
paul@1007 377
paul@1007 378
                    # Invoked attributes employ a separate context accessed
paul@1007 379
                    # during invocation.
paul@1007 380
paul@1007 381
                    if self.final_method in ("access-invoke", "static-invoke"):
paul@1007 382
                        emit(("<set_context>", accessor))
paul@1007 383
                        accessor = context = "<context>"
paul@1007 384
paul@1007 385
                    # A private context within the access is otherwise
paul@1007 386
                    # retained.
paul@1007 387
paul@1007 388
                    else:
paul@1007 389
                        emit(("<set_private_context>", accessor))
paul@1007 390
                        accessor = context = "<private_context>"
paul@1007 391
paul@1007 392
                # Perform the access only if not achieved directly.
paul@1007 393
paul@1007 394
                if num_remaining > 1 or self.final_method in ("access", "access-invoke", "assign"):
paul@1007 395
paul@1007 396
                    # Constrain instructions involving certain special
paul@1007 397
                    # attribute names.
paul@1007 398
paul@1021 399
                    store = attrname != "__data__" and "__store_via_attr_ref" or "__store_via_attr_ref__"
paul@1009 400
                    to_search = attrname != "__data__" and "any" or "object"
paul@1007 401
paul@1007 402
                    if assigning:
paul@1009 403
                        emit(("<set_attr_ref>", ("__check_and_get_object_attr_ref", accessor, attrname)))
paul@1021 404
                        emit((store, "<attr_ref>", "<assexpr>"))
paul@1007 405
                    else:
paul@1007 406
                        accessor = ("__check_and_load_via_%s" % to_search, accessor, attrname)
paul@1007 407
paul@1007 408
                num_remaining -= 1
paul@1007 409
paul@1007 410
        return accessor, context
paul@1007 411
paul@1007 412
    def process_attribute_access(self, accessor, context, test_accessor, emit):
paul@1007 413
paul@1007 414
        """
paul@1007 415
        Using 'accessor','context' and any 'test_accessor' operation, use 'emit'
paul@1007 416
        to generate instructions for the final attribute access. Return the
paul@1007 417
        potentially revised accessor.
paul@1007 418
        """
paul@1007 419
paul@1007 420
        # Define or emit the means of accessing the actual target.
paul@1007 421
paul@1007 422
        if self.final_method in ("static", "static-assign", "static-invoke"):
paul@1007 423
paul@1007 424
            if test_accessor:
paul@1007 425
                emit(test_accessor)
paul@1007 426
paul@1007 427
            # Assignments to known attributes.
paul@1007 428
paul@1007 429
            if self.final_method == "static-assign":
paul@1007 430
                parent, attrname = self.origin.rsplit(".", 1)
paul@1021 431
                store = attrname != "__data__" and "__store_via_attr_ref" or "__store_via_attr_ref__"
paul@1009 432
                emit(("<set_attr_ref>", ("__get_object_attr_ref", parent, attrname)))
paul@1021 433
                emit((store, "<attr_ref>", "<assexpr>"))
paul@1007 434
paul@1007 435
            # Invoked attributes employ a separate context.
paul@1007 436
paul@1007 437
            elif self.final_method in ("static", "static-invoke"):
paul@1007 438
                accessor = ("__load_static_ignore", self.origin)
paul@1007 439
paul@1007 440
        # Wrap accesses in context operations.
paul@1007 441
paul@1007 442
        if self.context_test == "test":
paul@1007 443
paul@1007 444
            # Test and combine the context with static attribute details.
paul@1007 445
paul@1007 446
            if self.final_method == "static":
paul@1007 447
                emit(("__load_static_test", context, self.origin))
paul@1007 448
paul@1007 449
            # Test the context, storing it separately if required for the
paul@1007 450
            # immediately invoked static attribute.
paul@1007 451
paul@1007 452
            elif self.final_method == "static-invoke":
paul@1007 453
                emit(("<test_context_static>", context, self.origin))
paul@1007 454
paul@1007 455
            # Test the context, storing it separately if required for an
paul@1007 456
            # immediately invoked attribute.
paul@1007 457
paul@1007 458
            elif self.final_method == "access-invoke":
paul@1007 459
                emit(("<test_context_revert>", context, accessor))
paul@1007 460
paul@1007 461
            # Test the context and update the attribute details if
paul@1007 462
            # appropriate.
paul@1007 463
paul@1007 464
            else:
paul@1007 465
                emit(("__test_context", context, accessor))
paul@1007 466
paul@1007 467
        elif self.context_test == "replace":
paul@1007 468
paul@1007 469
            # Produce an object with updated context.
paul@1007 470
paul@1007 471
            if self.final_method == "static":
paul@1007 472
                emit(("__load_static_replace", context, self.origin))
paul@1007 473
paul@1007 474
            # Omit the context update operation where the target is static
paul@1007 475
            # and the context is recorded separately.
paul@1007 476
paul@1007 477
            elif self.final_method == "static-invoke":
paul@1007 478
                pass
paul@1007 479
paul@1007 480
            # If a separate context is used for an immediate invocation,
paul@1007 481
            # produce the attribute details unchanged.
paul@1007 482
paul@1007 483
            elif self.final_method == "access-invoke":
paul@1007 484
                emit(accessor)
paul@1007 485
paul@1007 486
            # Update the context in the attribute details.
paul@1007 487
paul@1007 488
            else:
paul@1007 489
                emit(("__update_context", context, accessor))
paul@1007 490
paul@1007 491
        # Omit the accessor for assignments and for invocations of static
paul@1007 492
        # targets. Otherwise, emit the accessor which may involve the
paul@1007 493
        # invocation of a test.
paul@1007 494
paul@1007 495
        elif self.final_method not in ("assign", "static-assign", "static-invoke"):
paul@1007 496
            emit(accessor)
paul@1007 497
paul@1007 498
        return accessor
paul@1007 499
paul@1007 500
    def process_context_identity(self, context, emit):
paul@1007 501
paul@1007 502
        """
paul@1007 503
        Using 'context', use 'emit' to generate instructions to test the context
paul@1007 504
        identity.
paul@1007 505
        """
paul@1007 506
paul@1007 507
        if context:
paul@1007 508
paul@1007 509
            # Only verify the context for invocation purposes if a suitable
paul@1007 510
            # test has been performed.
paul@1007 511
paul@1007 512
            if self.context_test in ("ignore", "replace") or \
paul@1007 513
               self.final_method in ("access-invoke", "static-invoke"):
paul@1007 514
paul@1007 515
                emit(("<context_identity_verified>", context))
paul@1007 516
            else:
paul@1007 517
                emit(("<context_identity>", context))
paul@1007 518
paul@1007 519
    def write(self, f, location):
paul@1007 520
paul@1007 521
        "Write the plan to file 'f' with the given 'location' information."
paul@1007 522
paul@1007 523
        print >>f, encode_access_location(location), \
paul@1007 524
                   self.name or "{}", \
paul@1007 525
                   self.test and "-".join(self.test) or "{}", \
paul@1007 526
                   self.test_type or "{}", \
paul@1007 527
                   self.base or "{}", \
paul@1007 528
                   ".".join(self.traversed_attrnames) or "{}", \
paul@1007 529
                   ".".join(self.traversed_attrname_modes) or "{}", \
paul@1007 530
                   ".".join(self.remaining_attrnames) or "{}", \
paul@1007 531
                   self.context, self.context_test, \
paul@1007 532
                   self.first_method, self.final_method, self.origin or "{}", \
paul@1007 533
                   ",".join(self.accessor_kinds)
paul@1007 534
paul@1007 535
# vim: tabstop=4 expandtab shiftwidth=4