Skip to content

Conversation

@mthiboust
Copy link

@mthiboust mthiboust commented Nov 4, 2025

📝 Summary

Fixes #6940

This PR is a proof-of-concept to make autoreload more robust by ensuring that when a module changes, all modules that depend on it (even indirectly) are also reloaded. This is achieved by leveraging Ruff’s static analysis, which provides a dependency graph. A positive side effect is that ruff analyze graph supports both absolute and relative imports.

As explained in the issue, ruff analyze graph is not working as expected for third-party packages in site-packages. Only current project and packages installed in editable mode are supported for automatic reload on file updates.

Got inspiration from this blog post: https://vincerose.dev/posts/ruff-test-filtering/

🔍 Description of Changes

The code could have been more straightforward but I chose to keep the existing code structure.

If you consider going further, we should decide whether to completely replace the existing, keep the two and use this new implementation as the default one, with the former one as a fallback?

I don't know how to unit test this feature.

📋 Checklist

  • I have read the contributor guidelines.
  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • I have added tests for the changes made.
  • I have run the code and verified that it works as expected.

@mthiboust mthiboust requested a review from dmadisetti as a code owner November 4, 2025 17:47
@vercel
Copy link

vercel bot commented Nov 4, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
marimo-docs Ready Ready Preview Comment Nov 4, 2025 5:48pm

@github-actions
Copy link

github-actions bot commented Nov 4, 2025

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@mthiboust
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

Comment on lines 207 to 210
modname_to_cell_id[modname]
for modname in stale_modules
),
relatives=dataflow.import_block_relatives,
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this change, the root cell was successfully reloaded with all necessary modules (even the nested ones) but the dependent cells were not automatically rerun.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this may break tests (which is good, will remind us what the intended behavior is)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part I understand the less.

Comment on lines +294 to +299
modules_copy = modules.copy()
modules_paths = {
get_full_path(v.__file__): v
for k, v in modules_copy.items()
if hasattr(v, "__file__") and v.__file__ is not None
}
Copy link
Author

@mthiboust mthiboust Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ruff graph, it is better to use the full absolute path of the module as the dict key (ruff gives module paths, not module names)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you leave this as a comment?

Comment on lines +112 to +117
def get_full_path(p: str) -> str:
p_path = pathlib.Path(p)
if p_path.is_absolute():
return str(p_path.resolve())
else:
return str((pathlib.Path.cwd() / p_path).resolve())
Copy link
Author

@mthiboust mthiboust Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paths returned by ruff are relative paths relative to the current directory where the marimo edit command is run.

Comment on lines +96 to +109
def get_downstream_dependents(
dependents_map: dict[str, list[str]], changed_files: list[str]
) -> set[str]:
visited = set()
queue = deque(changed_files)

while queue:
current = queue.popleft()
if current not in visited:
visited.add(current)
for neighbor in dependents_map.get(current, []):
queue.append(neighbor)

return visited
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

@dmadisetti dmadisetti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! First glance looks mostly correct. Missing tests are the most outstanding issue- would you be willing to give them a pass?

Also, I set off our test suite, I'd bet something will fail as is 🤔

LOGGER.debug(
f"Reloading '{stale_modname}' and its downstream dependents {downstream_dependents}."
)
for module in [m] + downstream_dependents_modules:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chance of loading the same module multiple times (if a->b c->b and a, c, then b is called twice

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think collecting all outside of the loop, and then just traversing once makes the most sense


# Not sure how this can happen, but guard against it
if not (
hasattr(src_module, "__file__") and src_module.__file__ is not None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src_file = getattr(src_module, '__file__', None)

else:
return False

# Legacy way
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No point keeping dead code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know if this new implementation would completely replace the current one, or if users could switch from one to the other via settings


if GRAPH_MODE:
graph = reloader.get_module_dependency_graph(src_module)
if src_module_filename in get_downstream_dependents(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_dependent = any(...)
return is_dependent

Comment on lines 207 to 210
modname_to_cell_id[modname]
for modname in stale_modules
),
relatives=dataflow.import_block_relatives,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this may break tests (which is good, will remind us what the intended behavior is)

@mscolnick
Copy link
Contributor

hey @mthiboust this may have a bit of churn on the PR/review. it might be easier to make the more incremental fixes/changes (like the fallback mentioned in this comment)

@mthiboust
Copy link
Author

mthiboust commented Nov 5, 2025

hey @mthiboust this may have a bit of churn on the PR/review. it might be easier to make the more incremental fixes/changes (like the fallback mentioned in this comment)

This PR was more a proof-of-concept for now. I clarified the current status of the issue in this recent comment. In short, I don't think that the discussed fallback for relative imports will be sufficient to be useful if we are still lacking support for nested imports.

And implementing support for nested imports via ruff will avoid the relative import issue anyway because ruff analyze graph already supports both absolute and relative imports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

On module change "autorun" with nested imports

3 participants