-
Notifications
You must be signed in to change notification settings - Fork 44
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
Adds a -m
flag to modal run/shell/deploy/serve
#2852
Conversation
servicer, set_env_client, supports_dir, recwarn, monkeypatch, disable_auto_mount | ||
): | ||
monkeypatch.chdir(supports_dir) | ||
_run(["run", "-m", f"{app_module}::foo"]) |
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.
Can't use "expected stderr" since this is actually running inside the same process and pytest captures warnings separately from stderr...
@@ -1,7 +1,7 @@ | |||
# Copyright Modal Labs 2022 | |||
from modal import App, web_endpoint | |||
|
|||
app = App() | |||
app = App(include_source=True) |
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.
random automount fix...
62591d0
to
af587a9
Compare
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.
Sorry for the slow review here — but had a few thoughts!
modal/cli/run.py
Outdated
@@ -368,8 +368,9 @@ def get_command(self, ctx, func_ref): | |||
@click.option("-d", "--detach", is_flag=True, help="Don't stop the app if the local process dies or disconnects.") | |||
@click.option("-i", "--interactive", is_flag=True, help="Run the app in interactive mode.") | |||
@click.option("-e", "--env", help=ENV_OPTION_HELP, default=None) | |||
@click.option("-m", "--module", is_flag=True, help="use library module as a script") |
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.
It migh be helpful to align this explanation with the terminology that we use in the guide page on the two modes of App reference. In particular, we call the other mode "Script mode" so it's a little confusing to say that this flag invokes the module "as a script".
Also, nit: can we match the capitalization of the other help entries?
modal/cli/run.py
Outdated
"""Deploy a Modal application. | ||
|
||
**Usage:** | ||
hello |
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.
Hi!
modal/cli/run.py
Outdated
name: str = typer.Option("", help="Name of the deployment."), | ||
env: str = ENV_OPTION, | ||
stream_logs: bool = typer.Option(False, help="Stream logs from the app upon deployment."), | ||
tag: str = typer.Option("", help="Tag the deployment with a version."), | ||
module_mode: bool = typer.Option(False, "--module", "-m", help="Use a Python module path instead of a file path"), |
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.
If we're trying to match Python, I don't think it actually supports --module
, just -m
.
modal/cli/import_refs.py
Outdated
def import_app(app_ref: str) -> App: | ||
import_ref = parse_import_ref(app_ref) | ||
def import_app(app_ref: str, is_module: bool, base_cmd: str) -> App: | ||
import_ref = parse_import_ref(app_ref, is_module=is_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.
I wonder if there's a way to reduce the amount that we need to thread is_module
down through the various functions involved here. Do we actually need to hold the is_module
state? It feels like we might just need a validation function that we can use in the CLI entrypoints like
def validate_app_ref(app_ref: str, is_module: bool):
if uses_module_spelling(app_ref) and not is_module:
# Deprecation warning
elif not uses_module_spelling(app_ref) and is_module:
# Usage error
That issues a deprecation warning when app_ref
does not use the module spelling but is_module
is True (or vice versa)
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.
Yeah I also wanted to achieve something like that, but otoh it would be nice to force module mode when the user specifies it (and once we make this non-optional we should force script mode when not specifying it). If the import code itself still infers the import from the file name, there are edge cases where we would make the wrong choice without the option.
In most cases we can infer the import mode from file name, but there are edge cases like -m foo.py
where py
is actually a module named py.py
, in which case we'd have to pass along the option to import it correctly
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.
One idea is to eagerly parse the app ref and thread the ImportRef
object through everywhere, then at least its a single object that has all of the attributes related to what should be imported and how
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.
there are edge cases like -m foo.py where py is actually a module named py.py
Hah yeah that's an annoying edge case!
But yeah I guess I still feel like passing around app_ref, is_module
is a smell.
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 agree, but I think it's a little bit less smelly now that we at least encapsulate it in a single ImportRef
that defines "what/how to import" something. One refactoring option would be to "pull" the import logic up to the CLI functions themselves instead of delegating to import_and_filter
etc, and then pass around a (module
, object_path
) instead? It would be a few lines of repeated code across the different CLI methods, but perhaps a tad less smelly :)
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.
Yeah, TBF I responded here before I saw that you'd done some of refactoring, that said it does feel like properly handling the -m
flag is purely about the CLI and should probably be handled within the CLI itself.
modal/serving.py
Outdated
@@ -97,6 +105,8 @@ async def _run_watch_loop( | |||
async def _serve_app( | |||
app: "_App", | |||
app_ref: str, | |||
*, | |||
is_module: bool, |
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.
Do we consider serve_app
to be public API? It looks a lot like the App.run
and modal.runner.deploy_app
API analogues for the core CLI, so it feels like we should expose it just for completeness sake. OTOH it's a little less obvious how it would be useful to use it programmatically.
It bears on whether we want "internal" parameters like is_module
to appear in the signature. But see comment above about whether we can reduce the need to be passing is_module
around.
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 don't think we should make it public since it's quite clunky at the moment in having to specify the "app_ref" which is a very cli thing imo
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.
Yeah I guess that makes sense. Is there a reason it's written around the app_ref
and not the App object itself?
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.
Yeah, it's because of how the serve command works when files change ("auto reloading") - it starts a new interpreter that should reload everything based on the original command, so it needs the app ref for that reason :(
@mwaskom I changed the code to do an early parse_import_ref and then simply pass along the ImportRef object everywhere. This allows us to do both the validation and actual import logic using the is_module flag, regardless at which depth the import is actually performed, which is a bit cleaner |
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.
In terms of aligning with Python semantics, do we need to do anything special to look in a __main.py__
module when running in Module mode and the provided path goes only to a package
object? Not sure what makes the most sense there, but something to think about in terms of eliminating subtle differences with Python behavior.
modal/cli/import_refs.py
Outdated
if "" not in sys.path: | ||
# When running from a CLI like `modal run` | ||
# the current working directory isn't added to sys.path | ||
# so we add it in order to make module path specification possible | ||
sys.path.insert(0, "") # "" means the current working directory | ||
|
||
if file_or_module.endswith(".py"): | ||
if not import_ref.file_or_module.endswith(".py") or import_ref.is_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.
In what situations could the first condition be True
and the second False
? (i.e., a path that doesn't end in .py
but is not a 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.
That would be the case when users specify a module ref but don't send along the -m
flag - i.e the case where we issue the warning below
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 see. It's maybe a little confusing when reading the code in this context that import_ref.is_module
is data about how the user invoked Modal, not a statement about what sort of path we actually have.
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.
Yeah, maybe it should be called load_as_module
or use_module_mode
?
modal/cli/import_refs.py
Outdated
|
||
|
||
DEFAULT_APP_NAME = "app" | ||
|
||
|
||
def import_file_or_module(file_or_module: str): | ||
def import_file_or_module(import_ref: ImportRef, base_cmd: str = ""): | ||
# TODO: Remove the base_cmd argument here once we have removed the deprecation |
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'll still want to error out with a useful message when users invoke the entrypoint the wrong way right?
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.
Good point, removing comment
|
||
|
||
DEFAULT_APP_NAME = "app" | ||
|
||
|
||
def import_file_or_module(file_or_module: str): | ||
def import_file_or_module(import_ref: ImportRef, base_cmd: str = ""): |
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.
Should we be introducing an error here (or somewhere else?) for when you do modal run -m foo.py
where foo.py
is a local script, and foo.py
is not a path to a module? Or will that trigger an error later 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.
That would trigger an error later when we try to import the module since is_module=True
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.
Would it make sense to consolidate the code that does the validation in one place?
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.
Not sure what you mean - all of the module/script validation is in the import_file_or_module
function here.
"Validation" of the import itself being ok (file existing, having valid python syntax etc) is delegated to importlib.import_module which is also called in this function, so it should all be contained here afaik?
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 meant checking all the various ways that the spelling of the import ref and the use or non-use of the -m
flag can disagree. But maybe you can't determine that a path without .py
isn't a valid module until you actually try to import it?
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.
Oh maybe the problem is that i just didn't read far enough down to understand when "trigger an error later" actually applies 😁
modal/cli/run.py
Outdated
@@ -368,8 +375,9 @@ def get_command(self, ctx, func_ref): | |||
@click.option("-d", "--detach", is_flag=True, help="Don't stop the app if the local process dies or disconnects.") | |||
@click.option("-i", "--interactive", is_flag=True, help="Run the app in interactive mode.") | |||
@click.option("-e", "--env", help=ENV_OPTION_HELP, default=None) | |||
@click.option("-m", is_flag=True, help="Use a Python module path instead of a file path") |
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.
Should the verb here be something like "Interpret"? Since it's just going to use whatever you give it, but we're trying to parameterize how it uses it.
@prbot approve |
Please request a reviewer to follow up with a post-merge review. |
Merging now so we get the deprecation in, happy to do a followup refactor if we feel like the remaining smell is too bad :) |
@prbot approve |
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.
Approved 👍. @mwaskom will follow-up review this.
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.
LGTM. We probably have a bunch of suggested command lines in the guide / examples that we need to update? In most cases I don't think we actually exercise the code in tests so it won't warn, and it will be a little annoying to find :/
Warn if using cli with "module syntax" without
-m
, so we can enforce it laterNote that unlike
python
we are using-m
as a flag rather than an option marker, so you can have it at any point in your argument list, not just last. I think it also makes sense since ourfunc_ref
can have more parts than the module (with the::
function qualifier)Check these boxes or delete any item (or this section) if not relevant for this PR.
Note on protobuf: protobuf message changes in one place may have impact to
multiple entities (client, server, worker, database). See points above.
Changelog
Introduces an
-m
flag tomodal run
,modal shell
,modal serve
andmodal deploy
, which indicates that the modal app/function file is specified using python "module syntax" rather than a file path. In the future this will be a required flag when using module syntax.Old syntax:
New syntax (note the
-m
on the second line):