Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lean/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# limitations under the License.

from lean.commands.lean import lean
from lean.commands.completion import completion
from lean.commands.backtest import backtest
from lean.commands.build import build
from lean.commands.cloud import cloud
Expand All @@ -36,6 +37,7 @@
from lean.commands.private_cloud import private_cloud

lean.add_command(config)
lean.add_command(completion)
lean.add_command(cloud)
lean.add_command(data)
lean.add_command(decrypt)
Expand Down
3 changes: 2 additions & 1 deletion lean/commands/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# limitations under the License.

from click import group
from lean.components.util.click_aliased_command_group import AliasedCommandGroup

from lean.commands.cloud.backtest import backtest
from lean.commands.cloud.live.live import live
Expand All @@ -21,7 +22,7 @@
from lean.commands.cloud.status import status
from lean.commands.cloud.object_store import object_store

@group()
@group(cls=AliasedCommandGroup)
def cloud() -> None:
"""Interact with the QuantConnect cloud."""
# This method is intentionally empty
Expand Down
74 changes: 74 additions & 0 deletions lean/commands/completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional

from click import Choice, Context, echo, group, option, pass_context

from lean.components.util.click_aliased_command_group import AliasedCommandGroup
from lean.components.util.click_shell_completion import get_completion_script, install_completion, uninstall_completion


SHELL_OPTION = option("--shell",
"-s",
type=Choice(["powershell", "bash", "zsh", "fish"], case_sensitive=False),
default=None,
help="Target shell. Auto-detected if not specified.")


@group(cls=AliasedCommandGroup, invoke_without_command=True)
@SHELL_OPTION
@pass_context
def completion(ctx: Context, shell: Optional[str]) -> None:
"""Print the native shell completion script for your shell.

\b
PowerShell (current session):
lean completion --shell powershell | Out-String | Invoke-Expression

\b
Bash or Zsh (current session):
eval "$(lean completion --shell bash)"

\b
Fish (current session):
lean completion --shell fish | source
"""
if ctx.invoked_subcommand is None:
echo(get_completion_script(shell))


@completion.command(name="show", help="Print the native shell completion script for your shell")
@SHELL_OPTION
def show(shell: Optional[str]) -> None:
echo(get_completion_script(shell))


@completion.command(name="on", help="Enable shell completion in your shell profile")
@SHELL_OPTION
def on(shell: Optional[str]) -> None:
profile_path = install_completion(shell)
echo(f"Enabled shell completion in {profile_path}")
echo("Open a new terminal session for the change to take effect.")


@completion.command(name="off", help="Disable shell completion in your shell profile")
@SHELL_OPTION
def off(shell: Optional[str]) -> None:
profile_path, removed = uninstall_completion(shell)

if removed:
echo(f"Disabled shell completion in {profile_path}")
echo("Open a new terminal session for the change to take effect.")
else:
echo(f"Shell completion was not enabled in {profile_path}")
3 changes: 2 additions & 1 deletion lean/commands/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
# limitations under the License.

from click import group
from lean.components.util.click_aliased_command_group import AliasedCommandGroup

from lean.commands.config.get import get
from lean.commands.config.list import list
from lean.commands.config.set import set
from lean.commands.config.unset import unset


@group()
@group(cls=AliasedCommandGroup)
def config() -> None:
"""Configure Lean CLI options."""
# This method is intentionally empty
Expand Down
3 changes: 2 additions & 1 deletion lean/commands/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
# limitations under the License.

from click import group
from lean.components.util.click_aliased_command_group import AliasedCommandGroup

from lean.commands.data.download import download
from lean.commands.data.generate import generate


@group()
@group(cls=AliasedCommandGroup)
def data() -> None:
"""Download or generate data for local use."""
# This method is intentionally empty
Expand Down
1 change: 1 addition & 0 deletions lean/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def init(organization: Optional[str], language: Optional[str]) -> None:
- Synchronizing projects with the cloud: https://www.lean.io/docs/v2/lean-cli/projects/cloud-synchronization

