Repository: QuantConnect/lean-cli
Pull request #441 (2024-04-02 19:04:34)

Remove hardcoded python and dotnet version
- Remove hardcoded python and dotnet version, these will be retrieved from the docker labels of the image being used. See https://github.com/QuantConnect/Lean/pull/7890
- 


Patch:
diff --git a/lean/commands/library/add.py b/lean/commands/library/add.py
index 01fc74a..d6f7db9 100644
--- a/lean/commands/library/add.py
+++ b/lean/commands/library/add.py
@@ -13,10 +13,10 @@
 
 from pathlib import Path
 from typing import Any, Dict, Optional, Tuple
-from lean.constants import LEAN_STRICT_PYTHON_VERSION
 from click import command, argument, option
 
 from lean.click import LeanCommand, PathParameter
+from lean.constants import DEFAULT_LEAN_STRICT_PYTHON_VERSION
 from lean.container import container
 from lean.models.errors import MoreInfoError
 
@@ -124,7 +124,7 @@ def _is_pypi_file_compatible(file: Dict[str, Any], required_python_version) -> b
     return True
 
 
-def _get_pypi_package(name: str, version: Optional[str]) -> Tuple[str, str]:
+def _get_pypi_package(name: str, version: Optional[str], python_version: str) -> Tuple[str, str]:
     """Retrieves the properly-capitalized name and the latest compatible version of a package from PyPI.
 
     If the version is already given, this method checks whether that version is compatible with the Docker images.
@@ -148,7 +148,7 @@ def _get_pypi_package(name: str, version: Optional[str]) -> Tuple[str, str]:
     pypi_data = loads(response.text)
     name = pypi_data["info"]["name"]
 
-    required_python_version = StrictVersion(LEAN_STRICT_PYTHON_VERSION)
+    required_python_version = StrictVersion(python_version)
 
     last_compatible_version = None
     last_compatible_version_upload_time = None
@@ -237,7 +237,12 @@ def _add_pypi_package_to_python_project(project_dir: Path, name: str, version: O
     else:
         logger.info("Retrieving latest compatible version from PyPI")
 
-    name, version = _get_pypi_package(name, version)
+    project_config = container.project_config_manager.get_project_config(project_dir)
+    engine_image = container.cli_config_manager.get_engine_image(project_config.get("engine-image", None))
+    python_version = container.docker_manager.get_image_label(engine_image, 'strict_python_version',
+                                                              DEFAULT_LEAN_STRICT_PYTHON_VERSION)
+
+    name, version = _get_pypi_package(name, version, python_version)
 
     requirements_file = project_dir / "requirements.txt"
     logger.info(f"Adding {name} {version} to '{path_manager.get_relative_path(requirements_file)}'")
diff --git a/lean/commands/optimize.py b/lean/commands/optimize.py
index bf78e35..92611b2 100644
--- a/lean/commands/optimize.py
+++ b/lean/commands/optimize.py
@@ -336,7 +336,10 @@ def optimize(project: Path,
     build_and_configure_modules(addon_module, cli_addon_modules, organization_id, lean_config,
                                 kwargs, logger, environment_name)
 
-    run_options = lean_runner.get_basic_docker_config(lean_config, algorithm_file, output, None, release, should_detach)
+    container.update_manager.pull_docker_image_if_necessary(engine_image, update, no_update)
+
+    run_options = lean_runner.get_basic_docker_config(lean_config, algorithm_file, output, None, release, should_detach,
+                                                      engine_image)
 
     run_options["working_dir"] = "/Lean/Optimizer.Launcher/bin/Debug"
     run_options["commands"].append(f"dotnet QuantConnect.Optimizer.Launcher.dll{' --estimate' if estimate else ''}")
@@ -346,7 +349,6 @@ def optimize(project: Path,
               type="bind",
               read_only=True)
     )
-    container.update_manager.pull_docker_image_if_necessary(engine_image, update, no_update)
 
     # Add known additional run options from the extra docker config
     LeanRunner.parse_extra_docker_config(run_options, loads(extra_docker_config))
diff --git a/lean/commands/research.py b/lean/commands/research.py
index 739640a..4271558 100644
--- a/lean/commands/research.py
+++ b/lean/commands/research.py
@@ -128,12 +128,24 @@ def research(project: Path,
     for key, value in extra_config:
         lean_config[key] = value
 
+    project_config_manager = container.project_config_manager
+    cli_config_manager = container.cli_config_manager
+
+    project_config = project_config_manager.get_project_config(algorithm_file.parent)
+    research_image = cli_config_manager.get_research_image(image or project_config.get("research-image", None))
+
+    container.update_manager.pull_docker_image_if_necessary(research_image, update, no_update)
+
+    if str(research_image) != DEFAULT_RESEARCH_IMAGE:
+        logger.warn(f'A custom research image: "{research_image}" is being used!')
+
     run_options = lean_runner.get_basic_docker_config(lean_config,
                                                       algorithm_file,
                                                       temp_manager.create_temporary_directory(),
                                                       None,
                                                       False,
-                                                      detach)
+                                                      detach,
+                                                      research_image)
 
     # Mount the config in the notebooks directory as well
     local_config_path = next(m["Source"] for m in run_options["mounts"] if m["Target"].endswith("config.json"))
@@ -174,17 +186,6 @@ def research(project: Path,
     # Add known additional run options from the extra docker config
     LeanRunner.parse_extra_docker_config(run_options, loads(extra_docker_config))
 
-    project_config_manager = container.project_config_manager
-    cli_config_manager = container.cli_config_manager
-
-    project_config = project_config_manager.get_project_config(algorithm_file.parent)
-    research_image = cli_config_manager.get_research_image(image or project_config.get("research-image", None))
-
-    if str(research_image) != DEFAULT_RESEARCH_IMAGE:
-        logger.warn(f'A custom research image: "{research_image}" is being used!')
-
-    container.update_manager.pull_docker_image_if_necessary(research_image, update, no_update)
-
     try:
         container.docker_manager.run_image(research_image, **run_options)
     except APIError as error:
diff --git a/lean/components/docker/docker_manager.py b/lean/components/docker/docker_manager.py
index 5f08a22..57ab9f9 100644
--- a/lean/components/docker/docker_manager.py
+++ b/lean/components/docker/docker_manager.py
@@ -38,6 +38,16 @@ class DockerManager:
         self._temp_manager = temp_manager
         self._platform_manager = platform_manager
 
+    def get_image_label(self, image: DockerImage, label: str, default: str) -> str:
+        docker_image = self._get_docker_client().images.get(image.name)
+
+        for name, value in docker_image.labels.items():
+            if name == label:
+                self._logger.debug(f"Label '{label}' found in image '{image.name}', value {value}")
+                return value
+        self._logger.info(f"Label '{label}' not found in image '{image.name}', using default {default}")
+        return default
+
     def pull_image(self, image: DockerImage) -> None:
         """Pulls a Docker image.
 
