Python Eye —— 一款图形化Debug软件

Python Eye 使用文档

欢迎阅读此文档!


通过阅读本文档,你将学习如何使用本软件进行python代码调试

遇到困难时,请先阅读此文档寻找是否有解决办法,而不是报告问题

默认使用UTF-8编码,你可以点击菜单栏 文件 -> 重新用...编码打开 来更改文件编码方式,这同时也会设置下次打开代码的默认编码方式,你可以在弹出的提示框中看到当前文件的编码方式。
  • 代码编辑

本软件主打调试,并不提供直接的代码编辑功能,你可以使用 工具 -> 简易编辑 来打开Python自带的IDLE编辑器进行快速编辑;保存文件后记得点击 重载文件,或者直接使用快捷键 F5 刷新代码。
  • 选择Python解释器

你需要找到自己电脑上的 pythonw.exe 文件(不推荐选择python.exe文件,它们有一定区别),这个解释器将在进行调试时用来运行你的代码,它也可以是你创建的虚拟环境中的解释器,请确保它已经安装了相关库以便能够完整地运行你的代码。
在设置中可以选择自动搜索解释器,不过这只能找到系统环境变量中的第一个解释器。
选择的Python解释器版本必须在3.8及以上,最好是3.11.4及以上,尽量选择较新且较稳定的版本。(如果一定要用3.8以下的版本,你的代码必须以gbk编码)
  • 调试代码

处于菜单栏下方的灰色栏是操作栏,右边五个按钮用于控制代码的运行,左边的输入框和按钮用来设置变量追踪
这些功能也可以在菜单栏 调试 里找到,同时会有对应的快捷键提示(右侧对齐的是全局快捷键,只要看得见菜单栏就可以用;紧跟项名且在括号内的是局部快捷键,需要打开对应菜单才可以用)
这是本软件的核心功能,也是不同于其他代码调试软件的地方
在操作栏左侧的输入框里输入变量名称,点击右侧眼睛按钮或者直接按下回车,会在代码区域生成一个追踪块。这可以在你开始代码调试之前进行,在代码调试结束之后也不会消失,要删除它,双击并确认即可
你可以创建许多追踪快,它们是可以拖动的,并且相互之间会碰撞以避免重叠。将它们摆在你喜欢的位置后,你就可以开始调试代码,每次运行代码之后,追踪块都会获取自己追踪的变量值并更新,如果当前位置无法获取到变量也会有相应显示。这使得你可以实时地观察到自己想要的变量在程序执行过程中是如何变化的
追踪块的追踪实际上是对变量进行了一次访问,因此你甚至可以追踪 a > b 这样的一段python表达式,你会获得 True 或是 False 的追踪结果。不过需要注意的是,你不可以在追踪项中调用函数,因为这有可能更改代码的实际运行情况。事实上,我对此的检测是你的表达式中不能出现 “(”,但仍不排除你可以调用到Python提供的魔法方法,因此请注意这是否会更改你的代码运行情况
  • 保存调试状态

这是一个非常酷的功能,可以让你在调试中途进行保存,你将得到一个.eye后缀的文件,并且在任何时候都可以通过此文件还原调试状态,包括运行行数,断点设置,临时代码,变量追踪等,你甚至不需要附带源码,它就可以还原当前状态,前提是所使用的python解释器具备相应运行环境
但需要说明的是,虽然脱离源码赋予了eye文件极大的传播和携带优势,但代价是它只能是单文件代码,你只可以导入存在于python环境中的库,否则将无法还原调试

好了,工具基本介绍完了,至于如何利用以及它会有哪些更高级用法,等待你去探索!

从现在起,摆脱低效的 print,开启蟒蛇之眼!



Python Eye 调试的基本原理

——基于Python的Pdb模块

  • 官方介绍

pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。
它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,
列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。
它还支持事后调试,可以在程序控制下调用。
  • 开始魔改

pdb模块可以使用cmd命令运行,这也是我要使用的方法。
python -m pdb mycode.py
不过我需要的是图形化界面,所以还需要小小的改进 :)
在python的pdb.py文件中,占主体部分的是一个名为Pdb的类,可以看到它还继承了另两个类:
class Pdb(bdb.Bdb, cmd.Cmd):...
其中Cmd是用于提供命令行交互的基类,Bdb是用于python代码调试的基类。而我需要改变命令行的交互模式,所以应当去cmd.py文件中一探究竟。
最终我找到这样一个方法(部分代码已省略):
def cmdloop(self, intro=None):...while not stop:if self.cmdqueue:line = self.cmdqueue.pop(0)else:if self.use_rawinput:try:line = input(self.prompt)except EOFError:line = 'EOF'...
在一个while循环里有一行input,这就是实现命令行交互模式的基本逻辑,下面我只要替换掉这行input,直接通过某种方式对line赋值,就能够改变命令行的交互模式。
但是还有一个问题,即input函数会阻塞当前线程,一种解决办法是使用while循环卡住,然后在子线程里赋值,但是这样显然不够优雅,于是我采用了另一种方法:
import multiprocessing# 这是定义在类里面的方法,不是函数
self.input_queue = multiprocessing.Queue()
self.output_queue = multiprocessing.Queue()
def cmdloop(self):self.preloop()stop = Nonewhile not stop:if self.cmdqueue:line = self.cmdqueue.pop(0)else:self.output_queue.put('\n'.join(self.debugger_info_temp))self.debugger_info_temp = []line = self.input_queue.get(block=True)line = self.precmd(line)stop = self.postcmd(self.onecmd(line), line)
使用Queue类的get方法并将block设为True,这同样会阻塞当前线程,直到队列里面有东西可以取出,这与input的作用完全等效。(另外还去掉了一些不必要的语句)
最终我写了一个子类继承自Pdb,并重写了相关方法,部分代码如下:
class PythonEye(Pdb):HOST = 'localhost'BUFFSIZE = 1024*64def __init__(self, port):...threading.Thread(target=self.start_server).start()  # 通信线程...def cmdloop(self):self.preloop()stop = Nonewhile not stop:if self.cmdqueue:line = self.cmdqueue.pop(0)else:self.output_queue.put('\n'.join(self.debugger_info_temp))self.debugger_info_temp = []line = self.input_queue.get(block=True)line = self.precmd(line)stop = self.postcmd(self.onecmd(line), line)def do_clear(self, arg):# 这个方法也要重写是因为Pdb在这里有一行input让用户进一步确认是否操作,需要去掉......
另一边同样也有一个类与其交互:
class Debugger:HOST = 'localhost'BUFFSIZE = 1024*64def __init__(self, target, python, port):# target:待调试的py文件路径# python:解释器路径# port:通信端口号(多开时避免重复)...self.input_queue = Queue() #  输入命令队列self.output_queue = Queue()  # 返回信息队列def open_eye(self):  # 开启调试(可以重复开启)......def do_command(self, command):  # 输入一条指令并返回调试信息(会阻塞),!!注意:不能传空字符串...
其中Debugger类是需要手动创建的,并且要传入被调试代码路径,python解释器路径,通信端口号,然后调用open_eye方法会自动创建子进程并建立通信,用do_command方法输入命令,命令语法与返回信息就和pdb一样,返回信息需要进一步解释。

