python代码封装供第三方使用_拓展/Hook一个Python第三方库API的通用方案

一. 背景

在实际应用中,我们在使用一个第三方库的时候,有时候发现这个第三方库并不是特别满足我们的需要,比如说,少了一些API,或者我需要在原来的API上增加一些Hook操作,这时候大家可能很容易想到的是:实现这个第三方库的子类,重写API;

使用适配器模式,动态决定如何调用;

实现一个新类跟新API,新类持有一个第三方库的对象。

但是,这几种方式都有很大的弊端

第一种方案,只适合一些简单封装的Python第三方库,很多用了魔法方法的第三方库没办法满足需求,你会发现到时候会有很多API调用不到。(具体后面进行说明)

第二种方案跟第三种方案,比较死板,因为你改变了原来的API方式,而且你可以封装一部分API,但如果其他人想使用其他之前没调用到的API怎么办?

其实,Python提供了一些魔法函数,可以很方便实现我们的需求。

下面我们用一个第三方库pyuiautomator的拓展举例。

二.案例

pyuiautomator是一个Android端的UI测试库,在使用过程中我们发现,本身的控件行为,是没有做一些重试或者存在判断的。对于大部分使用者,不封装这些操作是没什么问题的,但是在一些特定场景下,我们可能需要在每次控件查找时,截一次图,方便生成事件操作流;对控件做操作时,先判断存不存在,并且允许一定的重试操作。相当于,我们需要改变部分API本身。

我们先来看看pyuiautomator的使用方法

from uiautomator import Device

d = Device('aa0f41a8')

print d.server.adb.device_serial() # 打印设备串号

d.press.home() # 按下home

e = d(className='android.widget.TextView', text='target ui') # 查找元素

if e.wait.exist:

e.click() # 点击元素

可以发现,Device在这里,似乎是构造函数调用了两次?但是我们可以通过代码发现,不是这样的。

class AutomatorDevice(object):

...

def __init__(self, serial=None, local_port=None, adb_server_host=None, adb_server_port=None):

self.server = AutomatorServer(

serial=serial,

local_port=local_port,

adb_server_host=adb_server_host,

adb_server_port=adb_server_port

)

def __call__(self, **kwargs):

return AutomatorDeviceObject(self, Selector(**kwargs))

我们可以发现,Device('aa0f41a8')调用的是__init__,但是d(className='...', text='...')调用的是__call__了,因此我们可以发现,实际上,在框架里,Device承载的是多个其他设备类的能力,在这时候,如果通过实现这个第三方库的子类,重写API,或者其他模式,都是不实用的。

正确方法如下

class MTDevice(Device):

def __init__(self, serial):

super(MTDevice, self).__init__(serial)

self.serial = serial

self.obj = UIObject()

def __call__(self, **kwargs):

s = super(MTDevice, self).__call__(**kwargs)

self.obj(session=s)

return self.obj

def press_back(self):

try:

self.press.back()

except Exception as e:

raise ActionError(e.message)

return True

def click(self, x=None, y=None, retry=3, strict_mode=True):

if retry == 0:

return on_retry_max(strict_mode, "{} click {} {} exception.".format(self.serial, x, y))

time.sleep(0.5)

if super(MTDevice, self).click(x, y):

return True

else:

time.sleep(0.2)

return self.click(x, y, retry-1, strict_mode)

class UIObject(object):

def __init__(self):

self.session = None

def click(self, retry=3, strict_mode=True):

if retry == 0:

return on_retry_max(strict_mode, str(self.session.info['text'].encode('utf-8')) + " click failed")

if self.session.wait.exists(timeout=10000):

if self.session.click():

return True

else:

logging.info(str(self.session.info['text'].encode('utf-8')) + " not click success, retry.")

time.sleep(0.2)

self.click(retry-1)

else:

time.sleep(0.2)

return self.click(retry-1)

def set_text(self, text, strict_mode=True):

if not self.session.wait.exists(timeout=10 * 1000):

if strict_mode:

return WaitTimeoutError(str(self.session.info['text'].encode('utf-8')) + " wait timeout")

else:

return False

return self.__set_text(text=text, strict_mode=strict_mode)

def __set_text(self, text, retry=3, strict_mode=True):

if retry == 0:

return on_retry_max(strict_mode, str(self.session.info['text'].encode('utf-8'))

+ " set_text {} failed".format(text))

if not self.session.set_text(text):

time.sleep(0.2)

return self.__set_text(text, retry-1, strict_mode)

else:

return True

def __getattr__(self, item):

try:

return getattr(self.session, item)

except Exception as e:

logging.error(e)

time.sleep(1)

return getattr(self.session, item)

def __call__(self, **kwargs):

self.__dict__.update(kwargs)

print

def __del__(self):

del self.session

在这里,可以看到,我这边扩展的库依然也是实现子类,但是在d(className='...', text='...')触发__call__时,调用的是一个持有super(MTDevice, self).__call__(**kwargs)返回的对象,因此,我可以把部分我们需要实现的API替换掉,或者增加一些重试操作,但是原来API调用方式全部没有任何影响。

class MTDevice(Device):

def __init__(self, serial):

super(MTDevice, self).__init__(serial)

self.serial = serial

self.obj = UIObject()

def __call__(self, **kwargs):

s = super(MTDevice, self).__call__(**kwargs)

self.obj(session=s)

return self.obj

...

class UIObject(object):

def __init__(self):

self.session = None

def click(self, retry=3, strict_mode=True):

if retry == 0:

return on_retry_max(strict_mode, str(self.session.info['text'].encode('utf-8')) + " click failed")

if self.session.wait.exists(timeout=10000):

if self.session.click():

return True

else:

logging.info(str(self.session.info['text'].encode('utf-8')) + " not click success, retry.")

time.sleep(0.2)

self.click(retry-1)

else:

time.sleep(0.2)

return self.click(retry-1)

def set_text(self, text, strict_mode=True):

if not self.session.wait.exists(timeout=10 * 1000):

if strict_mode:

return WaitTimeoutError(str(self.session.info['text'].encode('utf-8')) + " wait timeout")

else:

return False

return self.__set_text(text=text, strict_mode=strict_mode)

def __set_text(self, text, retry=3, strict_mode=True):

if retry == 0:

return on_retry_max(strict_mode, str(self.session.info['text'].encode('utf-8'))

+ " set_text {} failed".format(text))

if not self.session.set_text(text):

time.sleep(0.2)

return self.__set_text(text, retry-1, strict_mode)

else:

return True

def __getattr__(self, item):

try:

return getattr(self.session, item)

except Exception as e:

logging.error(e)

time.sleep(1)

return getattr(self.session, item)

def __call__(self, **kwargs):

self.__dict__.update(kwargs)

print

在这里,我替换掉了AutomatorDeviceObject类中的click/set_text操作,而如果其他人想用这个库的其他API,那这里会通过__getattr__来调用到该session对象里的原有API。

以上只有几十行代码,但是就可以轻松拓展一个第三方库,而且用这个方法,可以给原来第三方库的部分API打上patch。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值