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

下载链接在文末🎉🎉🎉

开发者QQ:2364209908
开发者邮箱:Fantastair@qq.com
公测已结束,暂停接受BUG反馈,但仍可以提供更新建议
正式版正在开发中。。。💪💪💪
敬请期待!

Python Eye 使用文档

欢迎阅读此文档!


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

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

  • 打开代码

    这是所有工作的第一步
    • 要打开你的代码,你可以点击加号或者直接将文件拖进框中
    • 只会打开 py 代码文件和本软件自定义的 eye 文件,其他文件都将被忽略
    • 如果一次性拖入了多个文件,只会打开第一个文件
  • 文件编码方式

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

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

你需要找到自己电脑上的 pythonw.exe 文件(不推荐选择 python.exe 文件,它们有一定区别),这个解释器将在进行调试时用来运行你的代码,它也可以是你创建的虚拟环境中的解释器,请确保它已经安装了相关库以便能够完整地运行你的代码。
在设置中可以选择自动搜索解释器,不过这只能找到系统环境变量中的第一个解释器。
Windows 下 Python 解释器默认路径:
C:\Users\ [用户名]\AppData(隐藏文件夹)\Local\Programs\Python\Python3x
MacOS 下 Python 解释器默认路径(不是自带的2.x版本):
/Users/[name]/Library/Frameworks/Python.framework/Versions/3x/bin/Python3x
选择的Python解释器版本必须在3.8及以上,最好是3.11.4及以上,尽量选择较新且较稳定的版本。(如果一定要用3.8以下的版本,你的代码必须以gbk编码)
  • 调试代码

处于菜单栏下方的灰色栏是操作栏,右边五个按钮用于控制代码的运行,左边的输入框和按钮用来设置变量追踪
  • 点击绿色三角开始调试,此时另外四个按钮将被释放,从左至右依次是:
    • 步过:运行下一行代码并暂停
    • 步进:运行下一行代码,如果有函数调用,则进入函数并在第一时间暂停;没有函数调用则等效于步过
    • 持续运行:一直运行程序,直到程序结束或者遇到断点
    • 结束函数:一直运行程序,直到函数return;如果不在函数内,则等效于持续运行
这些功能也可以在菜单栏 调试 里找到,同时会有对应的快捷键提示(右侧对齐的是全局快捷键,只要看得见菜单栏就可以用;紧跟项名且在括号内的是局部快捷键,需要打开对应菜单才可以用)
  • 提示行:蓝色的高亮行,表示将要运行但是还未运行的代码
  • 设置断点:开启调试以后,点击行号即可在对应行设置断点
    断点分为两种:
    • 普通断点:以红色圆点表示,左键单击行号创建;程序运行至当前行时,如果不准备暂停,则会被断点强制暂停(当前行并未被执行);除非被清除,否则该断点将一直存在
    • 临时断点:以黄色圆点表示,右键单击行号创建;功能与普通断点相同,但是在暂停程序时,会自动将自己清除
    要删除断点,再次单击行号即可(不区分左右键)。
  • 插入临时代码:这个功能在菜单栏 调试 中,可以在当前行运行你输入的任何单行python代码
  • 变量追踪

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

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

    • 请确保你选择的Python解释器版本在3.8及以上,否则你将只能打开gbk编码的代码文件(这点很重要,再强调一遍)
    • 如果你选择的解释器版本在 3.8 ~ 3.11.3 之间,那么在使用io模块时请注意:io.open 函数被我用io.open_code 函数覆盖了,你应使用 io.open_ 来实现原先的 io.open 功能(不涉及的请忽略)
    • 在调试代码时,执行下一条语句会阻塞主程序直到执行完毕,所以如果你的某一条语句有明显的耗时,主程序将会被卡住,此时Windows可能判定此程序未响应,这种情况下不用关闭程序,等待语句执行完毕即可
    • 本程序窗口默认大小是 1280×720 像素(720p),在某些较小的屏幕上如果显示不全,请到设置中勾选 自动缩放窗口 项;如果菜单栏溢出屏幕外,按下 Alt+N 打开 首选项 菜单,再按 S 键即可进入设置
    • Windows 在某些较大的屏幕上(比如4k屏),可能会设置屏幕缩放比例,这会导致程序无法正确识别屏幕尺寸,解决办法是右键主程序 pythoneye.exe ,点击 属性 -> 兼容性 -> 更改高DPI设置 ,勾选 替代高DPI缩放行为 ,确定即可

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

从现在起,摆脱低效的 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 = None
    while 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的作用完全等效。(另外还去掉了一些不必要的语句)
由于图形化界面使用pygame编写,需要我们提前封装,而调试模块则需要在用户的python环境下运行,因此需要使用多进程,我们采用socket库在本地实现进程间通信,当调试模块收到来自显示模块发送的命令时,调试模块会把命令添加到input_queue中被执行,然后将返回的结果存入output_queue并发送回显示模块,再由显示模块解释这些返回信息并反馈在图形窗口上,这就实现了使用图形化界面来进行调试。
最终我写了一个子类继承自Pdb,并重写了相关方法,部分代码如下:
class PythonEye(Pdb):
    HOST = 'localhost'
    BUFFSIZE = 1024*64

    def __init__(self, port):
        ...
        threading.Thread(target=self.start_server).start()  # 通信线程

    ...

    def cmdloop(self):
        self.preloop()
        stop = None
        while 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*64

    def __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提供了对文本的渲染方法,并且可以控制颜色、大小、字体、样式等外观参数,但是使用单色渲染并不适合作为代码被阅读,因此需要对代码提前分析,将特殊的语法字符进行高亮以提升代码的观感。
就像大多数IDE那样
  • 词法分析

简单来说就是将读取到的代码文本划分为一个个特定类型的单词,比如变量名、字符串、保留字等等,以方便对它们进行不同的显示。
我将使用正则表达式匹配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.lastgroup
        text = m.group()
        ...
        yield Token(form, text)
举个例子,对于以下代码的词法分析,我将得到这样一个结果:
import random

def 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] += 1
		else:
            # 合并
			if lenline == 0 or line[0][0] != '空格':
				start = 0
			else:
				start = line[0][2]
				for s in range(start):
					if s < length:
						tabtemp[s] += 1
					else:
						tabtemp.append(1)
						length += 1
			first_line = line_number == 1
			if start <= length or first_line:
				if first_line:
					start = line = 0
				for s in range(start, length):
					# 渲染
					...
		line_number -= 1
这样就得到了非常准确的缩进提示线。
最后看一下效果:

渲染效果1
渲染效果2



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



更新日期:2023.11.04
文档编写:Fantastair
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值