thriftpy icon indicating copy to clipboard operation
thriftpy copied to clipboard

Default values for TPayload objects change if instance is modified.

Open mrterry opened this issue 8 years ago • 4 comments

If you:

  • have TPayload object with a default value
  • make an instance with the default values
  • mutate that instance

all subsequent default instances have new defaults. The snippet below will reproduce

from thriftpy import thrift
class Person(thrift.TPayload):                                                  
    thrift_spec = {                                                             
        1: (thrift.TType.LIST, "phones", thrift.TType.I32),                     
    }                                                                           
    default_spec = [("phones", [])]

p = Person()
p.phones.append(3)

p2 = Person()
assert p2.phones == []  # FAIL.  default is now [3]

In Python, default values for function/methods are re-used across uses. http://docs.python-guide.org/en/latest/writing/gotchas/#mutable-default-arguments

We can solve this by requiring that default values actually be callables eg

default_spec = [("phones", list)]

or systematically making copies of the default values eg

class AltPerson(object):                                                        
    default_spec = [                                                            
        ('phones', []),                                                         
    ]                                                                           
                                                                                
    def __init__(self, **kwargs):                                               
        for name, default_value in self.default_spec:                           
            if name in kwargs:                                                  
                value = kwargs[name]                                            
            else:                                                               
                value = copy.copy(default_value)                                
            setattr(self, name, value)     

If you go with the first option (my preference), it becomes tricky to intentionally create TPayload objects with callable attributes. Eg if you wanted phones to be set to an object that is both indexable f[0] and callable f(). That said, if you're in that camp, you ought to know what you're doing and can do `Person(phones=

mrterry avatar Nov 22 '16 21:11 mrterry

This changed how the generated __init__ func works.

Previously:

def __init__(self, name='Alice', number=None):

now

def __init__(self, *args, **kwargs):

And the information lost.

lxyu avatar Nov 24 '16 03:11 lxyu

Doing it this way actually retains more information. Previously you had the signature:

def __init__(self, phones=[])

You can't distinguish between Person() and Person(phones=[]). This is important because the original signature, with a mutable default argument is all kinds of bad, and requires the copy.copy(default_value) business.

Also, there are places in you test suite that create TPayload objects using positional arguments ie Person(['phone1', 'phone2']) That's why I had to put in the *args bit. The logic in the PR exactly reproduces the behavior expected by the current test suite. I'm happy to add more tests to make this more explicit.

mrterry avatar Nov 24 '16 17:11 mrterry

Looks like this one can be closed because of https://github.com/eleme/thriftpy/pull/262

pawl avatar Jan 28 '17 19:01 pawl

if that PR is accepted, it will resolve this bug. right now the PR is limbo.

mrterry avatar Jan 28 '17 20:01 mrterry