Loader implementation
For normal imports, we don't need a global j
object, but for ease of access and the shell, we provide a loader which loads all sub-modules under jumpscale
under a single object, and imports any module once it's accessed, see the following different examples:
Using direct imports:
from jumpscale.sals import fs
print(fs.exists("/path/to/file"))
Using the loader:
from jumpscale.loader import j
print(j.sals.fs.exists("/path/to/file"))
When using the loader, the module jumpscale.sals.fs
won't be imported until accessed.
Code structure
the idea is with hierarchy like this
project1/
/rootnamespace (jumpscale)
/subnamespace1
... pkg1
... pkg2
/subnamespace2
... pkg1
... pkg2
project2/
/rootnamespace (jumpscale)
/subnamespace1
... pkg1
... pkg2
/subnamespace2
... pkg1
... pkg2
- we get all the paths of the
root namespace
- we get all the sub-namespaces
- we get all the inner packages and import any of them (lazily).
real example
js-ng
├── jumpscale <- root namespace
│ ├── clients <- subnamespace where people can register on
│ │ ├── base.py
│ │ ├── github <- package in subnamespace
│ │ │ ├── github.py
│ │ │ └── __init__.py
│ │ └── gogs
│ │ ├── gogs.py
│ │ └── __init__.py
│ ├── core
│ │ ├── config.py
│ │ ├── exceptions.py
│ │ └── logging.py
│ ├── data
│ │ ├── idgenerator
│ │ │ ├── idgenerator.py
│ │ │ └── __init__.py
│ │ └── serializers
│ │ ├── __init__.py
│ │ └── serializers.py
│ ├── loader.py
│ ├── sals
│ │ └── fs
│ │ ├── fs.py
│ │ └── __init__.py
│ └── tools
│ └── keygen
│ ├── __init__.py
│ └── keygen.py
├── README.md
└── tests
└── test_loads_j.py
js-sdk
├── jumpscale
│ ├── clients
│ │ └── gitlab
│ │ ├── gitlab.py
│ │ └── __init__.py
│ ├── sals
│ │ └── zos
│ │ ├── __init__.py
│ │ └── zos.py
│ └── tools
├── README.md
└── tests
└── test_success.py
How is it implemented?
The loader is implemented at loader.py
.
Each namespace level is exposed as a container type/class with properties that map to its sub-modules.
All this classes are generated in runtime using type
function.
The entry point is the expose_all
function, which takes a root module object, and a container class, and exposes this module's sub-modules under this container.
def expose_all(root_module: types.ModuleType, container_type: type):
"""
exposes all sub-modules and namespaces to be available under given container type (class)
Args:
root_module (types.ModuleType): module
ns_type (type): namepace type (class)
"""
for path in root_module.__path__:
for name in os.listdir(path):
if not os.path.isdir(os.path.join(path, name)) or name == "__pycache__":
continue
lazy_import_property = get_lazy_import_property(name, root_module, container_type)
setattr(container_type, name, lazy_import_property)
And it's used with a type called J
:
class J:
@property
def logger(self):
return self.core.logging
@property
def application(self):
return self.core.application
@property
def config(self):
return self.core.config
@property
def exceptions(self):
return self.core.exceptions
expose_all(jumpscale, J)
j = J()
Lazy imports
Lazy imports are done via property access:
def get_lazy_import_property(name, root_module, container_type):
def getter(self):
inner_name = f"__{name}"
if hasattr(self, inner_name):
return getattr(self, inner_name)
full_name = f"{root_module.__name__}.{name}"
mod = importlib.import_module(full_name)
if mod.__spec__.origin in ("namespace", None):
# if this module is a namespace, create a new container type
sub_container_type = get_container_type(full_name)
# expose all sub-modules of this imported module under this new type too
expose_all(mod, sub_container_type)
new_module = sub_container_type()
else:
# if it's just a module
if hasattr(mod, "export_module_as"):
new_module = mod.export_module_as()
else:
new_module = mod
setattr(self, inner_name, new_module)
return new_module
return property(getter)
So, when a property is accessed, it can be either:
- A namespace: we create a new container class for this namespace and use
expose_all
on it, and return an instance of this container class. - A module: we just import this module and return it, also we check if it's
export_module_as
function, so we expose its return value instead if the module itself.