milesrichardson / ParsePy

A relatively up-to-date fork of ParsePy, the Python wrapper for the Parse.com API. Originally maintained by @dgrtwo
MIT License
515 stars 184 forks source link

AttributeError exception when accessing current user using Parse Server #155

Closed francisjervis closed 7 years ago

francisjervis commented 7 years ago

EDIT FOR FUTURE: see https://github.com/milesrichardson/ParsePy/issues/155#issuecomment-271482650 for solution

Using Parse Server, I am encountering a crash at line 442 in datatypes.py setattr(self, key, ParseType.convert_from_parse(key, value)) when retrieving the currently logged in user. The user object appears to be being loaded correctly, looking at both the server logs and the error page from my (Django) app.

This only happens using the self-hosted server, not the Parse service.

francisjervis commented 7 years ago

I looked into this further and this appears to be a result of the /users/me endpoint in Parse Server adding additional attributes. The crash appeared to happen when parse-rest tried to set the className attribute. See https://github.com/ParsePlatform/parse-server/issues/3348

milesrichardson commented 7 years ago

Hey @francisjervis - thanks for investigating.

I doubt they will fix this upstream, since (1) this is not an official library, and (2) libraries shouldn't break when keys are appended (as opposed to missing). So I think the fault here is in this library.

I will take a look at this in the next couple of days. In the meantime, please comment here and/or submit a PR if you are able to fix the issue. It should be pretty straight forward.

Otherwise, a hacky would be to avoid the /users/me endpoint and just query the _User table directly. But that's obviously not optimal. :)

francisjervis commented 7 years ago

@milesrichardson Thanks for getting back to me; I agree, though given that parse-server is not supposed to break existing code, one could argue they should fix it too ;)

I am going to hazard a guess at this being a failure in the convert_from_parse method in datatypes.py where the className key-value pair is outside the expected inputs. As noted in the parse-server issue, the new keys are

"__type": "Object", "className": "_User","

I'm not 100% sure I understand how the lib handles converting responses to objects, so I can't offer much here - however I have tried retrieving users using a query, which does work as expected. Unfortunately I need to retrieve the current_user based on a session token passed from the browser cookie store, so I can't actually use that as a workaround (I think). If this is because the user object from a query doesn't have the extra keys, I'd be more inclined to see this as parse-server's issue (not that that necessarily means they will fix it, as you say).

milesrichardson commented 7 years ago

Yes, the issue is 99% chance in the convert_from_parse method. Feel free to play with it and see if you can get something to work (or even put a hacky conditional in there like "if User object") and submit a PR and I can make sure it doesn't break anything.

Re: the workaround, you can query the _Session class by the session token and use the select_related('user') method (equivalent to .include in the JS sdk) to also select the _Userobject that the _Session object points to in its user field.

milesrichardson commented 7 years ago

With a quick glance, I think the issue is probably in this line:

https://github.com/milesrichardson/ParsePy/blob/master/parse_rest/datatypes.py#L44

I will try to take a look at this later tonight. Let me know if you get a fix in the meantime.

francisjervis commented 7 years ago

OK - my hacky conditional idea was to not try to convert className but I suspect that would break things? I've run a version with

 def _init_attrs(self, args):
        print args
        for key, value in six.iteritems(args):
            print key
            setattr(self, key, ParseType.convert_from_parse(key, value))
            print key, value

and the crash is definitely happening after the iteration gets to key className.

I'll give that a try, thanks for the suggestion!

francisjervis commented 7 years ago

Or perhaps https://github.com/milesrichardson/ParsePy/blob/master/parse_rest/datatypes.py#L106 given where it seems to crash?

milesrichardson commented 7 years ago

That code is only for deserializing nested pointer objects. But the section below it might be relevant.

Have to head out now -- I'll take a look in a few hours. good luck

francisjervis commented 7 years ago

Some more logs - thanks, I will keep hacking ;)

File "/Users/francis/Library/Python/2.7/lib/python/site-packages/parse_rest/user.py", line 97, in current_user
    return cls(**User.GET(user_url))
  File "/Users/francis/Library/Python/2.7/lib/python/site-packages/parse_rest/datatypes.py", line 431, in __init__
    self._init_attrs(kw)
  File "/Users/francis/Library/Python/2.7/lib/python/site-packages/parse_rest/datatypes.py", line 444, in _init_attrs
    setattr(self, key, ParseType.convert_from_parse(key, value))
AttributeError: can't set attribute
milesrichardson commented 7 years ago

@francisjervis This should be fixed now. You can install from master like pip install git+https://github.com/milesrichardson/ParsePy

Since I included a link to this issue in a comment at the location of the fix in the code, I'm putting the solution in this comment.

The problem was that setattr(self, key, ParseType.convert_from_parse(key, value)) was trying to set the attribute className on class ParseResource, but className conflicts with the existing ParseResource property:

    @property
    def className(self):
        return self.__class__.__name__

The offending code:

for key, value in six.iteritems(args):
    setattr(self, key, ParseType.convert_from_parse(key, value))

As far as I can see there were two solutions:

1) Hard code an exception (no pun intended...) for the className attribute:

for key, value in six.iteritems(args):
    if key == 'className':
        continue
    setattr(self, key, ParseType.convert_from_parse(key, value))

2) Skip any AttributeError

for key, value in six.iteritems(args):
    try:
        setattr(self, key, ParseType.convert_from_parse(key, value))
    except AttributeError:
        continue

I chose solution 2 because it's more "future proof" in the case that there are any other attributes that conflict with existing properties of the ParseResource object.

As an aside, the reason this happened only with User objects is that User is derived from ParseResource, which has the conflicting className property, but all other objects are derived from Object, which does not have the conflicting className property.

francisjervis commented 7 years ago

Confirming this is working on my end, thanks for such a quick fix!