@@ -563,13 +573,13 @@ class DockerManager:
                 break
 
         return path
-    
+
     def get_container_port(self, container_name: str, internal_port: str) -> Optional[int]:
         """
         Returns a containers external port for a mapped internal port
         :param container_name: Name of the container
         :param internal_port: The internal port of container. If protocol not included
-        we assume /tcp. ex. 5678/tcp 
+        we assume /tcp. ex. 5678/tcp
         :return: The external port that is linked to it, or None if it does not exist
         """
 
diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py
index 531cd20..9a1e050 100644
--- a/lean/components/docker/lean_runner.py
+++ b/lean/components/docker/lean_runner.py
@@ -23,7 +23,8 @@ from lean.components.util.logger import Logger
 from lean.components.util.project_manager import ProjectManager
 from lean.components.util.temp_manager import TempManager
 from lean.components.util.xml_manager import XMLManager
-from lean.constants import MODULES_DIRECTORY, TERMINAL_LINK_PRODUCT_ID, LEAN_ROOT_PATH, DEFAULT_DATA_DIRECTORY_NAME
+from lean.constants import MODULES_DIRECTORY, LEAN_ROOT_PATH, DEFAULT_DATA_DIRECTORY_NAME, \
+    DEFAULT_LEAN_DOTNET_FRAMEWORK, DEFAULT_LEAN_PYTHON_VERSION
 from lean.constants import DOCKER_PYTHON_SITE_PACKAGES_PATH
 from lean.models.docker import DockerImage
 from lean.models.utils import DebuggingMethod