Here are some commands to get you going:
- Run `lean completion --shell powershell | Out-String | Invoke-Expression` to enable PowerShell completion in the current session
- Run `lean create-project "My Project"` to create a new project with starter code
- Run `lean cloud pull` to download all your QuantConnect projects to your local drive
- Run `lean backtest "My Project"` to backtest a project locally with the data in {DEFAULT_DATA_DIRECTORY_NAME}/
Expand Down
3 changes: 3 additions & 0 deletions lean/commands/lean.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
from lean import __version__
from lean.click import verbose_option
from lean.components.util.click_aliased_command_group import AliasedCommandGroup
from lean.components.util.click_shell_completion import register_shell_completion
from lean.container import container
from lean.models.errors import MoreInfoError

register_shell_completion()


@group(cls=AliasedCommandGroup, invoke_without_command=True)
@option("--version", is_flag=True, is_eager=True, help="Show the version and exit.")
Expand Down
3 changes: 2 additions & 1 deletion lean/commands/library/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
# limitations under the License.

from click import group
from lean.components.util.click_aliased_command_group import AliasedCommandGroup

from lean.commands.library.add import add
from lean.commands.library.remove import remove


@group()
@group(cls=AliasedCommandGroup)
def library() -> None:
"""Manage custom libraries in a project."""
# This method is intentionally empty
Expand Down
3 changes: 2 additions & 1 deletion lean/commands/private_cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
# limitations under the License.

from click import group
from lean.components.util.click_aliased_command_group import AliasedCommandGroup

from lean.commands.private_cloud.start import start
from lean.commands.private_cloud.stop import stop
from lean.commands.private_cloud.add_compute import add_compute


@group()
@group(cls=AliasedCommandGroup)
def private_cloud() -> None:
"""Interact with a QuantConnect private cloud."""
# This method is intentionally empty
Expand Down
42 changes: 31 additions & 11 deletions lean/components/util/click_aliased_command_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,49 @@


class AliasedCommandGroup(Group):
"""A click.Group wrapper that implements command aliasing."""
"""A click.Group wrapper that implements command aliasing and auto-completion/prefix matching."""

def get_command(self, ctx, cmd_name):
rv = super().get_command(ctx, cmd_name)
if rv is not None:
return rv

matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]

if not matches:
return None
elif len(matches) == 1:
return super().get_command(ctx, matches[0])

ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
Comment thread
shreejaykurhade marked this conversation as resolved.

def command(self, *args, **kwargs):
aliases = kwargs.pop('aliases', [])

if not args:
cmd_name = kwargs.pop("name", "")
else:
cmd_name = args[0]
args = args[1:]

alias_help = f"Alias for '{cmd_name}'"
if not aliases:
return super().command(*args, **kwargs)

def _decorator(f):
if args:
cmd_name = args[0]
cmd_args = args[1:]
else:
cmd_name = kwargs.get("name", f.__name__.lower().replace("_", "-"))
cmd_args = ()

alias_help = f"Alias for '{cmd_name}'"
cmd_kwargs = dict(kwargs)
cmd_kwargs.pop("name", None)

# Add the main command
cmd = super(AliasedCommandGroup, self).command(name=cmd_name, *args, **kwargs)(f)
cmd = super(AliasedCommandGroup, self).command(*cmd_args, name=cmd_name, **cmd_kwargs)(f)

# Add a command to the group for each alias with the same callback but using the alias as name
for alias in aliases:
alias_cmd = super(AliasedCommandGroup, self).command(name=alias,
short_help=alias_help,
*args,
**kwargs)(f)
*cmd_args,
**cmd_kwargs)(f)
alias_cmd.params = cmd.params

return cmd
Expand Down
4 changes: 2 additions & 2 deletions lean/components/util/click_group_default_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from click import Group
from lean.components.util.click_aliased_command_group import AliasedCommandGroup

class DefaultCommandGroup(Group):
class DefaultCommandGroup(AliasedCommandGroup):
"""allow a default command for a group"""

def command(self, *args, **kwargs):
Expand Down
Loading