Last active
July 11, 2022 07:36
-
-
Save captain-kark/81c60a1d6935fa50226dd87723e57a89 to your computer and use it in GitHub Desktop.
Named tuple support for importlib create_module allows for this
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
https://dev.to/dangerontheranger/dependency-injection-with-import-hooks-in-python-3-5hap | |
""" | |
import copy | |
import importlib.abc | |
import importlib.machinery | |
import importlib.util | |
import sys | |
from collections import namedtuple | |
def _is_module_resource(candidate, name): | |
return hasattr(candidate, '__module__') and candidate.__module__ == name | |
def _namedtuple_to_dict(value, name): | |
if _is_module_resource(value, name): | |
return dict(value) | |
clean_value = None | |
if isinstance(value, dict): | |
clean_value = copy.deepcopy(value) | |
for k, v in clean_value.items(): | |
if _is_module_resource(v, name): | |
clean_value[k] = dict(v) | |
return clean_value or value | |
class ModuleResource: | |
def __init__(self, module_name, description, filename_glob): | |
self.name = module_name | |
self.description = description | |
self.filename_glob = filename_glob | |
def intercept(self): | |
loader = ModuleResourceLoader(self.create_module, self.description) | |
finder = ModuleResourceFinder(self.name, loader, self.filename_glob) | |
sys.meta_path.append(finder) | |
def create_module(self, spec): | |
raise NotImplementedError("Subclasses must define a create_module method") | |
@staticmethod | |
def mapping_to_namedtuple(mapping, spec, typename): | |
def _iter_namedtuple_source(_): | |
for key, value in mapping.items(): | |
yield key, _namedtuple_to_dict(value, spec.name) | |
mapping_namedtuple = namedtuple(typename, mapping.keys(), module=spec.name) | |
mapping_namedtuple.__spec__ = spec | |
mapping_namedtuple.__iter__ = _iter_namedtuple_source | |
return mapping_namedtuple | |
class ModuleResourceFinder(importlib.abc.MetaPathFinder): | |
def __init__(self, name, loader, glob_pattern): | |
self.name = name | |
self._loader = loader | |
self.glob_pattern = glob_pattern | |
def find_spec(self, fullname, *args, **kwargs): | |
if fullname.startswith(self.name): | |
return importlib.util.spec_from_file_location( | |
fullname, | |
location=self.glob_pattern, | |
loader=self._loader | |
) | |
return None | |
class ModuleResourceLoader(importlib.abc.Loader): | |
def __init__(self, create_module, description): | |
self.create_module = create_module | |
self.description = description | |
def exec_module(self, module): | |
pass | |
def module_repr(self): | |
return self.description or f'A resource file loaded by {__name__}' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
https://dev.to/dangerontheranger/dependency-injection-with-import-hooks-in-python-3-5hap | |
""" | |
# this would be in mymodule/secrets, which could contain several .json files | |
import importlib.abc | |
import importlib.machinery | |
import importlib.util | |
import json | |
import sys | |
from collections import namedtuple | |
from pathlib import Path | |
class SecretsFinder(importlib.abc.MetaPathFinder): | |
def __init__(self, loader): | |
self._loader = loader | |
def find_spec(self, fullname, *args, **kwargs): | |
secrets_path = Path(Path(__file__).parent) / '*.json' | |
return importlib.util.spec_from_file_location(fullname, secrets_path, loader=self._loader) | |
class SecretsLoader(importlib.abc.Loader): | |
def create_module(self, spec): | |
secrets_name = spec.name.replace(f'{__name__}.', '') | |
secrets_file = Path(Path(__file__).parent) / f'{secrets_name}.json' | |
secrets_data = secrets_file.read_text() | |
return json.loads(secrets_data, cls=SecretsObject, spec=spec) | |
def exec_module(self, module): | |
pass | |
def module_repr(self): | |
return "A json file with secrets in it" | |
class SecretsObject(json.JSONDecoder): | |
def __init__(self, *args, **kwargs): | |
self.spec = kwargs.pop('spec') | |
json.JSONDecoder.__init__(self, object_hook=self._object_hook, *args, **kwargs) | |
def _object_hook(self, json_dict): | |
return namedtuple('secret', json_dict.keys(), spec=self.spec)(*json_dict.values()) | |
secrets_finder = SecretsFinder(SecretsLoader()) | |
sys.meta_path.append(secrets_finder) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
https://dev.to/dangerontheranger/dependency-injection-with-import-hooks-in-python-3-5hap | |
""" | |
import importlib.abc | |
import importlib.machinery | |
import importlib.util | |
import json | |
import sys | |
from collections import namedtuple | |
from pathlib import Path | |
class SecretsFinder(importlib.abc.MetaPathFinder): | |
def __init__(self, loader): | |
self._loader = loader | |
def find_spec(self, fullname, *args, **kwargs): | |
secrets_path = Path(Path(__file__).parent) / '*.json' | |
return importlib.util.spec_from_file_location(fullname, secrets_path, loader=self._loader) | |
class SecretsLoader(importlib.abc.Loader): | |
def create_module(self, spec): | |
secrets_name = spec.name.replace(f'{__name__}.', '') | |
secrets_file = Path(Path(__file__).parent) / f'{secrets_name}.json' | |
secrets_data = secrets_file.read_text() | |
# I wrap the resulting imported module "object"/namedtuple in a class object to hold __spec__ | |
return Secret(spec, json.loads(secrets_data, cls=SecretsObject)) | |
def exec_module(self, module): | |
pass | |
def module_repr(self): | |
return "A json file with secrets in it" | |
class SecretsObject(json.JSONDecoder): | |
def __init__(self, *args, **kwargs): | |
json.JSONDecoder.__init__(self, object_hook=self._object_hook, *args, **kwargs) | |
def _object_hook(self, json_dict): | |
return namedtuple('secret', json_dict.keys())(*json_dict.values()) | |
def secret_to_dict(secret): | |
""" | |
If I didn't have to wrap the return value in a dummy `Secret` object, this would be easier. | |
It will also make the process of writing a json.JSONEncoder for this work easier, too. | |
""" | |
results = {} | |
for key, value in secret._asdict().items(): | |
if hasattr(value, '_asdict') and callable(value._asdict): | |
results[key] = secret_to_dict(value) | |
else: | |
results[key] = value | |
return results | |
class Secret: | |
def __init__(self, spec, secrets_object): | |
self.__spec__ = spec | |
self.__data = secrets_object | |
def __getattr__(self, attr): | |
return getattr(self.__data, attr) | |
def __iter__(self): | |
for k, v in self.__data._asdict().items(): | |
yield k, secret_to_dict(v) | |
secrets_finder = SecretsFinder(SecretsLoader()) | |
sys.meta_path.append(secrets_finder) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment