从内到外的皮林特。 他是怎么做到的

编写出色代码的各种助手就在我们周围,包括linter,typechekery,发现漏洞的实用程序。 我们已经习惯并使用它,而没有像“黑匣子”这样的细节。 例如,很少有人了解Pylint的原理,而Pylint是用于优化和改进Python代码的必不可少的工具之一。

但是马克西姆·马扎夫(Maxim Mazaev)知道理解他的工具有多么重要,他在莫斯科Python Conf ++上告诉我们。 他通过实际例子展示了Pylint内部设备及其插件的知识如何帮助减少代码审查时间,提高代码质量并总体上提高开发效率。 以下是解密指令。



我们为什么需要Pylint?


如果您已经使用过它,那么可能会出现一个问题:“为什么知道Pylint内在是什么,这种知识如何提供帮助?”

通常,开发人员编写代码,启动lint,接收有关需要改进的消息,如何使代码更漂亮以及进行建议的更改的消息。 现在,该代码更易于阅读,不会为同事展示感到羞耻。

长期以来,他们与Cyan Institute中的Pylint的工作方式完全相同,只是增加了一点:他们更改了配置,删除了不必要的规则并增加了最大字符串长度。

但是在某些时候,他们遇到了一个问题,对此我不得不深入研究Pylint并弄清楚它是如何工作的。 这个问题是什么以及如何解决,请继续阅读。


关于演讲者: Maxim Mazaev( 反斜杠 ),已发展5年,在CIAN工作。 深入学习Python,异步和函数式编程。

关于青色


大多数人认为CIAN是一家拥有经纪人的房地产代理商,当他们发现我们拥有程序员而不是房地产经纪人时,他们感到非常惊讶。

我们是一家技术公司,没有经纪人,但是有很多程序员。

  • 每天有100万唯一身份用户。
  • 莫斯科和圣彼得堡最大的房地产买卖公告栏。 在2018年,他们进入了联邦级别,并在整个俄罗斯工作。
  • 开发团队有将近100个人,其中每天有30个人编写Python代码。

每天,成千上万行新代码投入生产。 该代码的要求非常简单:

  • 体面的质量守则。
  • 风格上的同质性。 所有开发人员都应编写近似相似的代码,在存储库中不要使用“ vinaigrette”。

为此,当然,您需要进行代码审查。

代码审查


CIAN中的代码审查分为两个阶段:

  1. 第一阶段是自动化的 。 由于我们使用微服务,因此Jenkins机器人可以运行测试,运行Pylint并检查微服务之间API的一致性。 如果在此阶段测试失败或短绒显示出一些奇怪的情况,则这是拒绝请求请求并发送代码进行修订的机会。
  2. 如果第一阶段成功,那么第二阶段将获得两个 开发人员的批准。 他们可以评估代码在业务逻辑方面的表现,批准请求请求或返回代码以进行修订。


代码审查问题


由于以下原因,拉取请求可能无法通过代码审查:

  • 当开发人员无效或错误地解决了问题时,业务逻辑中的错误;
  • 代码样式问题。

如果短绒检查代码,样式问题可能是什么?

每个用Python编写的人都知道有一个编写PEP-8代码的指南。 像任何标准一样,PEP-8非常通用,对于我们(作为开发人员)来说,这还不够。 我想在某些地方指定标准,在其他地方扩展。

因此,我们就代码的外观和工作方式制定了内部安排,并将其称为“ Decline Cian Proposals”



“拒绝Cian提案”-一组规则,现在大约有15条。每条规则都是拒绝请求并将其发送以进行修订的基础。

是什么阻碍了高效的代码审查?


我们的内部规则存在一个问题-小子不了解这些规则,如果他知道,那将很奇怪-它们是内部的。
执行任务的开发人员必须始终记住并牢记规则。 如果他忘记了其中一条规则,则在代码审阅过程中,审阅者会指出问题所在,任务将进行修订,并且任务的发布时间将增加。 完成并纠正错误后,测试人员需要记住任务中所包含的内容,以切换上下文。

