Skip to content

Commit

Permalink
Merge pull request #17 from DavidCEllis/eager_config
Browse files Browse the repository at this point in the history
Add environment variables/globals/arguments to enable/disable eager process/export
  • Loading branch information
DavidCEllis authored Jul 3, 2024
2 parents 8d0e073 + f73dd0a commit f34d6f6
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 7 deletions.
18 changes: 18 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@ laz = LazyImporter(
__getattr__, __dir__ = get_module_funcs(laz, __name__)
```

## Environment Variables ##

There are two environment variables that can be used to modify the behaviour for
debugging purposes.

If `DUCKTOOLS_EAGER_PROCESS` is set to any value other than 'False' (case insensitive)
the initial processing of imports will be done on instance creation.

Similarly if `DUCKTOOLS_EAGER_IMPORT` is set to any value other than 'False' all imports
will be performed eagerly on instance creation (this will also force processing on import).

If they are unset this is equivalent to being set to False.

If there is a lazy importer where it is known this will not work
(for instance if it is managing a circular dependency issue)
these can be overridden for an importer by passing values to `eager_process` and/or
`eager_import` arguments to the `LazyImporter` constructer as keyword arguments.

## How does it work ##

The following lazy importer:
Expand Down
32 changes: 26 additions & 6 deletions src/ducktools/lazyimporter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
when first accessed.
"""
import abc
import os
import sys

__version__ = "v0.5.0"
__version__ = "v0.5.1"
__all__ = [
"LazyImporter",
"ModuleImport",
Expand All @@ -41,6 +42,9 @@
"force_imports",
]

EAGER_PROCESS = os.environ.get("DUCKTOOLS_EAGER_PROCESS", "false").lower() != "false"
EAGER_IMPORT = os.environ.get("DUCKTOOLS_EAGER_IMPORT", "false").lower() != "false"


class ImportBase(metaclass=abc.ABCMeta):
module_name: str
Expand Down Expand Up @@ -593,6 +597,8 @@ def group_importers(inst):
"""
importers = {}

reserved_names = vars(type(inst)).keys() | vars(inst).keys()

for imp in inst._imports: # noqa
if getattr(imp, "import_level", 0) > 0 and inst._globals is None: # noqa
raise ValueError(
Expand All @@ -601,13 +607,17 @@ def group_importers(inst):

# import x, import x.y as z OR from x import y
if asname := getattr(imp, "asname", None):
if asname in reserved_names:
raise ValueError(f"{asname!r} clashes with a LazyImporter internal name.")
if asname in importers:
raise ValueError(f"{asname!r} used for multiple imports.")
importers[asname] = imp

# from x import y, z ...
elif asnames := getattr(imp, "asnames", None):
for asname in asnames:
if asname in reserved_names:
raise ValueError(f"{asname!r} clashes with a LazyImporter internal name.")
if asname in importers:
raise ValueError(f"{asname!r} used for multiple imports.")
importers[asname] = imp
Expand All @@ -622,31 +632,41 @@ def group_importers(inst):

class LazyImporter:
_imports: "list[ImportBase]"
_globals: dict
_globals: "dict | None"

_importers = _ImporterGrouper()

def __init__(self, imports, *, globs=None, eager_process=False):
def __init__(self, imports, *, globs=None, eager_process=None, eager_import=None):
"""
Create a LazyImporter to import modules and objects when they are accessed
on this importer object.
globals() must be provided to the importer if relative imports are used.
eager_process and eager_import are included to help with debugging, there are
global variables (that will be pulled from environment variables) that can be
used to force all processing or imports to be done eagerly. These can be
overridden by providing arguments here.
:param imports: list of imports
:type imports: list[ImportBase]
:param globs: globals object for relative imports
:type globs: dict[str, typing.Any]
:param eager_process: filter and check the imports eagerly
:type eager_process: bool
:type eager_process: Optional[bool]
:param eager_import: perform all imports eagerly
:type eager_import: Optional[bool]
"""
# Keep original imports for __repr__
self._imports = imports
self._globals = globs

if eager_process:
if eager_process or (EAGER_PROCESS and eager_process is None):
_ = self._importers

if eager_import or (EAGER_IMPORT and eager_import is None):
force_imports(self)

def __getattr__(self, name):
# This performs the imports associated with the name of the attribute
# and sets the result to that name.
Expand Down Expand Up @@ -764,4 +784,4 @@ def force_imports(importer):
:type importer: LazyImporter
"""
for attrib_name in dir(importer):
_ = getattr(importer, attrib_name)
getattr(importer, attrib_name)
6 changes: 5 additions & 1 deletion src/ducktools/lazyimporter/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ __all__: list[str] = [
"force_imports",
]

EAGER_PROCESS: bool
EAGER_IMPORT: bool

class ImportBase(metaclass=abc.ABCMeta):
module_name: str

Expand Down Expand Up @@ -148,7 +151,8 @@ class LazyImporter:
imports: list[ImportBase],
*,
globs: dict[str, Any] | None = ...,
eager_process: bool = ...,
eager_process: bool | None = ...,
eager_import: bool | None = ...,
) -> None: ...
def __getattr__(self, name: str) -> types.ModuleType | Any: ...
def __dir__(self): ...
Expand Down
17 changes: 17 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import pytest
from pathlib import Path

import ducktools.lazyimporter as lazyimporter


@pytest.fixture(scope="module", autouse=True)
def example_modules():
Expand All @@ -14,3 +16,18 @@ def example_modules():
yield
finally:
sys.path.remove(str(base_path))


@pytest.fixture(scope="session", autouse=True)
def false_defaults():
# Tests ignore the environment variable that can set these to True
process_state = lazyimporter.EAGER_PROCESS
import_state = lazyimporter.EAGER_IMPORT

lazyimporter.EAGER_PROCESS = False
lazyimporter.EAGER_IMPORT = False

yield

lazyimporter.EAGER_PROCESS = process_state
lazyimporter.EAGER_IMPORT = import_state
91 changes: 91 additions & 0 deletions tests/test_basic_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import pytest

import ducktools.lazyimporter as lazyimporter

from ducktools.lazyimporter import (
LazyImporter,
ModuleImport,
Expand All @@ -10,6 +12,7 @@
TryExceptImport,
TryExceptFromImport,
TryFallbackImport,
get_importer_state,
)


Expand Down Expand Up @@ -188,3 +191,91 @@ def test_submod_relative_import(self):
from example_modules.ex_othermod import laz

assert laz.submod_name == "ex_submod"


class TestEager:
def test_eager_process(self):
laz = LazyImporter([ModuleImport("functools")], eager_process=False)

assert "_importers" not in vars(laz)

laz = LazyImporter([ModuleImport("functools")], eager_process=True)

assert "_importers" in vars(laz)

def test_eager_import(self):
laz = LazyImporter([ModuleImport("functools")], eager_import=False)

assert "functools" not in vars(laz)

laz = LazyImporter([ModuleImport("functools")], eager_import=True)

assert "functools" in vars(laz)

def test_eager_process_glob(self):
initial_state = lazyimporter.EAGER_PROCESS

lazyimporter.EAGER_PROCESS = False

# EAGER_PROCESS = False and no value - should lazily process
laz = LazyImporter([ModuleImport("functools")])
assert "_importers" not in vars(laz)

# EAGER_PROCESS = False and eager_process = False - should lazily process
laz = LazyImporter([ModuleImport("functools")], eager_process=False)
assert "_importers" not in vars(laz)

# EAGER_PROCESS = False and eager_process = True - should eagerly process
laz = LazyImporter([ModuleImport("functools")], eager_process=True)
assert "_importers" in vars(laz)

lazyimporter.EAGER_PROCESS = True

# EAGER_PROCESS = True and no value - should eagerly process
laz = LazyImporter([ModuleImport("functools")])
assert "_importers" in vars(laz)

# EAGER_PROCESS = True and eager_process = False - should lazily process
laz = LazyImporter([ModuleImport("functools")], eager_process=False)
assert "_importers" not in vars(laz)

# EAGER_PROCESS = True and eager_process = True - should eagerly process
laz = LazyImporter([ModuleImport("functools")], eager_process=True)
assert "_importers" in vars(laz)

# Restore state
lazyimporter.EAGER_PROCESS = initial_state

def test_eager_import_glob(self):
initial_state = lazyimporter.EAGER_IMPORT

lazyimporter.EAGER_IMPORT = False

# EAGER_IMPORT = False and no value - should lazily import
laz = LazyImporter([ModuleImport("functools")])
assert "functools" not in vars(laz)

# EAGER_IMPORT = False and eager_import = False - should lazily import
laz = LazyImporter([ModuleImport("functools")], eager_import=False)
assert "functools" not in vars(laz)

# EAGER_IMPORT = False and eager_import = True - should eagerly import
laz = LazyImporter([ModuleImport("functools")], eager_import=True)
assert "functools" in vars(laz)

lazyimporter.EAGER_IMPORT = True

# EAGER_IMPORT = True and no value - should eagerly import
laz = LazyImporter([ModuleImport("functools")])
assert "functools" in vars(laz)

# EAGER_IMPORT = True and eager_import = False - should lazily import
laz = LazyImporter([ModuleImport("functools")], eager_import=False)
assert "functools" not in vars(laz)

# EAGER_IMPORT = True and eager_import = True - should eagerly import
laz = LazyImporter([ModuleImport("functools")], eager_import=True)
assert "functools" in vars(laz)

# Restore state
lazyimporter.EAGER_IMPORT = initial_state
10 changes: 10 additions & 0 deletions tests/test_import_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ def test_mixedimport_clash(self):

assert e.match("'matching_mod_name' used for multiple imports.")

def test_reserved_name(self):
with pytest.raises(ValueError) as e:
laz = LazyImporter(
[
FromImport("mod1", "objname", "_importers"),
],
eager_process=True,
)

assert e.match("'_importers' clashes with a LazyImporter internal name.")

class TestNoGlobals:
def test_relative_module_noglobals(self):
Expand Down

0 comments on commit f34d6f6

Please sign in to comment.