Skip to content

Instantly share code, notes, and snippets.

@captain-kark
Last active July 11, 2022 07:36
Show Gist options
  • Save captain-kark/81c60a1d6935fa50226dd87723e57a89 to your computer and use it in GitHub Desktop.
Save captain-kark/81c60a1d6935fa50226dd87723e57a89 to your computer and use it in GitHub Desktop.
Named tuple support for importlib create_module allows for this
"""
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__}'
"""
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)
"""
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