Python Eye 代码高亮的基本原理

——词法分析和语法分析

要显示代码,pygame提供了对文本的渲染方法,并且可以控制颜色、大小、字体、样式等外观参数,但是使用单色渲染并不适合作为代码被阅读,因此需要对代码提前分析,将特殊的语法字符进行高亮以提升代码的观感。
  • 词法分析

我将使用正则表达式匹配python代码中所有可能出现的语法类型,这将是一个非常非常长的正则表达式,因此需要先分别定义每一种类型,再将它们合并。
比如要匹配所有的标识符(简单理解为变量名),在python3中,变量名可以是由大小写字母、下划线、数字以及其它字符组成(除了标点符号),而且数字不能出现在开头,因此我将其分为首字符和其余字符两部分:
identifier = r'[^\+\-\*/%@<>&\|^~: =!`\#\$^\(\)\{\}\[\];\"\'\,\.\?0-9\s][^\+\-\*/%@<>&\|^~: =!`\#\$^\(\)\{\}\[\];\"\'\,\.\?\s]*'
这就是一个可以匹配出所有标识符的正则表达式,类似的,还可以得到其他表达式,将它们保存在一个元组里并且标上对应类型:
token_map = (('字符串', string),('标识符', identifier),('虚数', imagnumber),('浮点数', floatnumber),('整数', integer),('运算符', operator),('分隔符', separator),('注释', r'#'),('换行', r'\n'),('空格', spacetab),('其他', r'.'),)
因为可能出现 字符串里的字符被标识符匹配、浮点数的数字部分被整数匹配 等类似的情况,这些表达式的顺序需要进行特殊排布才不会发生错乱。最后,用for循环将这些表达式合并为一个:
token_main_obj = re.compile('|'.join(f'(?P<item[0]>item[1])' for item in token_map))
因为python有一些保留字符,如import、return等,所以我需要对标识符再次区分,如法炮制即可:
identifier_map = (('错误警告', error_warning),('魔法方法', magic_method),('内建函数', builtin_function),('内置类型', builtin_type),('常量', constant),('关键字', keyword),('定义', definition),('SELF', r'\Aself\Z'),)
# 有些不是保留关键字,而是要做针对性的特殊显示identifier_obj = re.compile('|'.join(f'(?P<item[0]>item[1])' for item in identifier_map))
清晰起见,我定义了一个命名元组保存分析出来的每一项,并且将它们放入一个生成器里面:
Token = namedtuple('Token', ('form', 'text'))def lexical_analysis(code):# 词法分析for m in token_main_obj.finditer(code):form = m.lastgrouptext = m.group()...yield Token(form, text)
举个例子,对于以下代码的词法分析,我将得到这样一个结果:
import randomdef get_type(arg):return type(arg)print(get_type(2))
Token(form='关键字', text='import')
Token(form='空格', text=' ')
Token(form='标识符', text='random')
Token(form='换行', text='\n')
Token(form='换行', text='\n')
Token(form='定义', text='def')
Token(form='空格', text=' ')
Token(form='标识符', text='get_type')
Token(form='分隔符', text='(')
Token(form='标识符', text='arg')
Token(form='分隔符', text=')')
Token(form='分隔符', text=':')
Token(form='换行', text='\n')
Token(form='空格', text='    ')
Token(form='关键字', text='return')
Token(form='空格', text=' ')
Token(form='内建函数', text='type')
Token(form='分隔符', text='(')
Token(form='标识符', text='arg')
Token(form='分隔符', text=')')
Token(form='换行', text='\n')
Token(form='换行', text='\n')
Token(form='内建函数', text='print')
Token(form='分隔符', text='(')
Token(form='标识符', text='get_type')
Token(form='分隔符', text='(')
Token(form='整数', text='2')
Token(form='分隔符', text=')')
Token(form='分隔符', text=')')
Token(form='换行', text='\n')
# 这是用for循环逐个print出来的,实际都在一个生成器里面
这就是代码高亮的第一步——词法分析,有了这些分析结果,就可以对其逐个根据语法规则配以不同的样式。
  • 语法分析

