329 lines
14 KiB
TypeScript
329 lines
14 KiB
TypeScript
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: [],
|
||
})
|
||
})
|
||
})
|