Package: ducktools-lazyimporter
ducktools: lazyimporterCreate an object to handle lazily importing from other modules.Nearly every form of "lazyimporter" module name is taken on PyPI so this is namespaced.Intended to help save on start time where some modules are only needed for specific
functions while allowing information showing the import information to appear at
the top of a module where expected.This form of import works by creating a specific LazyImporter object that lazily
imports modules or module attributes when the module or attribute is accessed
on the object.How to downloadDownload from PyPI:python -m pip install ducktools-lazyimporterExampleExample using the packaging module.__version__="v0.1.5"fromducktools.lazyimporterimportLazyImporter,FromImportlaz=LazyImporter([FromImport("packaging.version","Version")])defis_newer_version(version_no:str)->bool:"""Check if a version number given indicatesa newer version than this package."""this_ver=laz.Version(__version__)new_ver=laz.Version(version_no)returnnew_ver>this_ver# Import will only occur when the function is called and# laz.Version is accessedprint(is_newer_version("v0.2.0"))Hasn't this already been doneYes.But...Most implementations rely on stdlib modules that are themselves slow to import
(for example: typing, importlib.util, logging, inspect, ast).
By contrastlazyimporteronly uses modules that python imports on launch
as part ofsite.lazyimporterdoes not attempt to propagate laziness, only the modules provided
tolazyimporterdirectly will be imported lazily. Any subdependencies of those
modules will be imported eagerly as if the import statement is placed where the
importer attribute is first accessed.Use CaseThere are two main use cases this is designed for.Replacing in-line imports used in a moduleSometimes it is useful to use tools from a module that has a significant import time.
If this is part of a function/method that won't necessarily always be used it is
common to delay the import and place it inside the function/method.Regular import within function:defget_copy(obj):fromcopyimportdeepcopyreturndeepcopy(obj)With a LazyImporter:fromducktools.lazyimporterimportLazyImporter,FromImportlaz=LazyImporter([FromImport("copy","deepcopy")])defget_copy(obj):returnlaz.deepcopy(obj)While the LazyImporter is more verbose, it only invokes the import mechanism
once when first accessed, while placing the import within the function invokes
it every time the function is called. This can be a significant overhead if
the function ends up used in a loop.This also means that if the attribute is accessed anywhere it will be imported
and in place wherever it is used.Delaying the import of parts of a module's public APIEager import:from.submoduleimportuseful_tool__all__=[...,"useful_tool"]Lazy import:fromducktools.lazyimporterimportLazyImporter,FromImport,get_module_funcs__all__=[...,"useful_tool"]laz=LazyImporter([FromImport(".submodule","useful_tool")],globs=globals(),# If relative imports are used, globals() must be provided.)__getattr__,__dir__=get_module_funcs(laz,__name__)The import classesIn all of these instancesmodulesis intended as the first argument
toLazyImporterand all attributes would be accessed from theLazyImporterinstance and not in the global namespace.eg:fromducktools.lazyimporterimportLazyImporter,ModuleImportmodules=[ModuleImport("functools")]laz=LazyImporter(modules)laz.functools# provides access to the module "functools"ModuleImportModuleImportis used for your basic module style imports.fromducktools.lazyimporterimportModuleImportmodules=[ModuleImport("module"),ModuleImport("other_module","other_name"),ModuleImport("base_module.submodule",asname="short_name"),]is equivalent toimport module
import other_module as other_name
import base_module.submodule as short_namewhen provided to a LazyImporter.FromImport and MultiFromImportFromImportis used for standard 'from' imports,MultiFromImportfor importing
multiple items from the same module. By using aMultiFromImport, when the first
attribute is accessed, all will be assigned on the LazyImporter.fromducktools.lazyimporterimportFromImport,MultiFromImportmodules=[FromImport("dataclasses","dataclass"),FromImport("functools","partial","partfunc"),MultiFromImport("collections",["namedtuple",("defaultdict","dd")]),]is equivalent tofromdataclassesimportdataclassfromfunctoolsimportpartialaspartfuncfromcollectionsimportnamedtuple,defaultdictasddwhen provided to a LazyImporter.TryExceptImportTryExceptImportis 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.fromducktools.lazyimporterimportTryExceptImportmodules=[TryExceptImport("tomllib","tomli","tomllib"),]is equivalent totry:importtomllibastomllibexceptImportError:importtomliastomllibwhen provided to a LazyImporter.How does it workThe following lazy importer:fromducktools.lazyimporterimportLazyImporter,FromImportlaz=LazyImporter([FromImport("functools","partial")])Generates an object that's roughly equivalent to this:classSpecificLazyImporter:def__getattr__(self,name):ifname=="partial":fromfunctoolsimportpartialsetattr(self,name,partial)returnpartialraiseAttributeError(...)laz=SpecificLazyImporter()The first time the attribute is accessed the import is done and the output
is stored on the instance, so repeated access immediately gets the desired
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.)