This commit is contained in:
sneakers-the-rat 2024-10-15 23:42:42 -07:00
parent bec941080f
commit 921bc5e870
Signed by untrusted user who does not match committer: jonny
GPG key ID: 6DCB96EF1E4D232D
12 changed files with 254 additions and 1 deletions

View file

@ -1,3 +1,11 @@
# lazy-import
python with no imports
hell ya it's got side effects
## TODO
- oh frick we actually need to conert to a NodeTransformer so that
we can control the rendering of child nodes (aka we can recursively
get the names in the namespace rather than having an entry-only visitor)

11
pdm.lock Normal file
View file

@ -0,0 +1,11 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:6e3746c7427353cd94cfd6c1a5a6309fdfd871714e3b1213ded2befc6ef522a0"
[[metadata.targets]]
requires_python = ">=3.10"

19
pyproject.toml Normal file
View file

@ -0,0 +1,19 @@
[project]
name = "lazy-import"
version = "0.1.0"
description = "python with no imports"
authors = [
{name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"},
]
dependencies = []
requires-python = ">=3.10"
readme = "README.md"
license = {text = "EUPL-1.2"}
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[tool.pdm]
distribution = true

View file

@ -0,0 +1,4 @@
import sys
def getattr(name):
pass

123
src/lazy_import/ast.py Normal file
View file

@ -0,0 +1,123 @@
import ast
from collections import ChainMap
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class Name:
module: str
name: str | None = None
aliases: set = field(default_factory=set)
def __contains__(self, item: str):
return item in self.aliases or (
item == self.module
if self.name is None else
item == self.name
)
@classmethod
def from_ast_name(cls, name: ast.Name) -> 'Name':
return cls.from_str(name.id)
@classmethod
def from_str(cls, name: str) -> 'Name':
if len(name_parts := name.rsplit(".")) > 1:
return Name(module=name_parts[0], name=name_parts[1])
else:
return Name(module=name)
@dataclass
class NameCollection:
names: list[Name] = field(default_factory=set)
def add(self, new_name: ast.Name):
for name in self.names:
if new_name.id in name:
name.aliases.add(new_name.id)
return
self.names.append(Name.from_ast_name(new_name))
class NameVisitor(ast.NodeVisitor):
"""
Extract names to import and assign from an importless python module
"""
def __init__(self):
self.real_names = ChainMap()
self.fake_names = NameCollection()
def pop_ctx(self):
self.real_names = self.real_names.parents
def push_ctx(self):
self.real_names = self.real_names.new_child()
def visit_Import(self, node: ast.Import | ast.ImportFrom) -> None:
"""Add to names"""
for alias in node.names:
name = alias.asname if alias.asname else alias.name
self.real_names[name] = node
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
"""Add to names"""
self.visit_Import(node)
def visit_Name(self, node: ast.Name):
"""Either add to real names or fake names depending on ctx"""
if node.ctx == ast.Store():
self.real_names[node.id] = node
elif node.ctx == ast.Load() and node.id not in self.real_names:
self.fake_names.add(node)
elif node.ctx == ast.Del() and node.id in self.real_names:
del self.real_names[node.id]
else: # pragma: no cover
if node.ctx not in (ast.Del(), ast.Store(), ast.Load()):
raise ValueError('How did this happen!? wrong node ctx type?')
def visit_Attribute(self, node: ast.Attribute):
attr_name = flatten_attribute(node)
if node.ctx == ast.Load():
if attr_name not in self.real_names:
self.fake_names.add(Name.from_str(attr_name))
elif node.ctx == ast.Store():
self.real_names[attr_name] = node
elif node.ctx == ast.Del() and attr_name in self.real_names:
del self.real_names[attr_name]
else: # pragma: no cover
if node.ctx not in (ast.Del(), ast.Store(), ast.Load()):
raise ValueError('How did this happen!? wrong node ctx type?')
def visit_FunctionDef(self, node):
"""push context"""
self.push_ctx()
args = node.args
for arg in args.args:
self.real_names[arg.arg] = arg
if args.vararg:
self.real_names[args.vararg.arg] = args.vararg
if args.kwarg:
self.real_names[args.kwarg.arg] = args.kwarg
def visit_AsyncFunctionDef(self, node):
"""push context"""
self.push_ctx()
def visit_ClassDef(self, node):
"""push context"""
self.push_ctx()
def visit_Return(self, node):
"""pop context"""
self.pop_ctx()
def flatten_attribute(attr: ast.Attribute) -> str:
if isinstance(attr.value, ast.Attribute):
return '.'.join([flatten_attribute(attr.value), attr.attr])
elif isinstance(attr.value, ast.Name):
return '.'.join([attr.value.id, attr.attr])

View file

0
tests/__init__.py Normal file
View file

3
tests/conftest.py Normal file
View file

@ -0,0 +1,3 @@
import lazy_import
__all__ = ['lazy_import']

0
tests/data/__init__.py Normal file
View file

51
tests/data/input_file.py Normal file
View file

@ -0,0 +1,51 @@
import lazy_import
import collections.abc
import json as jay_son
from typing import List
from collections.abc import Callable
from collections.abc import ChainMap as cm
mod_variable = 10
def imports_are_lazy():
ast
typing.List
re.match('hell ya', 'hell ya')
# one day you should be able to do this
# numpy as np
def a_function(a: typing.Iterable) -> importlib.abc.Finder:
a = 1
class AClass():
zz = 1
yy = array.array()
def __init__(self):
_ = base64.b64decode(f"{binascii.hexlify(b'abc')}")
def regular_names_still_work():
# assert mod_variable == 10
x = 20
# assert x == 20
def afunc(y, z=1, *args, **kwargs):
z = 10
assert z == 10
assert y == 30
class AClass():
z = 1
afunc(30)
def test_names_are_lazy():
"""
you can just use the last unique segment
"""
_ = numpy.random.random(100)
_ = random
assert random is numpy.random.random

1
tests/test_ast.py Normal file
View file

@ -0,0 +1 @@
def test

33
tests/test_lazy_import.py Normal file
View file

@ -0,0 +1,33 @@
import lazy_import
from typing import List
import collections.abc
from collections.abc import Callable
from collections.abc import ChainMap as cm
mod_variable = 10
def test_imports_are_lazy():
re.match('hell ya', 'hell ya')
typing.List
def test_regular_names_still_work():
assert mod_variable == 10
x = 20
assert x == 20
def afunc(y):
z = 10
assert z == 10
assert y == 30
afunc(30)
def test_names_are_lazy():
"""
you can just use the last unique segment
"""
_ = numpy.random.random(100)
_ = random
assert random is numpy.random.random