这对于开发人员和审阅者都造成了问题。 结果,严重降低了代码审查的速度。 测试人员没有分析代码的逻辑,而是开始分析视觉样式,即,他​​们执行lint的工作:他们逐行扫描代码,并以导入格式查找缩进中的不一致之处。

我们想摆脱这个问题。

但是不要写信给我们您的棉绒呢?


看来该问题将通过一种工具解决,该工具将了解所有内部协议并能够检查其实现代码。 所以我们需要自己的短绒棉呢?

不完全是 这个想法很愚蠢,因为我们已经使用了Pylint。 这是一个方便的linter,受到开发人员的喜欢,并内置于所有流程中:它在Jenkins中运行,生成完全满意的精美报告,并以注释的形式提出请求。 一切都很好, 不需要第二个棉绒

那么,如果我们不想编写自己的Linter,该如何解决问题呢?

编写一个Pylint插件


您可以为Pylint编写插件,它们称为检查器。 根据每个内部规则,您可以编写自己的检查器,该检查器将对其进行检查。

考虑这种检查器的两个例子。

示例1


在某个时候,事实证明该代码包含许多“ TODO”形式的注释-承诺重构,删除不必要的代码或精美地重写它,但不是现在,而是以后。 这样的评论有问题-他们绝对不强迫您做任何事情。

问题


开发人员写了一个诺言,呼气而安心地去做下一个任务。


总结:

  • 带有诺言的评论多年来悬而未决;
  • 代码乱七八糟;
  • 技术债务已经积累了多年。

例如,一个三年前的开发人员曾承诺在成功发布后删除某些内容,但是发布是在三年后发生的吗? 也许是。 在这种情况下,我应该删除代码吗? 这是一个大问题,但很可能不是。

解决方案:为Pylint编写检查程序


您不能禁止开发人员写这样的评论,但是可以使他们做额外的工作:在跟踪器中创建任务以最终兑现承诺。 那我们绝对不会忘记她的。

我们需要找到TODO形式的所有注释,并确保每个注释都具有指向Jira中任务的链接。 写吧

什么是Pylint的检查器? 这是一个从检查器的基类继承并实现特定接口的类。

class TodoIssueChecker(BaseChecker): _ _implements_ _ = IRawChecker 

在我们的例子中,这是IRawChecker-所谓的“原始”检查器。

原始检查程序会遍历文件的各行,并且可以对某行执行某些操作。 在我们的例子中,检查器将在每一行中查找类似于注释和指向任务的链接的内容。