语法分析比较简单,逐个读取词法分析的结果,用栈来存储并更新当前代码的状态,同一类型的代码在不同状态下也会有不同样式。
>>> 还是举例说明
def func(a, b=(0, 0)):pass
读到def会将栈的状态更新为['def'],随后的func会被认为是函数名进行高亮;读到 ( 会变为['def('],后面的a和b将被认为是函数参数,特别的,b后面的 = 会使 b 变成关键字参数并进入['def(arg=']状态;后面的元组会将一个新的元素入栈['def(arg=', '('],这样元组里的内容就不会受到def的影响;遇到 ) 会将栈顶元素出栈……
其他类型的语法高亮也将经历类似的过程。
确定好样式后按行分组,为了便于管理,我用一个类来封装这些方法:
class Highlight:def grammar_analysis(self):# 语法分析...def render_line(self, line):# 渲染行...
  • 优化

因为是渲染代码,而代码里会出现大量重复的变量名、关键字等,因此我启用了缓存机制,每次渲染一个词之前先查找缓存里有没有相同样式、相同内容的词,如果有直接获取,没有则生成并缓存。这样在第一次渲染代码的时候将时间缩短到了原来的三分之一,第一次以后的渲染又在此基础上缩短到了二分之一。

  • 缩进提示线

这在阅读长代码段时非常有用,不过要画出这些线并不容易。计算每一行的缩进数是很简单的,但是代码间常常有空行,有些空行我希望提示线断开,因为语法上这里将被分隔;而有些则希望能够连接上下行,即使这一行没有任何代码。
要解决这个问题,就不能从代码开头开始分析,而是要从代码末行倒过来分析。每一行先计算缩进数,然后与前一行合并相同位置的缩进,前一行多出来的缩进截断并渲染,当前行多出来的缩进记录。而遇到空行只需复制前一行的缩进即可。(这里的“前一行”是之前分析的那一行)
def render_tabtip(self):tabtemp = []  # 记录提示线长度...for line in self.highlight.lexical_code[::-1]:  # 倒序读取...if lenline == 0 or lenline == 1 and line[0][0] == '空格':# 复制for s in range(length):tabtemp[s] += 1else:# 合并if lenline == 0 or line[0][0] != '空格':start = 0else:start = line[0][2]for s in range(start):if s < length:tabtemp[s] += 1else:tabtemp.append(1)length += 1first_line = line_number == 1if start <= length or first_line:if first_line:start = line = 0for s in range(start, length):# 渲染...line_number -= 1
这样就得到了非常准确的缩进提示线。
最后看一下效果:




文末附上下载链接,软件内部也可以检查到更新并且打开此链接

下载链接:蓝奏云网盘
网址:https://fantastair.lanzout.com/b03qfxm6b
密码:pythoneye



本文链接:https://my.lmcjl.com/post/9909.html

展开阅读全文

4 评论

留下您的评论.