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
|
# lazy-import
|
||||||
|
|
||||||
python with no imports
|
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