Skip to content

Commit

Permalink
Merge pull request #15 from DavidCEllis/improve_coverage
Browse files Browse the repository at this point in the history
Improve coverage, simplify readme, enhance docs
  • Loading branch information
DavidCEllis authored Feb 15, 2024
2 parents 6b7b896 + 15f38e6 commit b1bde87
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 155 deletions.
143 changes: 0 additions & 143 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,146 +238,3 @@ object and the import mechanism is only invoked once.
(The actual `__getattr__` function uses a dictionary lookup and delegates importing
to the FromImport class. Names are all dynamic and imports are done through
the `__import__` function.)

## Examining the importer state ##

The function `get_importer_state` is provided to show the state
of the lazy importer, showing which imports have run and which
are still deferred.

For example:

```python
from ducktools.lazyimporter import (
LazyImporter,
ModuleImport,
FromImport,
MultiFromImport,
get_importer_state,
)

# Setup attributes but don't perform any imports
laz = LazyImporter([
MultiFromImport(
"collections", [("namedtuple", "nt"), "OrderedDict"]
),
FromImport("pprint", "pprint"),
FromImport("functools", "partial"),
ModuleImport("inspect"),
])

print("Possible attributes:")
laz.pprint(dir(laz))
print()

print("pprint imported:")
laz.pprint(get_importer_state(laz))
print()

_ = laz.nt
print("Collections elements imported:")
laz.pprint(get_importer_state(laz))
print()

_ = laz.partial
print("Functools elements imported:")
laz.pprint(get_importer_state(laz))
print()
```

Output:
```
Possible attributes:
['OrderedDict', 'inspect', 'nt', 'partial', 'pprint']
pprint imported:
{'imported_attributes': {'pprint': <function pprint at ...>},
'lazy_attributes': ['OrderedDict', 'inspect', 'nt', 'partial']}
Collections elements imported:
{'imported_attributes': {'OrderedDict': <class 'collections.OrderedDict'>,
'nt': <function namedtuple at ...>,
'pprint': <function pprint at ...>},
'lazy_attributes': ['inspect', 'partial']}
Functools elements imported:
{'imported_attributes': {'OrderedDict': <class 'collections.OrderedDict'>,
'nt': <function namedtuple at ...>,
'partial': <class 'functools.partial'>,
'pprint': <function pprint at ...},
'lazy_attributes': ['inspect']}
```

## Extending ##

Perhaps these cases don't cover the import type you're looking for and you
need an extension. These can be made by subclassing `ImportBase`.

Subclasses of `ImportBase` require 3 things:

`module_name` attribute must be the name of the default module to be imported.

`asname` or `asnames` must be either the identifier or a list of identifiers
(respectively) to use to store attributes. This can be an attribute or a property.

`do_import` must be a method that takes 2 arguments `(self, globs=None)`, performs
the import and returns a dictionary of the form `{asname: <object>, ...}` for all of
the names defined in `asname`/`asnames`.

For example say you want an importer that can do this kind of import:

```python
import sys
if sys.version_info >= (3, 12):
import tomllib
else:
import tomli as tomllib
```

You could write something like this:

```python
# NOTE: This is a simplified example using importlib.import_module
import importlib
from ducktools.lazyimporter import ImportBase, LazyImporter


class IfElseImporter(ImportBase):
def __init__(self, condition, module_name, else_module_name, asname):
self.condition = condition
self.module_name = module_name
self.else_module_name = else_module_name
self.asname = asname

if not self.asname.isidentifier():
raise ValueError(f"{self.asname} is not a valid python identifier.")

def do_import(self, globs=None):
if globs is not None:
package = globs.get('__name__')
else:
package = None

if self.condition:
mod = importlib.import_module(self.module_name, package)
else:
mod = importlib.import_module(self.else_module_name, package)

return {self.asname: mod}

```

And then use it with:

```python
import sys

laz = LazyImporter([
IfElseImporter(
condition=sys.version_info >= (3, 12),
module_name="tomllib",
else_module_name="tomli",
asname="tomllib",
)
])
```
1 change: 1 addition & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@
```{eval-rst}
.. autofunction:: ducktools.lazyimporter::get_importer_state
.. autofunction:: ducktools.lazyimporter::get_module_funcs
.. autofunction:: ducktools.lazyimporter::force_imports
```
73 changes: 73 additions & 0 deletions docs/extending.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Extending by subclassing ImportBase #

Perhaps the included Import classes don't cover the import type you're looking
for and you need an extension. These can be made by subclassing `ImportBase`.

Subclasses of `ImportBase` require 3 things:

`module_name` attribute must be the name of the default module to be imported.

`asname` or `asnames` must be either the identifier or a list of identifiers
(respectively) to use to store attributes. This can be an attribute or a property.

`do_import` must be a method that takes 2 arguments `(self, globs=None)`, performs
the import and returns a dictionary of the form `{asname: <object>, ...}` for all of
the names defined in `asname`/`asnames`.

For example say you want an importer that can do this kind of import:

