Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite compat patching to fix module double-execution #490

Merged
merged 2 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 68 additions & 13 deletions klippy/compat.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,75 @@
"""
Kalico compatibility for legacy Klipper extras and plugins

This hook provides two features:

1. Rewrite non-package imports, e.g. `import mcu` effectively becomes `import klippy.mcu as mcu`
2. Provide aliases in sys.modules to prevent double-execution of modules.

The second is especially important as `is` and `isinstance` checks will fail between two seperately
imported copies of the same module.
"""

import functools
import importlib.machinery
import pathlib
import sys

ROOT = pathlib.Path(__file__).parent


class KlippyPathFinder(importlib.machinery.PathFinder):
@classmethod
def install_hook(cls):
# Place the KlippyMetaPathFinder *before* `PathFinder`
sys.meta_path.insert(-1, KlippyPathFinder())

# Setup aliases for already loaded modules
for name in list(sys.modules.keys()):
if name.startswith("klippy."):
sys.modules.setdefault(
name.removeprefix("klippy."),
sys.modules[name],
)

@classmethod
def patch_loader(cls, spec, *names):
"Patch a spec loader to add aliases for a module after import"

orig_exec_module = spec.loader.exec_module

@functools.wraps(orig_exec_module)
def aliased(module):
try:
return orig_exec_module(module)
finally:
for name in names:
if name not in sys.modules:
sys.modules[name] = module

spec.loader.exec_module = aliased

@classmethod
def find_spec(cls, fullname, path=None, target=None):
if not fullname.startswith("klippy."):
parts = fullname.split(".")
klippy_path = ROOT.joinpath(*parts)

if klippy_path.with_suffix(".py").is_file():
fullname = "klippy." + fullname
path = [str(klippy_path.parent)]

elif (klippy_path / "__init__.py").is_file():
fullname = "klippy." + fullname
path = [str(klippy_path)]

def hotpatch_modules():
"""
This is a compatibility shim for legacy external modules
to fix
Redirect legacy `import x` to `import klippy.x`
spec = super().find_spec(fullname, path, target)

"""
if spec and spec.name.startswith("klippy."):
cls.patch_loader(spec, spec.name.removeprefix("klippy."))

for module_name, module in list(sys.modules.items()):
if not module_name.startswith("klippy."):
continue
return spec

hotpatched_name = module_name.removeprefix("klippy.")
if hotpatched_name in sys.modules:
continue

sys.modules[hotpatched_name] = module
def install():
KlippyPathFinder.install_hook()
3 changes: 3 additions & 0 deletions klippy/extras/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def __init__(self, config):
)

def _telemetry_prompt(self):
if self.printer.get_start_args().get("debuginput"):
return

gcode = self.printer.lookup_object("gcode")

gcode.respond_info(
Expand Down
2 changes: 1 addition & 1 deletion klippy/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ def main():
"No log file specified!" " Severe timing issues may result!"
)

compat.hotpatch_modules()
compat.install()

gc.disable()

Expand Down
Loading