-
Notifications
You must be signed in to change notification settings - Fork 3
Reload watched (sub)modules automatically #6
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
Conversation
Adds a new step, ReloadModules, which reloads all watched modules except for the init module. This step needs to know which modules have changed, which is currently only available in the main loops variables (new_changed, last_changed), so I pass this information to the step if it's named "ReloadModules". Other alternatives could be some global state, or passing some general state to all the steps.
| def run(self): | ||
| os.system("cls" if os.name == "nt" else "clear") | ||
|
|
||
| class ReloadModules(Runnable): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps not really a Runnable when run requires an additional argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open to allowing run accept (*args, **kwargs). In that case, we could always provide a list of modules.
step.run(changed_modules=changed_modules)Though if the list of modules is passed to ReloadModules on __init__, perhaps it doesn't need any special run() treatment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit - I see that you're only reloading modules that have changed. In that case, we'd need to pass the list of changed modules.
| for module in sys.modules.copy().values(): | ||
| file = getattr(module, '__file__', None) | ||
| if file is not None: | ||
| modules[file] = module |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Could this module loop be a performance issue? An alternative could be to store the modules similar to watchfiles)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This far I haven't really explored possible performance hits. Polling is bad (in general).
Why?
I simply haven't yet needed hotload when working on codebases that are so big that performance becomes an issue.
In general - I'm open to exploring possibly controversial proposals under feature flags! If we can work trunk based, I think that's easiest. Edit - I replied a bit fast. I see that you thought about this.
Does this mean that merging this PR does not change hotload semantics unless one manually configures a ReloadedModules step? |
Almost, this line introduces it: Line 259 in 22ea81e
There may be one other small catch, if you:
Then the terminal will show output from the topmodule, using the older submodule, "hiding" the error message. If you change the submodule again, the error appears. Not a very big problem, but can be confusing the few times it happens. |
|
I guess we might get in trouble if module order reloading matters. But I think that would be a bad pattern - do stuff on "module load time" that depends on state in other modules. We could theoretically do a topological sort of the dependency graph, but that's a lot harder than just "reload the stuff on stdin". Also, the user chooses the order in which to reload stuff on passing stdin to hotload. |
| conf = { | ||
| "watch": [watchfiles], | ||
| "steps": [ClearTerminal(), reloaded_module], | ||
| "steps": [ClearTerminal(), ReloadModules(reloaded_module.module), reloaded_module], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ReloadModules(reloaded_module.module) reads kind of funny to me. Lots of "module", but not necessarily easy to understand what's going on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, reading the implementation is necessary to understand this line.
Naming the argument could help: ReloadModules(main=reloaded_module.module) or ReloadModules(exclude=reloaded_module.module).
But I am of course open to renaming ReloadModules and reloaded_module too.
I have not perfected names and such in this PR, I wanted to initially share the concept.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can polish later :)
|
A meta comment. I don't use all of hotload's functionality. So we could perhaps remove stuff to make it easier for ourselves. An example is Moving forward, I don't want to break your workflow. So writing down what our current use is would be good. Either in a markdown file, or as a test. Just a markdown file might be easier. Kind of like a public interface contract. Edit: I found out that I actually do use the hooks - when calling hotload with But a "cleaner" public interface would still be good. |
| return {path: _file_changed(path) for path in filepaths} | ||
|
|
||
| def _changed_modules(new_changed, last_changed): | ||
| list = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
list shadows the list class.
Perhaps call it changed_modules or something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
I only assume that any modules passed to stdin should reload before the init module. The order passed to stdin does not metter. Only modification of files matter, triggering just the file modified. Do you think this usually will work? If a dependency graph is needed, this PR will not work well. (It does however work well in my current case with not too many modules.) |
|
Good point. Actually -- regardless of whether it would work or not, I think this is the behavior we want.
|
That's a good idea actually. I could put ReloadedModules behind a command option. We can iron out other stuff first. Specifically, I'm not sure what |
|
I'm going to do the following:
To enable the feature flag, set the env var Ref commit rights to I'm back in a week, looking forward to chat then :) |
entrypoint lets your specify a function that hotload should call after it's done reloading. Example: def plus(x, y):
return x + 2
def hotload_entrypoint():
print(plus(1,2))
print(plus(10,20))
# now you can
#
# ls | hotload script.py --entrypoint hotload_entrypoint
#
# to run your script, without checking for env vars and stuff :) |
Possibly controversial proposal 😄
I was fooled enough times by forgetting to reload sub-modules programmatically that I wrote this.
The readme states:
I think this PR is a kind of middle ground. I do not try to detect all modules, only the ones written to stdin as
watchfiles.This commit adds a new step, ReloadModules, which reloads all watched modules except for the init module.
This step needs to know which modules have changed, which is currently only available in the main loop's variables (new_changed, last_changed), so I pass this information to the step if it's named "ReloadModules". Other alternatives could be some global state, or passing some general state to all the steps.