From 7f5a0d5977381c842ca1eafc0857f1e026edb385 Mon Sep 17 00:00:00 2001 From: Frank Hoffmann <15r10nk-git@polarbit.de> Date: Fri, 9 Feb 2024 21:30:57 +0100 Subject: [PATCH] fix: fixed windows support --- src/lazy_imports_lite/__init__.py | 6 +- src/lazy_imports_lite/__main__.py | 84 +- src/lazy_imports_lite/_hooks.py | 206 ++--- src/lazy_imports_lite/_loader.py | 194 ++--- src/lazy_imports_lite/_transformer.py | 382 ++++----- src/lazy_imports_lite/_utils.py | 18 +- tests/test_cli.py | 90 +- tests/test_loader.py | 1018 +++++++++++----------- tests/test_transformer.py | 1119 +++++++++++++------------ 9 files changed, 1567 insertions(+), 1550 deletions(-) diff --git a/src/lazy_imports_lite/__init__.py b/src/lazy_imports_lite/__init__.py index e6333aa..c7cc683 100644 --- a/src/lazy_imports_lite/__init__.py +++ b/src/lazy_imports_lite/__init__.py @@ -1,3 +1,3 @@ -from contextlib import nullcontext as eager_imports - -from ._hooks import LazyImportError +from contextlib import nullcontext as eager_imports + +from ._hooks import LazyImportError diff --git a/src/lazy_imports_lite/__main__.py b/src/lazy_imports_lite/__main__.py index 77e5f1a..d22abc0 100644 --- a/src/lazy_imports_lite/__main__.py +++ b/src/lazy_imports_lite/__main__.py @@ -1,41 +1,43 @@ -import argparse -import ast -import pathlib - -from lazy_imports_lite._transformer import TransformModuleImports -from lazy_imports_lite._utils import unparse - - -def main(): - parser = argparse.ArgumentParser( - prog="lazy-imports-lite", description="Tool for various file operations." - ) - subparsers = parser.add_subparsers( - title="subcommands", dest="subcommand", help="Available subcommands" - ) - - # Subcommand for preview - preview_parser = subparsers.add_parser( - "preview", help="Preview the contents of a file" - ) - preview_parser.add_argument("filename", help="Name of the file to preview") - - args = parser.parse_args() - - if args.subcommand == "preview": - transformer = TransformModuleImports() - code = pathlib.Path(args.filename).read_text() - tree = ast.parse(code) - new_tree = ast.fix_missing_locations(transformer.visit(tree)) - new_code = unparse(new_tree) - print(new_code) - - else: - print( - "Error: Please specify a valid subcommand. Use 'preview --help' for more information." - ) - exit(1) - - -if __name__ == "__main__": - main() +import argparse +import ast +import pathlib +import sys + +from lazy_imports_lite._transformer import TransformModuleImports +from lazy_imports_lite._utils import unparse + + +def main(): + parser = argparse.ArgumentParser( + prog="lazy-imports-lite", description="Tool for various file operations." + ) + subparsers = parser.add_subparsers( + title="subcommands", dest="subcommand", help="Available subcommands" + ) + + # Subcommand for preview + preview_parser = subparsers.add_parser( + "preview", help="Preview the contents of a file" + ) + preview_parser.add_argument("filename", help="Name of the file to preview") + + args = parser.parse_args() + + if args.subcommand == "preview": + transformer = TransformModuleImports() + code = pathlib.Path(args.filename).read_text() + tree = ast.parse(code) + new_tree = ast.fix_missing_locations(transformer.visit(tree)) + new_code = unparse(new_tree) + print(new_code) + + else: + print( + "Error: Please specify a valid subcommand. Use 'preview --help' for more information.", + file=sys.stderr, + ) + exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/lazy_imports_lite/_hooks.py b/src/lazy_imports_lite/_hooks.py index 59be318..1f059ea 100644 --- a/src/lazy_imports_lite/_hooks.py +++ b/src/lazy_imports_lite/_hooks.py @@ -1,103 +1,103 @@ -import importlib -from collections import defaultdict - - -class LazyObject: - __slots__ = ("v",) - - -class LazyImportError(BaseException): - def __init__(self, module, package): - self.module = module - self.package = package - - def __str__(self): - if self.package is None: - return f"Deferred importing of module '{self.module}' caused an error" - else: - return f"Deferred importing of module '{self.module}' in '{self.package}' caused an error" - - -class ImportFrom(LazyObject): - __slots__ = ("package", "module", "name", "v") - - def __init__(self, package, module, name): - self.package = package - self.module = module - self.name = name - - def __getattr__(self, name): - if name == "v": - module = safe_import(self.module, self.package) - try: - attr = getattr(module, self.name) - except AttributeError: - attr = safe_import(self.module + "." + self.name, self.package) - self.v = attr - return attr - else: - assert False - - -pending_imports = defaultdict(list) -imported_modules = set() - - -def safe_import(module, package=None): - try: - return importlib.import_module(module, package) - except LazyImportError: - raise - except: - raise LazyImportError(module, package) - - -class Import(LazyObject): - __slots__ = ("module", "v") - - def __init__(self, module): - self.module = module - m = self.module.split(".")[0] - - if m in imported_modules: - safe_import(self.module) - else: - pending_imports[m].append(module) - - def __getattr__(self, name): - if name == "v": - m = self.module.split(".")[0] - for pending in pending_imports[m]: - safe_import(pending) - result = safe_import(self.module.split(".")[0]) - imported_modules.add(m) - self.v = result - return result - else: - assert False - - -class ImportAs(LazyObject): - __slots__ = ("module", "v") - - def __init__(self, module): - self.module = module - - def __getattr__(self, name): - if name == "v": - module = safe_import(self.module) - self.v = module - return module - else: - assert False - - -def make_globals(global_provider): - def g(): - return { - k: v.v if isinstance(v, LazyObject) else v - for k, v in dict(global_provider()).items() - if k not in ("globals", "__lazy_imports_lite__") - } - - return g +import importlib +from collections import defaultdict + + +class LazyObject: + __slots__ = ("v",) + + +class LazyImportError(BaseException): + def __init__(self, module, package): + self.module = module + self.package = package + + def __str__(self): + if self.package is None: + return f"Deferred importing of module '{self.module}' caused an error" + else: + return f"Deferred importing of module '{self.module}' in '{self.package}' caused an error" + + +class ImportFrom(LazyObject): + __slots__ = ("package", "module", "name", "v") + + def __init__(self, package, module, name): + self.package = package + self.module = module + self.name = name + + def __getattr__(self, name): + if name == "v": + module = safe_import(self.module, self.package) + try: + attr = getattr(module, self.name) + except AttributeError: + attr = safe_import(self.module + "." + self.name, self.package) + self.v = attr + return attr + else: + assert False + + +pending_imports = defaultdict(list) +imported_modules = set() + + +def safe_import(module, package=None): + try: + return importlib.import_module(module, package) + except LazyImportError: + raise + except: + raise LazyImportError(module, package) + + +class Import(LazyObject): + __slots__ = ("module", "v") + + def __init__(self, module): + self.module = module + m = self.module.split(".")[0] + + if m in imported_modules: + safe_import(self.module) + else: + pending_imports[m].append(module) + + def __getattr__(self, name): + if name == "v": + m = self.module.split(".")[0] + for pending in pending_imports[m]: + safe_import(pending) + result = safe_import(self.module.split(".")[0]) + imported_modules.add(m) + self.v = result + return result + else: + assert False + + +class ImportAs(LazyObject): + __slots__ = ("module", "v") + + def __init__(self, module): + self.module = module + + def __getattr__(self, name): + if name == "v": + module = safe_import(self.module) + self.v = module + return module + else: + assert False + + +def make_globals(global_provider): + def g(): + return { + k: v.v if isinstance(v, LazyObject) else v + for k, v in dict(global_provider()).items() + if k not in ("globals", "__lazy_imports_lite__") + } + + return g diff --git a/src/lazy_imports_lite/_loader.py b/src/lazy_imports_lite/_loader.py index bdf12f4..75aa3fd 100644 --- a/src/lazy_imports_lite/_loader.py +++ b/src/lazy_imports_lite/_loader.py @@ -1,95 +1,99 @@ -import ast -import importlib.abc -import importlib.machinery -import importlib.metadata -import os -import sys -import types -from functools import lru_cache - -from ._hooks import LazyObject -from ._transformer import TransformModuleImports - - -class LazyModule(types.ModuleType): - def __getattribute__(self, name): - v = super().__getattribute__(name) - if isinstance(v, LazyObject): - return v.v - return v - - def __setattr__(self, name, value): - try: - v = super().__getattribute__(name) - except: - super().__setattr__(name, value) - else: - if isinstance(v, LazyObject): - v.v = value - else: - super().__setattr__(name, value) - - -@lru_cache -def is_enabled_by_metadata(name): - if name != "lazy_imports_lite": - try: - metadata = importlib.metadata.metadata(name) - except importlib.metadata.PackageNotFoundError: - return False - - if metadata is None: - return False # pragma: no cover - - if metadata["Keywords"] is None: - return False - - keywords = metadata["Keywords"].split(",") - if "lazy-imports-lite-enabled" in keywords: - return True - - return False - - -class LazyLoader(importlib.abc.Loader, importlib.machinery.PathFinder): - def find_spec(self, fullname, path=None, target=None): - if "LAZY_IMPORTS_LITE_DISABLE" in os.environ: - return None - - spec = super().find_spec(fullname, path, target) - - if spec is None: - return None - - if spec.origin is None: - return None # pragma: no cover - - name = spec.name.split(".")[0] - - if is_enabled_by_metadata(name) and spec.origin.endswith(".py"): - spec.loader = self - return spec - - return None - - def create_module(self, spec): - return LazyModule(spec.name) - - def exec_module(self, module): - origin: str = module.__spec__.origin - with open(origin) as f: - mod_raw = f.read() - mod_ast = ast.parse(mod_raw, origin, "exec") - transformer = TransformModuleImports() - new_ast = transformer.visit(mod_ast) - - ast.fix_missing_locations(new_ast) - mod_code = compile(new_ast, origin, "exec") - exec(mod_code, module.__dict__) - del module.__dict__["__lazy_imports_lite__"] - del module.__dict__["globals"] - - -def setup(): - if not any(isinstance(m, LazyLoader) for m in sys.meta_path): - sys.meta_path.insert(0, LazyLoader()) +import ast +import importlib.abc +import importlib.machinery +import importlib.metadata +import os +import sys +import types +from functools import lru_cache + +from ._hooks import LazyObject +from ._transformer import TransformModuleImports + + +class LazyModule(types.ModuleType): + def __getattribute__(self, name): + v = super().__getattribute__(name) + if isinstance(v, LazyObject): + return v.v + return v + + def __setattr__(self, name, value): + try: + v = super().__getattribute__(name) + except: + super().__setattr__(name, value) + else: + if isinstance(v, LazyObject): + v.v = value + else: + super().__setattr__(name, value) + + +@lru_cache +def is_enabled_by_metadata(name): + if name != "lazy_imports_lite": + try: + metadata = importlib.metadata.metadata(name) + except importlib.metadata.PackageNotFoundError: + return False + + if metadata is None: + return False # pragma: no cover + + if metadata["Keywords"] is None: + return False + + keywords = metadata["Keywords"].split(",") + if "lazy-imports-lite-enabled" in keywords: + return True + + return False + + +class LazyLoader(importlib.abc.Loader, importlib.machinery.PathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname.startswith("encodings."): + # fix wired windows bug + return None + + if "LAZY_IMPORTS_LITE_DISABLE" in os.environ: + return None + + spec = super().find_spec(fullname, path, target) + + if spec is None: + return None + + if spec.origin is None: + return None # pragma: no cover + + name = spec.name.split(".")[0] + + if is_enabled_by_metadata(name) and spec.origin.endswith(".py"): + spec.loader = self + return spec + + return None + + def create_module(self, spec): + return LazyModule(spec.name) + + def exec_module(self, module): + origin: str = module.__spec__.origin + with open(origin) as f: + mod_raw = f.read() + mod_ast = ast.parse(mod_raw, origin, "exec") + transformer = TransformModuleImports() + new_ast = transformer.visit(mod_ast) + + ast.fix_missing_locations(new_ast) + mod_code = compile(new_ast, origin, "exec") + exec(mod_code, module.__dict__) + del module.__dict__["__lazy_imports_lite__"] + del module.__dict__["globals"] + + +def setup(): + if not any(isinstance(m, LazyLoader) for m in sys.meta_path): + sys.meta_path.insert(0, LazyLoader()) diff --git a/src/lazy_imports_lite/_transformer.py b/src/lazy_imports_lite/_transformer.py index 45a560e..6603173 100644 --- a/src/lazy_imports_lite/_transformer.py +++ b/src/lazy_imports_lite/_transformer.py @@ -1,191 +1,191 @@ -import ast -import typing -from typing import Any - -header = """ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals=__lazy_imports_lite__.make_globals(lambda g=globals:g()) -""" -header_ast = ast.parse(header).body - - -class TransformModuleImports(ast.NodeTransformer): - def __init__(self): - self.transformed_imports = [] - self.functions = [] - self.context = [] - - self.globals = set() - self.locals = set() - self.in_function = False - - def visit_ImportFrom(self, node: ast.ImportFrom) -> Any: - if self.context[-1] != "Module": - return node - - if node.module == "__future__": - return node - - new_nodes = [] - for alias in node.names: - name = alias.asname or alias.name - - module = "." * (node.level) + (node.module or "") - new_nodes.append( - ast.Assign( - targets=[ast.Name(id=name, ctx=ast.Store())], - value=ast.Call( - func=ast.Attribute( - value=ast.Name(id="__lazy_imports_lite__", ctx=ast.Load()), - attr="ImportFrom", - ctx=ast.Load(), - ), - args=[ - ast.Name(id="__package__", ctx=ast.Load()), - ast.Constant(value=module, kind=None), - ast.Constant(alias.name, kind=None), - ], - keywords=[], - ), - ) - ) - self.transformed_imports.append(name) - return new_nodes - - def visit_Import(self, node: ast.Import) -> Any: - if len(self.context) > 1: - return node - - new_nodes = [] - for alias in node.names: - if alias.asname: - name = alias.asname - new_nodes.append( - ast.Assign( - targets=[ast.Name(id=name, ctx=ast.Store())], - value=ast.Call( - func=ast.Attribute( - value=ast.Name( - id="__lazy_imports_lite__", ctx=ast.Load() - ), - attr="ImportAs", - ctx=ast.Load(), - ), - args=[ast.Constant(value=alias.name, kind=None)], - keywords=[], - ), - ) - ) - self.transformed_imports.append(name) - else: - name = alias.name.split(".")[0] - new_nodes.append( - ast.Assign( - targets=[ast.Name(id=name, ctx=ast.Store())], - value=ast.Call( - func=ast.Attribute( - value=ast.Name( - id="__lazy_imports_lite__", ctx=ast.Load() - ), - attr="Import", - ctx=ast.Load(), - ), - args=[ast.Constant(value=alias.name, kind=None)], - keywords=[], - ), - ) - ) - self.transformed_imports.append(name) - - return new_nodes - - def visit_FunctionDef(self, node: ast.FunctionDef) -> Any: - return self.handle_function(node) - - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> Any: - return self.handle_function(node) - - def visit_Lambda(self, node: ast.Lambda) -> Any: - return self.handle_function(node) - - def handle_function(self, function): - for field, value in ast.iter_fields(function): - if field != "body": - if isinstance(value, list): - setattr(function, field, [self.visit(v) for v in value]) - elif isinstance(value, ast.AST): - setattr(function, field, self.visit(value)) - self.functions.append(function) - - return function - - def handle_function_body(self, function: ast.FunctionDef): - args = [ - *function.args.posonlyargs, - *function.args.args, - function.args.vararg, - *function.args.kwonlyargs, - function.args.kwarg, - ] - - self.locals = {arg.arg for arg in args if arg is not None} - - self.globals = set() - - self.in_function = True - - if isinstance(function.body, list): - function.body = [self.visit(b) for b in function.body] - else: - function.body = self.visit(function.body) - - def visit_Global(self, node: ast.Global) -> Any: - self.globals.update(node.names) - return self.generic_visit(node) - - def visit_Name(self, node: ast.Name) -> Any: - if isinstance(node.ctx, ast.Store) and ( - node.id not in self.globals or not self.in_function - ): - self.locals.add(node.id) - - if node.id in self.transformed_imports and node.id not in self.locals: - old_ctx = node.ctx - node.ctx = ast.Load() - return ast.Attribute(value=node, attr="v", ctx=old_ctx) - else: - return node - - def visit_Module(self, module: ast.Module) -> Any: - module = typing.cast(ast.Module, self.generic_visit(module)) - assert len(self.context) == 0 - - pos = 0 - - def is_import_from_future(node): - return ( - isinstance(node, ast.Expr) - and isinstance(node.value, ast.Constant) - and isinstance(node.value.value, str) - or isinstance(node, ast.ImportFrom) - and node.module == "__future__" - ) - - if module.body: - while is_import_from_future(module.body[pos]): - pos += 1 - module.body[pos:pos] = header_ast - - self.context = ["FunctionBody"] - while self.functions: - f = self.functions.pop() - self.handle_function_body(f) - - return module - - def generic_visit(self, node: ast.AST) -> ast.AST: - ctx_len = len(self.context) - self.context.append(type(node).__name__) - result = super().generic_visit(node) - self.context = self.context[:ctx_len] - return result +import ast +import typing +from typing import Any + +header = """ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals=__lazy_imports_lite__.make_globals(lambda g=globals:g()) +""" +header_ast = ast.parse(header).body + + +class TransformModuleImports(ast.NodeTransformer): + def __init__(self): + self.transformed_imports = [] + self.functions = [] + self.context = [] + + self.globals = set() + self.locals = set() + self.in_function = False + + def visit_ImportFrom(self, node: ast.ImportFrom) -> Any: + if self.context[-1] != "Module": + return node + + if node.module == "__future__": + return node + + new_nodes = [] + for alias in node.names: + name = alias.asname or alias.name + + module = "." * (node.level) + (node.module or "") + new_nodes.append( + ast.Assign( + targets=[ast.Name(id=name, ctx=ast.Store())], + value=ast.Call( + func=ast.Attribute( + value=ast.Name(id="__lazy_imports_lite__", ctx=ast.Load()), + attr="ImportFrom", + ctx=ast.Load(), + ), + args=[ + ast.Name(id="__package__", ctx=ast.Load()), + ast.Constant(value=module, kind=None), + ast.Constant(alias.name, kind=None), + ], + keywords=[], + ), + ) + ) + self.transformed_imports.append(name) + return new_nodes + + def visit_Import(self, node: ast.Import) -> Any: + if len(self.context) > 1: + return node + + new_nodes = [] + for alias in node.names: + if alias.asname: + name = alias.asname + new_nodes.append( + ast.Assign( + targets=[ast.Name(id=name, ctx=ast.Store())], + value=ast.Call( + func=ast.Attribute( + value=ast.Name( + id="__lazy_imports_lite__", ctx=ast.Load() + ), + attr="ImportAs", + ctx=ast.Load(), + ), + args=[ast.Constant(value=alias.name, kind=None)], + keywords=[], + ), + ) + ) + self.transformed_imports.append(name) + else: + name = alias.name.split(".")[0] + new_nodes.append( + ast.Assign( + targets=[ast.Name(id=name, ctx=ast.Store())], + value=ast.Call( + func=ast.Attribute( + value=ast.Name( + id="__lazy_imports_lite__", ctx=ast.Load() + ), + attr="Import", + ctx=ast.Load(), + ), + args=[ast.Constant(value=alias.name, kind=None)], + keywords=[], + ), + ) + ) + self.transformed_imports.append(name) + + return new_nodes + + def visit_FunctionDef(self, node: ast.FunctionDef) -> Any: + return self.handle_function(node) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> Any: + return self.handle_function(node) + + def visit_Lambda(self, node: ast.Lambda) -> Any: + return self.handle_function(node) + + def handle_function(self, function): + for field, value in ast.iter_fields(function): + if field != "body": + if isinstance(value, list): + setattr(function, field, [self.visit(v) for v in value]) + elif isinstance(value, ast.AST): + setattr(function, field, self.visit(value)) + self.functions.append(function) + + return function + + def handle_function_body(self, function: ast.FunctionDef): + args = [ + *function.args.posonlyargs, + *function.args.args, + function.args.vararg, + *function.args.kwonlyargs, + function.args.kwarg, + ] + + self.locals = {arg.arg for arg in args if arg is not None} + + self.globals = set() + + self.in_function = True + + if isinstance(function.body, list): + function.body = [self.visit(b) for b in function.body] + else: + function.body = self.visit(function.body) + + def visit_Global(self, node: ast.Global) -> Any: + self.globals.update(node.names) + return self.generic_visit(node) + + def visit_Name(self, node: ast.Name) -> Any: + if isinstance(node.ctx, ast.Store) and ( + node.id not in self.globals or not self.in_function + ): + self.locals.add(node.id) + + if node.id in self.transformed_imports and node.id not in self.locals: + old_ctx = node.ctx + node.ctx = ast.Load() + return ast.Attribute(value=node, attr="v", ctx=old_ctx) + else: + return node + + def visit_Module(self, module: ast.Module) -> Any: + module = typing.cast(ast.Module, self.generic_visit(module)) + assert len(self.context) == 0 + + pos = 0 + + def is_import_from_future(node): + return ( + isinstance(node, ast.Expr) + and isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + or isinstance(node, ast.ImportFrom) + and node.module == "__future__" + ) + + if module.body: + while is_import_from_future(module.body[pos]): + pos += 1 + module.body[pos:pos] = header_ast + + self.context = ["FunctionBody"] + while self.functions: + f = self.functions.pop() + self.handle_function_body(f) + + return module + + def generic_visit(self, node: ast.AST) -> ast.AST: + ctx_len = len(self.context) + self.context.append(type(node).__name__) + result = super().generic_visit(node) + self.context = self.context[:ctx_len] + return result diff --git a/src/lazy_imports_lite/_utils.py b/src/lazy_imports_lite/_utils.py index 08ebb6d..2519765 100644 --- a/src/lazy_imports_lite/_utils.py +++ b/src/lazy_imports_lite/_utils.py @@ -1,9 +1,9 @@ -import sys - -if sys.version_info >= (3, 9): - from ast import unparse -else: - from astunparse import unparse as _unparse # type: ignore - - def unparse(node): - return _unparse(node).strip() +import sys + +if sys.version_info >= (3, 9): + from ast import unparse +else: + from astunparse import unparse as _unparse # type: ignore + + def unparse(node): + return _unparse(node).strip() diff --git a/tests/test_cli.py b/tests/test_cli.py index bc90a04..ebbf1fb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,45 +1,45 @@ -import subprocess as sp -import sys - -import pytest -from inline_snapshot import snapshot - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="3.8 unparses differently") -def test_cli(tmp_path): - file = tmp_path / "example.py" - - file.write_text( - """\ -from foo import bar - - -def f(): - print(bar()) - print(bar()) -""" - ) - result = sp.run(["lazy-imports-lite", "preview", str(file)], capture_output=True) - assert result.returncode == 0 - assert result.stdout.decode() == snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -bar = __lazy_imports_lite__.ImportFrom(__package__, 'foo', 'bar') - -def f(): - print(bar.v()) - print(bar.v()) -""" - ) - - -def test_cli_invalid_args(): - result = sp.run(["python", "-m", "lazy_imports_lite"], capture_output=True) - assert result.returncode == 1 - assert result.stdout.decode() == snapshot( - """\ -Error: Please specify a valid subcommand. Use 'preview --help' for more information. -""" - ) - assert result.stderr.decode() == snapshot("") +import subprocess as sp +import sys + +import pytest +from inline_snapshot import snapshot + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason="3.8 unparses differently") +def test_cli(tmp_path): + file = tmp_path / "example.py" + + file.write_text( + """\ +from foo import bar + + +def f(): + print(bar()) + print(bar()) +""" + ) + result = sp.run(["lazy-imports-lite", "preview", str(file)], capture_output=True) + assert result.returncode == 0 + assert result.stdout.decode().replace("\r\n", "\n") == snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +bar = __lazy_imports_lite__.ImportFrom(__package__, 'foo', 'bar') + +def f(): + print(bar.v()) + print(bar.v()) +""" + ) + + +def test_cli_invalid_args(): + result = sp.run([sys.executable, "-m", "lazy_imports_lite"], capture_output=True) + assert result.returncode == 1 + assert result.stdout.decode().replace("\r\n", "\n") == snapshot("") + assert result.stderr.decode().replace("\r\n", "\n") == snapshot( + """\ +Error: Please specify a valid subcommand. Use 'preview --help' for more information. +""" + ) diff --git a/tests/test_loader.py b/tests/test_loader.py index 6383ed3..24770d0 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,504 +1,514 @@ -import os -import re -import subprocess -import subprocess as sp -import sys -from contextlib import contextmanager -from pathlib import Path -from tempfile import TemporaryDirectory - -from inline_snapshot import snapshot - -python_version = f"python{sys.version_info[0]}.{sys.version_info[1]}" - - -def write_files(dir, content): - for path, text in content.items(): - path = dir / path - path.parent.mkdir(exist_ok=True, parents=True) - path.write_text(text) - - -@contextmanager -def package(name, content): - with TemporaryDirectory() as d: - package_dir = Path(d) / name - package_dir.mkdir() - - write_files(package_dir, content) - - subprocess.run( - [sys.executable, "-m", "pip", "install", str(package_dir)], - input=b"y", - check=True, - ) - - yield - - subprocess.run( - [sys.executable, "-m", "pip", "uninstall", name], input=b"y", check=True - ) - - -def check_script( - package_files, - script, - *, - transformed_stdout="", - transformed_stderr="", - normal_stdout="", - normal_stderr="", -): - package_files = { - "pyproject.toml": """ - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name="test-pck" -keywords=["lazy-imports-lite-enabled"] -version="0.0.1" -""", - **package_files, - } - - def normalize_output(output: bytes): - text = output.decode() - text = text.replace(sys.exec_prefix, "") - text = re.sub("at 0x[0-9a-f]*>", "at >", text) - text = re.sub("line [0-9]*", "line ", text) - text = text.replace(python_version, "") - text = text.replace(str(script_dir), "") - if " \n" in text: - text = text.replace("\n", "⏎\n") - return text - - with package("test_pck", package_files), TemporaryDirectory() as script_dir: - print(sys.exec_prefix) - script_dir = Path(script_dir) - - script_file = script_dir / "script.py" - script_file.write_text(script) - - normal_result = sp.run( - [sys.executable, str(script_file)], - cwd=str(script_dir), - env={**os.environ, "LAZY_IMPORTS_LITE_DISABLE": "True"}, - capture_output=True, - ) - - transformed_result = sp.run( - [sys.executable, str(script_file)], cwd=str(script_dir), capture_output=True - ) - - n_stdout = normalize_output(normal_result.stdout) - t_stdout = normalize_output(transformed_result.stdout) - n_stderr = normalize_output(normal_result.stderr) - t_stderr = normalize_output(transformed_result.stderr) - - assert normal_stdout == n_stdout - assert transformed_stdout == ( - "" if n_stdout == t_stdout else t_stdout - ) - - assert normal_stderr == n_stderr - assert transformed_stderr == ( - "" if n_stderr == t_stderr else t_stderr - ) - - -def test_loader(): - check_script( - { - "test_pck/__init__.py": """\ -from .mx import x -from .my import y - -def use_x(): - return x - -def use_y(): - return y -""", - "test_pck/mx.py": """\ -print('imported mx') -x=5 -""", - "test_pck/my.py": """\ -print('imported my') -y=5 -""", - }, - """\ -from test_pck import use_x, use_y -print("y:",use_y()) -print("x:",use_x()) -""", - transformed_stdout=snapshot( - """\ -imported my -y: 5 -imported mx -x: 5 -""" - ), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -imported mx -imported my -y: 5 -x: 5 -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_loader_keywords(): - check_script( - { - "test_pck/__init__.py": """\ -from .mx import x -print("imported init") - -def use_x(): - return x - -""", - "test_pck/mx.py": """\ -print('imported mx') -x=5 -""", - }, - """\ -from test_pck import use_x -print("x:",use_x()) -""", - transformed_stdout=snapshot( - """\ -imported init -imported mx -x: 5 -""" - ), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -imported mx -imported init -x: 5 -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_lazy_module_attr(): - check_script( - { - "test_pck/__init__.py": """\ -from .mx import x -from .my import y - -""", - "test_pck/mx.py": """\ -print('imported mx') -x=5 -""", - "test_pck/my.py": """\ -print('imported my') -y=5 -""", - }, - """\ -from test_pck import y -print("y:",y) - -from test_pck import x -print("x:",x) -""", - transformed_stdout=snapshot( - """\ -imported my -y: 5 -imported mx -x: 5 -""" - ), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -imported mx -imported my -y: 5 -x: 5 -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_lazy_module_content(): - check_script( - { - "test_pck/__init__.py": """\ -from .mx import x -from .my import y - -""", - "test_pck/mx.py": """\ -x=5 -""", - "test_pck/my.py": """\ -y=5 -""", - }, - """\ -import test_pck - -print(test_pck) -print(vars(test_pck).keys()) -""", - transformed_stdout=snapshot( - """\ -/lib//site-packages/test_pck/__init__.py'> -dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'y']) -""" - ), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -/lib//site-packages/test_pck/__init__.py'> -dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'my', 'y']) -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_lazy_module_content_import_from(): - check_script( - { - "test_pck/__init__.py": """\ -from .mx import x -print("inside",globals().keys()) - -try: - print("mx",mx) -except: - print("no mx") - -print("inside",globals().keys()) - -def later(): - print("later",globals().keys()) -""", - "test_pck/mx.py": """\ -x=5 -""", - }, - """\ -import test_pck - -print("outside",vars(test_pck).keys()) - -test_pck.later() -""", - transformed_stdout=snapshot( - """\ -inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x']) -mx /lib//site-packages/test_pck/mx.py'> -inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx']) -outside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx', 'later']) -later dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx', 'later']) -""" - ), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x']) -mx /lib//site-packages/test_pck/mx.py'> -inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x']) -outside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'later']) -later dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'later']) -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_import_module_with_error(): - check_script( - { - "test_pck/__init__.py": """\ -import test_pck.m -print(test_pck.m.v) -""", - "test_pck/m.py": """\ -raise ValueError() -""", - }, - """\ -try: - from test_pck import v -except BaseException as e: - while e: - print(f"{type(e).__name__}: {e}") - e=e.__cause__ if e.__suppress_context__ else e.__context__ -""", - transformed_stdout=snapshot( - """\ -LazyImportError: Deferred importing of module 'test_pck.m' caused an error⏎ -ValueError: ⏎ -""" - ), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -ValueError: ⏎ -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_load_chain_of_modules_with_error(): - check_script( - { - "test_pck/__init__.py": """\ -from .m import v -print(v) -""", - "test_pck/m/__init__.py": """\ -from .x import v -print(v) -""", - "test_pck/m/x.py": """\ -from .y import v -print(v) -""", - "test_pck/m/y.py": """\ -raise ValueError() -""", - }, - """\ -try: - from test_pck import v -except BaseException as e: - while e: - print(f"{type(e).__name__}: {e}") - e=e.__cause__ if e.__suppress_context__ else e.__context__ -""", - transformed_stdout=snapshot( - """\ -LazyImportError: Deferred importing of module '.y' in 'test_pck.m' caused an error⏎ -ValueError: ⏎ -""" - ), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -ValueError: ⏎ -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_lazy_module_import_from_empty_init(): - check_script( - { - "test_pck/__init__.py": """\ -""", - "test_pck/ma.py": """\ -a=5 -""", - "test_pck/mb.py": """\ -from test_pck import ma -a=ma.a -""", - }, - """\ -from test_pck import mb - -print(mb.a) -""", - transformed_stdout=snapshot(""), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -5 -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_lazy_module_setattr(): - check_script( - { - "test_pck/__init__.py": """\ -from .ma import b - -def foo(): - print(b()) - -""", - "test_pck/ma.py": """\ -def b(): - return 5 -""", - }, - """\ -from test_pck import foo -import test_pck - -foo() -test_pck.b=lambda:6 -foo() - -""", - transformed_stdout=snapshot(""), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ -5 -6 -""" - ), - normal_stderr=snapshot(""), - ) - - -def test_loader_is_used(): - check_script( - { - "test_pck/__init__.py": """\ - -def foo(): - print("foo") - -""", - }, - """\ -import test_pck - -print(type(test_pck.__spec__.loader)) - -""", - transformed_stdout=snapshot( - """\ - -""" - ), - transformed_stderr=snapshot(""), - normal_stdout=snapshot( - """\ - -""" - ), - normal_stderr=snapshot(""), - ) +import os +import re +import subprocess +import subprocess as sp +import sys +import typing +from contextlib import contextmanager +from pathlib import Path +from tempfile import TemporaryDirectory + +from inline_snapshot import snapshot + +python_version = f"python{sys.version_info[0]}.{sys.version_info[1]}" + + +def write_files(dir, content): + for path, text in content.items(): + path = dir / path + path.parent.mkdir(exist_ok=True, parents=True) + path.write_text(text) + + +@contextmanager +def package(name, content): + with TemporaryDirectory() as d: + package_dir = Path(d) / name + package_dir.mkdir() + + write_files(package_dir, content) + + subprocess.run( + [sys.executable, "-m", "pip", "install", str(package_dir)], + input=b"y", + check=True, + ) + + yield + + subprocess.run( + [sys.executable, "-m", "pip", "uninstall", name], input=b"y", check=True + ) + + +def check_script( + package_files, + script, + *, + transformed_stdout="", + transformed_stderr="", + normal_stdout="", + normal_stderr="", +): + package_files = { + "pyproject.toml": """ + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name="test-pck" +keywords=["lazy-imports-lite-enabled"] +version="0.0.1" +""", + **package_files, + } + + def normalize_output(output: bytes): + text = output.decode() + text = text.replace("\r\n", "\n") + + prefix = re.escape(sys.exec_prefix.replace("\\", "\\\\")) + backslash = "\\" + text = re.sub( + f"'{prefix}[^']*site-packages([^']*)'", + lambda m: f"'{typing.cast(str,m[1]).replace(backslash*2,'/')}'", + text, + ) + + text = re.sub("at 0x[0-9a-fA-F]*>", "at >", text) + text = re.sub("line [0-9]*", "line ", text) + text = text.replace(python_version, "") + text = text.replace(str(script_dir), "") + if " \n" in text: + text = text.replace("\n", "⏎\n") + return text + + with package("test_pck", package_files), TemporaryDirectory() as script_dir: + print(sys.exec_prefix) + script_dir = Path(script_dir) + + script_file = script_dir / "script.py" + script_file.write_text(script) + + normal_result = sp.run( + [sys.executable, str(script_file)], + cwd=str(script_dir), + env={**os.environ, "LAZY_IMPORTS_LITE_DISABLE": "True"}, + capture_output=True, + ) + + transformed_result = sp.run( + [sys.executable, str(script_file)], cwd=str(script_dir), capture_output=True + ) + + n_stdout = normalize_output(normal_result.stdout) + t_stdout = normalize_output(transformed_result.stdout) + n_stderr = normalize_output(normal_result.stderr) + t_stderr = normalize_output(transformed_result.stderr) + + assert normal_stdout == n_stdout + assert transformed_stdout == ( + "" if n_stdout == t_stdout else t_stdout + ) + + assert normal_stderr == n_stderr + assert transformed_stderr == ( + "" if n_stderr == t_stderr else t_stderr + ) + + +def test_loader(): + check_script( + { + "test_pck/__init__.py": """\ +from .mx import x +from .my import y + +def use_x(): + return x + +def use_y(): + return y +""", + "test_pck/mx.py": """\ +print('imported mx') +x=5 +""", + "test_pck/my.py": """\ +print('imported my') +y=5 +""", + }, + """\ +from test_pck import use_x, use_y +print("y:",use_y()) +print("x:",use_x()) +""", + transformed_stdout=snapshot( + """\ +imported my +y: 5 +imported mx +x: 5 +""" + ), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +imported mx +imported my +y: 5 +x: 5 +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_loader_keywords(): + check_script( + { + "test_pck/__init__.py": """\ +from .mx import x +print("imported init") + +def use_x(): + return x + +""", + "test_pck/mx.py": """\ +print('imported mx') +x=5 +""", + }, + """\ +from test_pck import use_x +print("x:",use_x()) +""", + transformed_stdout=snapshot( + """\ +imported init +imported mx +x: 5 +""" + ), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +imported mx +imported init +x: 5 +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_lazy_module_attr(): + check_script( + { + "test_pck/__init__.py": """\ +from .mx import x +from .my import y + +""", + "test_pck/mx.py": """\ +print('imported mx') +x=5 +""", + "test_pck/my.py": """\ +print('imported my') +y=5 +""", + }, + """\ +from test_pck import y +print("y:",y) + +from test_pck import x +print("x:",x) +""", + transformed_stdout=snapshot( + """\ +imported my +y: 5 +imported mx +x: 5 +""" + ), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +imported mx +imported my +y: 5 +x: 5 +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_lazy_module_content(): + check_script( + { + "test_pck/__init__.py": """\ +from .mx import x +from .my import y + +""", + "test_pck/mx.py": """\ +x=5 +""", + "test_pck/my.py": """\ +y=5 +""", + }, + """\ +import test_pck + +print(test_pck) +print(vars(test_pck).keys()) +""", + transformed_stdout=snapshot( + """\ +/test_pck/__init__.py'> +dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'y']) +""" + ), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +/test_pck/__init__.py'> +dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'my', 'y']) +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_lazy_module_content_import_from(): + check_script( + { + "test_pck/__init__.py": """\ +from .mx import x +print("inside",globals().keys()) + +try: + print("mx",mx) +except: + print("no mx") + +print("inside",globals().keys()) + +def later(): + print("later",globals().keys()) +""", + "test_pck/mx.py": """\ +x=5 +""", + }, + """\ +import test_pck + +print("outside",vars(test_pck).keys()) + +test_pck.later() +""", + transformed_stdout=snapshot( + """\ +inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x']) +mx /test_pck/mx.py'> +inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx']) +outside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx', 'later']) +later dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx', 'later']) +""" + ), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x']) +mx /test_pck/mx.py'> +inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x']) +outside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'later']) +later dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'later']) +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_import_module_with_error(): + check_script( + { + "test_pck/__init__.py": """\ +import test_pck.m +print(test_pck.m.v) +""", + "test_pck/m.py": """\ +raise ValueError() +""", + }, + """\ +try: + from test_pck import v +except BaseException as e: + while e: + print(f"{type(e).__name__}: {e}") + e=e.__cause__ if e.__suppress_context__ else e.__context__ +""", + transformed_stdout=snapshot( + """\ +LazyImportError: Deferred importing of module 'test_pck.m' caused an error⏎ +ValueError: ⏎ +""" + ), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +ValueError: ⏎ +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_load_chain_of_modules_with_error(): + check_script( + { + "test_pck/__init__.py": """\ +from .m import v +print(v) +""", + "test_pck/m/__init__.py": """\ +from .x import v +print(v) +""", + "test_pck/m/x.py": """\ +from .y import v +print(v) +""", + "test_pck/m/y.py": """\ +raise ValueError() +""", + }, + """\ +try: + from test_pck import v +except BaseException as e: + while e: + print(f"{type(e).__name__}: {e}") + e=e.__cause__ if e.__suppress_context__ else e.__context__ +""", + transformed_stdout=snapshot( + """\ +LazyImportError: Deferred importing of module '.y' in 'test_pck.m' caused an error⏎ +ValueError: ⏎ +""" + ), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +ValueError: ⏎ +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_lazy_module_import_from_empty_init(): + check_script( + { + "test_pck/__init__.py": """\ +""", + "test_pck/ma.py": """\ +a=5 +""", + "test_pck/mb.py": """\ +from test_pck import ma +a=ma.a +""", + }, + """\ +from test_pck import mb + +print(mb.a) +""", + transformed_stdout=snapshot(""), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +5 +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_lazy_module_setattr(): + check_script( + { + "test_pck/__init__.py": """\ +from .ma import b + +def foo(): + print(b()) + +""", + "test_pck/ma.py": """\ +def b(): + return 5 +""", + }, + """\ +from test_pck import foo +import test_pck + +foo() +test_pck.b=lambda:6 +foo() + +""", + transformed_stdout=snapshot(""), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ +5 +6 +""" + ), + normal_stderr=snapshot(""), + ) + + +def test_loader_is_used(): + check_script( + { + "test_pck/__init__.py": """\ + +def foo(): + print("foo") + +""", + }, + """\ +import test_pck + +print(type(test_pck.__spec__.loader)) + +""", + transformed_stdout=snapshot( + """\ + +""" + ), + transformed_stderr=snapshot(""), + normal_stdout=snapshot( + """\ + +""" + ), + normal_stderr=snapshot(""), + ) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index 03a09b2..d78b1fc 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -1,559 +1,560 @@ -import ast -import re -import subprocess as sp -import sys -from pathlib import Path -from tempfile import TemporaryDirectory - -from inline_snapshot import snapshot -from lazy_imports_lite._transformer import TransformModuleImports -from lazy_imports_lite._utils import unparse - - -def check_transform(code, transformed_code, stdout, stderr): - content = { - "bar/__init__.py": """ -foo='bar.foo' -baz='bar.baz' -""", - "bar/foo.py": """ -a='bar.foo.a' -b='bar.foo.b' -c='bar.foo.c' -""", - "x.py": "y='x.y'", - "z.py": "", - } - - def test(dir: Path, code: str): - for path, text in content.items(): - path = dir / path - path.parent.mkdir(exist_ok=True, parents=True) - path.write_text(text) - (dir / "script.py").write_text(code) - result = sp.run([sys.executable, "script.py"], cwd=dir, capture_output=True) - - def normalize_output(output: bytes): - text = output.decode() - text = text.replace(str(dir), "") - text = re.sub("at 0x[0-9a-f]*>", "at >", text) - return text - - assert stderr == normalize_output(result.stderr) - assert stdout == normalize_output(result.stdout) - - with TemporaryDirectory() as d: - d = Path(d) - - test(d / "original", code) - - transformer = TransformModuleImports() - tree = ast.parse(code) - new_tree = ast.fix_missing_locations(transformer.visit(tree)) - new_code = unparse(new_tree) - new_code = new_code.replace("lambda :", "lambda:") - - if sys.version_info >= (3, 9): - # unparse does not produce the same code for 3.8 - assert new_code == transformed_code - - test(d / "transformed", new_code) - - -def test_transform_module_imports(): - check_transform( - """ -from bar.foo import a,b,c as d -import bar as baz -import bar.foo as f -import bar -if True: - from x import y - import z - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') -b = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'b') -d = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'c') -baz = __lazy_imports_lite__.ImportAs('bar') -f = __lazy_imports_lite__.ImportAs('bar.foo') -bar = __lazy_imports_lite__.Import('bar') -if True: - from x import y - import z\ -""" - ), - snapshot(""), - snapshot(""), - ) - - -def test_import_from(): - check_transform( - """ -from bar.foo import a - -print(a) - - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') -print(a.v)\ -""" - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_function_lazy(): - check_transform( - """ -from bar.foo import a - -def f(): - return a - -print(f()) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') - -def f(): - return a.v -print(f())\ -""" - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_function_override(): - check_transform( - """ -from bar.foo import a - -def f(): - a=5 - return a -print(f()) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') - -def f(): - a = 5 - return a -print(f())\ -""" - ), - snapshot( - """\ -5 -""" - ), - snapshot(""), - ) - - -def test_function_override_global(): - check_transform( - """ -from bar.foo import a - -def f(): - global a - a=5 - return a -print(f()) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') - -def f(): - global a - a.v = 5 - return a.v -print(f())\ -""" - ), - snapshot( - """\ -5 -""" - ), - snapshot(""), - ) - - -def test_function_arg(): - check_transform( - """ -from bar.foo import a - -def f(a=5): - return a -print(f()) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') - -def f(a=5): - return a -print(f())\ -""" - ), - snapshot( - """\ -5 -""" - ), - snapshot(""), - ) - - -def test_function_default_arg(): - check_transform( - """ -from bar.foo import a - -def f(b=a): - return b -print(f()) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') - -def f(b=a.v): - return b -print(f())\ -""" - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_globals(): - check_transform( - """ -from bar.foo import a - -for e in sorted(globals().items()): - if e[0]!="__file__": - print(*e) - - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') -for e in sorted(globals().items()): - if e[0] != '__file__': - print(*e)\ -""" - ), - snapshot( - """\ -__annotations__ {} -__builtins__ -__cached__ None -__doc__ None -__loader__ <_frozen_importlib_external.SourceFileLoader object at > -__name__ __main__ -__package__ None -__spec__ None -a bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_import(): - check_transform( - """ -import bar -print(bar.foo) -import bar.foo - -print(bar.foo.a) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -bar = __lazy_imports_lite__.Import('bar') -print(bar.v.foo) -bar = __lazy_imports_lite__.Import('bar.foo') -print(bar.v.foo.a)\ -""" - ), - snapshot( - """\ -bar.foo -bar.foo.a -""" - ), - snapshot(""), - ) - - check_transform( - """ -import bar.foo -import bar - -print(bar.foo.a) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -bar = __lazy_imports_lite__.Import('bar.foo') -bar = __lazy_imports_lite__.Import('bar') -print(bar.v.foo.a)\ -""" - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_import_as(): - check_transform( - """ -import bar.foo as f - -print(f.a) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -f = __lazy_imports_lite__.ImportAs('bar.foo') -print(f.v.a)\ -""" - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_lambda(): - check_transform( - """ -import bar.foo as f - -print((lambda:f.a)()) - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -f = __lazy_imports_lite__.ImportAs('bar.foo') -print((lambda: f.v.a)())\ -""" - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_async_function(): - check_transform( - """ -import bar.foo as f - -async def foo(): - print(f.a) - -import asyncio - -asyncio.run(foo()) - - """, - snapshot( - """\ -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -f = __lazy_imports_lite__.ImportAs('bar.foo') - -async def foo(): - print(f.v.a) -asyncio = __lazy_imports_lite__.Import('asyncio') -asyncio.v.run(foo())\ -""" - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_import_from_future(): - check_transform( - """ -"doc string" -from __future__ import annotations -import bar.foo as f - -print(f.a) - - """, - snapshot( - '''\ -"""doc string""" -from __future__ import annotations -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -f = __lazy_imports_lite__.ImportAs('bar.foo') -print(f.v.a)\ -''' - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_transform_default_argument(): - check_transform( - """ -"doc string" -from __future__ import annotations -import bar.foo as f - -def foo(a=lambda:f.a): - print(a()) -foo() - - """, - snapshot( - '''\ -"""doc string""" -from __future__ import annotations -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -f = __lazy_imports_lite__.ImportAs('bar.foo') - -def foo(a=lambda: f.v.a): - print(a()) -foo()\ -''' - ), - snapshot( - """\ -bar.foo.a -""" - ), - snapshot(""), - ) - - -def test_transform_decorators(): - check_transform( - """ -"doc string" -from __future__ import annotations -import bar.foo as f - -def deco(thing): - def w(f): - print("in w",thing.a) - return f - return w - - -@deco(f) -def foo(): - print("in f",f.a) - -print("call") - -foo() - - """, - snapshot( - '''\ -"""doc string""" -from __future__ import annotations -import lazy_imports_lite._hooks as __lazy_imports_lite__ -globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) -f = __lazy_imports_lite__.ImportAs('bar.foo') - -def deco(thing): - - def w(f): - print('in w', thing.a) - return f - return w - -@deco(f.v) -def foo(): - print('in f', f.v.a) -print('call') -foo()\ -''' - ), - snapshot( - """\ -in w bar.foo.a -call -in f bar.foo.a -""" - ), - snapshot(""), - ) +import ast +import re +import subprocess as sp +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +from inline_snapshot import snapshot +from lazy_imports_lite._transformer import TransformModuleImports +from lazy_imports_lite._utils import unparse + + +def check_transform(code, transformed_code, stdout, stderr): + content = { + "bar/__init__.py": """ +foo='bar.foo' +baz='bar.baz' +""", + "bar/foo.py": """ +a='bar.foo.a' +b='bar.foo.b' +c='bar.foo.c' +""", + "x.py": "y='x.y'", + "z.py": "", + } + + def test(dir: Path, code: str): + for path, text in content.items(): + path = dir / path + path.parent.mkdir(exist_ok=True, parents=True) + path.write_text(text) + (dir / "script.py").write_text(code) + result = sp.run([sys.executable, "script.py"], cwd=dir, capture_output=True) + + def normalize_output(output: bytes): + text = output.decode() + text = text.replace("\r\n", "\n") + text = text.replace(str(dir), "") + text = re.sub("at 0x[0-9a-fA-F]*>", "at >", text) + return text + + assert stderr == normalize_output(result.stderr) + assert stdout == normalize_output(result.stdout) + + with TemporaryDirectory() as d: + d = Path(d) + + test(d / "original", code) + + transformer = TransformModuleImports() + tree = ast.parse(code) + new_tree = ast.fix_missing_locations(transformer.visit(tree)) + new_code = unparse(new_tree) + new_code = new_code.replace("lambda :", "lambda:") + + if sys.version_info >= (3, 9): + # unparse does not produce the same code for 3.8 + assert new_code == transformed_code + + test(d / "transformed", new_code) + + +def test_transform_module_imports(): + check_transform( + """ +from bar.foo import a,b,c as d +import bar as baz +import bar.foo as f +import bar +if True: + from x import y + import z + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') +b = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'b') +d = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'c') +baz = __lazy_imports_lite__.ImportAs('bar') +f = __lazy_imports_lite__.ImportAs('bar.foo') +bar = __lazy_imports_lite__.Import('bar') +if True: + from x import y + import z\ +""" + ), + snapshot(""), + snapshot(""), + ) + + +def test_import_from(): + check_transform( + """ +from bar.foo import a + +print(a) + + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') +print(a.v)\ +""" + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_function_lazy(): + check_transform( + """ +from bar.foo import a + +def f(): + return a + +print(f()) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') + +def f(): + return a.v +print(f())\ +""" + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_function_override(): + check_transform( + """ +from bar.foo import a + +def f(): + a=5 + return a +print(f()) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') + +def f(): + a = 5 + return a +print(f())\ +""" + ), + snapshot( + """\ +5 +""" + ), + snapshot(""), + ) + + +def test_function_override_global(): + check_transform( + """ +from bar.foo import a + +def f(): + global a + a=5 + return a +print(f()) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') + +def f(): + global a + a.v = 5 + return a.v +print(f())\ +""" + ), + snapshot( + """\ +5 +""" + ), + snapshot(""), + ) + + +def test_function_arg(): + check_transform( + """ +from bar.foo import a + +def f(a=5): + return a +print(f()) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') + +def f(a=5): + return a +print(f())\ +""" + ), + snapshot( + """\ +5 +""" + ), + snapshot(""), + ) + + +def test_function_default_arg(): + check_transform( + """ +from bar.foo import a + +def f(b=a): + return b +print(f()) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') + +def f(b=a.v): + return b +print(f())\ +""" + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_globals(): + check_transform( + """ +from bar.foo import a + +for e in sorted(globals().items()): + if e[0]!="__file__": + print(*e) + + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +a = __lazy_imports_lite__.ImportFrom(__package__, 'bar.foo', 'a') +for e in sorted(globals().items()): + if e[0] != '__file__': + print(*e)\ +""" + ), + snapshot( + """\ +__annotations__ {} +__builtins__ +__cached__ None +__doc__ None +__loader__ <_frozen_importlib_external.SourceFileLoader object at > +__name__ __main__ +__package__ None +__spec__ None +a bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_import(): + check_transform( + """ +import bar +print(bar.foo) +import bar.foo + +print(bar.foo.a) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +bar = __lazy_imports_lite__.Import('bar') +print(bar.v.foo) +bar = __lazy_imports_lite__.Import('bar.foo') +print(bar.v.foo.a)\ +""" + ), + snapshot( + """\ +bar.foo +bar.foo.a +""" + ), + snapshot(""), + ) + + check_transform( + """ +import bar.foo +import bar + +print(bar.foo.a) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +bar = __lazy_imports_lite__.Import('bar.foo') +bar = __lazy_imports_lite__.Import('bar') +print(bar.v.foo.a)\ +""" + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_import_as(): + check_transform( + """ +import bar.foo as f + +print(f.a) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +f = __lazy_imports_lite__.ImportAs('bar.foo') +print(f.v.a)\ +""" + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_lambda(): + check_transform( + """ +import bar.foo as f + +print((lambda:f.a)()) + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +f = __lazy_imports_lite__.ImportAs('bar.foo') +print((lambda: f.v.a)())\ +""" + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_async_function(): + check_transform( + """ +import bar.foo as f + +async def foo(): + print(f.a) + +import asyncio + +asyncio.run(foo()) + + """, + snapshot( + """\ +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +f = __lazy_imports_lite__.ImportAs('bar.foo') + +async def foo(): + print(f.v.a) +asyncio = __lazy_imports_lite__.Import('asyncio') +asyncio.v.run(foo())\ +""" + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_import_from_future(): + check_transform( + """ +"doc string" +from __future__ import annotations +import bar.foo as f + +print(f.a) + + """, + snapshot( + '''\ +"""doc string""" +from __future__ import annotations +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +f = __lazy_imports_lite__.ImportAs('bar.foo') +print(f.v.a)\ +''' + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_transform_default_argument(): + check_transform( + """ +"doc string" +from __future__ import annotations +import bar.foo as f + +def foo(a=lambda:f.a): + print(a()) +foo() + + """, + snapshot( + '''\ +"""doc string""" +from __future__ import annotations +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +f = __lazy_imports_lite__.ImportAs('bar.foo') + +def foo(a=lambda: f.v.a): + print(a()) +foo()\ +''' + ), + snapshot( + """\ +bar.foo.a +""" + ), + snapshot(""), + ) + + +def test_transform_decorators(): + check_transform( + """ +"doc string" +from __future__ import annotations +import bar.foo as f + +def deco(thing): + def w(f): + print("in w",thing.a) + return f + return w + + +@deco(f) +def foo(): + print("in f",f.a) + +print("call") + +foo() + + """, + snapshot( + '''\ +"""doc string""" +from __future__ import annotations +import lazy_imports_lite._hooks as __lazy_imports_lite__ +globals = __lazy_imports_lite__.make_globals(lambda g=globals: g()) +f = __lazy_imports_lite__.ImportAs('bar.foo') + +def deco(thing): + + def w(f): + print('in w', thing.a) + return f + return w + +@deco(f.v) +def foo(): + print('in f', f.v.a) +print('call') +foo()\ +''' + ), + snapshot( + """\ +in w bar.foo.a +call +in f bar.foo.a +""" + ), + snapshot(""), + )