@@ -97,7 +98,8 @@ class LeanRunner:
                                                    output_dir,
                                                    debugging_method,
                                                    release,
-                                                   detach)
+                                                   detach,
+                                                   image)
 
         # Add known additional run options from the extra docker config
         self.parse_extra_docker_config(run_options, extra_docker_config)
@@ -175,7 +177,8 @@ class LeanRunner:
                                 output_dir: Path,
                                 debugging_method: Optional[DebuggingMethod],
                                 release: bool,
-                                detach: bool) -> Dict[str, Any]:
+                                detach: bool,
+                                image: DockerImage) -> Dict[str, Any]:
         """Creates a basic Docker config to run the engine with.
 
         This method constructs the parts of the Docker config that is the same for both the engine and the optimizer.
@@ -186,6 +189,7 @@ class LeanRunner:
         :param debugging_method: the debugging method if debugging needs to be enabled, None if not
         :param release: whether C# projects should be compiled in release configuration instead of debug
         :param detach: whether LEAN should run in a detached container
+        :param image: The docker image that will be used
         :return: the Docker configuration containing basic configuration to run Lean
         """
         from docker.types import Mount
@@ -309,7 +313,10 @@ class LeanRunner:
             # Create a C# project used to resolve the dependencies of the modules
             run_options["commands"].append("mkdir /ModulesProject")
             run_options["commands"].append("dotnet new sln -o /ModulesProject")
-            run_options["commands"].append("dotnet new classlib -o /ModulesProject -f net6.0 --no-restore")
+
+            framework_ver = self._docker_manager.get_image_label(image, 'target_framework',
+                                                                 DEFAULT_LEAN_DOTNET_FRAMEWORK)
+            run_options["commands"].append(f"dotnet new classlib -o /ModulesProject -f {framework_ver} --no-restore")
             run_options["commands"].append("rm /ModulesProject/Class1.cs")
 
             # Add all modules to the project, automatically resolving all dependencies
@@ -325,7 +332,8 @@ class LeanRunner:
 
         # Set up language-specific run options
         self.setup_language_specific_run_options(run_options, project_dir, algorithm_file,
-                                            set_up_common_csharp_options_called, release)
+                                                 set_up_common_csharp_options_called, release,
+                                                 image)
 
         # Save the final Lean config to a temporary file so we can mount it into the container
         config_path = self._temp_manager.create_temporary_directory() / "config.json"
@@ -359,11 +367,12 @@ class LeanRunner:
 
         return run_options
 
-    def set_up_python_options(self, project_dir: Path, run_options: Dict[str, Any]) -> None:
+    def set_up_python_options(self, project_dir: Path, run_options: Dict[str, Any], image: DockerImage) -> None:
         """Sets up Docker run options specific to Python projects.
 
         :param project_dir: the path to the project directory
         :param run_options: the dictionary to append run options to
+        :param image: the docker image that will be used
         """
 
         from docker.types import Mount
@@ -410,10 +419,14 @@ class LeanRunner:
             "mode": "rw"
         }
 
+        python_version = self._docker_manager.get_image_label(image, 'python_version',
+                                                              DEFAULT_LEAN_PYTHON_VERSION)
+        site_packages_path = DOCKER_PYTHON_SITE_PACKAGES_PATH.replace('{LEAN_PYTHON_VERSION}', python_version)
+
         # Mount a volume to the user packages directory so we don't install packages every time
         site_packages_volume = self._docker_manager.create_site_packages_volume(requirements_txt)
         run_options["volumes"][site_packages_volume] = {
-            "bind": f"{DOCKER_PYTHON_SITE_PACKAGES_PATH}",
+            "bind": f"{site_packages_path}",
             "mode": "rw"
         }
 
