import ast
import shutil
import subprocess
from pathlib import Path
ALLOWED_FUNCTIONS = {
'abs',
'all',
'any',
'bin',
'chr',
'divmod',
'eval',
'exec',
'hash',
'hex',
'id',
'isinstance',
'iter',
'len',
'locals',
'max',
'min',
'next',
'oct',
'ord',
'pow',
'print',
'repr',
'round',
'sorted',
'sum',
}
ALLOWED_CLASSES = {
'object',
'type',
'bool',
'int',
'float',
'complex',
'str',
'bytes',
'list',
'tuple',
'dict',
'set',
'frozenset',
'range',
'enumerate',
'reversed',
'zip',
'slice',
'property',
'BaseException',
'Exception',
'SystemExit',
'KeyboardInterrupt',
'ArithmeticError',
'OverflowError',
'ZeroDivisionError',
'LookupError',
'IndexError',
'KeyError',
'RuntimeError',
'NotImplementedError',
'RecursionError',
'AttributeError',
'AssertionError',
'MemoryError',
'NameError',
'SyntaxError',
'OSError',
'TimeoutError',
'TypeError',
'ValueError',
'StopIteration',
}
COPY_FILES = [
'typing.pyi',
'typing_extensions.pyi',
'_collections_abc.pyi',
'abc.pyi',
'types.pyi',
'dataclasses.pyi',
'enum.pyi',
're.pyi',
'collections/__init__.pyi',
'collections/abc.pyi',
'_typeshed/__init__.pyi',
'_typeshed/_type_checker_internals.pyi',
'pathlib/__init__.pyi',
'pathlib/types.pyi',
'json/__init__.pyi',
'json/encoder.pyi',
'json/decoder.pyi',
'tomllib.pyi',
'math.pyi',
'datetime.pyi',
'itertools.pyi',
'random.pyi',
'posixpath.pyi',
'genericpath.pyi',
]
VERSIONS = """\
# DO NOT EDIT THIS FILE DIRECTLY
# instead edit crates/monty-typeshed/update.py
# this file should match the modules
# which monty's minimimal typeshed includes
_collections_abc: 3.3-
_typeshed: 3.0- # not present at runtime, only for type checking
abc: 3.0- # not importable at runtime, only for type checking
asyncio: 3.4-
base64: 3.0-
binascii: 3.0-
bisect: 3.0-
builtins: 3.0-
collections: 3.0-
copy: 3.0-
csv: 3.0-
dataclasses: 3.7-
datetime: 3.0-
decimal: 3.0-
functools: 3.0-
genericpath: 3.0- # not importable at runtime, only for type checking (posixpath re-exports from it)
hashlib: 3.0-
itertools: 3.0-
json: 3.0-
math: 3.0-
os: 3.0-
os.path: 3.0-
pathlib: 3.4-
pathlib.types: 3.14-
posixpath: 3.0-
random: 3.0-
re: 3.0-
sys: 3.0-
time: 3.0-
tomllib: 3.11-
typing: 3.5-
typing_extensions: 3.7-
types: 3.0-
unicodedata: 3.0-
"""
CRATE_DIR = Path(__file__).parent
REPO_ROOT = CRATE_DIR.parent.parent
VENDOR_DIR = CRATE_DIR / 'vendor' / 'typeshed'
STDLIB_DIR = VENDOR_DIR / 'stdlib'
CUSTOM_DIR = CRATE_DIR / 'custom'
TYPESHED_REPO_DIR = CRATE_DIR / 'typeshed-repo'
TYPESHED_REPO_URL = 'git@github.com:python/typeshed.git'
COMMIT = '0e16ea31d2e188fdc126cb31e7c4fcc6b5a8da96'
def clone_or_update_typeshed() -> None:
if TYPESHED_REPO_DIR.exists():
result = subprocess.run(
['git', 'rev-parse', 'HEAD'],
cwd=TYPESHED_REPO_DIR,
check=True,
capture_output=True,
text=True,
)
if result.stdout.strip() == COMMIT:
print(f'{TYPESHED_REPO_DIR} already at {COMMIT}')
else:
print(f'{TYPESHED_REPO_DIR} exists, fetching {COMMIT}...')
subprocess.run(
['git', 'fetch', 'origin', COMMIT],
cwd=TYPESHED_REPO_DIR,
check=True,
capture_output=True,
)
print(f'Checking out {COMMIT}...')
subprocess.run(
['git', 'checkout', COMMIT],
cwd=TYPESHED_REPO_DIR,
check=True,
capture_output=True,
)
else:
print(f'Cloning typeshed to {TYPESHED_REPO_DIR}...')
subprocess.run(
['git', 'clone', TYPESHED_REPO_URL, str(TYPESHED_REPO_DIR)],
check=True,
capture_output=True,
)
print(f'Checking out {COMMIT}...')
subprocess.run(
['git', 'checkout', COMMIT],
cwd=TYPESHED_REPO_DIR,
check=True,
capture_output=True,
)
status = subprocess.run(
['git', 'status', '--porcelain'],
cwd=TYPESHED_REPO_DIR,
check=True,
capture_output=True,
text=True,
)
if status.stdout.strip():
raise ValueError(
f'{TYPESHED_REPO_DIR} has local modifications that would leak into the vendored stubs;'
f' discard them (or delete the directory to re-clone) and re-run:\n{status.stdout}'
)
def filter_statements(nodes: list[ast.stmt]) -> list[ast.stmt]:
result: list[ast.stmt] = []
for node in nodes:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if node.name in ALLOWED_FUNCTIONS:
result.append(node)
elif isinstance(node, ast.ClassDef):
if node.name.startswith('_') or node.name in ALLOWED_CLASSES:
result.append(node)
elif isinstance(node, ast.If):
filtered = filter_if_block(node)
if filtered is not None:
result.append(filtered)
else:
result.append(node)
return result
def filter_if_block(node: ast.If) -> ast.If | None:
filtered_body = filter_statements(node.body)
filtered_orelse = filter_statements(node.orelse)
if not filtered_body and not filtered_orelse:
return None
new_node = ast.If(
test=node.test,
body=filtered_body if filtered_body else [ast.Pass()],
orelse=filtered_orelse,
)
return ast.copy_location(new_node, node)
def filter_builtins(source: str) -> str:
tree = ast.parse(source)
tree.body = filter_statements(tree.body)
ast.fix_missing_locations(tree)
return ast.unparse(tree)
def main() -> int:
clone_or_update_typeshed()
print(f'At python/typeshed commit {COMMIT}')
if VENDOR_DIR.exists():
print(f'Removing existing {VENDOR_DIR}...')
shutil.rmtree(VENDOR_DIR)
src_stdlib = TYPESHED_REPO_DIR / 'stdlib'
builtins_path = src_stdlib / 'builtins.pyi'
source = builtins_path.read_text()
filtered = filter_builtins(source)
STDLIB_DIR.mkdir(parents=True, exist_ok=True)
(STDLIB_DIR / 'builtins.pyi').write_text(filtered)
print(f'Wrote {(STDLIB_DIR / "builtins.pyi").relative_to(REPO_ROOT)}')
(STDLIB_DIR / 'VERSIONS').write_text(VERSIONS)
print(f'Wrote {(STDLIB_DIR / "VERSIONS").relative_to(REPO_ROOT)}')
(VENDOR_DIR / 'source_commit.txt').write_text(f'{COMMIT}\n')
print(f'Wrote {(VENDOR_DIR / "source_commit.txt").relative_to(REPO_ROOT)}')
for file_path in COPY_FILES:
src_file = src_stdlib / file_path
if src_file.exists():
dest_file = STDLIB_DIR / file_path
dest_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_file, dest_file)
else:
raise ValueError(f'{file_path} not found in typeshed')
print(f'Copied {len(COPY_FILES)} stdlib typeshed files')
custom_count = 0
for file in CUSTOM_DIR.rglob('*.pyi'):
dest_file = STDLIB_DIR / file.relative_to(CUSTOM_DIR)
dest_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(file, dest_file)
custom_count += 1
print(f'Copied {custom_count} custom typeshed files')
return 0
if __name__ == '__main__':
exit(main())