From 9f4435bf4a8d55fff7d9c53dc963b222325caaaf Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 25 Jun 2026 13:04:18 -0500 Subject: [PATCH 1/3] Migrate off pkg_resources and distutils to fix setuptools compatibility --- .github/workflows/build.yml | 2 +- .github/workflows/regression-testing.yml | 2 +- lean/commands/library/add.py | 14 +++++++------- lean/commands/library/remove.py | 4 ++-- lean/components/docker/lean_runner.py | 4 ++-- lean/components/util/project_manager.py | 4 ++-- lean/components/util/update_manager.py | 4 ++-- requirements.txt | 2 +- setup.py | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db74534d..d63035b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependencies run: | - pip install setuptools==69.5.1 wheel + pip install setuptools wheel pip install -r requirements.txt # Static analysis tools diff --git a/.github/workflows/regression-testing.yml b/.github/workflows/regression-testing.yml index 7bbed604..bbd2452f 100644 --- a/.github/workflows/regression-testing.yml +++ b/.github/workflows/regression-testing.yml @@ -48,7 +48,7 @@ jobs: - name: Install dependencies run: | - pip install setuptools==69.5.1 wheel + pip install setuptools wheel pip install -r requirements.txt - name: Run normal tests diff --git a/lean/commands/library/add.py b/lean/commands/library/add.py index 6ef3d40e..8dbebfe7 100644 --- a/lean/commands/library/add.py +++ b/lean/commands/library/add.py @@ -110,15 +110,15 @@ def _is_pypi_file_compatible(file: Dict[str, Any], required_python_version) -> b :param required_python_version: the Python version to check compatibility for :return: True if the file is compatible with the given Python version, False if not """ - from pkg_resources import Requirement + from packaging.requirements import Requirement - major, minor, patch = required_python_version.version + major, minor = required_python_version.major, required_python_version.minor if file["python_version"] not in [f"py{major}", f"py{major}{minor}", f"cp{major}", f"cp{major}{minor}", "source"]: return False if file["requires_python"] is not None: requires_python = file["requires_python"].rstrip(",") - if str(required_python_version) not in Requirement.parse(f"python{requires_python}").specifier: + if str(required_python_version) not in Requirement(f"python{requires_python}").specifier: return False return True @@ -135,7 +135,7 @@ def _get_pypi_package(name: str, version: Optional[str], python_version: str) -> """ from json import loads from dateutil.parser import isoparse - from distutils.version import StrictVersion + from packaging.version import Version response = container.http_client.get(f"https://pypi.org/pypi/{name}/json", raise_for_status=False) @@ -148,7 +148,7 @@ def _get_pypi_package(name: str, version: Optional[str], python_version: str) -> pypi_data = loads(response.text) name = pypi_data["info"]["name"] - required_python_version = StrictVersion(python_version) + required_python_version = Version(python_version) last_compatible_version = None last_compatible_version_upload_time = None @@ -187,7 +187,7 @@ def _add_python_package_to_requirements(requirements_file: Path, name: str, vers :param name: the name of the package :param version: the version of the package """ - from pkg_resources import Requirement + from packaging.requirements import Requirement if not requirements_file.is_file(): requirements_file.touch() @@ -199,7 +199,7 @@ def _add_python_package_to_requirements(requirements_file: Path, name: str, vers for line in requirements_lines: try: - requirement = Requirement.parse(line) + requirement = Requirement(line) if requirement.name.lower() == name.lower(): new_lines.append(f"{name}=={version}") requirement_added = True diff --git a/lean/commands/library/remove.py b/lean/commands/library/remove.py index ed950398..468fd71c 100644 --- a/lean/commands/library/remove.py +++ b/lean/commands/library/remove.py @@ -64,7 +64,7 @@ def _remove_pypi_package_from_python_project(project_dir: Path, name: str) -> No :param project_dir: the path to the project directory :param name: the name of the library to remove """ - from pkg_resources import Requirement + from packaging.requirements import Requirement logger = container.logger path_manager = container.path_manager @@ -80,7 +80,7 @@ def _remove_pypi_package_from_python_project(project_dir: Path, name: str) -> No for line in requirements_content.splitlines(): try: - requirement = Requirement.parse(line) + requirement = Requirement(line) if requirement.name.lower() != name.lower(): new_lines.append(line) except ValueError: diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py index 14597c2c..67b776a9 100644 --- a/lean/components/docker/lean_runner.py +++ b/lean/components/docker/lean_runner.py @@ -557,12 +557,12 @@ def _concat_python_requirements(self, requirements_files: List[Path]) -> str: :param requirements_files: the paths to the requirements.txt files :return: the normalized requirements from all given requirements.txt files """ - from pkg_resources import Requirement + from packaging.requirements import Requirement requirements = [] for file in requirements_files: for line in file.read_text(encoding="utf-8").splitlines(): try: - requirements.append(Requirement.parse(line)) + requirements.append(Requirement(line)) except ValueError: pass diff --git a/lean/components/util/project_manager.py b/lean/components/util/project_manager.py index 25533217..7a375fd3 100644 --- a/lean/components/util/project_manager.py +++ b/lean/components/util/project_manager.py @@ -707,7 +707,7 @@ def generate_rider_config(self, project_dir: Path) -> bool: :param project_dir: the directory of the project :return: True if the configuration was generated successfully or changes where made, False if otherwise. """ - from pkg_resources import resource_string + from importlib.resources import files ssh_dir = Path("~/.lean/ssh").expanduser() @@ -717,7 +717,7 @@ def generate_rider_config(self, project_dir: Path) -> bool: file_name = ssh_dir / name if not file_name.exists() or file_name.stat().st_size == 0: with (ssh_dir / name).open("wb+") as file: - file.write(resource_string("lean", f"ssh/{name}")) + file.write((files("lean") / "ssh" / name).read_bytes()) made_changes = False # Find Rider's global configuration directory for versions < 2022 diff --git a/lean/components/util/update_manager.py b/lean/components/util/update_manager.py index 0848162b..0aae9320 100644 --- a/lean/components/util/update_manager.py +++ b/lean/components/util/update_manager.py @@ -68,9 +68,9 @@ def warn_if_cli_outdated(self, force: bool = False) -> None: return latest_version = response.json()["info"]["version"] - from distutils.version import StrictVersion + from packaging.version import Version - if StrictVersion(latest_version) > StrictVersion(current_version): + if Version(latest_version) > Version(current_version): self._logger.warn(f"A new release of the Lean CLI is available ({current_version} -> {latest_version})") self._logger.warn("Run `pip install --upgrade lean` to update to the latest version") diff --git a/requirements.txt b/requirements.txt index ebade130..75941be7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # This file contains development dependencies # Production dependencies are stored in setup.py -setuptools==69.5.1 +setuptools -e . wheel diff --git a/setup.py b/setup.py index cc4ae04a..8eae6950 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ def get_stubs_version_range() -> str: "python-dateutil>=2.8.2", "lxml>=4.9.0", "joblib>=1.1.0", - "setuptools", + "packaging", f"quantconnect-stubs{get_stubs_version_range()}", "cryptography>=41.0.4", ] From bb40a4cbe9e727d820a224de72967d4cef7d49e0 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 24 Jun 2026 16:13:38 -0500 Subject: [PATCH 2/3] Fix root help test for Click 8.4 --- tests/test_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 9acef3f5..e877505d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -20,14 +20,14 @@ def test_lean_shows_help_when_called_without_arguments() -> None: result = CliRunner().invoke(lean, []) assert result.exit_code == 0 - assert "Usage: lean [OPTIONS] COMMAND [ARGS]..." in result.output + assert "Usage: lean [OPTIONS]" in result.output def test_lean_shows_help_when_called_with_help_option() -> None: result = CliRunner().invoke(lean, ["--help"]) assert result.exit_code == 0 - assert "Usage: lean [OPTIONS] COMMAND [ARGS]..." in result.output + assert "Usage: lean [OPTIONS]" in result.output def test_lean_shows_error_when_running_unknown_command() -> None: From 1362686dafe7facb797d6a016cd23a61df145453 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 25 Jun 2026 16:59:31 -0500 Subject: [PATCH 3/3] Replace pkg_resources in PyInstaller spec --- scripts/main.spec | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/scripts/main.spec b/scripts/main.spec index e00bde00..30dc230d 100644 --- a/scripts/main.spec +++ b/scripts/main.spec @@ -4,32 +4,29 @@ block_cipher = None def Entrypoint(dist, group, name, **kwargs): - import pkg_resources + from importlib.metadata import distribution, entry_points # get toplevel packages of distribution from metadata def get_toplevel(dist): - distribution = pkg_resources.get_distribution(dist) - if distribution.has_metadata('top_level.txt'): - return list(distribution.get_metadata('top_level.txt').split()) - else: - return [] + top_level = distribution(dist).read_text('top_level.txt') + return top_level.split() if top_level else [] kwargs.setdefault('hiddenimports', []) packages = [] - for distribution in kwargs['hiddenimports']: - packages += get_toplevel(distribution) + for distribution_name in kwargs['hiddenimports']: + packages += get_toplevel(distribution_name) kwargs.setdefault('pathex', []) # get the entry point - ep = pkg_resources.get_entry_info(dist, group, name) - # insert path of the egg at the verify front of the search path - kwargs['pathex'] = [ep.dist.location] + kwargs['pathex'] + ep = next(ep for ep in entry_points(group=group) if ep.name == name) + # insert path of the distribution at the verify front of the search path + kwargs['pathex'] = [str(distribution(dist).locate_file(''))] + kwargs['pathex'] # script name must not be a valid module name to avoid name clashes on import script_path = os.path.join(workpath, name + '-script.py') print("creating script for entry point", dist, group, name) with open(script_path, 'w') as fh: - print("import", ep.module_name, file=fh) - print("%s.%s()" % (ep.module_name, '.'.join(ep.attrs)), file=fh) + print("import", ep.module, file=fh) + print("%s.%s()" % (ep.module, ep.attr), file=fh) for package in packages: print("import", package, file=fh)