HPI/my/core/experimental.py
karlicoss d3f9a8e8b6
core: migrate code to benefit from 3.9 stuff (#401)
for now keeping ruff on 3.8 target version, need to sort out modules as well
2024-10-19 20:55:09 +01:00

66 lines
2.7 KiB
Python

from __future__ import annotations
import sys
import types
from typing import Any
# The idea behind this one is to support accessing "overlaid/shadowed" modules from namespace packages
# See usage examples here:
# - https://github.com/karlicoss/hpi-personal-overlay/blob/master/src/my/util/hpi_heartbeat.py
# - https://github.com/karlicoss/hpi-personal-overlay/blob/master/src/my/twitter/all.py
# Suppose you want to use my.twitter.talon, which isn't in the default all.py
# You could just copy all.py to your personal overlay, but that would mean duplicating
# all the code and possible upstream changes.
# Alternatively, you could import the "original" my.twitter.all module from "overlay" my.twitter.all
# _ORIG = import_original_module(__name__, __file__)
# this would magically take care of package import path etc,
# and should import the "original" my.twitter.all as _ORIG
# After that you can call its methods, extend etc.
def import_original_module(
module_name: str,
file: str,
*,
star: bool = False,
globals: dict[str, Any] | None = None,
) -> types.ModuleType:
module_to_restore = sys.modules[module_name]
# NOTE: we really wanna to hack the actual package of the module
# rather than just top level my.
# since that would be a bit less disruptive
module_pkg = module_to_restore.__package__
assert module_pkg is not None
parent = sys.modules[module_pkg]
my_path = parent.__path__._path # type: ignore[attr-defined]
my_path_orig = list(my_path)
def fixup_path() -> None:
for i, p in enumerate(my_path_orig):
starts = file.startswith(p)
if i == 0:
# not sure about this.. but I guess it'll always be 0th element?
assert starts, (my_path_orig, file)
if starts:
my_path.remove(p)
# should remove exactly one item
assert len(my_path) + 1 == len(my_path_orig), (my_path_orig, file)
try:
fixup_path()
try:
del sys.modules[module_name]
# NOTE: we're using __import__ instead of importlib.import_module
# since it's closer to the actual normal import (e.g. imports subpackages etc properly )
# fromlist=[None] forces it to return rightmost child
# (otherwise would just return 'my' package)
res = __import__(module_name, fromlist=[None]) # type: ignore[list-item]
if star:
assert globals is not None
globals.update({k: v for k, v in vars(res).items() if not k.startswith('_')})
return res
finally:
sys.modules[module_name] = module_to_restore
finally:
my_path[:] = my_path_orig