Repository: pyo3/maturin
Issue: import_hook doesn't rebuild and reinstall the package if the Rust code changes

### Bug Description

If I edit the Rust source code I am importing with `maturin.import_hook`, I would expect that a new Python process would rebuild the code when importing in the new process. Instead, the package is only ever compiled once.

### Your maturin version (`maturin --version`)

1.1.0

### Your Python version (`python -V`)

3.11.4

### Your pip version (`pip -V`)

23.2

### What bindings you're using

pyo3

### Does `cargo build` work?

- [ ] Yes, it works

### If on windows, have you checked that you aren't accidentally using unix path (those with the forward slash `/`)?

- [ ] Yes

### Steps to Reproduce

I have a `rust_pointvec.rs` with PyO3 module defined.

I run:

```python
import maturin.import_hook
maturin.import_hook.install()
import rust_pointvec
```

It compiles and installs.

I then modify the Rust code, and rerun the Python script. No rebuild or recompilation happens, the old compiled module is imported, and there is no maturin logging, unlike the initial run.

My guess at the problem: now that the package is installed, the import hook is bypassed since it can just use the default importer. Perhaps instead of `sys.meta_path.append()` the hook should be inserted earlier in `sys.meta_path`, e.g. `rustimport` makes sure to be first: https://github.com/mityax/rustimport/blob/master/rustimport/import_hook.py#L65C2-L65C2



Changed files:
maturin/import_hook.py

diff --git a/maturin/import_hook.py b/maturin/import_hook.py
index 36d63c62..f86b4a29 100644
--- a/maturin/import_hook.py
+++ b/maturin/import_hook.py
@@ -8,6 +8,7 @@ import pathlib
 import shutil
 import subprocess
 import sys
+from contextvars import ContextVar
 from importlib import abc
 from importlib.machinery import ModuleSpec
 from types import ModuleType
@@ -19,6 +20,11 @@ except ModuleNotFoundError:
     import tomli as tomllib  # type: ignore
 
 
+# Track if we have already built the package, so we can avoid infinite
+# recursion.
+_ALREADY_BUILT = ContextVar("_ALREADY_BUILT", default=False)
+
+
 class Importer(abc.MetaPathFinder):
     """A meta-path importer for the maturin based packages"""
 
@@ -34,6 +40,10 @@ class Importer(abc.MetaPathFinder):
     ) -> ModuleSpec | None:
         if fullname in sys.modules:
             return None
+        if _ALREADY_BUILT.get():
+            # At this point we'll just import normally.
+            return None
+
         mod_parts = fullname.split(".")
         module_name = mod_parts[-1]
 
@@ -74,7 +84,13 @@ class Loader(abc.Loader):
         self.fullname = fullname
 
     def load_module(self, fullname: str) -> ModuleType:
-        return importlib.import_module(self.fullname)
+        # By the time we're loading, the package should've already been built
+        # by the previous step of finding the spec.
+        old_value = _ALREADY_BUILT.set(True)
+        try:
+            return importlib.import_module(self.fullname)
+        finally:
+            _ALREADY_BUILT.reset(old_value)
 
 
 def _is_cargo_project(cargo_toml: pathlib.Path, module_name: str) -> bool:
@@ -150,7 +166,7 @@ def install(bindings: str | None = None, release: bool = False) -> Importer | No
     if _have_importer():
         return None
     importer = Importer(bindings=bindings, release=release)
-    sys.meta_path.append(importer)
+    sys.meta_path.insert(0, importer)
     return importer