Suggest to refactor the `ready`, `payload` function of the related entity classes
When I fixed the bug on the Friendship class (within PR #96 ), I found the old code is not clear, and causes some error.
class Friendship(Accessory, Acceptable):
def __init__(self, friendship_id: str):
"""
initialization constructor for friendship
"""
self.friendship_id = friendship_id
self._payload: Optional[FriendshipPayload] = None
log.info('Friendship constructor %s', friendship_id)
if self.__class__ is Friendship:
raise Exception(
'Friendship class can not be instanciated directly!')
if self.puppet is None:
raise Exception(
'Friendship class can not be instanciated without a puppet!')
@classmethod
def load(cls, friendship_id: str) -> Friendship:
"""
load friendship without payload, which loads in a lazy way
:param friendship_id:
:return: initialized friendship
"""
return cls(friendship_id)
@property
def payload(self) -> FriendshipPayload:
"""
get the FriendShipPayload as a property
:return:
"""
if self._payload is None:
self.ready()
if self._payload is None:
raise Exception('can"t load friendship payload')
return self._payload
def is_ready(self) -> bool:
"""
check if friendship is ready
"""
return self.puppet is None or self.payload is None
async def ready(self):
"""
load friendship payload
"""
if not self.is_ready():
friendship_search_response = await self.puppet.friendship_payload(
friendship_id=self.friendship_id)
self._payload = friendship_search_response
if self.payload is None:
raise Exception('can"t not load friendship payload %s'
% self.friendship_id)
The procedure which uses this class is inside the function Wechaty.init_puppet_event_bridge:
- in
friendship_listener, we callself.Friendship.load(payload.friendship_id)to instanciate aFriendshipobject, now the newly createdFriendshipobject has no_payloaddata. - then we call
await friendship.ready(), which trigger the lazy load to fetch theFriendship.payloadfrom server. - emit the fully loaded
Friendshipobject by thefriendshipsignal, so that theonmethods registered can get the fully loadedFriendshipobjects as an argument.
The outer procedure is well, but inside the Friendship class there are some issues.
For the first one:
The @property payload is not garanteed to be not None, so the old implementation intent to call ready() first to ensure its existance.
@property
def payload(self) -> FriendshipPayload:
"""
get the FriendShipPayload as a property
:return:
"""
if self._payload is None:
self.ready()
if self._payload is None:
raise Exception('can"t load friendship payload')
return self._payload
But obviously the @property payload function is not async, neither does it awaits the ready() function, and it causes an infinite loop when triggered in my test.
So I changed this function in my PR:
@property
def payload(self) -> FriendshipPayload:
"""
get the FriendShipPayload as a property
:return:
"""
if self._payload is None:
self.ready()
if self._payload is None:
raise Exception('can"t load friendship payload')
return self._payload
What I've changed is I made it only returns the _payload object, and the return value is changed to Optional, not garanteed to be not null.
So comes the problem, when I submit the PR, the integration test is faild.
For example, for the following line:
contact = self.wechaty.Contact.load(self.payload.contact_id)
I tells that the self payload is Nullable, so self.payload.contact_id do not pass the test.
In order to make it pass, I have to change the line as below:
if not self.payload:
raise Exception('payload not ready ...')
contact = self.wechaty.Contact.load(self.payload.contact_id)
Adding a verbosing case detection to check its existance, just in order to get across the typing test.
So why we need a _payload and payload duplicate property? Type checking is not the python way.
So I think all the ready, is_ready, payload, _payload should be restructured all together.
In fact, we want all actions which requires _payload exists should work on a payload which has a type garanteed to be not None.
So every time we access the payload, we just lazy load it, like:
@property
async def payload(self) -> FriendshipPayload:
"""
get the FriendShipPayload as a property
:return:
"""
if not self._payload:
self._payload = await self.puppet.friendship_payload(
friendship_id=self.friendship_id)
return self._payload
Then we do not need async ready() and is_ready() any more.
When we want to use the payload (garanteed to be not None), or we want to preload the payload, we just await it, for the code mentioned above
if not self.payload:
raise Exception('payload not ready ...')
contact = self.wechaty.Contact.load(self.payload.contact_id)
we can rewrite it as below:
payload = await self.payload
contact = self.wechaty.Contact.load(payload.contact_id)
The code is prettier, and no type error anymore.
So after all, @wj-Mcat, I want to know whether this pattern is a good practise, if it do, we can refactor all classes with payload in this way. Or further, we can move all payload proccess to an abstract base class.