@@ -424,7 +437,7 @@ class LeanRunner:
         # We only need to do this if it hasn't already been done before for this site packages volume
         # To keep track of this we create a special file in the site packages directory after installation
         # If this file already exists we can skip pip install completely
-        marker_file = f"{DOCKER_PYTHON_SITE_PACKAGES_PATH}/pip-install-done"
+        marker_file = f"{site_packages_path}/pip-install-done"
         run_options["commands"].extend([
             f"! test -f {marker_file} && pip install --user --progress-bar off -r /requirements.txt",
             f"touch {marker_file}"
@@ -452,12 +465,14 @@ class LeanRunner:
         requirements = sorted(set(requirements))
         return "\n".join(requirements)
 
-    def set_up_csharp_options(self, project_dir: Path, run_options: Dict[str, Any], release: bool) -> None:
+    def set_up_csharp_options(self, project_dir: Path, run_options: Dict[str, Any], release: bool,
+                              image: DockerImage) -> None:
         """Sets up Docker run options specific to C# projects.
 
         :param project_dir: the path to the project directory
         :param run_options: the dictionary to append run options to
         :param release: whether C# projects should be compiled in release configuration instead of debug
+        :param image: the docker image that will be used
         """
         compile_root = self._get_csharp_compile_root(project_dir)
 
@@ -474,11 +489,13 @@ class LeanRunner:
         for path in compile_root.rglob("*.csproj"):
             self._ensure_csproj_uses_correct_lean(compile_root, path, csproj_temp_dir, run_options)
 
+        framework_ver = self._docker_manager.get_image_label(image, 'target_framework',
+                                                             DEFAULT_LEAN_DOTNET_FRAMEWORK)
         # Set up the MSBuild properties
         msbuild_properties = {
             "Configuration": "Release" if release else "Debug",
             "Platform": "AnyCPU",
-            "TargetFramework": "net6.0",
+            "TargetFramework": framework_ver,
             "OutputPath": "/Compile/bin",
             "GenerateAssemblyInfo": "false",
             "GenerateTargetFrameworkAttribute": "false",
@@ -722,14 +739,14 @@ for library_id, library_data in project_assets["targets"][project_target].items(
             lean_config[config_key] = disk_provider
 
     def setup_language_specific_run_options(self, run_options, project_dir, algorithm_file,
-                                            set_up_common_csharp_options_called, release) -> None:
+                                            set_up_common_csharp_options_called, release, image: DockerImage) -> None:
         # Set up language-specific run options
         if algorithm_file.name.endswith(".py"):
-            self.set_up_python_options(project_dir, run_options)
+            self.set_up_python_options(project_dir, run_options, image)
         else:
             if not set_up_common_csharp_options_called:
                 self.set_up_common_csharp_options(run_options)
-            self.set_up_csharp_options(project_dir, run_options, release)
+            self.set_up_csharp_options(project_dir, run_options, release, image)
 
     def format_error_before_logging(self, chunk: str):
         from lean.components.util import compiler
diff --git a/lean/components/util/project_manager.py b/lean/components/util/project_manager.py
index 12cb5e7..21d9518 100644
--- a/lean/components/util/project_manager.py
+++ b/lean/components/util/project_manager.py
@@ -15,16 +15,19 @@ from datetime import datetime
 from pathlib import Path
 from typing import List, Optional, Union, Tuple
 from lean.components import reserved_names
+from lean.components.config.cli_config_manager import CLIConfigManager
 from lean.components.config.lean_config_manager import LeanConfigManager
 from lean.components.config.project_config_manager import ProjectConfigManager
+from lean.components.docker.docker_manager import DockerManager
 from lean.components.util.logger import Logger
 from lean.components.util.path_manager import PathManager
 from lean.components.util.platform_manager import PlatformManager
 from lean.components.util.xml_manager import XMLManager
-from lean.constants import PROJECT_CONFIG_FILE_NAME
+from lean.constants import PROJECT_CONFIG_FILE_NAME, DEFAULT_LEAN_DOTNET_FRAMEWORK
 from lean.models.api import QCLanguage, QCProject, QCProjectLibrary
 from lean.models.utils import LeanLibraryReference
 
+
 class ProjectManager:
     """The ProjectManager class provides utilities for handling a single project."""
 
@@ -34,7 +37,9 @@ class ProjectManager:
                  lean_config_manager: LeanConfigManager,
                  path_manager: PathManager,
                  xml_manager: XMLManager,
-                 platform_manager: PlatformManager) -> None:
+                 platform_manager: PlatformManager,
+                 cli_config_manager: CLIConfigManager,
+                 docker_manager: DockerManager) -> None:
         """Creates a new ProjectManager instance.
 
         :param logger: the logger to use to log messages with
@@ -50,6 +55,8 @@ class ProjectManager:
         self._path_manager = path_manager
         self._xml_manager = xml_manager
         self._platform_manager = platform_manager
+        self._cli_config_manager = cli_config_manager
+        self._docker_manager = docker_manager
 
     def find_algorithm_file(self, input: Path) -> Path:
         """Returns the path to the file containing the algorithm.
@@ -186,7 +193,11 @@ class ProjectManager:
             self._generate_pycharm_config(project_dir)
         else:
             self._generate_vscode_csharp_config(project_dir)
-            self._generate_csproj(project_dir)
+
+            image = self._cli_config_manager.get_engine_image()
+            framework_ver = self._docker_manager.get_image_label(image, 'target_framework',
+                                                                 DEFAULT_LEAN_DOTNET_FRAMEWORK)
+            self._generate_csproj(project_dir, framework_ver)
             self.generate_rider_config(project_dir)
 
     def delete_project(self, project_dir: Path) -> None:
@@ -677,12 +688,13 @@ class ProjectManager:
 }}
         """)
 
-    def _generate_csproj(self, project_dir: Path) -> None:
+    def _generate_csproj(self, project_dir: Path, framework_version: str) -> None:
         """Generates a .csproj file for the given project and returns the path to it.
 
         :param project_dir: the path of the new project
         """
-        self._generate_file(project_dir / f"{project_dir.name}.csproj", self.get_csproj_file_default_content())
+        self._generate_file(project_dir / f"{project_dir.name}.csproj",
+                            self.get_csproj_file_default_content(framework_version))
 
     def generate_rider_config(self, project_dir: Path) -> bool:
         """Generates C# debugging configuration for Rider.
