Skip to content

Commit 2a97e4c

Browse files
Further improvements
1 parent 57040d2 commit 2a97e4c

File tree

95 files changed

+882
-976
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+882
-976
lines changed

lean/click.py

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,17 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313

14-
from datetime import datetime
1514
from pathlib import Path
1615
from typing import Optional, List
1716

18-
import click
17+
from click import Command, Context, Parameter, ParamType, Option as ClickOption
1918

2019
from lean.constants import DEFAULT_LEAN_CONFIG_FILE_NAME
2120
from lean.container import container
2221
from lean.models.errors import MoreInfoError
2322
from lean.models.logger import Option
2423

25-
26-
class LeanCommand(click.Command):
24+
class LeanCommand(Command):
2725
"""A click.Command wrapper with some Lean CLI customization."""
2826

2927
def __init__(self,
@@ -54,17 +52,21 @@ def __init__(self,
5452
self.context_settings["ignore_unknown_options"] = allow_unknown_options
5553
self.context_settings["allow_extra_args"] = allow_unknown_options
5654

57-
def invoke(self, ctx: click.Context):
55+
def invoke(self, ctx: Context):
5856
if self._requires_lean_config:
59-
lean_config_manager = container.lean_config_manager()
57+
58+
from time import time
59+
start = time()
60+
61+
lean_config_manager = container.lean_config_manager
6062
try:
6163
# This method will raise an error if the directory cannot be found
6264
lean_config_manager.get_cli_root_directory()
6365
except Exception:
6466
# Use one of the cached Lean config locations to avoid having to abort the command
6567
lean_config_paths = lean_config_manager.get_known_lean_config_paths()
6668
if len(lean_config_paths) > 0:
67-
lean_config_path = container.logger().prompt_list("Select the Lean configuration file to use", [
69+
lean_config_path = container.logger.prompt_list("Select the Lean configuration file to use", [
6870
Option(id=p, label=str(p)) for p in lean_config_paths
6971
])
7072
lean_config_manager.set_default_lean_config_path(lean_config_path)
@@ -75,37 +77,36 @@ def invoke(self, ctx: click.Context):
7577
"https://www.lean.io/docs/v2/lean-cli/key-concepts/troubleshooting#02-Common-Errors"
7678
)
7779

78-
import sys
79-
if self._requires_docker and "pytest" not in sys.modules:
80-
import os
81-
is_system_linux = container.platform_manager().is_system_linux()
82-
83-
# The CLI uses temporary directories in /tmp because sometimes it may leave behind files owned by root
84-
# These files cannot be deleted by the CLI itself, so we rely on the OS to empty /tmp on reboot
85-
# The Snap version of Docker does not provide access to files outside $HOME, so we can't support it
86-
if is_system_linux:
87-
import shutil
88-
docker_path = shutil.which("docker")
80+
if self._requires_docker:
81+
from sys import modules, executable, argv
82+
if "pytest" not in modules and container.platform_manager.is_system_linux():
83+
from shutil import which
84+
from os import getuid, execlp
85+
# The CLI uses temporary directories in /tmp because sometimes it may leave behind files owned by root
86+
# These files cannot be deleted by the CLI itself, so we rely on the OS to empty /tmp on reboot
87+
# The Snap version of Docker does not provide access to files outside $HOME, so we can't support it
88+
89+
docker_path = which("docker")
8990
if docker_path is not None and docker_path.startswith("/snap"):
9091
raise MoreInfoError(
9192
"The Lean CLI does not work with the Snap version of Docker, please re-install Docker via the official installation instructions",
9293
"https://docs.docker.com/engine/install/")
9394

94-
# A usual Docker installation on Linux requires the user to use sudo to run Docker
95-
# If we detect that this is the case and the CLI was started without sudo we elevate automatically
96-
if is_system_linux and os.getuid() != 0 and container.docker_manager().is_missing_permission():
97-
container.logger().info(
98-
"This command requires access to Docker, you may be asked to enter your password")
95+
# A usual Docker installation on Linux requires the user to use sudo to run Docker
96+
# If we detect that this is the case and the CLI was started without sudo we elevate automatically
97+
if getuid() != 0 and container.docker_manager.is_missing_permission():
98+
container.logger.info(
99+
"This command requires access to Docker, you may be asked to enter your password")
99100

100-
args = ["sudo", "--preserve-env=HOME", sys.executable, *sys.argv]
101-
os.execlp(args[0], *args)
101+
args = ["sudo", "--preserve-env=HOME", executable, *argv]
102+
execlp(args[0], *args)
102103

103104
if self._allow_unknown_options:
104-
import itertools
105+
from itertools import chain
105106
# Unknown options are passed to ctx.args and need to be parsed manually
106107
# We parse them to ctx.params so they're available like normal options
107108
# Because of this all commands with allow_unknown_options=True must have a **kwargs argument
108-
arguments = list(itertools.chain(*[arg.split("=") for arg in ctx.args]))
109+
arguments = list(chain(*[arg.split("=") for arg in ctx.args]))
109110

110111
skip_next = False
111112
for index in range(len(arguments) - 1):
@@ -119,7 +120,7 @@ def invoke(self, ctx: click.Context):
119120
ctx.params[option] = value
120121
skip_next = True
121122

122-
update_manager = container.update_manager()
123+
update_manager = container.update_manager
123124
update_manager.show_announcements()
124125

125126
result = super().invoke(ctx)
@@ -128,20 +129,20 @@ def invoke(self, ctx: click.Context):
128129

129130
return result
130131

131-
def get_params(self, ctx: click.Context):
132+
def get_params(self, ctx: Context):
132133
params = super().get_params(ctx)
133134

134135
# Add --lean-config option if the command requires a Lean config
135136
if self._requires_lean_config:
136-
params.insert(len(params) - 1, click.Option(["--lean-config"],
137+
params.insert(len(params) - 1, ClickOption(["--lean-config"],
137138
type=PathParameter(exists=True, file_okay=True, dir_okay=False),
138139
help=f"The Lean configuration file that should be used (defaults to the nearest {DEFAULT_LEAN_CONFIG_FILE_NAME})",
139140
expose_value=False,
140141
is_eager=True,
141142
callback=self._parse_config_option))
142143

143144
# Add --verbose option
144-
params.insert(len(params) - 1, click.Option(["--verbose"],
145+
params.insert(len(params) - 1, ClickOption(["--verbose"],
145146
help="Enable debug logging",
146147
is_flag=True,
147148
default=False,
@@ -151,20 +152,20 @@ def get_params(self, ctx: click.Context):
151152

152153
return params
153154

154-
def _parse_config_option(self, ctx: click.Context, param: click.Parameter, value: Optional[Path]) -> None:
155+
def _parse_config_option(self, ctx: Context, param: Parameter, value: Optional[Path]) -> None:
155156
"""Parses the --config option."""
156157
if value is not None:
157-
lean_config_manager = container.lean_config_manager()
158+
lean_config_manager = container.lean_config_manager
158159
lean_config_manager.set_default_lean_config_path(value)
159160

160-
def _parse_verbose_option(self, ctx: click.Context, param: click.Parameter, value: Optional[bool]) -> None:
161+
def _parse_verbose_option(self, ctx: Context, param: Parameter, value: Optional[bool]) -> None:
161162
"""Parses the --verbose option."""
162163
if value:
163-
logger = container.logger()
164+
logger = container.logger
164165
logger.debug_logging_enabled = True
165166

166167

167-
class PathParameter(click.ParamType):
168+
class PathParameter(ParamType):
168169
"""A limited version of click.Path which uses pathlib.Path."""
169170

170171
def __init__(self, exists: bool = False, file_okay: bool = True, dir_okay: bool = True):
@@ -188,10 +189,10 @@ def __init__(self, exists: bool = False, file_okay: bool = True, dir_okay: bool
188189
self.name = "path"
189190
self._path_type = "Path"
190191

191-
def convert(self, value: str, param: click.Parameter, ctx: click.Context) -> Path:
192+
def convert(self, value: str, param: Parameter, ctx: Context) -> Path:
192193
path = Path(value).expanduser().resolve()
193194

194-
if not container.path_manager().is_path_valid(path):
195+
if not container.path_manager.is_path_valid(path):
195196
self.fail(f"{self._path_type} '{value}' is not a valid path.", param, ctx)
196197

197198
if self._exists and not path.exists():
@@ -206,15 +207,16 @@ def convert(self, value: str, param: click.Parameter, ctx: click.Context) -> Pat
206207
return path
207208

208209

209-
class DateParameter(click.ParamType):
210+
class DateParameter(ParamType):
210211
"""A click parameter which returns datetime.datetime objects and requires yyyyMMdd input."""
211212

212213
name = "date"
213214

214-
def get_metavar(self, param: click.Parameter) -> str:
215+
def get_metavar(self, param: Parameter) -> str:
215216
return "[yyyyMMdd]"
216217

217-
def convert(self, value: str, param: click.Parameter, ctx: click.Context) -> datetime:
218+
def convert(self, value: str, param: Parameter, ctx: Context):
219+
from datetime import datetime
218220
for date_format in ["%Y%m%d", "%Y-%m-%d"]:
219221
try:
220222
return datetime.strptime(value, date_format)
@@ -229,7 +231,9 @@ def ensure_options(options: List[str]) -> None:
229231
230232
:param options: the Python names of the options that must have values
231233
"""
232-
ctx = click.get_current_context()
234+
from click import get_current_context
235+
236+
ctx = get_current_context()
233237

234238
missing_options = []
235239
for key, value in ctx.params.items():
@@ -251,7 +255,9 @@ def ensure_options(options: List[str]) -> None:
251255
option = next(param for param in ctx.command.params if param.name == name)
252256
help_records.append(option.get_help_record(ctx))
253257

254-
help_formatter = click.HelpFormatter(max_width=120)
258+
from click import HelpFormatter
259+
260+
help_formatter = HelpFormatter(max_width=120)
255261
help_formatter.write_dl(help_records)
256262

257263
raise RuntimeError(f"""

lean/commands/backtest.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pathlib import Path
1717
from typing import Optional
1818
from click import command, option, argument, Choice
19+
1920
from lean.click import LeanCommand, PathParameter
2021
from lean.constants import DEFAULT_ENGINE_IMAGE, LEAN_ROOT_PATH
2122
from lean.container import container, Logger
@@ -41,7 +42,7 @@ def _migrate_python_pycharm(logger: Logger, project_dir: Path) -> None:
4142
if not workspace_xml_path.is_file():
4243
return
4344

44-
xml_manager = container.xml_manager()
45+
xml_manager = container.xml_manager
4546
current_content = xml_manager.parse(workspace_xml_path.read_text(encoding="utf-8"))
4647

4748
config = current_content.find('.//configuration[@name="Debug with Lean CLI"]')
@@ -55,7 +56,7 @@ def _migrate_python_pycharm(logger: Logger, project_dir: Path) -> None:
5556
made_changes = False
5657
has_library_mapping = False
5758

58-
library_dir = container.lean_config_manager().get_cli_root_directory() / "Library"
59+
library_dir = container.lean_config_manager.get_cli_root_directory() / "Library"
5960

6061
if library_dir.is_dir():
6162
library_dir = f"$PROJECT_DIR$/{path.relpath(library_dir, project_dir)}".replace("\\", "/")
@@ -82,7 +83,7 @@ def _migrate_python_pycharm(logger: Logger, project_dir: Path) -> None:
8283
if made_changes:
8384
workspace_xml_path.write_text(xml_manager.to_string(current_content), encoding="utf-8")
8485

85-
logger = container.logger()
86+
logger = container.logger
8687
logger.warn("Your run configuration has been updated to work with the latest version of LEAN")
8788
logger.warn("Please restart the debugger in PyCharm and run this command again")
8889

@@ -105,7 +106,7 @@ def _migrate_python_vscode(project_dir: Path) -> None:
105106
made_changes = False
106107
has_library_mapping = False
107108

108-
library_dir = container.lean_config_manager().get_cli_root_directory() / "Library"
109+
library_dir = container.lean_config_manager.get_cli_root_directory() / "Library"
109110
if not library_dir.is_dir():
110111
library_dir = None
111112

@@ -132,7 +133,7 @@ def _migrate_csharp_rider(logger: Logger, project_dir: Path) -> None:
132133
from click import Abort
133134

134135
made_changes = False
135-
xml_manager = container.xml_manager()
136+
xml_manager = container.xml_manager
136137

137138
for dir_name in [f".idea.{project_dir.stem}", f".idea.{project_dir.stem}.dir"]:
138139
workspace_xml_path = project_dir / ".idea" / dir_name / ".idea" / "workspace.xml"
@@ -155,7 +156,7 @@ def _migrate_csharp_rider(logger: Logger, project_dir: Path) -> None:
155156
made_changes = True
156157

157158
if made_changes:
158-
container.project_manager().generate_rider_config()
159+
container.project_manager.generate_rider_config()
159160

160161
logger.warn("Your run configuration has been updated to work with the .NET 5 version of LEAN")
161162
logger.warn("Please restart Rider and start debugging again")
@@ -207,7 +208,7 @@ def _migrate_csharp_csproj(project_dir: Path) -> None:
207208
if csproj_path is None:
208209
return
209210

210-
xml_manager = container.xml_manager()
211+
xml_manager = container.xml_manager
211212

212213
current_content = xml_manager.parse(csproj_path.read_text(encoding="utf-8"))
213214
if current_content.find(".//PropertyGroup/DefaultItemExcludes") is not None:
@@ -230,12 +231,12 @@ def _select_organization() -> QCMinimalOrganization:
230231
231232
:return: the selected organization
232233
"""
233-
api_client = container.api_client()
234+
api_client = container.api_client
234235

235236
organizations = api_client.organizations.get_all()
236237
options = [Option(id=organization, label=organization.name) for organization in organizations]
237238

238-
logger = container.logger()
239+
logger = container.logger
239240
return logger.prompt_list("Select the organization to purchase and download data with", options)
240241

241242

@@ -304,10 +305,10 @@ def backtest(project: Path,
304305
You can override this using the --image option.
305306
Alternatively you can set the default engine image for all commands using `lean config set engine-image <image>`.
306307
"""
307-
logger = container.logger()
308-
project_manager = container.project_manager()
308+
logger = container.logger
309+
project_manager = container.project_manager
309310
algorithm_file = project_manager.find_algorithm_file(Path(project))
310-
lean_config_manager = container.lean_config_manager()
311+
lean_config_manager = container.lean_config_manager
311312
from datetime import datetime
312313
if output is None:
313314
output = algorithm_file.parent / "backtests" / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
@@ -343,21 +344,21 @@ def backtest(project: Path,
343344

344345
lean_config_manager.configure_data_purchase_limit(lean_config, data_purchase_limit)
345346

346-
cli_config_manager = container.cli_config_manager()
347-
project_config_manager = container.project_config_manager()
347+
cli_config_manager = container.cli_config_manager
348+
project_config_manager = container.project_config_manager
348349

349350
project_config = project_config_manager.get_project_config(algorithm_file.parent)
350351
engine_image = cli_config_manager.get_engine_image(image or project_config.get("engine-image", None))
351352

352353
if str(engine_image) != DEFAULT_ENGINE_IMAGE:
353354
logger.warn(f'A custom engine image: "{engine_image}" is being used!')
354355

355-
container.update_manager().pull_docker_image_if_necessary(engine_image, update)
356+
container.update_manager.pull_docker_image_if_necessary(engine_image, update)
356357

357358
if not output.exists():
358359
output.mkdir(parents=True)
359360

360-
output_config_manager = container.output_config_manager()
361+
output_config_manager = container.output_config_manager
361362
lean_config["algorithm-id"] = str(output_config_manager.get_backtest_id(output))
362363

363364
# Set backtest name
@@ -367,7 +368,7 @@ def backtest(project: Path,
367368
if python_venv is not None and python_venv != "":
368369
lean_config["python-venv"] = f'{"/" if python_venv[0] != "/" else ""}{python_venv}'
369370

370-
lean_runner = container.lean_runner()
371+
lean_runner = container.lean_runner
371372
lean_runner.run_lean(lean_config,
372373
"backtesting",
373374
algorithm_file,

0 commit comments

Comments
 (0)