对于检查器,您需要确定它将发出的消息列表:

 msgs = { '9999': ('  TODO    ', issue-code-in-todo', ' ')} 

该消息有:

  • 描述简短而冗长;
  • 检查代码和一个简短的助记符名称,用于确定它是哪种消息。

消息代码的格式为“ C1234”,其中:

  • 对于不同类型的消息,首字母已明确标准化: [C]发明; [W]警告; [E]哟; [F] atal; [R]分解。 多亏了这封信,该报告立即显示了正在发生的事情:提醒人们必须紧急解决的协议或致命问题。
  • Pylint独有的4个随机数。

如果不需要检查,则需要使用该代码来禁用检查。 您可以编写Pylint:disable和简短的字母数字代码或助记符名称:

 # Pylint: disable=C9999 # Pylint: disable=issue-code-in-todo 

Pylint的作者建议放弃字母数字代码并使用助记符,因为它更直观。

下一步是定义一个名为process_module的方法。



这个名字很重要。 该方法应以这种方式调用,因为Pylint将随后对其进行调用。

节点参数传递给模块。 在这种情况下,它的类型或类型无关紧要,记住节点具有逐行返回文件的方法是很重要的。

您可以浏览文件,并针对每一行检查注释和指向任务的链接。 如果有评论,但没有链接,则使用检查代码和行号以“ issue-code-in-todo”形式发出警告。 该算法非常简单。

注册检查器,以便Pylint知道它。 这是通过register函数完成的:

 def register(linter: Pylinter) -> None: linter. register_checker ( TodoIssueChecker(linter) ) 

  • Pylint的一个实例进入该函数。
  • 它调用register_checker方法。
  • 我们将检查器传递给该方法。

重要的一点:检查器模块必须位于PYTHONPATH中,以便Pylint以后可以导入它。

测试文件将对注册的检查器进行检查,该文件带有不带任务链接的注释。

 $ cat work. # T0D0:   , -! $ pylint work. --load-plugins todo_checker … 

对于测试,运行Pylint,将模块传递给它,使用load-plugins参数传递检查器,然后在linter内部运行两个阶段。

阶段1.插件初始化


  • 所有带有插件的模块均已导入。 Pylint具有内部和外部检查器。 它们都汇合在一起并被进口。
  • 我们注册-module.register(自己) 。 对于每个检查器,都会调用register函数,并在其中传递Pylint实例。
  • 执行检查:参数的有效性,消息,选项和报告是否以正确的格式存在。

阶段2。解析棋盘池


在阶段1之后,将保留不同类型的检查器的完整列表:

  • AST检查器;
  • 原始检查器;
  • 令牌检查器。



从列表中,我们选择与原始检查器接口相关的那些:我们查看哪些检查器实现了IRawChecker接口并将它们自己带走。

对于每个选定的检查器,调用checker.process_module(模块)方法,然后运行检查。

结果


再次在测试文件上运行检查器:

 $ cat work. # T0D0:   , -! $ pylint work,  --load-plugins todo_checker : 0,0:   T0D0     (issue-code-in-todo) 

将会出现一条消息,指出有关于TODO的评论,没有指向任务的链接。

问题已解决,现在在代码审查过程中,开发人员无需用眼睛扫描代码,查找注释,向代码作者发出提醒,即已达成协议,建议您保留链接。 一切都会自动发生,并且代码审查会更快。

例2。关键字参数


有些函数带有位置参数。 如果有很多参数,那么当他们调用函数时,不清楚参数在哪里以及为什么需要它。

问题


例如,我们有一个函数:

 get_offer_by_cian_id( "sale", rue, 859483, ) 

该代码包含saleTrue,目前尚不清楚它们的含义。 如果仅使用命名参数来调用其中包含许多参数的函数,则更加方便:

 get_offer_by_cian_id( deal_type="sale", truncate=True, cian_id=859483, ) 

这是一个很好的代码,其中可以立即清楚地知道哪个参数在哪里,并且我们不会混淆它们的顺序。 让我们尝试编写一个检查此类情况的检查器。

在这种情况下,上一个示例中使用的“原始”检查器很难编写。 您可以添加超复杂的正则表达式,但是此类代码很难阅读。 Pylint使得基于AST抽象语法树编写另一种类型的检查器成为可能,我们将使用它。

关于AST的歌词


AST或抽象语法树是代码的树表示形式,其中顶点是操作数,叶子是运算符。

例如,将具有一个位置参数和两个命名参数的函数调用转换为抽象树:


有一个类型为Call的顶点,它具有:

  • 函数属性称为func;
  • 位置参数args的列表,其中有一个类型为Const且值为112的节点;
  • 命名参数关键字列表。

在这种情况下的任务:

  • 在模块中找到所有类型为Call(函数调用)的节点。
  • 计算函数接受的参数总数。
  • 如果有两个以上的参数,请确保节点中没有位置参数。
  • 如果有位置参数,则显示警告。


 ll( func=Name(name='get_offer'), args=[Const(value=1298880)], keywords=[ … ]))] 

从Pylint的角度来看,基于AST的检查器是一个继承自基本检查器类并实现IAstroidChecker接口的类:

 class NonKeywordArgsChecker(BaseChecker): -_ _implements_ _ = IAstroidChecker 