@@ -952,13 +964,13 @@ class ProjectManager:
         return list(set(libraries)), list(set(libraries_not_found))
 
     @staticmethod
-    def get_csproj_file_default_content() -> str:
+    def get_csproj_file_default_content(framework_version: str) -> str:
         return """
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
         <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
-        <TargetFramework>net6.0</TargetFramework>
+        <TargetFramework>{TARGET_FRAMEWORK}</TargetFramework>
         <OutputPath>bin/$(Configuration)</OutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <DefaultItemExcludes>$(DefaultItemExcludes);backtests/*/code/**;live/*/code/**;optimizations/*/code/**</DefaultItemExcludes>
@@ -969,7 +981,7 @@ class ProjectManager:
         <PackageReference Include="QuantConnect.DataSource.Libraries" Version="2.5.*"/>
     </ItemGroup>
 </Project>
-        """
+        """.replace('{TARGET_FRAMEWORK}', framework_version)
 
     @staticmethod
     def get_csproj_file_path(project_dir: Path) -> Path:
diff --git a/lean/constants.py b/lean/constants.py
index 53e4ff3..f2fe348 100644
--- a/lean/constants.py
+++ b/lean/constants.py
@@ -21,14 +21,13 @@ CUSTOM_FOUNDATION = "lean-cli/foundation"
 CUSTOM_ENGINE = "lean-cli/engine"
 CUSTOM_RESEARCH = "lean-cli/research"
 
-# The python version of docker image
-LEAN_PYTHON_VERSION = "3.8"
-
-# The strict python version of docker image
-LEAN_STRICT_PYTHON_VERSION =  f"{LEAN_PYTHON_VERSION}.13"
+# we get these values from the image labels, but we still have defaults just in case
+DEFAULT_LEAN_PYTHON_VERSION = "3.11"
+DEFAULT_LEAN_STRICT_PYTHON_VERSION = f"{DEFAULT_LEAN_PYTHON_VERSION}.7"
+DEFAULT_LEAN_DOTNET_FRAMEWORK = "net6.0"
 
 # The path to the root python directory in docker image
-DOCKER_PYTHON_SITE_PACKAGES_PATH = f"/root/.local/lib/python{LEAN_PYTHON_VERSION}/site-packages"
+DOCKER_PYTHON_SITE_PACKAGES_PATH = "/root/.local/lib/python{LEAN_PYTHON_VERSION}/site-packages"
 
 # The file in which general CLI configuration is stored
 LEAN_ROOT_PATH = "/Lean/Launcher/bin/Debug"
@@ -91,9 +90,6 @@ UPDATE_CHECK_INTERVAL_DOCKER_IMAGE = 24 * 7
 # The interval in hours at which the CLI checks for new announcements
 UPDATE_CHECK_INTERVAL_ANNOUNCEMENTS = 24
 
-# The product id of the Terminal Link module
-TERMINAL_LINK_PRODUCT_ID = 44
-
 # The name of the Docker network which all Lean CLI containers are ran on
 DOCKER_NETWORK = "lean_cli"
 
diff --git a/lean/container.py b/lean/container.py
index 3f82d5f..f4f4466 100644
--- a/lean/container.py
+++ b/lean/container.py
@@ -95,12 +95,18 @@ class Container:
         self.output_config_manager = OutputConfigManager(self.lean_config_manager)
         self.optimizer_config_manager = OptimizerConfigManager(self.logger)
 
+        self.docker_manager = docker_manager
+        if not self.docker_manager:
+            self.docker_manager = DockerManager(self.logger, self.temp_manager, self.platform_manager)
+
         self.project_manager = ProjectManager(self.logger,
                                               self.project_config_manager,
                                               self.lean_config_manager,
                                               self.path_manager,
                                               self.xml_manager,
-                                              self.platform_manager)
+                                              self.platform_manager,
+                                              self.cli_config_manager,
+                                              self.docker_manager)
         self.library_manager = LibraryManager(self.logger,
                                               self.project_manager,
                                               self.project_config_manager,
@@ -139,10 +145,6 @@ class Container:
                                                          self.project_manager,
                                                          self.organization_manager)
 
-        self.docker_manager = docker_manager
-        if not self.docker_manager:
-            self.docker_manager = DockerManager(self.logger, self.temp_manager, self.platform_manager)
-
         self.lean_runner = lean_runner
         if not self.lean_runner:
             self.lean_runner = LeanRunner(self.logger,


Test patch:
diff --git a/tests/commands/test_research.py b/tests/commands/test_research.py
index c00e65d..a6a2281 100644
--- a/tests/commands/test_research.py
+++ b/tests/commands/test_research.py
@@ -18,7 +18,7 @@ from unittest import mock
 from click.testing import CliRunner
 
 from lean.commands import lean
-from lean.constants import DEFAULT_RESEARCH_IMAGE, LEAN_ROOT_PATH, LEAN_PYTHON_VERSION
+from lean.constants import DEFAULT_RESEARCH_IMAGE, LEAN_ROOT_PATH
 from lean.container import container
 from lean.models.docker import DockerImage
 from tests.test_helpers import create_fake_lean_cli_directory
@@ -166,7 +166,6 @@ def test_research_opens_browser_when_container_started(open) -> None:
     logs = f"""
 [I 21:06:21.500 LabApp] Writing notebook server cookie secret to /root/.local/share/jupyter/runtime/notebook_cookie_secret
 [W 21:06:21.692 LabApp] All authentication is disabled.  Anyone who can connect to this server will be able to run code.
