pyobject 1.2.5__cp313-cp313-win32.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.
Binary file
pyobject/__init__.py ADDED
@@ -0,0 +1,122 @@
1
+ """一个提供操作Python对象底层工具的模块。
2
+ A utility tool with some submodules for operating internal python objects.
3
+ """
4
+ import sys
5
+ from warnings import warn
6
+ from pprint import pprint
7
+
8
+ __email__="3076711200@qq.com"
9
+ __author__="qfcy qq:3076711200"
10
+ __version__="1.2.5"
11
+
12
+ _ignore_names=["__builtins__","__doc__"]
13
+ __all__=["objectname","bases","describe","desc"]
14
+
15
+ def objectname(obj):
16
+ """objectname(obj) - Returns the name of an object in the format xxmodule.xxclass.
17
+ For example: objectname(int) -> 'builtins.int'."""
18
+ if not obj.__class__==type:obj=obj.__class__
19
+ if obj.__module__=="__main__":return obj.__name__
20
+ return "{}.{}".format(obj.__module__,obj.__name__)
21
+
22
+ ###无递归版本
23
+ ##def base(obj):
24
+ ## while True:
25
+ ## try:
26
+ ## obj=obj.__bases__
27
+ ## except AttributeError:obj=[obj.__class__]
28
+ ## if not obj:break
29
+ ## else:obj=obj[0]
30
+ ## print(obj)
31
+ def bases(obj,level=0,tab=4):
32
+ '''bases(obj) - Prints the base classes of the given object.
33
+ tab: The number of spaces for indentation, default is 4.'''
34
+ if not obj.__class__==type:obj=obj.__class__
35
+ if obj.__bases__:
36
+ if level:print(' '*(level*tab),end='')
37
+ print(*obj.__bases__,sep=',')
38
+ for cls in obj.__bases__:
39
+ bases(cls,level,tab)
40
+
41
+ _trans_table=str.maketrans("\n\t"," ") # 去除特殊字符
42
+ def _shortrepr(obj,maxlength=150):
43
+ result=repr(obj).translate(_trans_table)
44
+ if len(result)>maxlength:
45
+ return result[:maxlength]+"..."
46
+ return result
47
+
48
+ ###无递归的旧版
49
+ ##def describe(obj,verbose=True,needhelp=False,**kwargs):
50
+ ## "Prints the properties of an object."
51
+ ## __builtins__.print(repr(obj)+" :",**kwargs)
52
+ ## for attr in dir(obj):
53
+ ## if verbose or not attr.startswith("_"):
54
+ ## if not attr in ignore_names:
55
+ ## value=getattr(obj,attr)
56
+ ## pprint("{}:{}".format(attr,value),**kwargs)
57
+ ## if needhelp:help(value)
58
+ def describe(obj,level=0,maxlevel=1,tab=4,verbose=False,file=None):
59
+ '''"Describe" an object by printing its attributes.
60
+ Parameters:
61
+ maxlevel: The number of levels to print the object's attributes.
62
+ tab: The number of spaces for indentation, default is 4.
63
+ verbose: A boolean value indicating whether to print special methods (e.g., __init__).
64
+ file: A file-like object for printing output.
65
+ '''
66
+ if file is None:file=sys.stdout
67
+ if level==maxlevel:
68
+ result=repr(obj)
69
+ if result.startswith('[') or result.startswith('{'):pprint(result)
70
+ else:print(result,file=file)
71
+ elif level>maxlevel:raise ValueError(
72
+ "Argument level is larger than maxlevel")
73
+ else:
74
+ print(_shortrepr(obj)+': ',file=file)
75
+ if type(obj) is type:
76
+ print("Base classes of the object:",file=file)
77
+ bases(obj,level+1,tab)
78
+ print(file=file)
79
+ for attr in dir(obj):
80
+ if verbose or not attr.startswith("_"):
81
+ print(' '*tab*(level+1)+attr+': ',end='',file=file)
82
+ try:
83
+ if not attr in _ignore_names:
84
+ describe(getattr(obj,attr),level+1,maxlevel,
85
+ tab,verbose,file)
86
+ else:print(_shortrepr(getattr(obj,attr)),file=file)
87
+ except AttributeError:
88
+ print("<AttributeError!>",file=file)
89
+
90
+ desc=describe #别名
91
+ # 导入其他子模块中的函数和类
92
+ try:
93
+ from pyobject.browser import browse
94
+ __all__.append("browse")
95
+ # SystemError: 修复Python 3.4下的bug
96
+ except (ImportError,SystemError):warn("Failed to import module pyobject.browser.")
97
+ try:
98
+ from pyobject.search import make_list,make_iter,search #,test_make_list,test_search
99
+ __all__.extend(["make_list","make_iter","search"])
100
+ # 同上
101
+ except (ImportError,SystemError):warn("Failed to import pyobject.search.")
102
+
103
+ try:
104
+ from pyobject.code_ import Code
105
+ __all__.append("Code")
106
+ except (ImportError,SystemError):warn("Failed to import pyobject.code_.")
107
+ try:
108
+ from pyobj_extension import *
109
+ __all__.extend(["convptr","py_incref","py_decref","getrealrefcount",
110
+ "setrefcount","list_in"])
111
+ except ImportError:warn("Failed to import module pyobj_extension.")
112
+
113
+ def test():
114
+ try:
115
+ describe(type,verbose=True)
116
+ except BaseException as err:
117
+ print("STOPPED!",file=sys.stderr)
118
+ if not type(err) is KeyboardInterrupt:raise
119
+ return 1
120
+ else:return 0
121
+
122
+ if __name__=="__main__":sys.exit(test())
pyobject/browser.py ADDED
@@ -0,0 +1,461 @@
1
+ "以图形方式浏览Python对象的模块。A module providing a visual interface to browse Python objects."
2
+ import sys,os,types,typing,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 isfunction,ismethod,isgeneratorfunction,isgenerator,iscode
8
+ try:from pyobject import objectname,_shortrepr
9
+ except ImportError:from __init__ import objectname,_shortrepr
10
+
11
+ _IMAGE_PATH=os.path.join(os.path.split(__file__)[0],"images")
12
+
13
+ def isfunc(obj):
14
+ # 判断一个对象是否为函数或方法
15
+ if isfunction(obj) or ismethod(obj):return True
16
+ # 使用typing而不用types.WrapperDescriptorType是为了与旧版本兼容
17
+ func_types=[types.LambdaType,types.BuiltinFunctionType,
18
+ types.BuiltinMethodType,typing.WrapperDescriptorType,
19
+ typing.MethodWrapperType,typing.MethodDescriptorType]
20
+ if sys.version_info.minor>=7:
21
+ ClassMethodDescriptorType=types.ClassMethodDescriptorType
22
+ else: # 3.7之前的旧版本
23
+ ClassMethodDescriptorType=type(dict.__dict__['fromkeys'])
24
+ func_types.append(ClassMethodDescriptorType)
25
+ for type in func_types:
26
+ if isinstance(obj,type):
27
+ return True
28
+ return False
29
+ def isdict(obj):
30
+ # 判断一个对象是否为字典
31
+ dict_types=[dict,types.MappingProxyType]
32
+ for type in dict_types:
33
+ if isinstance(obj,type):return True
34
+ return False
35
+
36
+ def get_dpi_scale():
37
+ # 获取Windows的当前DPI,仅Windows可用
38
+ hdc = ctypes.windll.user32.GetDC(0)
39
+ dpi_x = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88) # 88: LOGPIXELSX
40
+ dpi_y = ctypes.windll.gdi32.GetDeviceCaps(hdc, 90) # 90: LOGPIXELSY
41
+ ctypes.windll.user32.ReleaseDC(0, hdc)
42
+ return dpi_x/96, dpi_y/96
43
+
44
+ class ScrolledTreeview(ttk.Treeview):
45
+ "A scrollable Treeview widget inherited from ttk.Treeview."
46
+ def __init__(self,master,**options):
47
+ self.frame=tk.Frame(master)
48
+ ttk.Treeview.__init__(self,self.frame,**options)
49
+
50
+ self.hscroll=ttk.Scrollbar(self.frame,orient=tk.HORIZONTAL,
51
+ command=self.xview)
52
+ self.vscroll=ttk.Scrollbar(self.frame,command=self.yview)
53
+ self["xscrollcommand"]=self.hscroll.set
54
+ self["yscrollcommand"]=self.vscroll.set
55
+ #self.hscroll.pack(side=tk.BOTTOM,fill=tk.X)
56
+ self.vscroll.pack(side=tk.RIGHT,fill=tk.Y)
57
+ ttk.Treeview.pack(self,side=tk.BOTTOM,expand=True,fill=tk.BOTH)
58
+ def pack(self,**options):
59
+ self.frame.pack(**options)
60
+ def grid(self,**options):
61
+ self.frame.grid(**options)
62
+ def place(self,**options):
63
+ self.frame.place(**options)
64
+
65
+ class ObjectBrowser():
66
+ title="Python Object Browser"
67
+ def __init__(self,master,obj,verbose=False,name="obj",
68
+ multi_window=False,refresh_history=True,
69
+ root_obj=None,rootobj_name=None):
70
+ self.master=master
71
+ self.verbose=verbose
72
+ self.name=name
73
+ self.multi_window=multi_window # 是否多窗口
74
+ self.refresh_history=refresh_history
75
+ self.root_obj = root_obj if root_obj is not None else obj # 根对象的名称,用于历史记录
76
+ self.rootobj_name = rootobj_name or name
77
+ self.history=[(obj,name)] # 单窗口浏览的历史记录,由多个(对象,路径)的元组组成
78
+ self.history_index=0
79
+
80
+ self.master.title(self.title)
81
+ try:
82
+ self.master.iconbitmap(os.path.join(_IMAGE_PATH,"python.ico"))
83
+ except tk.TclError:pass
84
+ self.load_image()
85
+ self.create_widgets()
86
+ self.browse(obj,name=name)
87
+ def create_widgets(self):
88
+ # 创建控件
89
+ toolbar=tk.Frame(self.master)
90
+ if not self.multi_window:
91
+ tk.Button(toolbar,image=self.back_image,command=self.back).pack(side=tk.LEFT)
92
+ tk.Button(toolbar,image=self.forward_image,command=self.forward).pack(side=tk.LEFT)
93
+ tk.Button(toolbar,image=self.refresh_image,command=self.navigate_history #refresh
94
+ ).pack(side=tk.LEFT)
95
+ self.label=tk.Label(toolbar,anchor="w")
96
+ self.label.pack(side=tk.LEFT,fill=tk.X)
97
+ toolbar.pack(side=tk.TOP,fill=tk.X)
98
+ self.tvw=ScrolledTreeview(self.master,column='.',selectmode=tk.EXTENDED)
99
+ self.tvw.heading("#0",text="Attribute/Index/Key")
100
+ self.tvw.heading("#1",text="Value")
101
+ self.tvw.column("#0", stretch = 0, # 不跟随窗口大小的变化拉伸
102
+ width = int(160*(get_dpi_scale()[0] if sys.platform == "win32" else 1)))
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.master.bind("<F5>",self.navigate_history) #refresh)
110
+
111
+ self.functions_tag=self.tvw.insert('',index=0,text="Functions/Methods")
112
+ self.attributes_tag=self.tvw.insert('',index=1,text="Attributes")
113
+ self.classes_tag=self.tvw.insert('',index=2,text="Classes")
114
+ self.lst_tag=self.tvw.insert('',index=3,text="List data")
115
+ self.dict_tag=self.tvw.insert('',index=4,text="Dictionary data")
116
+ self.tvw.item(self.attributes_tag,open=True) # 展开项
117
+ self.tvw.item(self.classes_tag,open=True)
118
+ self.tvw.item(self.lst_tag,open=True)
119
+ self.tvw.item(self.dict_tag,open=True)
120
+ if sys.platform == "win32": # 高dpi支持
121
+ style = ttk.Style(self.master)
122
+ style.configure("Treeview", rowheight=int(20*get_dpi_scale()[1]))
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="Add",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):
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
+ max_length = 500 # 避免引发性能问题
209
+ # 添加属性
210
+ attrs=dir(obj)
211
+ for i in range(len(attrs)):
212
+ attr=attrs[i]
213
+ if self.verbose or not attr.startswith("_"):
214
+ try:
215
+ object=getattr(obj,attr)
216
+ value=_shortrepr(object,max_length)
217
+ image=self._get_image(object)
218
+ self.tvw.insert(self._get_type(object), tk.END, #attr,
219
+ text=attr, image=image,
220
+ values=(value,)) # values从第二列开始
221
+ except Exception as error: # 显示错误消息
222
+ value='<{}: {}>'.format(type(error).__name__,str(error))
223
+ self.tvw.insert(self.attributes_tag, tk.END,
224
+ text=attr, image=self.obj_image,
225
+ values=(value,),tags=("error",))
226
+ # __bases__属性一般不会出现在dir()的返回值中
227
+ if hasattr(obj,"__bases__") and "__bases__" not in attrs:
228
+ bases=obj.__bases__
229
+ self.tvw.insert(self.classes_tag, tk.END, "__bases__",
230
+ text="__bases__", image=self._get_image(bases),
231
+ values=(repr(bases),))
232
+ # 添加列表数据
233
+ if isinstance(obj,(list,tuple)):
234
+ for i in range(len(obj)):
235
+ index=str(i)
236
+ try:
237
+ object=obj[i]
238
+ value=_shortrepr(object,max_length)
239
+ image=self._get_image(object)
240
+ self.tvw.insert(self.lst_tag, tk.END,
241
+ text=index, image=image,
242
+ values=(value,))
243
+ except Exception as err:
244
+ value='<{}!>'.format(type(err).__name__)
245
+ self.tvw.insert(self.lst_tag, tk.END,
246
+ text=index, image=self.obj_image,
247
+ values=(value,),tags=("error",))
248
+
249
+ # 添加字典数据
250
+ if isdict(obj):
251
+ for key in obj.keys():
252
+ key_name=repr(key)
253
+ try:
254
+ object=obj[key]
255
+ value=_shortrepr(object,max_length)
256
+ image=self._get_image(object)
257
+ self.tvw.insert(self.dict_tag, tk.END,
258
+ text=key_name, image=image,
259
+ values=(value,))
260
+ except Exception as err:
261
+ value='<{}!>'.format(type(err).__name__)
262
+ self.tvw.insert(self.dict_tag, tk.END,
263
+ text=key_name, image=self.obj_image,
264
+ values=(value,),tags=("error",))
265
+
266
+ self.okbtn['state'] = tk.DISABLED # 由于刷新后不会再选中刷新前的数据
267
+ self.on_select() # 重置菜单的状态 (是否可点击)
268
+
269
+ def browse(self,obj,name="obj"):
270
+ "浏览一个新对象(obj)。"
271
+ self.obj=obj # 更新self.obj及名称
272
+ self.name=name
273
+ self.refresh()
274
+ def on_select(self,event=None):
275
+ selection = self.tvw.selection()
276
+ if len(selection) != 1:
277
+ self.okbtn['state'] = tk.DISABLED
278
+ self.menu.entryconfig("Add",state=tk.DISABLED)
279
+ self.editor.delete(0,tk.END)
280
+ if len(selection) > 1:
281
+ self.menu.entryconfig("Open in new window",state=tk.NORMAL)
282
+ else:
283
+ self.menu.entryconfig("Open in new window",state=tk.DISABLED)
284
+ if len(selection) > 0:
285
+ self.menu.entryconfig("Delete",state=tk.NORMAL)
286
+ else:
287
+ self.menu.entryconfig("Delete",state=tk.DISABLED)
288
+ else:
289
+ parent=self.tvw.parent(selection[0])
290
+ if parent:
291
+ #value_str=self.tvw.item(selection[0])["values"][0] # 现已弃用,由于使用了_shortrepr()
292
+ attr=self.tvw.item(selection)["text"]
293
+ if parent==self.dict_tag:
294
+ value_str=repr(self.obj[eval(attr)])
295
+ elif parent==self.lst_tag:
296
+ value_str=repr(self.obj[int(attr)])
297
+ else:
298
+ value_str=repr(getattr(self.obj,attr))
299
+ self.editor.delete(0,tk.END)
300
+ self.editor.insert(0,value_str)
301
+ if parent==self.lst_tag and isinstance(self.obj,tuple):
302
+ self.okbtn['state'] = tk.DISABLED # 元组的属性不可编辑
303
+ self.menu.entryconfig("Add",state=tk.DISABLED)
304
+ self.menu.entryconfig("Delete",state=tk.DISABLED)
305
+ else:
306
+ self.okbtn['state'] = tk.NORMAL
307
+ self.menu.entryconfig("Add",state=tk.NORMAL)
308
+ self.menu.entryconfig("Delete",state=tk.NORMAL)
309
+ self.menu.entryconfig("Open in new window",state=tk.NORMAL)
310
+ else:
311
+ self.okbtn['state'] = tk.DISABLED
312
+ self.editor.delete(0,tk.END)
313
+ if selection[0]==self.lst_tag and not isinstance(self.obj,list) \
314
+ or selection[0]==self.dict_tag and not isinstance(self.obj,dict):
315
+ self.menu.entryconfig("Add",state=tk.DISABLED)
316
+ else:self.menu.entryconfig("Add",state=tk.NORMAL)
317
+ self.menu.entryconfig("Open in new window",state=tk.DISABLED)
318
+ self.menu.entryconfig("Delete",state=tk.DISABLED)
319
+ def on_open(self,event=None,new_window=False):
320
+ #当双击Treeview或按回车时, 进一步浏览选中的属性。
321
+ #selection为所有选中项的元组,以('Hxxx',)的形式表示
322
+ for selection in self.tvw.selection():
323
+ parent=self.tvw.parent(selection)
324
+ if not parent:continue # 如果没有父项
325
+ attr=self.tvw.item(selection)["text"]
326
+ if parent==self.dict_tag:
327
+ obj=self.obj[eval(attr)]
328
+ path="%s[%s]" % (self.name,attr)
329
+ elif parent==self.lst_tag:
330
+ obj=self.obj[int(attr)]
331
+ path="%s[%s]" % (self.name,attr)
332
+ else:
333
+ obj=getattr(self.obj,attr)
334
+ path=self.name+"."+attr
335
+ if not (new_window or self.multi_window):
336
+ self.obj=obj
337
+ self.name=path
338
+ self.history=self.history[:self.history_index+1] # 清除后面的数据
339
+ self.history.append((obj,path))
340
+ self.history_index+=1
341
+ self.refresh()
342
+ break # 跳转到新对象后,停止处理其他选中的项
343
+ else:browse(obj,self.verbose,path,mainloop=False, # 在新窗口中浏览
344
+ multi_window=self.multi_window,refresh_history=self.refresh_history,
345
+ root_obj=self.root_obj,rootobj_name=self.rootobj_name)
346
+ def open_in_new_window(self):
347
+ self.on_open(new_window=True)
348
+ def ok_click(self,event=None):
349
+ if self.okbtn["state"]==tk.DISABLED:
350
+ return
351
+ selected=self.tvw.selection()[0]
352
+ parent=self.tvw.parent(selected)
353
+ item=self.tvw.item(selected)
354
+ attr=item["text"]
355
+ value=eval(self.editor.get())
356
+ if parent==self.dict_tag:
357
+ self.obj[eval(attr)]=value
358
+ elif parent==self.lst_tag:
359
+ self.obj[int(attr)]=value
360
+ else:
361
+ setattr(self.obj,attr,value)
362
+ self.tvw.item(selected,values=(repr(value),),
363
+ image=self._get_image(value))
364
+ def new_item(self):
365
+ if not self.tvw.selection():return
366
+ selected=self.tvw.selection()[0]
367
+ if self.dict_tag in (selected,self.tvw.parent(selected)):
368
+ key=simpledialog.askstring("New item","Enter key (use quotes for strings):")
369
+ if not key:return
370
+ value=simpledialog.askstring("New item","Enter value:")
371
+ if not value:return
372
+ self.obj[eval(key)]=eval(value)
373
+ elif self.lst_tag in (selected,self.tvw.parent(selected)):
374
+ index=simpledialog.askstring("New item","Enter index for new item (0, 1, 2, ...):")
375
+ if not index:return
376
+ value=simpledialog.askstring("New item","Enter value:")
377
+ if not value:return
378
+ self.obj.insert(int(index),eval(value))
379
+ else:
380
+ attr=simpledialog.askstring("New item","Enter attribute name (no quotes needed):")
381
+ if not attr:return
382
+ value=simpledialog.askstring("New item","Enter value:")
383
+ if not value:return
384
+ setattr(self.obj,attr,eval(value))
385
+ self.navigate_history()
386
+ def del_item(self,event=None):
387
+ # 删除选中的项
388
+ for selection in self.tvw.selection():
389
+ parent=self.tvw.parent(selection)
390
+ if not parent:continue # 如果没有父项
391
+ attr=self.tvw.item(selection)["text"]
392
+ if parent==self.dict_tag:
393
+ del self.obj[eval(attr)]
394
+ elif parent==self.lst_tag:
395
+ del self.obj[int(attr)]
396
+ else:
397
+ delattr(self.obj,attr)
398
+ self.refresh()
399
+ def navigate_history(self,event=None): # 转到当前的历史记录
400
+ obj,path = self.history[self.history_index]
401
+ if self.refresh_history:
402
+ try:
403
+ # 对象的属性可能有改变,重新获取对象的属性
404
+ scope={self.rootobj_name:self.root_obj} # 获取第一个浏览的根对象及其名称
405
+ object=eval(path,scope)
406
+ except Exception: # 默认使用新获取的对象,只有出错时,才使用旧的对象
407
+ object=obj
408
+ else:object=obj
409
+ self.browse(object,path)
410
+ def back(self): # 后退
411
+ if self.history_index!=0:
412
+ self.history_index-=1
413
+ self.navigate_history()
414
+ def forward(self): # 前进
415
+ if self.history_index!=len(self.history)-1:
416
+ self.history_index+=1
417
+ self.navigate_history()
418
+
419
+ def browse(object,verbose=True,name="obj",
420
+ mainloop=True,multi_window=False,refresh_history=True,
421
+ root_obj=None,rootobj_name=None):
422
+ """Browse a Python object through a graphical interface.
423
+ verbose: Similar to describe, it indicates whether to print special methods \
424
+ of the object (e.g., __init__).
425
+ name: Specifies the display name of the object.
426
+ mainloop: Determines whether the function waits for the window to close before \
427
+ exiting, meaning whether the browse function will be blocking.
428
+ multi_window: Indicates whether to open a new window when double-clicking \
429
+ (or pressing Enter) to browse a new object.
430
+ refresh_history: Determines whether to re-fetch the object's \
431
+ attributes when navigating backward or forward. If True, editing attributes \
432
+ and then navigating will show changes in the object; otherwise, it will not.
433
+ root_obj and rootobj_name: Specify the root object and its name (for internal use)."""
434
+ root=tk.Tk()
435
+ if sys.platform == "win32": # 高dpi支持
436
+ dpi_x,dpi_y = get_dpi_scale()
437
+ else:dpi_x = dpi_y = 1
438
+ width, height = 480,400
439
+ root.geometry("%dx%d" % (width*dpi_x,height*dpi_y))
440
+ ObjectBrowser(root,object,verbose,name,multi_window=multi_window,
441
+ refresh_history=refresh_history,root_obj=root_obj,
442
+ rootobj_name=rootobj_name)
443
+ if mainloop:root.mainloop()
444
+
445
+ def test():
446
+ if sys.platform == 'win32': # Windows下的高DPI支持
447
+ ctypes.OleDLL('shcore').SetProcessDpiAwareness(1)
448
+ class Test:
449
+ def __init__(self):
450
+ self.a='foo';self._cnt=0
451
+ self.list=["foo","bar",1]
452
+ self.tuple=("foo","bar",2)
453
+ self.dict={"a":"bar","b":1}
454
+ @property
455
+ def cnt(self):
456
+ self._cnt+=1
457
+ return self._cnt
458
+
459
+ browse(Test(),verbose=True)
460
+
461
+ if __name__=="__main__":test()