```python
import sys
if sys.version_info >= (3, 12):
import tomllib
else:
import tomli as tomllib
```

You could write something like this:

```python
# NOTE: This is a simplified example using importlib.import_module
import importlib
from ducktools.lazyimporter import ImportBase, LazyImporter


class IfElseImporter(ImportBase):
def __init__(self, condition, module_name, else_module_name, asname):
self.condition = condition
self.module_name = module_name
self.else_module_name = else_module_name
self.asname = asname

if not self.asname.isidentifier():
raise ValueError(f"{self.asname} is not a valid python identifier.")

def do_import(self, globs=None):
if globs is not None:
package = globs.get('__name__')
else:
package = None

if self.condition:
mod = importlib.import_module(self.module_name, package)
else:
mod = importlib.import_module(self.else_module_name, package)

return {self.asname: mod}

```

And then use it with:

```python
import sys

laz = LazyImporter([
IfElseImporter(
condition=sys.version_info >= (3, 12),
module_name="tomllib",
else_module_name="tomli",
asname="tomllib",
)
])
```
90 changes: 90 additions & 0 deletions docs/import_classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## The import classes ##

In all of these instances `modules` is intended as the first argument
to `LazyImporter` and all attributes would be accessed from the
`LazyImporter` instance and not in the global namespace.

eg:
```python
from ducktools.lazyimporter import LazyImporter, ModuleImport

modules = [ModuleImport("functools")]
laz = LazyImporter(modules)
laz.functools # provides access to the module "functools"
```

### ModuleImport ###

`ModuleImport` is used for your basic module style imports.

```python
from ducktools.lazyimporter import ModuleImport

modules = [
ModuleImport("module"),
ModuleImport("other_module", "other_name"),
ModuleImport("base_module.submodule", asname="short_name"),
]
```

is equivalent to

```
import module
import other_module as other_name
import base_module.submodule as short_name
```

when provided to a LazyImporter.

### FromImport and MultiFromImport ###

`FromImport` is used for standard 'from' imports, `MultiFromImport` for importing
multiple items from the same module. By using a `MultiFromImport`, when the first
attribute is accessed, all will be assigned on the LazyImporter.

```python
from ducktools.lazyimporter import FromImport, MultiFromImport

modules = [
FromImport("dataclasses", "dataclass"),
FromImport("functools", "partial", "partfunc"),
MultiFromImport("collections", ["namedtuple", ("defaultdict", "dd")]),
]
```

is equivalent to

```python
from dataclasses import dataclass
from functools import partial as partfunc
from collections import namedtuple, defaultdict as dd
```

when provided to a LazyImporter.

### TryExceptImport ###

`TryExceptImport` is used for compatibility where a module may not be available
and so a fallback module providing the same functionality should be used. For
example when a newer version of python has a stdlib module that has replaced
a third party module that was used previously.

```python
from ducktools.lazyimporter import TryExceptImport

modules = [
TryExceptImport("tomllib", "tomli", "tomllib"),
]
```

is equivalent to

```python
try:
import tomllib as tomllib
except ImportError:
import tomli as tomllib
```

when provided to a LazyImporter.
68 changes: 68 additions & 0 deletions docs/importer_state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Importer internal state #

The function `get_importer_state` is provided to show the state
of the lazy importer, showing which imports have run and which
are still deferred.

For example:

```python
from ducktools.lazyimporter import (
LazyImporter,
ModuleImport,
FromImport,
MultiFromImport,
get_importer_state,
)

# Setup attributes but don't perform any imports
laz = LazyImporter([
MultiFromImport(
"collections", [("namedtuple", "nt"), "OrderedDict"]
),
FromImport("pprint", "pprint"),
FromImport("functools", "partial"),
ModuleImport("inspect"),
])

print("Possible attributes:")
laz.pprint(dir(laz))
print()

print("pprint imported:")
laz.pprint(get_importer_state(laz))
print()

_ = laz.nt
print("Collections elements imported:")
laz.pprint(get_importer_state(laz))
print()

_ = laz.partial
print("Functools elements imported:")
laz.pprint(get_importer_state(laz))
print()
```

Output:
```
Possible attributes:
['OrderedDict', 'inspect', 'nt', 'partial', 'pprint']
pprint imported:
{'imported_attributes': {'pprint': <function pprint at ...>},
'lazy_attributes': ['OrderedDict', 'inspect', 'nt', 'partial']}
Collections elements imported:
{'imported_attributes': {'OrderedDict': <class 'collections.OrderedDict'>,
'nt': <function namedtuple at ...>,
'pprint': <function pprint at ...>},
'lazy_attributes': ['inspect', 'partial']}
Functools elements imported:
{'imported_attributes': {'OrderedDict': <class 'collections.OrderedDict'>,
'nt': <function namedtuple at ...>,
'partial': <class 'functools.partial'>,
'pprint': <function pprint at ...},
'lazy_attributes': ['inspect']}
```
Loading

0 comments on commit b1bde87

Please sign in to comment.