-[I 21:06:21.698 LabApp] JupyterLab extension loaded from /opt/miniconda3/lib/python{LEAN_PYTHON_VERSION}/site-packages/jupyterlab
 [I 21:06:21.698 LabApp] JupyterLab application directory is /opt/miniconda3/share/jupyter/lab
 [I 21:06:21.700 LabApp] Serving notebooks from local directory: /Lean/Launcher/bin/Debug/Notebooks
 [I 21:06:21.700 LabApp] The Jupyter Notebook is running at:
diff --git a/tests/components/docker/test_lean_runner.py b/tests/components/docker/test_lean_runner.py
index 36534aa..1520b5f 100644
--- a/tests/components/docker/test_lean_runner.py
+++ b/tests/components/docker/test_lean_runner.py
@@ -76,7 +76,9 @@ def create_lean_runner(docker_manager: mock.Mock) -> LeanRunner:
                                      lean_config_manager,
                                      path_manager,
                                      xml_manager,
-                                     platform_manager)
+                                     platform_manager,
+                                     cli_config_manager,
+                                     docker_manager)
 
     return LeanRunner(logger,
                       project_config_manager,
diff --git a/tests/components/util/test_library_manager.py b/tests/components/util/test_library_manager.py
index f0263e3..92382d7 100644
--- a/tests/components/util/test_library_manager.py
+++ b/tests/components/util/test_library_manager.py
@@ -16,44 +16,15 @@ from unittest import mock
 
 import pytest
 
-from lean.components.config.lean_config_manager import LeanConfigManager
-from lean.components.config.project_config_manager import ProjectConfigManager
-from lean.components.config.storage import Storage
 from lean.components.util.library_manager import LibraryManager
-from lean.components.util.path_manager import PathManager
-from lean.components.util.platform_manager import PlatformManager
-from lean.components.util.project_manager import ProjectManager
-from lean.components.util.xml_manager import XMLManager
 from lean.container import container
 from lean.models.utils import LeanLibraryReference
+from tests.conftest import initialize_container
 from tests.test_helpers import create_fake_lean_cli_directory
 
 
 def _create_library_manager() -> LibraryManager:
-    cache_storage = Storage(str(Path("~/.lean/cache").expanduser()))
-    lean_config_manager = LeanConfigManager(mock.Mock(),
-                                            mock.Mock(),
-                                            mock.Mock(),
-                                            mock.Mock(),
-                                            cache_storage)
-    platform_manager = PlatformManager()
-    path_manager = PathManager(lean_config_manager, platform_manager)
-    xml_manager = XMLManager()
-    project_config_manager = ProjectConfigManager(xml_manager)
-    logger = mock.Mock()
-    project_manager = ProjectManager(logger,
-                                     project_config_manager,
-                                     lean_config_manager,
-                                     path_manager,
-                                     xml_manager,
-                                     platform_manager)
-
-    return LibraryManager(logger,
-                          project_manager,
-                          project_config_manager,
-                          lean_config_manager,
-                          path_manager,
-                          xml_manager)
+    return initialize_container().library_manager
 
 
 def _project_has_library_reference_in_config(project_dir: Path, library_dir: Path) -> bool:
@@ -96,7 +67,7 @@ def test_get_csharp_lean_library_path_for_csproj_file(project_depth: int) -> Non
     # plus 'Library/CSharp Library/CSharp Project.csproj' (2 more parts),
     # plus the csproj file name (1 more part)
     expected_csproj_file_path = \
-        (Path("/".join(".." for i in range(project_depth + 1))) / library_dir_relative_to_cli / "CSharp Library.csproj")
+        (Path("/".join(".." for _ in range(project_depth + 1))) / library_dir_relative_to_cli / "CSharp Library.csproj")
     assert Path(csproj_file_path) == expected_csproj_file_path
 
 
diff --git a/tests/components/util/test_project_manager.py b/tests/components/util/test_project_manager.py
index cd33162..604c230 100644
--- a/tests/components/util/test_project_manager.py
+++ b/tests/components/util/test_project_manager.py
@@ -20,37 +20,18 @@ from unittest import mock
 import pytest
 from lxml import etree
 
-from lean.components.config.lean_config_manager import LeanConfigManager
 from lean.components.config.project_config_manager import ProjectConfigManager
 from lean.components.config.storage import Storage
-from lean.components.util.path_manager import PathManager
-from lean.components.util.platform_manager import PlatformManager
 from lean.components.util.project_manager import ProjectManager
 from lean.components.util.xml_manager import XMLManager
 from lean.container import container
-from lean.models.api import QCLanguage, QCProjectLibrary, QCProject
+from lean.models.api import QCLanguage, QCProjectLibrary
+from tests.conftest import initialize_container
 from tests.test_helpers import create_fake_lean_cli_directory, create_api_project
 
 
 def _create_project_manager() -> ProjectManager:
-    logger = mock.Mock()
-    xml_manager = XMLManager()
-    project_config_manager = ProjectConfigManager(xml_manager)
-    cache_storage = Storage(str(Path("~/.lean/cache").expanduser()))
-    platform_manager = PlatformManager()
-    lean_config_manager = LeanConfigManager(mock.Mock(),
-                                            mock.Mock(),
-                                            project_config_manager,
-                                            mock.Mock(),
-                                            cache_storage)
-    path_manager = PathManager(lean_config_manager, platform_manager)
-
-    return ProjectManager(logger,
-                          project_config_manager,
-                          lean_config_manager,
-                          path_manager,
-                          xml_manager,
-                          platform_manager)
+    return initialize_container().project_manager
 
 
 def _lists_are_equal(list1: List[Any], list2: List[Any]) -> bool:
diff --git a/tests/conftest.py b/tests/conftest.py
index 547f3ca..c3a8f4c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -19,6 +19,8 @@ import certifi
 import pytest
 from pyfakefs.fake_filesystem import FakeFilesystem
 from responses import RequestsMock
+
+from lean.constants import DEFAULT_LEAN_DOTNET_FRAMEWORK
 from lean.models.api import QCMinimalOrganization
 
 from lean.container import container
@@ -38,6 +40,7 @@ def initialize_container(docker_manager_to_use=None, lean_runner_to_use=None, ap
     docker_manager = mock.Mock()
     if docker_manager_to_use:
         docker_manager = docker_manager_to_use
+    docker_manager.get_image_label = mock.MagicMock(return_value=DEFAULT_LEAN_DOTNET_FRAMEWORK)
 
     lean_runner = mock.Mock()
     if lean_runner_to_use:
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index df289aa..1738797 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -15,6 +15,8 @@ import json
 from datetime import datetime
 from pathlib import Path
 from typing import List
+
+from lean.constants import DEFAULT_LEAN_DOTNET_FRAMEWORK
 from lean.models.cli import (cli_brokerages, cli_data_downloaders, cli_data_queue_handlers,
                              cli_addon_modules, cli_history_provider)
 
@@ -44,7 +46,7 @@ def _get_csharp_project_files(path: Path) -> dict:
             "algorithm-language": "CSharp",
             "parameters": {}
         }),
-        (path / "CSharp Project.csproj"): ProjectManager.get_csproj_file_default_content()
+        (path / "CSharp Project.csproj"): ProjectManager.get_csproj_file_default_content(DEFAULT_LEAN_DOTNET_FRAMEWORK)
     }
 
 
@@ -65,7 +67,7 @@ def _get_fake_libraries() -> dict:
             "parameters": {}
         }),
         (Path.cwd() / "Library" / "CSharp Library" / "CSharp Library.csproj"):
-            ProjectManager.get_csproj_file_default_content()
+            ProjectManager.get_csproj_file_default_content(DEFAULT_LEAN_DOTNET_FRAMEWORK)
     }