与第一个示例一样,检查列表描述,消息代码,短助记符名称在消息列表中指示:

 msgs = { '9191': (' ', keyword-only-args', ' ')} 

下一步是定义visit_call方法:

 def visit_call(self, node: Call) 

该方法不必被称为。 其中最重要的是visit_前缀,然后是我们感兴趣的顶点的名称,并带有一个小写字母。

  • AST解析器遍历树,并针对每个顶点查看检查器是否已定义visit_ <Name>接口。
  • 如果是这样,则调用它。
  • 递归地遍历她的所有孩子。
  • 离开节点时,它将调用leave_ <Name>方法。

在此示例中,visit_call方法将接收一个Call-type节点作为输入,并查看它是否具有两个以上的参数,以及是否存在位置参数以发出警告并将代码传递给节点本身。

 def visit_call(self, n): if node.args and len(node.args + node.keywords) > 2: self.add_message( 'keyword-only-args', node=node ) 

我们像上一个示例一样注册检查程序:我们转移Pylint实例,调用register_checker,传递检查程序本身并启动它。

 def register(linter: Pylinter) -> None: linter.register_checker( TodoIssueChecker(linter) ) 

这是一个测试函数调用的示例,其中有3个参数,并且其中只有一个被命名:

 $ cat work. get_offers(1, True, deal_type="sale") $ Pylint work.py --load-plugins non_kwargs_checker … 

从我们的角度来看,此函数可能被错误地调用。 启动Pylint。

如上例所示,插件初始化阶段1完全重复。

阶段2。AST的模块解析


该代码被解析为AST树。 该分析由Astroid库执行。

为什么是Astroid而不是AST(stdlib)


Astroid内部不使用标准的Python AST模块,而是使用typed_ast类型的AST解析器 ,其特征在于它支持PEP 484 类型提示 。 有趣的是,AST中存在相同的错误,并且可以并行修复。

 from module import Entity def foo(bar): # type: (Entity) -> None return 

以前,Astroid使用标准的AST模块,在该模块中可能会遇到使用第二个Python注释中定义的taiphint的问题。 如果通过Pylint检查此代码,则在一定程度上它会在未使用的导入时起誓,因为导入的Entity类仅在注释中存在。

在某个时候,Guido Van Rossum在GitHub上访问了Astroid并说:“伙计,您有Pylint,在这种情况下发誓,我们有一个支持所有这些的类型化AST解析器。 成为朋友吧!”

工作已经开始沸腾了! 2年过去了,这个春天Pylint切换到了AST解析器类型,并不再对此发誓。 taiphint的进口不再标记为未使用。

Astroid使用AST解析器将代码解析为树,然后在构建代码时做一些有趣的事情。 例如,如果您使用import * ,那么它将导入所有带有星号的内容,并将其添加到本地,以防止未使用的导入产生错误。

当所有属性都动态生成时,在某些基于元类的复杂模型的情况下,可以使用Transform插件 。 在这种情况下,Astroid很难理解其含义。 在检查时,Pylint会发誓,模型在访问时没有这样的属性,使用Transform插件可以解决问题:

  • 帮助Astroid修改抽象树并了解Python的动态特性。
  • 用有用的信息补充AST。

一个典型的例子是pylint-django 。 当使用复杂的django模型时,短绒棉通常会以未知的属性发誓。 Pylint-django只是解决了这个问题。

阶段3.解析检查池


我们返回检查器。 我们再次有一个检查器列表,从中我们可以找到实现AST检查器接口的检查器。

阶段4.按节点类型解析检查器


接下来,我们为每个检查器找到方法,它们可以是两种类型:

  • visit_ <节点名称>
  • 级别<节点名称>。

知道在树中行走时需要为节点调用哪些节点会很高兴。 因此,他们理解字典,其中的键是节点的名称,值是对访问此节点的事实感兴趣的那些检查程序的列表。

 _visit_methods = dict( < > : [checker1, checker2 ... checkerN] ) 

与离开方法相同:节点名称形式的键,对从该节点退出的事实感兴趣的检查者列表。

 _leave_methods = dict( < >: [checker1, checker2 ... checkerN] ) 

启动Pylint。 它显示警告,我们有一个函数,其中有两个以上的参数,并且其中有一个位置参数:

 $ cat work. get_offers(1, True, deal_type="sale") $ Pylint work.py --load-plugins non_kwargs_checker C: 0, 0:  c >2      (keyword-only-args) 

问题已解决。 现在,代码审查程序员无需读取函数的参数; lint会为他们完成此工作。 我们节省了时间节省了代码审查的时间 ,并使任务在生产中更快地进行。

并编写测试?


Pylint允许您对检查程序进行单元测试,这非常简单。 从linter的角度来看,测试检查器看起来像是从抽象CheckerTestCase继承的类。 有必要指出正在检查中的检查器。

 class TestNonKwArgsChecker(CheckerTestCase): CHECKER_CLASS = NonKeywordArgsChecker 

步骤1.我们从正在检查的代码部分中创建一个测试AST节点。

 node = astroid.extract_node( "get_offers(3, 'magic', 'args')" ) 

步骤2.验证进入节点的检查程序是否抛出或未抛出相应的消息:

 with self.assertAddsMessages(message): self.checker.visit_call(node) 

令牌检查器


还有另一种检查器,称为TokenChecker 。 它根据词法分析器的原理工作。 Python有一个标记化模块,该模块完成词法扫描程序的工作并将代码分解为标记列表。 它可能看起来像这样:


变量名称,函数名称和关键字成为NAME类型的令牌,而分隔符,方括号和冒号成为OP类型的令牌。 此外,还有用于缩进,换行和反向转换的单独标记。

Pylint如何与TokenChecker一起使用:

  • 被测模块已标记。
  • 大量的令牌被传递给实现ITokenChecker的所有检查器,并且调用process_tokens (令牌)方法。

我们尚未找到TokenChecker的用法,但是Pylint使用了一些示例:

  • 拼写检查 。 例如,您可以使用text类型的所有标记,查看词汇素养,从停用词列表中检查单词,等等。
  • 检查缩进 ,空格。
  • 处理字符串 。 例如,您可以验证Python 3不使用Unicode文字,或验证字节字符串中仅存在ASCI字符。

结论


我们在代码审查方面遇到了问题。 开发人员执行了linter的工作,将时间花在了毫无意义的代码扫描上,并通知了作者错误。 通过Pylint,我们:

  • 将例行检查转移到短绒棉衣,并在其中实施内部协议。
  • 提高了速度和质量代码审查。
  • 减少了拒绝请求请求的数量,并且在生产中通过任务的时间变得更少了。

一个简单的检查程序在半小时内就可以完成,而复杂的检查程序在几个小时内就可以完成。 该检查程序比编写代码节省了更多的时间,并且可以为几个未拒绝的请求请求而奋斗。

您可以在官方文档中了解有关Pylint的更多信息以及如何为它编写检查器,但是就编写检查器而言,它相当差。 例如,关于TokenChecker,仅提及其中,而不涉及如何编写检查器本身。 有关更多信息,请参见GitHub上的Pylint源 。 您可以查看标准包装中的检查器,并受到启发来编写自己的检查器。

了解Pylint内部设计可节省工时并简化工作
性能并改进代码。 节省您的时间,编写好的代码,
使用短绒。
下一次莫斯科Python Conf ++会议将于2019年4月5日举行,您现在可以预订早鸟票。 最好收集您的想法并申请报告,然后访问将是免费的,精美的面包也将作为奖励,其中包括指导编写报告的过程。

我们的会议是一个与志趣相投的人,行业引擎开会,交流和讨论Python开发人员喜欢的事物的平台:后端和Web,数据收集和处理,AI / ML,测试,IoT。 秋天的情况如何, 请在我们的Python频道观看视频报告并订阅该频道-很快我们将发布会议中的最佳报告以免费提供。

Source: https://habr.com/ru/post/zh-CN433474/


All Articles