pyobject 1.3.5.1__cp315-cp315-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyobject/__init__.py +152 -0
- pyobject/browser.py +475 -0
- pyobject/code.py +314 -0
- pyobject/images/back.gif +0 -0
- pyobject/images/codeobject.gif +0 -0
- pyobject/images/dict.gif +0 -0
- pyobject/images/empty_dict.gif +0 -0
- pyobject/images/empty_list.gif +0 -0
- pyobject/images/empty_tuple.gif +0 -0
- pyobject/images/forward.gif +0 -0
- pyobject/images/function.gif +0 -0
- pyobject/images/list.gif +0 -0
- pyobject/images/number.gif +0 -0
- pyobject/images/python.gif +0 -0
- pyobject/images/python.ico +0 -0
- pyobject/images/refresh.gif +0 -0
- pyobject/images/string.gif +0 -0
- pyobject/images/tuple.gif +0 -0
- pyobject/objproxy/__init__.py +777 -0
- pyobject/objproxy/builtin_hook.py +138 -0
- pyobject/objproxy/optimize.py +159 -0
- pyobject/objproxy/utils.py +112 -0
- pyobject/pyobj_extension.c +463 -0
- pyobject/pyobj_extension.cp315-win_amd64.pyd +0 -0
- pyobject/search.py +247 -0
- pyobject/tests/test_code.py +4 -0
- pyobject/tests/test_objproxy.py +151 -0
- pyobject/tests/test_objproxy_perf.py +50 -0
- pyobject-1.3.5.1.dist-info/METADATA +25 -0
- pyobject-1.3.5.1.dist-info/RECORD +33 -0
- pyobject-1.3.5.1.dist-info/WHEEL +5 -0
- pyobject-1.3.5.1.dist-info/licenses/LICENSE +21 -0
- pyobject-1.3.5.1.dist-info/top_level.txt +1 -0
pyobject/__init__.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""A multifunctional all-in-one utility tool for managing internal Python \
|
|
2
|
+
objects, compatible with nearly all Python 3 versions.
|
|
3
|
+
"""
|
|
4
|
+
import sys, types
|
|
5
|
+
from warnings import warn
|
|
6
|
+
from pprint import pprint
|
|
7
|
+
from inspect import isfunction,ismethod
|
|
8
|
+
try:
|
|
9
|
+
from types import WrapperDescriptorType,MethodWrapperType,\
|
|
10
|
+
MethodDescriptorType,ClassMethodDescriptorType
|
|
11
|
+
except ImportError: # 低于3.7的版本
|
|
12
|
+
from typing import WrapperDescriptorType,MethodWrapperType,\
|
|
13
|
+
MethodDescriptorType
|
|
14
|
+
ClassMethodDescriptorType = type(dict.__dict__['fromkeys'])
|
|
15
|
+
|
|
16
|
+
__version__="1.3.5.1"
|
|
17
|
+
|
|
18
|
+
__all__=["objectname","bases","describe","desc"]
|
|
19
|
+
_always_ignored_names=["__builtins__"]
|
|
20
|
+
|
|
21
|
+
BASIC_TYPES = (int, float, str, bytes, bytearray,
|
|
22
|
+
list, tuple, dict, set)
|
|
23
|
+
MAXLENGTH = 150
|
|
24
|
+
|
|
25
|
+
def isfunc(obj):
|
|
26
|
+
# 判断一个对象是否为函数或方法
|
|
27
|
+
if isfunction(obj) or ismethod(obj):return True
|
|
28
|
+
# 使用typing而不用types.WrapperDescriptorType是为了与旧版本兼容
|
|
29
|
+
func_types=[types.LambdaType,types.BuiltinFunctionType,
|
|
30
|
+
types.BuiltinMethodType,WrapperDescriptorType,
|
|
31
|
+
MethodWrapperType,MethodDescriptorType,
|
|
32
|
+
ClassMethodDescriptorType]
|
|
33
|
+
for type_ in func_types:
|
|
34
|
+
if isinstance(obj,type_):
|
|
35
|
+
return True
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def objectname(obj):
|
|
39
|
+
# 返回对象的全名,类似__qualname__属性
|
|
40
|
+
# if hasattr(obj,"__qualname__"):return obj.__qualname__
|
|
41
|
+
if not obj.__class__==type:obj=obj.__class__
|
|
42
|
+
if not hasattr(obj,"__module__") or obj.__module__=="__main__":
|
|
43
|
+
return obj.__name__
|
|
44
|
+
return "{}.{}".format(obj.__module__,obj.__name__)
|
|
45
|
+
|
|
46
|
+
def bases(obj,level=0,tab=4):
|
|
47
|
+
# 打印对象的基类
|
|
48
|
+
if not obj.__class__==type:obj=obj.__class__
|
|
49
|
+
if obj.__bases__:
|
|
50
|
+
if level:print(' '*(level*tab),end='')
|
|
51
|
+
print(*obj.__bases__,sep=',')
|
|
52
|
+
for cls in obj.__bases__:
|
|
53
|
+
bases(cls,level,tab)
|
|
54
|
+
|
|
55
|
+
_trans_table=str.maketrans("\n\t"," ") # 替换特殊字符为空格
|
|
56
|
+
def shortrepr(obj,maxlength=None,repr_func=None):
|
|
57
|
+
if repr_func is None:repr_func = repr
|
|
58
|
+
if maxlength is None:maxlength = MAXLENGTH
|
|
59
|
+
result=repr_func(obj).translate(_trans_table)
|
|
60
|
+
if len(result)>maxlength:
|
|
61
|
+
return result[:maxlength]+"..."
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
def describe(obj,level=0,maxlevel=1,tab=4,verbose=False,file=None,
|
|
65
|
+
maxlength=None,ignore_funcs=False):
|
|
66
|
+
'''"Describe" an object by printing its attributes.
|
|
67
|
+
Parameters:
|
|
68
|
+
maxlevel: The number of levels to print the object's attributes.
|
|
69
|
+
tab: The number of spaces for indentation, default is 4.
|
|
70
|
+
verbose: Whether to output attributes starting with "_" (e.g., __init__).
|
|
71
|
+
maxlength: The maximum output length of one object.
|
|
72
|
+
ignore_funcs: If set to True, methods or functions of the object will not be output.
|
|
73
|
+
file: A file-like object for printing output.
|
|
74
|
+
'''
|
|
75
|
+
if file is None:file=sys.stdout
|
|
76
|
+
if level==maxlevel:
|
|
77
|
+
result=repr(obj)
|
|
78
|
+
if result.startswith('[') or result.startswith('{'):pprint(result)
|
|
79
|
+
else:print(result,file=file)
|
|
80
|
+
elif level>maxlevel:
|
|
81
|
+
raise ValueError("level is larger than maxlevel")
|
|
82
|
+
else:
|
|
83
|
+
print(shortrepr(obj,maxlength)+': ',file=file)
|
|
84
|
+
if isinstance(obj, type):
|
|
85
|
+
print(' '*tab*(level+1),end='',file=file)
|
|
86
|
+
print("Base classes of the object:",file=file)
|
|
87
|
+
bases(obj,level+1,tab)
|
|
88
|
+
print(file=file)
|
|
89
|
+
if verbose or type(obj) not in BASIC_TYPES or level == 0: # 基本类型(不包括子类)不在第一层时不展示属性
|
|
90
|
+
for attr in dir(obj):
|
|
91
|
+
if verbose or not attr.startswith("_"):
|
|
92
|
+
print(' '*tab*(level+1),end='',file=file)
|
|
93
|
+
try:
|
|
94
|
+
value = getattr(obj,attr)
|
|
95
|
+
except AttributeError:
|
|
96
|
+
print(attr+": <AttributeError!>",file=file)
|
|
97
|
+
continue
|
|
98
|
+
if ignore_funcs and isfunc(value):
|
|
99
|
+
continue
|
|
100
|
+
print(attr+": ",end='',file=file)
|
|
101
|
+
if attr in _always_ignored_names:
|
|
102
|
+
describe(value,level+1,maxlevel,tab,verbose,file,maxlength)
|
|
103
|
+
else:
|
|
104
|
+
print(shortrepr(value,maxlength),file=file)
|
|
105
|
+
if isinstance(obj, list):
|
|
106
|
+
print("\n"+' '*tab*(level+1),end='',file=file)
|
|
107
|
+
print("List items of the object:",file=file)
|
|
108
|
+
for i,item in enumerate(obj):
|
|
109
|
+
print(' '*tab*(level+1)+"[%d]: "%i,end='',file=file)
|
|
110
|
+
describe(item,level+1,maxlevel,tab,verbose,file,maxlength)
|
|
111
|
+
if isinstance(obj, dict):
|
|
112
|
+
print("\n"+' '*tab*(level+1),end='',file=file)
|
|
113
|
+
print("Dictionary items of the object:",file=file)
|
|
114
|
+
for key in obj.keys():
|
|
115
|
+
print(' '*tab*(level+1)+"[%s]: "%repr(key),end='',file=file)
|
|
116
|
+
try:
|
|
117
|
+
describe(obj[key],level+1,maxlevel,tab,verbose,file,maxlength)
|
|
118
|
+
except KeyError:
|
|
119
|
+
print("<KeyError!>",file=file)
|
|
120
|
+
|
|
121
|
+
desc = describe #别名
|
|
122
|
+
|
|
123
|
+
# 导入其他子模块中的函数和类
|
|
124
|
+
try:
|
|
125
|
+
from pyobject.browser import browse
|
|
126
|
+
__all__.append("browse")
|
|
127
|
+
except Exception:warn("Failed to import module pyobject.browser.")
|
|
128
|
+
try:
|
|
129
|
+
from pyobject.search import make_list,make_iter,search
|
|
130
|
+
__all__.extend(["make_list","make_iter","search"])
|
|
131
|
+
except Exception:warn("Failed to import pyobject.search.")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
from pyobject.code import Code
|
|
135
|
+
__all__.append("Code")
|
|
136
|
+
except Exception:warn("Failed to import pyobject.code.")
|
|
137
|
+
try:
|
|
138
|
+
from pyobject.pyobj_extension import *
|
|
139
|
+
__all__.extend(["convptr","py_incref","py_decref","getrealrefcount",
|
|
140
|
+
"setrefcount","list_in","getrefcount_nogil","setrefcount_nogil",
|
|
141
|
+
"get_type_flag","set_type_flag","set_type_base","set_type_bases",
|
|
142
|
+
"set_type_mro","get_type_subclasses","set_type_subclasses",
|
|
143
|
+
"set_type_subclasses_by_cls","get_string_intern_dict"])
|
|
144
|
+
except Exception:warn("Failed to import pyobject.pyobj_extension.")
|
|
145
|
+
try:
|
|
146
|
+
from pyobject.objproxy import ObjChain,ProxiedObj,unproxy_obj
|
|
147
|
+
__all__.extend(["ObjChain","ProxiedObj","unproxy_obj"])
|
|
148
|
+
except Exception as err:
|
|
149
|
+
warn("Failed to import pyobject.objproxy (%s): %s"%(type(err).__name__,err))
|
|
150
|
+
|
|
151
|
+
if __name__=="__main__":
|
|
152
|
+
describe(type,verbose=True)
|
pyobject/browser.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"A module providing a visual interface to browse Python objects."
|
|
2
|
+
import sys,os,types,ctypes
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
import tkinter.ttk as ttk
|
|
5
|
+
import tkinter.messagebox as msgbox
|
|
6
|
+
import tkinter.simpledialog as simpledialog
|
|
7
|
+
from inspect import iscode,ismodule
|
|
8
|
+
try:
|
|
9
|
+
from types import WrapperDescriptorType,MethodWrapperType,\
|
|
10
|
+
MethodDescriptorType,ClassMethodDescriptorType
|
|
11
|
+
except ImportError: # 低于3.7的版本
|
|
12
|
+
from typing import WrapperDescriptorType,MethodWrapperType,\
|
|
13
|
+
MethodDescriptorType
|
|
14
|
+
ClassMethodDescriptorType = type(dict.__dict__['fromkeys'])
|
|
15
|
+
|
|
16
|
+
try:from pyobject import objectname,shortrepr,isfunc
|
|
17
|
+
except ImportError:
|
|
18
|
+
from __init__ import objectname,shortrepr,isfunc
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_IMAGE_PATH=os.path.join(os.path.split(__file__)[0],"images")
|
|
22
|
+
SKIP=(WrapperDescriptorType, MethodWrapperType,\
|
|
23
|
+
MethodDescriptorType, ClassMethodDescriptorType)
|
|
24
|
+
TYPE_EXTRA_ATTRS = ("__basicsize__","__dictoffset__","__flags__",
|
|
25
|
+
"__itemsize__","__weakrefoffset__")
|
|
26
|
+
TYPE_EXTRA_CLASS_ATTRS = ("__base__","__bases__","__mro__")
|
|
27
|
+
DICT_TYPES = [dict, types.MappingProxyType]
|
|
28
|
+
if sys.version_info >= (3, 13): # 3.13+
|
|
29
|
+
FrameLocalsProxy = type((lambda:sys._getframe())().f_locals)
|
|
30
|
+
DICT_TYPES.append(FrameLocalsProxy)
|
|
31
|
+
|
|
32
|
+
def isdict(obj):
|
|
33
|
+
# 判断对象是否为字典
|
|
34
|
+
for type in DICT_TYPES:
|
|
35
|
+
if isinstance(obj,type):
|
|
36
|
+
return True
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
class ScrolledTreeview(ttk.Treeview):
|
|
40
|
+
"A scrollable Treeview widget inherited from ttk.Treeview."
|
|
41
|
+
def __init__(self,master,**options):
|
|
42
|
+
self.frame=tk.Frame(master)
|
|
43
|
+
ttk.Treeview.__init__(self,self.frame,**options)
|
|
44
|
+
|
|
45
|
+
self.hscroll=ttk.Scrollbar(self.frame,orient=tk.HORIZONTAL,
|
|
46
|
+
command=self.xview)
|
|
47
|
+
self.vscroll=ttk.Scrollbar(self.frame,command=self.yview)
|
|
48
|
+
self["xscrollcommand"]=self.hscroll.set
|
|
49
|
+
self["yscrollcommand"]=self.vscroll.set
|
|
50
|
+
#self.hscroll.pack(side=tk.BOTTOM,fill=tk.X)
|
|
51
|
+
self.vscroll.pack(side=tk.RIGHT,fill=tk.Y)
|
|
52
|
+
ttk.Treeview.pack(self,side=tk.BOTTOM,expand=True,fill=tk.BOTH)
|
|
53
|
+
def pack(self,*args,**options):
|
|
54
|
+
self.frame.pack(*args,**options)
|
|
55
|
+
def grid(self,*args,**options):
|
|
56
|
+
self.frame.grid(*args,**options)
|
|
57
|
+
def place(self,*args,**options):
|
|
58
|
+
self.frame.place(*args,**options)
|
|
59
|
+
|
|
60
|
+
class ObjectBrowser():
|
|
61
|
+
title = "Python Object Browser"
|
|
62
|
+
MAX_VIEW_LEN = 512 # 避免引发性能问题
|
|
63
|
+
MAX_EDITVALUE_LEN = 3072
|
|
64
|
+
WIDTH, HEIGHT = 480, 400
|
|
65
|
+
def __init__(self,master,obj,verbose=False,name="root_obj",
|
|
66
|
+
multi_window=False,refresh_history=True,
|
|
67
|
+
root_obj=None,rootobj_name=None):
|
|
68
|
+
self.master=master
|
|
69
|
+
self.verbose=verbose
|
|
70
|
+
self.name=name
|
|
71
|
+
self.multi_window=multi_window # 是否多窗口
|
|
72
|
+
self.refresh_history=refresh_history
|
|
73
|
+
self.root_obj = root_obj if root_obj is not None else obj # 根对象的名称,用于历史记录
|
|
74
|
+
self.rootobj_name = rootobj_name or name
|
|
75
|
+
self.history=[(obj,name)] # 单窗口浏览的历史记录,由多个(对象,路径)的元组组成
|
|
76
|
+
self.history_index=0
|
|
77
|
+
self.ui_scale = self.master.winfo_fpixels('1i') / 96
|
|
78
|
+
|
|
79
|
+
self.master.title(self.title)
|
|
80
|
+
self.master.geometry("%dx%d" % (self.WIDTH*self.ui_scale,
|
|
81
|
+
self.HEIGHT*self.ui_scale))
|
|
82
|
+
try:
|
|
83
|
+
self.master.iconbitmap(os.path.join(_IMAGE_PATH,"python.ico"))
|
|
84
|
+
except tk.TclError:pass
|
|
85
|
+
self.load_image()
|
|
86
|
+
self.create_widgets()
|
|
87
|
+
self.browse(obj,name=name,_first=True)
|
|
88
|
+
def create_widgets(self):
|
|
89
|
+
# 创建控件
|
|
90
|
+
toolbar=tk.Frame(self.master)
|
|
91
|
+
if not self.multi_window:
|
|
92
|
+
tk.Button(toolbar,image=self.back_image,command=self.back).pack(side=tk.LEFT)
|
|
93
|
+
tk.Button(toolbar,image=self.forward_image,command=self.forward).pack(side=tk.LEFT)
|
|
94
|
+
tk.Button(toolbar,image=self.refresh_image,command=self.navigate_history #refresh
|
|
95
|
+
).pack(side=tk.LEFT)
|
|
96
|
+
self.label=tk.Label(toolbar,anchor="w")
|
|
97
|
+
self.label.pack(side=tk.LEFT,fill=tk.X)
|
|
98
|
+
toolbar.pack(side=tk.TOP,fill=tk.X)
|
|
99
|
+
self.tvw=ScrolledTreeview(self.master,column='.',selectmode=tk.EXTENDED)
|
|
100
|
+
self.tvw.heading("#0",text="Attribute/Index/Key")
|
|
101
|
+
self.tvw.heading("#1",text="Value")
|
|
102
|
+
self.tvw.column("#0", stretch=0, width=int(160*self.ui_scale)) # 不跟随窗口大小的变化拉伸
|
|
103
|
+
self.tvw.column("#1", stretch=1)
|
|
104
|
+
self.tvw.bind("<<TreeviewSelect>>",self.on_select)
|
|
105
|
+
self.tvw.bind("<Double-Button-1>",self.on_open)
|
|
106
|
+
self.tvw.bind("<Key-Return>",self.on_open)
|
|
107
|
+
self.tvw.bind("<Key-Delete>",self.del_item)
|
|
108
|
+
self.tvw.tag_configure("error",foreground="red") # 经测试, Python 3.7-3.9中无法显示颜色效果(bug?)
|
|
109
|
+
self.tvw.tag_configure("gray",foreground="gray")
|
|
110
|
+
self.master.bind("<F5>",self.navigate_history)
|
|
111
|
+
|
|
112
|
+
self.functions_tag=self.tvw.insert('',index=0,text="Functions/Methods")
|
|
113
|
+
self.attributes_tag=self.tvw.insert('',index=1,text="Attributes")
|
|
114
|
+
self.classes_tag=self.tvw.insert('',index=2,text="Classes")
|
|
115
|
+
self.lst_tag=self.tvw.insert('',index=3,text="List data")
|
|
116
|
+
self.dict_tag=self.tvw.insert('',index=4,text="Dictionary data")
|
|
117
|
+
self.tvw.item(self.attributes_tag,open=True) # 展开项
|
|
118
|
+
self.tvw.item(self.classes_tag,open=True)
|
|
119
|
+
self.tvw.item(self.lst_tag,open=True)
|
|
120
|
+
self.tvw.item(self.dict_tag,open=True)
|
|
121
|
+
style = ttk.Style(self.master) # 高dpi支持
|
|
122
|
+
style.configure("Treeview", rowheight=int(20*self.ui_scale))
|
|
123
|
+
|
|
124
|
+
editor = ttk.Labelframe(self.master, text='Edit value',
|
|
125
|
+
width=100, height=100)
|
|
126
|
+
self.okbtn=ttk.Button(editor,text="OK",
|
|
127
|
+
command=self.ok_click,state=tk.DISABLED)
|
|
128
|
+
self.okbtn.pack(side=tk.RIGHT)
|
|
129
|
+
self.editor = tk.Entry(editor,width=45)
|
|
130
|
+
self.editor.pack(side=tk.LEFT,expand=True,fill=tk.X)
|
|
131
|
+
self.editor.bind("<Key-Return>",self.ok_click)
|
|
132
|
+
editor.pack(side=tk.BOTTOM,fill=tk.X)
|
|
133
|
+
self.tvw.pack(side=tk.BOTTOM,expand=True,fill=tk.BOTH)
|
|
134
|
+
|
|
135
|
+
self.menu=tk.Menu(self.master,tearoff=False)
|
|
136
|
+
self.menu.add_command(label="Open in new window",command=self.open_in_new_window,
|
|
137
|
+
state=tk.DISABLED)
|
|
138
|
+
self.menu.add_command(label="New attribute",command=self.new_item,state=tk.DISABLED)
|
|
139
|
+
self.menu.add_command(label="Delete",command=self.del_item,state=tk.DISABLED)
|
|
140
|
+
def on_rightclick(event):
|
|
141
|
+
if len(self.tvw.selection()) <= 1:
|
|
142
|
+
self.tvw.event_generate("<Button-1>",x=event.x,y=event.y) # 选择当前右击的项
|
|
143
|
+
self.menu.post(event.x_root,event.y_root)
|
|
144
|
+
self.tvw.bind("<B3-ButtonRelease>",on_rightclick)
|
|
145
|
+
def load_image(self):
|
|
146
|
+
# 加载images文件夹下的图片
|
|
147
|
+
self.back_image=tk.PhotoImage(master=self.master,
|
|
148
|
+
file=os.path.join(_IMAGE_PATH,"back.gif"))
|
|
149
|
+
self.forward_image=tk.PhotoImage(master=self.master,
|
|
150
|
+
file=os.path.join(_IMAGE_PATH,"forward.gif"))
|
|
151
|
+
self.refresh_image=tk.PhotoImage(master=self.master,
|
|
152
|
+
file=os.path.join(_IMAGE_PATH,"refresh.gif"))
|
|
153
|
+
self.obj_image=tk.PhotoImage(master=self.master,
|
|
154
|
+
file=os.path.join(_IMAGE_PATH,"python.gif"))
|
|
155
|
+
self.num_image=tk.PhotoImage(master=self.master,
|
|
156
|
+
file=os.path.join(_IMAGE_PATH,"number.gif"))
|
|
157
|
+
self.str_image=tk.PhotoImage(master=self.master,
|
|
158
|
+
file=os.path.join(_IMAGE_PATH,"string.gif"))
|
|
159
|
+
self.list_image=tk.PhotoImage(master=self.master,
|
|
160
|
+
file=os.path.join(_IMAGE_PATH,"list.gif"))
|
|
161
|
+
self.empty_list_image=tk.PhotoImage(master=self.master,
|
|
162
|
+
file=os.path.join(_IMAGE_PATH,"empty_list.gif"))
|
|
163
|
+
self.tuple_image=tk.PhotoImage(master=self.master,
|
|
164
|
+
file=os.path.join(_IMAGE_PATH,"tuple.gif"))
|
|
165
|
+
self.empty_tuple_image=tk.PhotoImage(master=self.master,
|
|
166
|
+
file=os.path.join(_IMAGE_PATH,"empty_tuple.gif"))
|
|
167
|
+
self.dict_image=tk.PhotoImage(master=self.master,
|
|
168
|
+
file=os.path.join(_IMAGE_PATH,"dict.gif"))
|
|
169
|
+
self.empty_dict_image=tk.PhotoImage(master=self.master,
|
|
170
|
+
file=os.path.join(_IMAGE_PATH,"empty_dict.gif"))
|
|
171
|
+
self.func_image=tk.PhotoImage(master=self.master,
|
|
172
|
+
file=os.path.join(_IMAGE_PATH,"function.gif"))
|
|
173
|
+
self.code_image=tk.PhotoImage(master=self.master,
|
|
174
|
+
file=os.path.join(_IMAGE_PATH,"codeobject.gif"))
|
|
175
|
+
def clear(self):
|
|
176
|
+
# 清除Treeview的数据
|
|
177
|
+
for root in self.tvw.get_children(""): # 获取根项
|
|
178
|
+
for item in self.tvw.get_children(root):
|
|
179
|
+
self.tvw.delete(item)
|
|
180
|
+
def _get_image(self,obj):
|
|
181
|
+
if isinstance(obj,int) or isinstance(obj,float):
|
|
182
|
+
return self.num_image
|
|
183
|
+
elif isinstance(obj,str):
|
|
184
|
+
return self.str_image
|
|
185
|
+
elif isinstance(obj,tuple):
|
|
186
|
+
return self.tuple_image if len(obj) else self.empty_tuple_image
|
|
187
|
+
elif isinstance(obj,list):
|
|
188
|
+
return self.list_image if len(obj) else self.empty_list_image
|
|
189
|
+
elif isdict(obj):
|
|
190
|
+
return self.dict_image if len(obj) else self.empty_dict_image
|
|
191
|
+
elif isfunc(obj):
|
|
192
|
+
return self.func_image
|
|
193
|
+
elif iscode(obj):
|
|
194
|
+
return self.code_image
|
|
195
|
+
else:return self.obj_image
|
|
196
|
+
def _get_type(self,obj):
|
|
197
|
+
if isfunc(obj):
|
|
198
|
+
return self.functions_tag
|
|
199
|
+
elif isinstance(obj,type):
|
|
200
|
+
return self.classes_tag
|
|
201
|
+
else:return self.attributes_tag
|
|
202
|
+
def refresh(self,event=None,_first=False): # _first: 是否为初次加载
|
|
203
|
+
# 更新自身显示的数据
|
|
204
|
+
obj=self.obj
|
|
205
|
+
self.master.title("{} - {}".format(self.title,objectname(obj)))
|
|
206
|
+
self.label["text"]=" Path: %s Object: %s" % (self.name, shortrepr(obj))
|
|
207
|
+
self.clear()
|
|
208
|
+
if _first and ismodule(obj):
|
|
209
|
+
self.tvw.item(self.functions_tag,open=True)
|
|
210
|
+
# 添加属性
|
|
211
|
+
attrs=dir(obj)
|
|
212
|
+
if isinstance(obj,type):
|
|
213
|
+
for attr in TYPE_EXTRA_ATTRS:
|
|
214
|
+
if hasattr(obj,attr):
|
|
215
|
+
attrs.append(attr) # 对类添加额外的,一般不会出现在dir()的返回值的属性
|
|
216
|
+
for i in range(len(attrs)):
|
|
217
|
+
attr=attrs[i]
|
|
218
|
+
if self.verbose or not attr.startswith("_"):
|
|
219
|
+
try:
|
|
220
|
+
object_=getattr(obj,attr)
|
|
221
|
+
value=shortrepr(object_,self.MAX_VIEW_LEN)
|
|
222
|
+
image=self._get_image(object_)
|
|
223
|
+
tags=("gray",) if isinstance(object_,SKIP) else () # 将部分类型设为灰色,如MethodWrapperType
|
|
224
|
+
self.tvw.insert(self._get_type(object_), tk.END, #attr,
|
|
225
|
+
text=attr, image=image,
|
|
226
|
+
values=(value,),tags=tags) # values从第二列开始
|
|
227
|
+
except Exception as error: # 显示错误消息
|
|
228
|
+
value='<{}: {}>'.format(type(error).__name__,str(error))
|
|
229
|
+
self.tvw.insert(self.attributes_tag, tk.END,
|
|
230
|
+
text=attr, image=self.obj_image,
|
|
231
|
+
values=(value,),tags=("error",))
|
|
232
|
+
# 添加类特有的属性 (不会在dir()出现)
|
|
233
|
+
if isinstance(obj,type):
|
|
234
|
+
for attr in TYPE_EXTRA_CLASS_ATTRS:
|
|
235
|
+
if not hasattr(obj,attr):continue
|
|
236
|
+
try:
|
|
237
|
+
object_=getattr(obj,attr)
|
|
238
|
+
value=shortrepr(object_,self.MAX_VIEW_LEN)
|
|
239
|
+
self.tvw.insert(self.classes_tag, tk.END,
|
|
240
|
+
text=attr, image=self._get_image(object_),
|
|
241
|
+
values=(value,))
|
|
242
|
+
except Exception as error: # 显示错误
|
|
243
|
+
value='<{}: {}>'.format(type(error).__name__,str(error))
|
|
244
|
+
self.tvw.insert(self.classes_tag, tk.END,
|
|
245
|
+
text=attr, image=self.obj_image,
|
|
246
|
+
values=(value,),tags=("error",))
|
|
247
|
+
|
|
248
|
+
# 添加列表数据
|
|
249
|
+
if isinstance(obj,(list,tuple)):
|
|
250
|
+
for i in range(len(obj)):
|
|
251
|
+
index=str(i)
|
|
252
|
+
try:
|
|
253
|
+
object_=obj[i]
|
|
254
|
+
value=shortrepr(object_,self.MAX_VIEW_LEN)
|
|
255
|
+
image=self._get_image(object_)
|
|
256
|
+
self.tvw.insert(self.lst_tag, tk.END,
|
|
257
|
+
text=index, image=image,
|
|
258
|
+
values=(value,))
|
|
259
|
+
except Exception as err:
|
|
260
|
+
value='<{}!>'.format(type(err).__name__)
|
|
261
|
+
self.tvw.insert(self.lst_tag, tk.END,
|
|
262
|
+
text=index, image=self.obj_image,
|
|
263
|
+
values=(value,),tags=("error",))
|
|
264
|
+
|
|
265
|
+
# 添加字典数据
|
|
266
|
+
if isdict(obj):
|
|
267
|
+
for key in obj.keys():
|
|
268
|
+
key_name=repr(key)
|
|
269
|
+
try:
|
|
270
|
+
object_=obj[key]
|
|
271
|
+
value=shortrepr(object_,self.MAX_VIEW_LEN)
|
|
272
|
+
image=self._get_image(object_)
|
|
273
|
+
self.tvw.insert(self.dict_tag, tk.END,
|
|
274
|
+
text=key_name, image=image,
|
|
275
|
+
values=(value,))
|
|
276
|
+
except Exception as err:
|
|
277
|
+
value='<{}!>'.format(type(err).__name__)
|
|
278
|
+
self.tvw.insert(self.dict_tag, tk.END,
|
|
279
|
+
text=key_name, image=self.obj_image,
|
|
280
|
+
values=(value,),tags=("error",))
|
|
281
|
+
|
|
282
|
+
self.okbtn['state'] = tk.DISABLED # 由于刷新后不会再选中刷新前的数据
|
|
283
|
+
self.on_select() # 重置菜单的状态 (是否可点击)
|
|
284
|
+
|
|
285
|
+
def browse(self,obj,name="obj",_first=False):
|
|
286
|
+
"浏览一个新对象(obj)。"
|
|
287
|
+
self.obj=obj # 更新self.obj及名称
|
|
288
|
+
self.name=name
|
|
289
|
+
self.refresh(_first=_first)
|
|
290
|
+
def on_select(self,event=None):
|
|
291
|
+
selection = self.tvw.selection()
|
|
292
|
+
if len(selection) != 1:
|
|
293
|
+
self.okbtn['state'] = tk.DISABLED
|
|
294
|
+
self.menu.entryconfig("New attribute",state=tk.DISABLED)
|
|
295
|
+
self.editor.delete(0,tk.END)
|
|
296
|
+
if len(selection) > 1:
|
|
297
|
+
self.menu.entryconfig("Open in new window",state=tk.NORMAL)
|
|
298
|
+
else:
|
|
299
|
+
self.menu.entryconfig("Open in new window",state=tk.DISABLED)
|
|
300
|
+
if len(selection) > 0:
|
|
301
|
+
self.menu.entryconfig("Delete",state=tk.NORMAL)
|
|
302
|
+
else:
|
|
303
|
+
self.menu.entryconfig("Delete",state=tk.DISABLED)
|
|
304
|
+
else:
|
|
305
|
+
parent=self.tvw.parent(selection[0])
|
|
306
|
+
if parent:
|
|
307
|
+
#value_str=self.tvw.item(selection[0])["values"][0]
|
|
308
|
+
attr=self.tvw.item(selection)["text"]
|
|
309
|
+
if parent==self.dict_tag:
|
|
310
|
+
value_str=repr(self.obj[eval(attr)])
|
|
311
|
+
elif parent==self.lst_tag:
|
|
312
|
+
value_str=repr(self.obj[int(attr)])
|
|
313
|
+
else:
|
|
314
|
+
value_str=repr(getattr(self.obj,attr))
|
|
315
|
+
if len(value_str) > self.MAX_EDITVALUE_LEN: # 限制最大长度
|
|
316
|
+
value_str = value_str[:self.MAX_EDITVALUE_LEN]+" ..."
|
|
317
|
+
self.editor.delete(0,tk.END)
|
|
318
|
+
self.editor.insert(0,value_str)
|
|
319
|
+
if parent==self.lst_tag and isinstance(self.obj,tuple):
|
|
320
|
+
self.okbtn['state'] = tk.DISABLED # 元组的属性不可编辑
|
|
321
|
+
self.menu.entryconfig("New attribute",state=tk.DISABLED)
|
|
322
|
+
self.menu.entryconfig("Delete",state=tk.DISABLED)
|
|
323
|
+
else:
|
|
324
|
+
self.okbtn['state'] = tk.NORMAL
|
|
325
|
+
self.menu.entryconfig("New attribute",state=tk.NORMAL)
|
|
326
|
+
self.menu.entryconfig("Delete",state=tk.NORMAL)
|
|
327
|
+
self.menu.entryconfig("Open in new window",state=tk.NORMAL)
|
|
328
|
+
else:
|
|
329
|
+
self.okbtn['state'] = tk.DISABLED
|
|
330
|
+
self.editor.delete(0,tk.END)
|
|
331
|
+
if selection[0]==self.lst_tag and not isinstance(self.obj,list) \
|
|
332
|
+
or selection[0]==self.dict_tag and not isinstance(self.obj,dict):
|
|
333
|
+
self.menu.entryconfig("New attribute",state=tk.DISABLED)
|
|
334
|
+
else:self.menu.entryconfig("New attribute",state=tk.NORMAL)
|
|
335
|
+
self.menu.entryconfig("Open in new window",state=tk.DISABLED)
|
|
336
|
+
self.menu.entryconfig("Delete",state=tk.DISABLED)
|
|
337
|
+
def on_open(self,event=None,new_window=False):
|
|
338
|
+
#当双击Treeview或按回车时, 进一步浏览选中的属性。
|
|
339
|
+
#selection为所有选中项的元组,以('Hxxx',)的形式表示
|
|
340
|
+
for selection in self.tvw.selection():
|
|
341
|
+
parent=self.tvw.parent(selection)
|
|
342
|
+
if not parent:continue # 如果没有父项
|
|
343
|
+
attr=self.tvw.item(selection)["text"]
|
|
344
|
+
if parent==self.dict_tag:
|
|
345
|
+
obj=self.obj[eval(attr)]
|
|
346
|
+
path="%s[%s]" % (self.name,attr)
|
|
347
|
+
elif parent==self.lst_tag:
|
|
348
|
+
obj=self.obj[int(attr)]
|
|
349
|
+
path="%s[%s]" % (self.name,attr)
|
|
350
|
+
else:
|
|
351
|
+
obj=getattr(self.obj,attr)
|
|
352
|
+
path=self.name+"."+attr
|
|
353
|
+
if not (new_window or self.multi_window):
|
|
354
|
+
self.obj=obj
|
|
355
|
+
self.name=path
|
|
356
|
+
self.history=self.history[:self.history_index+1] # 清除后面的数据
|
|
357
|
+
self.history.append((obj,path))
|
|
358
|
+
self.history_index+=1
|
|
359
|
+
self.refresh()
|
|
360
|
+
break # 跳转到新对象后,停止处理其他选中的项
|
|
361
|
+
else:browse(obj,self.verbose,path,mainloop=False, # 在新窗口中浏览
|
|
362
|
+
multi_window=self.multi_window,refresh_history=self.refresh_history,
|
|
363
|
+
root_obj=self.root_obj,rootobj_name=self.rootobj_name)
|
|
364
|
+
def open_in_new_window(self):
|
|
365
|
+
self.on_open(new_window=True)
|
|
366
|
+
def ok_click(self,event=None):
|
|
367
|
+
if self.okbtn["state"]==tk.DISABLED:
|
|
368
|
+
return
|
|
369
|
+
selected=self.tvw.selection()[0]
|
|
370
|
+
parent=self.tvw.parent(selected)
|
|
371
|
+
item=self.tvw.item(selected)
|
|
372
|
+
attr=item["text"]
|
|
373
|
+
value=eval(self.editor.get())
|
|
374
|
+
if parent==self.dict_tag:
|
|
375
|
+
self.obj[eval(attr)]=value
|
|
376
|
+
elif parent==self.lst_tag:
|
|
377
|
+
self.obj[int(attr)]=value
|
|
378
|
+
else:
|
|
379
|
+
setattr(self.obj,attr,value)
|
|
380
|
+
self.tvw.item(selected,values=(shortrepr(value,self.MAX_VIEW_LEN),),
|
|
381
|
+
image=self._get_image(value))
|
|
382
|
+
def new_item(self):
|
|
383
|
+
if not self.tvw.selection():return
|
|
384
|
+
selected=self.tvw.selection()[0]
|
|
385
|
+
if self.dict_tag in (selected,self.tvw.parent(selected)):
|
|
386
|
+
key=simpledialog.askstring("New item","Enter key (use quotes for strings):")
|
|
387
|
+
if not key:return
|
|
388
|
+
value=simpledialog.askstring("New item","Enter value:")
|
|
389
|
+
if not value:return
|
|
390
|
+
self.obj[eval(key)]=eval(value)
|
|
391
|
+
elif self.lst_tag in (selected,self.tvw.parent(selected)):
|
|
392
|
+
index=simpledialog.askstring("New item","Enter index for new item (0, 1, 2, ...):")
|
|
393
|
+
if not index:return
|
|
394
|
+
value=simpledialog.askstring("New item","Enter value:")
|
|
395
|
+
if not value:return
|
|
396
|
+
self.obj.insert(int(index),eval(value))
|
|
397
|
+
else:
|
|
398
|
+
attr=simpledialog.askstring("New item","Enter attribute name (no quotes needed):")
|
|
399
|
+
if not attr:return
|
|
400
|
+
value=simpledialog.askstring("New item","Enter value:")
|
|
401
|
+
if not value:return
|
|
402
|
+
setattr(self.obj,attr,eval(value))
|
|
403
|
+
self.navigate_history() # 刷新
|
|
404
|
+
def del_item(self,event=None):
|
|
405
|
+
# 删除选中的项
|
|
406
|
+
for selection in self.tvw.selection():
|
|
407
|
+
parent=self.tvw.parent(selection)
|
|
408
|
+
if not parent:continue # 如果没有父项
|
|
409
|
+
attr=self.tvw.item(selection)["text"]
|
|
410
|
+
if parent==self.dict_tag:
|
|
411
|
+
del self.obj[eval(attr)]
|
|
412
|
+
elif parent==self.lst_tag:
|
|
413
|
+
del self.obj[int(attr)]
|
|
414
|
+
else:
|
|
415
|
+
delattr(self.obj,attr)
|
|
416
|
+
self.refresh()
|
|
417
|
+
def navigate_history(self,event=None): # 转到当前的历史记录
|
|
418
|
+
obj,path = self.history[self.history_index]
|
|
419
|
+
if self.refresh_history:
|
|
420
|
+
try:
|
|
421
|
+
# 对象的属性可能有改变,重新获取对象的属性
|
|
422
|
+
scope={self.rootobj_name:self.root_obj} # 获取第一个浏览的根对象及其名称
|
|
423
|
+
object_=eval(path,scope)
|
|
424
|
+
except Exception: # 默认使用新获取的对象,只有出错时,才使用旧的对象
|
|
425
|
+
object_=obj
|
|
426
|
+
else:object_=obj
|
|
427
|
+
self.browse(object_,path)
|
|
428
|
+
def back(self): # 后退
|
|
429
|
+
if self.history_index!=0:
|
|
430
|
+
self.history_index-=1
|
|
431
|
+
self.navigate_history()
|
|
432
|
+
def forward(self): # 前进
|
|
433
|
+
if self.history_index!=len(self.history)-1:
|
|
434
|
+
self.history_index+=1
|
|
435
|
+
self.navigate_history()
|
|
436
|
+
|
|
437
|
+
def browse(object,verbose=True,name="root_obj",
|
|
438
|
+
mainloop=True,multi_window=False,refresh_history=True,
|
|
439
|
+
root_obj=None,rootobj_name=None):
|
|
440
|
+
"""Browse a Python object through a graphical interface.
|
|
441
|
+
verbose: Similar to describe, it indicates whether to print special methods \
|
|
442
|
+
of the object (e.g., __init__).
|
|
443
|
+
name: Specifies the display name of the object.
|
|
444
|
+
mainloop: Determines whether the function waits for the window to close before \
|
|
445
|
+
exiting, meaning whether the browse function will be blocking.
|
|
446
|
+
multi_window: Indicates whether to open a new window when double-clicking \
|
|
447
|
+
(or pressing Enter) to browse a new object.
|
|
448
|
+
refresh_history: Determines whether to re-fetch the object's \
|
|
449
|
+
attributes when navigating backward or forward. If True, editing attributes \
|
|
450
|
+
and then navigating will show changes in the object; otherwise, it will not.
|
|
451
|
+
root_obj and rootobj_name: Specify the root object and its name (for internal use)."""
|
|
452
|
+
root=tk.Tk()
|
|
453
|
+
ObjectBrowser(root,object,verbose,name,multi_window=multi_window,
|
|
454
|
+
refresh_history=refresh_history,root_obj=root_obj,
|
|
455
|
+
rootobj_name=rootobj_name)
|
|
456
|
+
if mainloop:root.mainloop()
|
|
457
|
+
|
|
458
|
+
def test():
|
|
459
|
+
if sys.platform == 'win32': # Windows下的高DPI支持
|
|
460
|
+
ctypes.OleDLL('shcore').SetProcessDpiAwareness(1)
|
|
461
|
+
|
|
462
|
+
class Test:
|
|
463
|
+
def __init__(self):
|
|
464
|
+
self.a='foo';self._view_cnt=0
|
|
465
|
+
self.list=["foo","bar",1]
|
|
466
|
+
self.tuple=("foo","bar",2)
|
|
467
|
+
self.dict={"a":"bar","b":1}
|
|
468
|
+
@property
|
|
469
|
+
def view_cnt(self):
|
|
470
|
+
self._view_cnt+=1
|
|
471
|
+
return self._view_cnt
|
|
472
|
+
|
|
473
|
+
browse(Test(),verbose=True)
|
|
474
|
+
|
|
475
|
+
if __name__=="__main__":test()
|