init
This commit is contained in:
parent
bec941080f
commit
921bc5e870
12 changed files with 254 additions and 1 deletions
|
@ -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
11
pdm.lock
Normal 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
19
pyproject.toml
Normal 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
|
4
src/lazy_import/__init__.py
Normal file
4
src/lazy_import/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
import sys
|
||||
|
||||
def getattr(name):
|
||||
pass
|
123
src/lazy_import/ast.py
Normal file
123
src/lazy_import/ast.py
Normal 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])
|
0
src/lazy_import/importer.py
Normal file
0
src/lazy_import/importer.py
Normal file
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
3
tests/conftest.py
Normal file
3
tests/conftest.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
import lazy_import
|
||||
|
||||
__all__ = ['lazy_import']
|
0
tests/data/__init__.py
Normal file
0
tests/data/__init__.py
Normal file
51
tests/data/input_file.py
Normal file
51
tests/data/input_file.py
Normal 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
1
tests/test_ast.py
Normal file
|
@ -0,0 +1 @@
|
|||
def test
|
33
tests/test_lazy_import.py
Normal file
33
tests/test_lazy_import.py
Normal 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
|
||||
|
Loading…
Reference in a new issue