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 { return withBlocks(adapter, 'python', source, outline) } function facts(source: string): Promise> { 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: [], }) }) })