Files
superwork/vscode/src/codeReader/languages/python.test.ts
T

329 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { factsByLine, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { createPythonAdapter } from './python'
const adapter = createPythonAdapter(WASM_DIR)
function tree(source: string): Promise<Outline[]> {
return withBlocks(adapter, 'python', source, outline)
}
function facts(source: string): Promise<Record<number, unknown>> {
return withBlocks(adapter, 'python', source, factsByLine)
}
// fixtures/sample.py:行号 1 起
const SAMPLE: Outline[] = [
{ at: 1, kind: 'comment' },
{ at: 3, kind: 'import' },
{ at: 4, kind: 'import' },
{ at: 6, kind: 'const' },
{ at: 7, kind: 'variable' },
{ at: [10, 11], kind: 'function', kids: [
{ at: 12, kind: 'comment' },
{ at: 13, kind: 'variable' },
{ at: 14, kind: 'if', kids: [{ at: 15, kind: 'return' }] },
{ at: 16, kind: 'else', kids: [{ at: 17, kind: 'variable' }] },
{ at: 18, kind: 'else', kids: [{ at: 19, kind: 'statement' }] },
{ at: 20, kind: 'loop', kids: [
{ at: 21, kind: 'if', kids: [{ at: 22, kind: 'statement' }] },
] },
{ at: 23, kind: 'try', kids: [
{ at: 24, kind: 'statement', kids: [{ at: 25, kind: 'statement' }] },
] },
{ at: 26, kind: 'catch', kids: [{ at: 27, kind: 'statement' }] },
{ at: 28, kind: 'finally', kids: [{ at: 29, kind: 'statement' }] },
{ at: 30, kind: 'return' },
] },
{ at: 33, kind: 'class', kids: [
{ at: 34, kind: 'field' },
{ at: 35, kind: 'field' },
{ at: 36, kind: 'field' },
{ at: 37, kind: 'variable' },
{ at: 39, kind: 'function', kids: [{ at: 40, kind: 'return' }] },
] },
]
describe('python 代码块树', () => {
it('样例覆盖 import / def / docstring / if-elif-else / for / try-except-finally / with / 推导式 / class 字段', async () => {
expect(await tree(readFixture('sample.py'))).toEqual(SAMPLE)
})
it('装饰器算进头部;冒号后、主体前的注释归子块;循环的 else 是兄弟块;match 的 case 是子块 if', async () => {
const source = [
'@cache',
'def load(key):',
' # 先查缓存',
' while key:',
' key = key[1:]',
' else:',
' pass',
' match key:',
' case \'a\':',
' return 1',
' case _:',
' return 2',
].join('\n')
expect(await tree(source)).toEqual([
{ at: [1, 2], kind: 'function', kids: [
{ at: 3, kind: 'comment' },
{ at: 4, kind: 'loop', kids: [{ at: 5, kind: 'variable' }] },
{ at: 6, kind: 'else', kids: [{ at: 7, kind: 'statement' }] },
{ at: 8, kind: 'if', kids: [
{ at: 9, kind: 'if', kids: [{ at: 10, kind: 'return' }] },
{ at: 11, kind: 'if', kids: [{ at: 12, kind: 'return' }] },
] },
] },
])
})
it('docstring 只认主体开头;不是开头的字符串表达式是语句', async () => {
const source = [
'def f():',
' x = 1',
' """not a docstring"""',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'function', kids: [{ at: 2, kind: 'variable' }, { at: 3, kind: 'statement' }] },
])
})
})
describe('python 函数、类的 docstring', () => {
const source = [
'class K:',
' """类说明"""',
' def run(self): # 行尾',
' # 先说一句',
' """跑一遍。',
'',
' 细节。',
' """ # 行尾',
' # 真正的注释',
' return 1',
'def g(): "单行"',
].join('\n')
it('算进函数、类头部、不单独成块,它前面的注释也在头部里;后面的注释照旧成块', async () => {
expect(await tree(source)).toEqual([
{ at: [1, 2], kind: 'class', kids: [
{ at: [3, 8], kind: 'function', kids: [
{ at: 9, kind: 'comment' },
{ at: 10, kind: 'return' },
] },
] },
{ at: 11, kind: 'function' },
])
})
it('进 facts.doc:是那个字符串节点;没有 docstring 的函数没有;模块开头的 docstring 照旧是注释块', async () => {
const found = await facts(source)
expect(found[1]).toMatchObject({ name: 'K', doc: '"""类说明"""' })
expect(found[3]).toMatchObject({ name: 'run', doc: '"""跑一遍。\n\n 细节。\n """' })
expect(await tree('"""模块说明"""\nclass K:\n pass')).toEqual([{ at: 1, kind: 'comment' }, { at: 2, kind: 'class', kids: [{ at: 3, kind: 'statement' }] }])
expect(found[11]).toMatchObject({ name: 'g', doc: '"单行"' })
expect(await facts('def f():\n x = 1\n """not a docstring"""')).not.toHaveProperty('1.doc')
})
})
describe('python 属性表信息(facts)', () => {
it('sample.py:import / from import、模块和类里的赋值、函数和方法', async () => {
const sample = await facts(readFixture('sample.py'))
expect(sample[3]).toEqual({ sources: ['os'], items: [{ kind: 'module', module: 'os' }] })
expect(sample[4]).toEqual({ sources: ['pathlib'], items: [{ kind: 'named', name: 'Path', alias: 'P' }] })
expect(sample[6]).toEqual({ access: 'public', declarators: [{ name: 'MAX_DEPTH', value: '8' }] })
expect(sample[10]).toEqual({
name: 'find_env_files',
doc: '"""Return every .env file under root."""',
access: 'public',
async: false,
decorators: [],
parameters: [{ name: 'root', optional: false, type: 'str' }],
returnType: 'list[str]',
})
expect(sample[17]).toEqual({ declarators: [{ name: 'found', value: 'found[:MAX_DEPTH]' }] })
expect(sample[37]).toEqual({ access: 'public', declarators: [{ name: 'label', value: '\'env\'' }] })
expect(sample[39]).toMatchObject({
name: 'summary',
access: 'public',
parameters: [{ name: 'self', optional: false }, { name: 'verbose', optional: false, type: 'bool', default: 'False' }],
returnType: 'str',
})
})
it('routes.py:future 导入、多行括号导入、带装饰器的 async def、await 赋值', async () => {
const routes = await facts(readFixture('routes.py'))
expect(routes[6]).toEqual({ sources: ['__future__'], items: [{ kind: 'named', name: 'annotations' }] })
expect(routes[19]).toEqual({
sources: ['.schemas'],
items: [
{ kind: 'named', name: 'UserAvatarUploadResponse' },
{ kind: 'named', name: 'UserDetailResponse' },
{ kind: 'named', name: 'UserUpdateRequest' },
],
})
expect(routes[29]).toEqual({
name: 'get_user',
doc: '"""获取用户详情。\n\n 管理员可查看任意用户,普通用户只能查看自己。\n """',
access: 'public',
async: true,
decorators: ['@router.get("/{user_id}", response_model=UserDetailResponse)'],
parameters: [
{ name: 'user_id', optional: false, type: 'Int4Path' },
{ name: 'db', optional: false, type: 'AsyncSession', default: 'Depends(get_db)' },
{ name: 'current_user', optional: false, type: 'User', default: 'Depends(get_current_user)' },
],
returnType: 'UserDetailResponse',
})
expect(routes[118]).toEqual({ declarators: [{ name: 'payload', value: 'await file.read()' }] })
})
it('导入:import a.b as c, d 每个模块一个来源;from . import、from m import *', async () => {
const source = [
'import os.path as osp, sys',
'from . import x',
'from m import *',
].join('\n')
expect(await facts(source)).toEqual({
1: { sources: ['os.path', 'sys'], items: [{ kind: 'module', module: 'os.path', alias: 'osp' }, { kind: 'module', module: 'sys' }] },
2: { sources: ['.'], items: [{ kind: 'named', name: 'x' }] },
3: { sources: ['m'], items: [{ kind: 'namespace' }] },
})
})
it('形参:*args / **kwargs 带前缀,* 和 / 分隔符不算参数', async () => {
const found = await facts('async def f(a, /, b: int, *args: int, c=1, d: str = \'x\', **kw) -> None:\n pass')
expect(found[1]).toEqual({
name: 'f',
access: 'public',
async: true,
decorators: [],
parameters: [
{ name: 'a', optional: false },
{ name: 'b', optional: false, type: 'int' },
{ name: '*args', optional: false, type: 'int' },
{ name: 'c', optional: false, default: '1' },
{ name: 'd', optional: false, type: 'str', default: '\'x\'' },
{ name: '**kw', optional: false },
],
returnType: 'None',
})
})
it('访问级别:单下划线开头私有、__dunder__ 公共;解构全是私有名才私有;函数体里的没有', async () => {
const source = [
'_cache = {}',
'__all__ = [\'f\']',
'a, _b = 1, 2',
'_c, _d = 1, 2',
'total: int',
'class K:',
' def __init__(self):',
' self.x = 1',
' def _helper(self):',
' def inner():',
' pass',
].join('\n')
const found = await facts(source)
expect(found[1]).toMatchObject({ access: 'private' })
expect(found[2]).toMatchObject({ access: 'public' })
expect(found[3]).toEqual({ access: 'public', declarators: [{ name: 'a, _b', value: '1, 2' }] })
expect(found[4]).toMatchObject({ access: 'private' })
expect(found[5]).toEqual({ access: 'public', declarators: [{ name: 'total', type: 'int' }] })
expect(found[7]).toMatchObject({ name: '__init__', access: 'public' })
expect(found[8]).toEqual({ declarators: [{ name: 'self.x', value: '1' }] })
expect(found[9]).toMatchObject({ name: '_helper', access: 'private' })
expect(found[10]).not.toHaveProperty('access')
})
it('字典字面量的各项:字符串键、名字键、嵌套字典连它的各项、**展开;空字典是空数组,别的值没有;类属性也有', async () => {
const source = [
'_KEYS = {',
' "published": "contests:publish",',
' key: 1,',
' # 注释不算一项',
' "nested": {"a": {}, "b": [1]},',
' **other,',
'}',
'EMPTY = {}',
'ITEMS = [1]',
'class K:',
' labels = {"x": 1}',
].join('\n')
const found = await facts(source)
expect(found[1]).toMatchObject({ access: 'private', declarators: [{ name: '_KEYS', entries: [
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
{ kind: 'pair', key: 'key', value: '1' },
{ kind: 'pair', key: '"nested"', value: '{"a": {}, "b": [1]}', entries: [
{ kind: 'pair', key: '"a"', value: '{}', entries: [] },
{ kind: 'pair', key: '"b"', value: '[1]' },
] },
{ kind: 'spread', value: 'other' },
] }] })
expect(found[8]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}', entries: [] }] })
expect(found[9]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
expect(found[11]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}', entries: [{ kind: 'pair', key: '"x"', value: '1' }] }] })
})
})
describe('python 类', () => {
it('classes.py:docstring(连同它前面的注释)算进类头部,类里的类、函数里的类照常成块', async () => {
expect(await tree(readFixture('classes.py'))).toEqual([
{ at: 1, kind: 'comment' },
{ at: 3, kind: 'import' },
{ at: [6, 10], kind: 'class', kids: [
{ at: 12, kind: 'field' },
{ at: 14, kind: 'function', kids: [{ at: 15, kind: 'variable' }] },
] },
{ at: [18, 20], kind: 'class', kids: [{ at: 21, kind: 'variable' }] },
{ at: [24, 25], kind: 'class', kids: [
{ at: 26, kind: 'class', kids: [{ at: 27, kind: 'variable' }] },
{ at: 29, kind: 'class', kids: [{ at: 30, kind: 'statement' }] },
] },
{ at: 33, kind: 'class', kids: [{ at: 34, kind: 'statement' }] },
{ at: 37, kind: 'function', kids: [
{ at: [38, 39], kind: 'class' },
{ at: 40, kind: 'return' },
] },
])
})
it('classes.py:名称、说明、访问级别(同函数的下划线规则,类里的类也按它,函数里的没有)、装饰器、父类(参数列表照写的顺序,关键字参数也算)', async () => {
const found = await facts(readFixture('classes.py'))
expect(found[6]).toEqual({
name: 'AppException',
doc: '"""应用异常的基类。\n\n 带上 HTTP 状态码。\n """',
access: 'public',
decorators: [],
typeParameters: [],
bases: [{ value: 'Exception' }],
implements: [],
})
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppException' }] })
expect(found[24]).toEqual({
name: 'Meta',
access: 'public',
decorators: ['@dataclass(frozen=True)'],
typeParameters: [],
bases: [{ value: 'Base' }, { value: 'metaclass=ABCMeta' }],
implements: [],
})
expect(found[26]).toMatchObject({ name: 'Config', access: 'public', bases: [] })
expect(found[29]).toMatchObject({ name: '_Hidden', access: 'private' })
expect(found[33]).toMatchObject({ name: '_Private', access: 'private' })
expect(found[38]).toEqual({ name: 'Local', doc: '"""局部类。"""', decorators: [], typeParameters: [], bases: [], implements: [] })
})
it('没有「抽象」这个概念;Python 3.12 的泛型参数进 typeParameters;*bases、**kw 也是父类里的一项', async () => {
expect((await facts('class G[T](Generic[T], *mixins, **kw):\n pass'))[1]).toEqual({
name: 'G',
access: 'public',
decorators: [],
typeParameters: ['T'],
bases: [{ value: 'Generic[T]' }, { value: '*mixins' }, { value: '**kw' }],
implements: [],
})
})
})