Skip to content

Commit 80e83c8

Browse files
authored
Add missing reserved directory names (#560)
* Handle reserved and invalid names * try fix bugs * Fix failing tests * Address requested changes * Nit changes
1 parent dbc018f commit 80e83c8

File tree

13 files changed

+123
-12
lines changed

13 files changed

+123
-12
lines changed

lean/commands/library/add.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ def add(project: Path, name: str, version: Optional[str], no_local: bool) -> Non
296296
$ lean library add "My Python Project" tensorflow --version 2.5.0
297297
$ lean library add "My Python Project" "Library/My Python Library"
298298
"""
299+
from lean.components import reserved_names
300+
299301
logger = container.logger
300302
project_config = container.project_config_manager.get_project_config(project)
301303
project_language = project_config.get("algorithm-language", None)
@@ -305,6 +307,12 @@ def add(project: Path, name: str, version: Optional[str], no_local: bool) -> Non
305307
"https://www.lean.io/docs/v2/lean-cli/projects/project-management#02-Create-Projects")
306308

307309
library_manager = container.library_manager
310+
311+
if not container.path_manager.is_path_valid(Path(name)):
312+
raise MoreInfoError(
313+
f"Invalid path name. Can only contain letters, numbers & spaces. Can not start with empty char ' ' or be a reserved name [ {', '.join(reserved_names)} ]",
314+
"https://www.lean.io/docs/v2/lean-cli/key-concepts/troubleshooting#02-Common-Errors")
315+
308316
library_dir = Path(name).expanduser().resolve()
309317

310318
if library_manager.is_lean_library(library_dir):

lean/components/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
# limitations under the License.
1313

1414
reserved_names = ["CON", "PRN", "AUX", "NUL",
15-
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
16-
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"]
15+
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
16+
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
17+
"bin", "obj", "__pycache__", ".ipynb_checkpoints", ".idea", ".vscode"]
18+
19+
output_reserved_names = ["backtests", "live", "optimizations", "storage", "report"]
1720

1821
forbidden_characters = ["\\", ":", "*", "?", '"', "<", ">", "|"]

lean/components/util/path_manager.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from pathlib import Path
1515

16-
from lean.components import reserved_names, forbidden_characters
16+
from lean.components import reserved_names, output_reserved_names, forbidden_characters
1717
from lean.components.config.lean_config_manager import LeanConfigManager
1818
from lean.components.util.platform_manager import PlatformManager
1919

@@ -47,8 +47,8 @@ def is_name_valid(self, name: str) -> bool:
4747
:param name: the name to validate
4848
:return: True if the name is valid on Windows operating system, False if not
4949
"""
50-
import re
51-
return re.match(r'^[-_a-zA-Z0-9/\s]*$', name) is not None
50+
from re import match
51+
return (match(r'^[-_a-zA-Z0-9/\s]*$', name) is not None) and (name not in reserved_names + output_reserved_names)
5252

5353
def is_path_valid(self, path: Path) -> bool:
5454
"""Returns whether the given path is a valid project path in the current operating system.
@@ -66,6 +66,9 @@ def is_path_valid(self, path: Path) -> bool:
6666
except OSError:
6767
return False
6868

69+
if path.name in output_reserved_names:
70+
return False
71+
6972
# On Windows path.exists() doesn't throw for paths like CON/file.txt
7073
# Trying to create them does raise errors, so we manually validate path components
7174
# We follow the rules of windows for every OS
@@ -75,7 +78,7 @@ def is_path_valid(self, path: Path) -> bool:
7578
return False
7679

7780
for reserved_name in reserved_names:
78-
if component.upper() == reserved_name or component.upper().startswith(reserved_name + "."):
81+
if component == reserved_name or component.upper() == reserved_name or component.upper().startswith(reserved_name + "."):
7982
return False
8083

8184
for forbidden_character in forbidden_characters:

lean/components/util/project_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from datetime import datetime
1515
from pathlib import Path
1616
from typing import List, Optional, Union, Tuple
17-
from lean.components import reserved_names
17+
from lean.components import reserved_names, output_reserved_names
1818
from lean.components.config.cli_config_manager import CLIConfigManager
1919
from lean.components.config.lean_config_manager import LeanConfigManager
2020
from lean.components.config.project_config_manager import ProjectConfigManager
@@ -134,7 +134,7 @@ def get_source_files(self, directory: Path) -> List[Path]:
134134

135135
for obj in directory.iterdir():
136136
if obj.is_dir():
137-
if (obj.name in ["bin", "obj", ".ipynb_checkpoints", "backtests", "live", "optimizations"] or
137+
if (obj.name in reserved_names + output_reserved_names or
138138
obj.name.startswith(".") or
139139
# ignore python virtual environments
140140
(obj / "pyvenv.cfg").is_file()):

tests/commands/cloud/test_backtest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from click.testing import CliRunner
1818

1919
from lean.commands import lean
20-
from lean.container import container
2120
from lean.models.api import QCBacktest
2221
from tests.test_helpers import create_api_project, create_fake_lean_cli_directory
2322
from tests.conftest import initialize_container

tests/commands/cloud/test_optimize.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
from lean.commands import lean
2020
from lean.components.config.optimizer_config_manager import NodeType, OptimizerConfigManager
21-
from lean.container import container
2221
from lean.models.api import QCOptimization, QCOptimizationBacktest, QCOptimizationEstimate
2322
from lean.models.optimizer import (OptimizationConstraint, OptimizationExtremum, OptimizationParameter,
2423
OptimizationTarget)

tests/commands/library/test_add.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from lean.container import container
2121
from lean.models.utils import LeanLibraryReference
2222
from tests.test_helpers import create_fake_lean_cli_directory, create_fake_lean_cli_directory_with_subdirectories
23+
from lean.components import reserved_names
2324

2425

2526
def _assert_library_reference_was_added_to_project_config_file(project_dir: Path, library_dir: Path) -> None:
@@ -123,3 +124,12 @@ def test_library_add_fails_when_library_directory_is_not_valid(library_dir: str)
123124

124125
assert result.exit_code != 0
125126

127+
@pytest.mark.parametrize("library_name", reserved_names)
128+
def test_library_add_fails_when_library_directory_is_not_valid(library_name) -> None:
129+
create_fake_lean_cli_directory()
130+
131+
project_dir = Path("CSharp Project")
132+
library_dir = Path(f"Library/{library_name}")
133+
result = CliRunner().invoke(lean, ["library", "add", str(project_dir), str(library_dir), "--no-local"])
134+
assert result.exit_code != 0
135+
assert result.exception.args[0] == f"Invalid path name. Can only contain letters, numbers & spaces. Can not start with empty char ' ' or be a reserved name [ {', '.join(reserved_names)} ]"

tests/commands/test_backtest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from lean.commands import lean
2323
from lean.components.util.xml_manager import XMLManager
24+
from lean.components import reserved_names, output_reserved_names
2425
from lean.constants import DEFAULT_ENGINE_IMAGE
2526
from lean.container import container
2627
from lean.models.api import QCLanguage
@@ -95,6 +96,13 @@ def test_backtest_calls_lean_runner_with_custom_output_directory() -> None:
9596
{},
9697
{})
9798

99+
@pytest.mark.parametrize("output_name", reserved_names + output_reserved_names)
100+
def test_backtest_fails_when_given_is_invalid(output_name: str) -> None:
101+
create_fake_lean_cli_directory()
102+
103+
result = CliRunner().invoke(lean, ["backtest", "Python Project", "--output", f"Python Project/custom/{output_name}"])
104+
105+
assert result.output == f"Usage: lean backtest [OPTIONS] PROJECT\nTry 'lean backtest --help' for help.\n\nError: Invalid value for '--output': Directory 'Python Project/custom/{output_name}' is not a valid path.\n"
98106

99107
def test_backtest_calls_lean_runner_with_release_mode() -> None:
100108
create_fake_lean_cli_directory()

tests/commands/test_live.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from click.testing import CliRunner
2424

2525
from lean.commands import lean
26+
from lean.components import reserved_names, output_reserved_names
2627
from lean.constants import DEFAULT_ENGINE_IMAGE
2728
from lean.container import container
2829
from lean.models.docker import DockerImage
@@ -1167,6 +1168,27 @@ def test_live_non_interactive_deploy_with_live_and_historical_provider_missed_li
11671168

11681169
assert result.exit_code == 0
11691170

1171+
@pytest.mark.parametrize("output_name", reserved_names + output_reserved_names)
1172+
def test_live_non_interactive_deploy_fails_when_given_is_invalid(output_name: str) -> None:
1173+
create_fake_lean_cli_directory()
1174+
create_fake_environment("live-paper", True)
1175+
1176+
container.initialize(docker_manager=mock.Mock(), lean_runner=mock.Mock(), api_client = mock.MagicMock())
1177+
1178+
provider_live_option = ["--data-provider-live", "Polygon", "--polygon-api-key", "123"]
1179+
1180+
provider_history_option = ["--data-provider-historical", "Polygon"]
1181+
1182+
result = CliRunner().invoke(lean, ["live", "deploy", "--brokerage", "Paper Trading",
1183+
*provider_live_option,
1184+
*provider_history_option,
1185+
"Python Project",
1186+
"--output",
1187+
output_name
1188+
])
1189+
1190+
assert result.output == f"Usage: lean live deploy [OPTIONS] PROJECT\nTry 'lean live deploy --help' for help.\n\nError: Invalid value for '--output': Directory '{output_name}' is not a valid path.\n"
1191+
11701192
def test_live_non_interactive_deploy_with_real_brokerage_without_credentials() -> None:
11711193
create_fake_lean_cli_directory()
11721194
create_fake_environment("live-paper", True)

tests/commands/test_optimize.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from lean.commands import lean
2222
from lean.components.cloud.module_manager import ModuleManager
2323
from lean.components.config.storage import Storage
24+
from lean.components import reserved_names, output_reserved_names
2425
from lean.constants import DEFAULT_ENGINE_IMAGE, LEAN_ROOT_PATH
2526
from lean.container import container
2627
from lean.models.docker import DockerImage
@@ -204,6 +205,23 @@ def test_optimize_creates_output_directory_when_not_existing_yet() -> None:
204205

205206
assert (Path.cwd() / "output").is_dir()
206207

208+
@pytest.mark.parametrize("output_directory", reserved_names + output_reserved_names)
209+
def test_optimize_fails_when_output_directory_is_invalid(output_directory: str) -> None:
210+
create_fake_lean_cli_directory()
211+
212+
docker_manager = mock.Mock()
213+
docker_manager.run_image.side_effect = run_image
214+
container.initialize(docker_manager=docker_manager)
215+
container.optimizer_config_manager = _get_optimizer_config_manager_mock()
216+
217+
Storage(str(Path.cwd() / "Python Project" / "config.json")).set("parameters", {"param1": "1"})
218+
219+
result = CliRunner().invoke(lean, ["optimize", "Python Project", "--output", f"Python Project/custom/{output_directory}"])
220+
221+
assert result.exit_code != 0
222+
223+
assert result.output == f"Usage: lean optimize [OPTIONS] PROJECT\nTry 'lean optimize --help' for help.\n\nError: Invalid value for '--output': Directory 'Python Project/custom/{output_directory}' is not a valid path.\n"
224+
207225

208226
def test_optimize_copies_code_to_output_directory() -> None:
209227
create_fake_lean_cli_directory()

0 commit comments

Comments
 (0)