-
Notifications
You must be signed in to change notification settings - Fork 23
CLI #64
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
Merged
Merged
CLI #64
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
ac05a08
init cli
klamike c4fde73
fmt
klamike c68d181
fmt2
klamike 4225d91
cleanup and add repl
klamike 7576e8b
custom group
klamike ad4f12d
fmt
klamike 28ff607
shorter name
klamike 3748deb
cleanup
klamike 889fe60
rm dev deps
klamike ab1c44f
tests
klamike 8f657de
cleanup tests
klamike 0f10ff7
test with click on CI
klamike eb088ee
cleaner env var
klamike 1995bfb
force needed?
klamike 78464d0
fix test error
klamike 262a9aa
rm error handling test
klamike 987d4db
don't check no arg exit
klamike 86cd655
cleanup
klamike 16b0232
Merge remote-tracking branch 'origin/main' into pr/klamike/64
f563838
remove pyjuliapkg script
dce1abd
remove aliases for now
3532f6a
nitpick
d34b93d
rename env var
94dccf6
document the CLI
2dbed6d
removed repl alias for run
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """Entry point for python -m juliapkg.""" | ||
|
|
||
| if __name__ == "__main__": | ||
| from .cli import cli | ||
|
|
||
| cli() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| """Command-line interface for juliapkg.""" | ||
|
|
||
| import os | ||
| import subprocess | ||
| import sys | ||
|
|
||
| from .deps import STATE, add, resolve, rm, status, update | ||
|
|
||
| try: | ||
| import click | ||
|
|
||
| class JuliaPkgGroup(click.Group): | ||
| """Custom group to avoid long stacktraces when Julia exits with an error.""" | ||
|
|
||
| @property | ||
| def always_show_python_error(self) -> bool: | ||
| return os.environ.get("JULIAPKG_ALWAYS_SHOW_PYTHON_ERROR_CLI", "0") == "1" | ||
|
|
||
| @staticmethod | ||
| def _is_graceful_exit(e: subprocess.CalledProcessError) -> bool: | ||
| """Try to guess if a CalledProcessError was Julia gracefully exiting.""" | ||
| return e.returncode == 1 | ||
|
|
||
| def invoke(self, ctx): | ||
| try: | ||
| return super().invoke(ctx) | ||
| except subprocess.CalledProcessError as e: | ||
| # Julia already printed an error message | ||
| if ( | ||
| JuliaPkgGroup._is_graceful_exit(e) | ||
| and not self.always_show_python_error | ||
| ): | ||
| click.get_current_context().exit(1) | ||
| else: | ||
| raise | ||
|
|
||
| cli = JuliaPkgGroup(help="JuliaPkg - Manage your Julia dependencies from Python.") | ||
|
|
||
| @cli.command(name="add") | ||
| @click.argument("package") | ||
| @click.option("--uuid", required=True, help="UUID of the package") | ||
| @click.option("--version", help="Version constraint") | ||
| @click.option("--dev", is_flag=True, help="Add as development dependency") | ||
| @click.option("--path", help="Local path to package") | ||
| @click.option("--subdir", help="Subdirectory within the package") | ||
| @click.option("--url", help="Git URL for the package") | ||
| @click.option("--rev", help="Git revision/branch/tag") | ||
| @click.option("--target", help="Target environment") | ||
| def add_cli(package, uuid, version, dev, path, subdir, url, rev, target): | ||
| """Add a Julia package to the project.""" | ||
| add( | ||
| package, | ||
| uuid=uuid, | ||
| version=version, | ||
| dev=dev, | ||
| path=path, | ||
| subdir=subdir, | ||
| url=url, | ||
| rev=rev, | ||
| target=target, | ||
| ) | ||
| click.echo(f"Queued addition of {package}. Run `resolve` to apply changes.") | ||
|
|
||
| @cli.command(name="resolve") | ||
| @click.option("--force", is_flag=True, help="Force resolution") | ||
| @click.option("--dry-run", is_flag=True, help="Dry run (don't actually install)") | ||
| @click.option("--update", is_flag=True, help="Update dependencies") | ||
| def resolve_cli(force, dry_run, update): | ||
| """Resolve and install Julia dependencies.""" | ||
| resolve(force=force, dry_run=dry_run, update=update) | ||
| click.echo("Resolved dependencies.") | ||
|
|
||
| @cli.command(name="remove") | ||
| @click.argument("package") | ||
| @click.option("--target", help="Target environment") | ||
| def remove_cli(package, target): | ||
| """Remove a Julia package from the project.""" | ||
| rm(package, target=target) | ||
| click.echo(f"Queued removal of {package}. Run `resolve` to apply changes.") | ||
|
|
||
| cli.add_command(remove_cli, name="rm") | ||
|
|
||
| @cli.command(name="status") | ||
| @click.option("--target", help="Target environment") | ||
| def status_cli(target): | ||
| """Show the status of Julia packages in the project.""" | ||
| status(target=target) | ||
|
|
||
| cli.add_command(status_cli, name="st") | ||
|
|
||
| @cli.command(name="update") | ||
| @click.option("--dry-run", is_flag=True, help="Dry run (don't actually install)") | ||
| def update_cli(dry_run): | ||
| """Update Julia packages in the project.""" | ||
| update(dry_run=dry_run) | ||
|
|
||
| cli.add_command(update_cli, name="up") | ||
|
|
||
| @cli.command(name="run", context_settings=dict(ignore_unknown_options=True)) | ||
| @click.argument("args", nargs=-1) | ||
| def run_cli(args): | ||
| """Pass-through to Julia CLI. | ||
|
|
||
| For example, use `run` to launch a REPL or `run script.jl` to run a script. | ||
| """ | ||
| resolve() | ||
| executable = STATE["executable"] | ||
| project = STATE["project"] | ||
|
|
||
| env = os.environ.copy() | ||
| if sys.executable: | ||
| # prefer PythonCall to use the current Python executable | ||
| # TODO: this is a hack, it would be better for PythonCall to detect that | ||
| # Julia is being called from Python | ||
| env.setdefault("JULIA_PYTHONCALL_EXE", sys.executable) | ||
| cmd = [ | ||
| executable, | ||
| "--project=" + project, | ||
| ] | ||
| for arg in args: | ||
| if arg.startswith("--project"): | ||
| raise ValueError("Do not specify --project when using pyjuliapkg.") | ||
| cmd.append(arg) | ||
| subprocess.run( | ||
| cmd, | ||
| check=True, | ||
| env=env, | ||
| ) | ||
|
|
||
| cli.add_command(run_cli, name="repl") | ||
|
|
||
| except ImportError: | ||
|
|
||
| def cli(): | ||
| raise ImportError( | ||
| "`click` is required to use the juliapkg CLI. " | ||
| "Please install it with `pip install click` or " | ||
| '`pip install "pyjuliapkg[cli]".' | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| cli() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import importlib | ||
| import sys | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
|
|
||
| from juliapkg.cli import cli | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def runner(): | ||
| try: | ||
| from click.testing import CliRunner | ||
|
|
||
| return CliRunner() | ||
| except ImportError: | ||
| pytest.skip("click is not available") | ||
|
|
||
|
|
||
| class TestCLI: | ||
| def test_cli_help(self, runner): | ||
| result = runner.invoke(cli, ["--help"]) | ||
| assert result.exit_code == 0 | ||
| assert "JuliaPkg - Manage your Julia dependencies from Python." in result.output | ||
|
|
||
| def test_cli_no_args(self, runner): | ||
| result = runner.invoke(cli, []) | ||
| assert "Usage:" in result.output | ||
|
|
||
| def test_repl_with_project(self, runner): | ||
| result = runner.invoke(cli, ["repl", "--project=/tmp/test"]) | ||
| assert result.exit_code != 0 | ||
| assert "Do not specify --project when using pyjuliapkg" in str(result.exception) | ||
|
|
||
| def test_run_command(self, runner): | ||
| result = runner.invoke(cli, ["run", "-e", "using Pkg; Pkg.status()"]) | ||
| assert result.exit_code == 0 | ||
|
|
||
| def test_basic_usage(self, runner): | ||
| result = runner.invoke( | ||
| cli, ["add", "Example", "--uuid", "7876af07-990d-54b4-ab0e-23690620f79a"] | ||
| ) | ||
| assert result.exit_code == 0 | ||
| assert "Queued addition of Example" in result.output | ||
|
|
||
| result = runner.invoke(cli, ["resolve"]) | ||
| assert result.exit_code == 0 | ||
|
|
||
| result = runner.invoke(cli, ["status"]) | ||
| assert result.exit_code == 0 | ||
| assert "Example" in result.output | ||
|
|
||
| result = runner.invoke(cli, ["remove", "Example"]) | ||
| assert result.exit_code == 0 | ||
| assert "Queued removal of Example" in result.output | ||
|
|
||
| result = runner.invoke(cli, ["resolve", "--force"]) | ||
| assert result.exit_code == 0 | ||
|
|
||
| def test_click_not_available(self): | ||
| with patch.dict(sys.modules, {"click": None, "juliapkg.cli": None}): | ||
| del sys.modules["juliapkg.cli"] | ||
| cli_module = importlib.import_module("juliapkg.cli") | ||
|
|
||
| with pytest.raises(ImportError) as exc_info: | ||
| cli_module.cli() | ||
|
|
||
| assert "`click` is required to use the juliapkg CLI" in str(exc_info.value) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
The thinking here is that the user does not care to see the
subprocess.CalledProcessErrorstacktrace most of the time. So, for the CLI only, why not hide it?It may be nice to put something like
Hint: Detected a Julia error; the Python stacktrace is hidden. Set JULIAPKG_ALWAYS_SHOW_PYTHON_ERROR_CLI=1 to see the full stacktrace.