编写更便捷。配合各种IDE工具,可以实现定义跳转,类型提示等。
编码更可靠。既然有了类型定义的加持,许多工具能够在静态编码阶段就能提前发现语义错误。
重构更放心。明确了接口的出入参,使代码重构更明确更稳定。
# 加类型前
def add(a, b):
return a + b
# 加类型后
def add(a:int, b:int) -> int:
return a + b
annotate-ast,过程中的AST树标记工具。
merge-pyi,把生成的pyi文件合并回原文件中,甚至还能做到隐藏类型,在类型检查时再加载。
pytd-tool,解析pyi文件的工具,解析成pytype自定义的PYTD文件。
pytype-single,再给定所有依赖的pyi文件的前提下,可以解析单个Python文件。
pyxref,交叉引用的生成器。
Watchman功能, 可以监听代码文件,追踪改动。
Query功能,可以对源码做局部区域性的检查,例如查询某行中一个表达式的类型、查询一个类的全部方法并返回成列表等,避免了全局检查。
速度快。相较于 mypy 及其它用 Python 写的检查工具,它的速度是 5 倍甚至更多。
不依赖 Python 环境。它用 TypeScript 写成,运行于 node 上,不依赖 Python 环境或第三方包。
可配置性强。支持自由地配置,支持指定不同的运行环境(PYTHONPATH 设置、Python 版本、平台目标)。
检查项齐全。支持类型检查及其它语法项的检查(如 PEP-484、PEP-526、PEP-544),以及函数返回值、类变量、全局变量的检查,甚至可以检查条件循环语句。
命令行工具。它包含两个 VS Code 插件:一个命令行工具和一个语言服务器协议(Language Server Protocol)。
内置 Stubs 。使用的是 Typeshed 的副本(注:使用静态的 pyi 文件,检查内置模块、标准库和三方件 )。
语言服务特性。悬停提示信息、符号定义的跳转、实时的编辑反馈。
import logging
import sys
import os
import importlab.environment
import importlab.fs
import importlab.graph
import importlab.output
from importlab import parsepy
from sempy import util
from sempy import environment_util
from pytype.pyi import parser
示例Demo,通过Importlab工具,解析项目空间的依赖关系,以及对应的pyi文件:
def main():
# 指定要解析的目录
ROOT = '/path/to/demo_project'
# 指定TYPESHED目录,可以从这里下载:https://github.com/python/typeshed
TYPESHED_HOME = '/path/to/typeshed_home'
util.setup_logging()
# 载入typeshed,如果TYPESHED_HOME配置的不对,会返回None
typeshed = environment_util.initialize_typeshed_or_return_none(TYPESHED_HOME)
# 载入目标目录有效文件
inputs = util.load_all_py_files(ROOT)
# 生成用于生成import_graph的环境
env = environment_util.create_importlab_environment(inputs, typeshed)
# 基于pyi和工程文件生成import graph
import_graph = importlab.graph.ImportGraph.create(env, inputs, trim=True)
# 打印整个依赖树
logging.info('Source tree:\n%s', importlab.output.formatted_deps_list(import_graph))
# import模块的别名 e.g. import numpy as np -> {'np': 'numpy'}
alias_map = {}
# 引入模块的名称和具体pyi文件的映射 e.g. import os -> {'os': '/path/to/os/__init__.pyi'}
import_path_map = {}
# alias_map的value,可以和import_path_map的key对应,通过alias_map的key这个变量名去找真正的实现文件
for file_name in inputs:
# 如果有pyi文件匹配,则会放入resolved
# 如果依赖了Build_in依赖,会被跳过,不返回
# 如果依赖了自定义依赖,会放入unresolved,需要自己进一步解析,定位到项目工程文件
(resolved, unresolved) = import_graph.get_file_deps(file_name)
for item in resolved:
item_name = item.replace('.pyi', '') \
.replace('.py', '') \
.replace('/__init__', '').split('/')[-1]
import_path_map[item_name] = item
for item in unresolved:
file_path = os.path.join(ROOT, item.new_name + '.py')
import_path_map[item.name] = file_path
import_stmts = parsepy.get_imports(file_name, env.python_version)
for import_stmt in import_stmts:
alias_map[import_stmt.new_name] = import_stmt.name
print('以下为通过importlab解析方式获取的import关系\n\n')
# 对于代码搜索场景,只需要alias_map,既可以通过正在使用的对象关联到引入的模块
print('\n\n#################################\n\n')
print('对于代码搜索场景,只需要alias_map,既可以通过正在使用的对象关联到引入的模块')
print('alias_map: ', alias_map)
# 对于代码补全场景,需要进一步解析当前文件以及引用的pyi文件,如果当前文件是__init__文件,则要进一步去该目录下的所有文件方法中全局搜索
print('\n\n#################################\n\n')
print('对于代码补全场景,需要进一步解析当前文件以及引用的pyi文件,如果当前文件是__init__文件,则要进一步去该目录下的所有文件方法中全局搜索')
print('import_path_map: ', import_path_map)
print('\n\n\n以下为通过pytype工具,解析pyi文件AST来分析三方依赖返回类型,从而解析出当前变量的类型\n\n')
# 通过pytype的解析,去解析依赖的pyi文件,获得调用方法的返回值
fname = '/path/to/parsed_file'
with open(fname, 'r') as reader:
lines = reader.readlines()
sourcecode = '\n'.join(lines)
ret = parser.parse_string(sourcecode, filename=fname, python_version=3)
constant_map = dict()
function_map = dict()
for key in import_path_map.keys():
v = import_path_map[key]
with open(v, 'r') as reader:
lines = reader.readlines()
src = '\n'.join(lines)
try:
res = parser.parse_pyi(src, v, key, 3)
except:
continue
# Alias
# Classes
for constant in res.constants:
constant_map[constant.name] = constant.type.name
for function in res.functions:
signatures = function.signatures
sig_list = []
for signature in signatures:
sig_list.append((signature.params, signature.return_type))
function_map[function.name] = sig_list
var_type_from_pyi_list = []
for alias in ret.aliases:
variable_name = alias.name
if alias.type is not None:
typename_in_source = alias.type.name
typename = typename_in_source
# 引入别名的case,把它转化回来
if '.' not in typename:
# 只是普通的别名,不是函数调用的返回值,忽略
continue
if typename.split('.')[0] in alias_map:
real_module_name = alias_map[typename.split('.')[0]]
typename = real_module_name + typename[typename.index('.'):]
if typename in function_map:
possible_return_types = [item[1].name for item in function_map[typename]]
var_type_from_pyi_list.append((variable_name, possible_return_types))
if typename in constant_map:
possible_return_type = constant_map[typename]
var_type_from_pyi_list.append((variable_name, possible_return_type))
pass
print('\n\n#################################\n\n')
print('这些都是从PYI文件中分析出来的返回值类型')
for item in var_type_from_pyi_list:
print('变量名:', item[0], '返回类型:', item[1])
if __name__ == '__main__':
sys.exit(main())
# demo.py
import os as abcdefg
import re
from demo import utils
from demo import refs
cwd = abcdefg.getcwd()
support_version = abcdefg.supports_bytes_environ
pattern = re.compile(r'.*')
add_res = utils.add(1, 3)
mul_res = refs.multi(3, 5)
c = abs(1)
env = environment_util.create_importlab_environment(inputs, typeshed)
import_graph = importlab.graph.ImportGraph.create(env, inputs, trim=True)
# 如果有pyi文件匹配,则会放入resolved
# 如果依赖了Build_in依赖,会被跳过,不返回
# 如果依赖了自定义依赖,会放入unresolved,需要自己进一步解析,定位到项目工程文件
(resolved, unresolved) = import_graph.get_file_deps(file_name)
{'ast': 'ast', 'astpretty': 'astpretty', 'abcdefg': 'os', 're': 're', 'utils': 'demo.utils', 'refs': 'demo.refs', 'JsonRpcStreamReader': 'pyls_jsonrpc.streams.JsonRpcStreamReader'}
import_path_map: {'ast': '/Users/zhangxindong/Desktop/search/code/sempy/sempy/typeshed/stdlib/ast.pyi', 'astpretty': '/Users/zhangxindong/Desktop/search/code/sempy/venv/lib/python3.9/site-packages/astpretty.py', 'os': '/Users/zhangxindong/Desktop/search/code/sempy/sempy/typeshed/stdlib/os/__init__.pyi', 're': '/Users/zhangxindong/Desktop/search/code/sempy/sempy/typeshed/stdlib/re.pyi', 'utils': '/Users/zhangxindong/Desktop/search/code/sempy/sempy/demo/utils.py', 'refs': '/Users/zhangxindong/Desktop/search/code/sempy/sempy/demo/refs/__init__.py', 'streams': '/Users/zhangxindong/Desktop/search/code/sempy/venv/lib/python3.9/site-packages/pyls_jsonrpc/streams.py'}
print('\n\n\n以下为通过pytype工具,解析pyi文件AST来分析三方依赖返回类型,从而解析出当前变量的类型\n\n')
# 通过pytype的解析,去解析依赖的pyi文件,获得调用方法的返回值
fname = '/path/to/parsed_file'
with open(fname, 'r') as reader:
lines = reader.readlines()
sourcecode = '\n'.join(lines)
ret = parser.parse_string(sourcecode, filename=fname, python_version=3)
constant_map = dict()
function_map = dict()
for key in import_path_map.keys():
v = import_path_map[key]
with open(v, 'r') as reader:
lines = reader.readlines()
src = '\n'.join(lines)
try:
res = parser.parse_pyi(src, v, key, 3)
except:
continue
# Alias
# Classes
for constant in res.constants:
constant_map[constant.name] = constant.type.name
for function in res.functions:
signatures = function.signatures
sig_list = []
for signature in signatures:
sig_list.append((signature.params, signature.return_type))
function_map[function.name] = sig_list
var_type_from_pyi_list = []
for alias in ret.aliases:
variable_name = alias.name
if alias.type is not None:
typename_in_source = alias.type.name
typename = typename_in_source
# 引入别名的case,把它转化回来
if '.' not in typename:
# 只是普通的别名,不是函数调用的返回值,忽略
continue
if typename.split('.')[0] in alias_map:
real_module_name = alias_map[typename.split('.')[0]]
typename = real_module_name + typename[typename.index('.'):]
if typename in function_map:
possible_return_types = [item[1].name for item in function_map[typename]]
# print('The possible return type of', typename_in_source, 'is', possible_return_types)
var_type_from_pyi_list.append((variable_name, possible_return_types))
if typename in constant_map:
possible_return_type = constant_map[typename]
var_type_from_pyi_list.append((variable_name, possible_return_type))
pass
pattern = re.compile(r'.*')
这些都是从pyi文件中分析出来的返回值类型。
变量名 cwd 返回类型:['str']
变量名 support_version 返回类型:bool
变量名 pattern 返回类型:['typing.Pattern', 'typing.Pattern']
阿里云开发者社区邀你参加
《我的Java打怪日记》征文
阿里云开发者社区为广大开发者提供了包含博客、问答、在线学习、上云工具、创业扶持等一系列服务,以帮助大家学习、交流、云上工作。现在特别推出《我的Java打怪日记》有奖征文活动,欢迎更多热爱技术的开发者加入,分享和技术相关的学习、思考和成长感悟。