From be8f67e88fcef53f5178c5a9fb08ed79e5e00dce Mon Sep 17 00:00:00 2001 From: antazoey Date: Wed, 18 Dec 2024 07:06:12 +0700 Subject: [PATCH] fix: Python & NPM dependencies when not installed in Python/NPM cause `ape pm list` to fail (#2419) --- src/ape/api/projects.py | 2 +- src/ape/managers/project.py | 22 +++++++++++++---- src/ape_pm/__init__.py | 2 +- src/ape_pm/_cli.py | 8 +++++-- tests/functional/test_dependencies.py | 24 +++++++++++++++++++ .../only-dependencies/ape-config.yaml | 3 +++ tests/integration/cli/test_compile.py | 5 +++- tests/integration/cli/test_pm.py | 10 ++++---- 8 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/ape/api/projects.py b/src/ape/api/projects.py index 5349ede9dc..6a4cf254f7 100644 --- a/src/ape/api/projects.py +++ b/src/ape/api/projects.py @@ -29,7 +29,7 @@ class DependencyAPI(BaseInterfaceModel): def package_id(self) -> str: """ The full name of the package, used for storage. - Example: ``OpenZeppelin/openzepplin-contracts``. + Example: ``OpenZeppelin/openzeppelin-contracts``. """ @property diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py index e2f1ee591b..a1b183e5f9 100644 --- a/src/ape/managers/project.py +++ b/src/ape/managers/project.py @@ -655,7 +655,13 @@ def installed(self) -> bool: if self._installation is not None: return True - elif self.project_path.is_dir(): + try: + project_path = self.project_path + except ProjectError: + # Fails when version ID errors out (bad config / missing required install etc.) + return False + + if project_path.is_dir(): if any(x for x in self.project_path.iterdir() if not x.name.startswith(".")): return True @@ -1202,13 +1208,22 @@ def get_project_dependencies( """ for api in self.config_apis: + try: + api_version_id = api.version_id + except Exception: + api_version_id = None + if (name is not None and api.name != name and api.package_id != name) or ( - version is not None and api.version_id != version + version is not None and api_version_id != version ): continue # Ensure the dependency API data is known. - dependency = self.add(api) + if api_version_id is not None: + dependency = self.add(api) + else: + # Errored. + dependency = Dependency(api) if allow_install: try: @@ -1464,7 +1479,6 @@ def add(self, dependency: Union[dict, DependencyAPI]) -> Dependency: Returns: class:`~ape.managers.project.Dependency` """ - api = self.decode_dependency(**dependency) if isinstance(dependency, dict) else dependency self.packages_cache.cache_api(api) diff --git a/src/ape_pm/__init__.py b/src/ape_pm/__init__.py index 291b3540b3..af93ff9e4e 100644 --- a/src/ape_pm/__init__.py +++ b/src/ape_pm/__init__.py @@ -15,7 +15,7 @@ def dependencies(): yield "github", _dependencies.GithubDependency yield "local", _dependencies.LocalDependency yield "npm", _dependencies.NpmDependency - yield ("python", "pypi"), _dependencies.PythonDependency + yield ("python", "pypi", "site_package"), _dependencies.PythonDependency @plugins.register(plugins.ProjectPlugin) diff --git a/src/ape_pm/_cli.py b/src/ape_pm/_cli.py index c87e268adc..f7a3e388e0 100644 --- a/src/ape_pm/_cli.py +++ b/src/ape_pm/_cli.py @@ -27,7 +27,6 @@ def _list(cli_ctx, list_all): """ List installed packages """ - dm = cli_ctx.dependency_manager packages = [] dependencies = [*list(dm.get_project_dependencies(use_cache=True, allow_install=False))] @@ -56,9 +55,14 @@ def _list(cli_ctx, list_all): else dependency.package_id ) + try: + version = dependency.version + except ProjectError: + version = "" + item = { "name": name, - "version": dependency.version, + "version": version, "installed": is_installed, "compiled": is_compiled, } diff --git a/tests/functional/test_dependencies.py b/tests/functional/test_dependencies.py index f51a00a331..a89a7451e9 100644 --- a/tests/functional/test_dependencies.py +++ b/tests/functional/test_dependencies.py @@ -216,6 +216,22 @@ def test_add_dependency_with_dependencies(project, with_dependencies_project_pat assert actual.version == "local" +def test_get_project_dependencies(project, with_dependencies_project_path): + installed_package = {"name": "web3", "site_package": "web3"} + not_installed_package = { + "name": "apethisisnotarealpackageape", + "site_package": "apethisisnotarealpackageape", + } + with project.temp_config(dependencies=[installed_package, not_installed_package]): + dm = project.dependencies + actual = list(dm.get_project_dependencies()) + assert len(actual) == 2 + assert actual[0].name == "web3" + assert actual[0].installed + assert actual[1].name == "apethisisnotarealpackageape" + assert not actual[1].installed + + def test_install(project, mocker): with project.isolate_in_tempdir() as tmp_project: contracts_path = tmp_project.path / "src" @@ -664,6 +680,14 @@ def test_installed(self, dependency): dependency.install() assert dependency.installed + def test_installed_version_id_fails(self, project): + api = PythonDependency( + site_package="apethisdependencyisnotinstalled", + name="apethisdependencyisnotinstalled", + ) + dependency = Dependency(api, project) + assert not dependency.installed + def test_compile(self, project): with create_tempdir() as path: api = LocalDependency(local=path, name="ooga", version="1.0.0") diff --git a/tests/integration/cli/projects/only-dependencies/ape-config.yaml b/tests/integration/cli/projects/only-dependencies/ape-config.yaml index f1c320fdb6..083155e2f5 100644 --- a/tests/integration/cli/projects/only-dependencies/ape-config.yaml +++ b/tests/integration/cli/projects/only-dependencies/ape-config.yaml @@ -7,6 +7,9 @@ dependencies: config_override: contracts_folder: sources + - name: dependency-that-is-not-installed + site_package: apedependencythatisnotinstalledape + compile: # NOTE: this should say `include_dependencies: false` below. # (it gets replaced with `true` in a test temporarily) diff --git a/tests/integration/cli/test_compile.py b/tests/integration/cli/test_compile.py index 938b2dbe3a..39a25fc980 100644 --- a/tests/integration/cli/test_compile.py +++ b/tests/integration/cli/test_compile.py @@ -363,9 +363,12 @@ def test_compile_only_dependency(ape_cli, runner, integ_project, clean_cache, ap "--include-dependencies", ) result = runner.invoke(ape_cli, arguments, catch_exceptions=False) - assert result.exit_code == 0, result.output + assert result.exit_code == 1, result.output # exit_code=1 because 1 dependency is bad. assert expected_log_message in result.output + error_str = "Dependency 'apedependencythatisnotinstalledape' not installed." + assert error_str in result.output + @skip_projects_except("with-contracts") def test_raw_compiler_output_bytecode(integ_project): diff --git a/tests/integration/cli/test_pm.py b/tests/integration/cli/test_pm.py index 8b51fcfd32..c55822e6a5 100644 --- a/tests/integration/cli/test_pm.py +++ b/tests/integration/cli/test_pm.py @@ -308,8 +308,9 @@ def test_list(pm_runner, integ_project): # NOTE: Not using f-str here so we can see the spacing. expected = """ -NAME VERSION INSTALLED COMPILED -dependency-in-project-only local False False +NAME VERSION INSTALLED COMPILED +apedependencythatisnotinstalledape False False +dependency-in-project-only local False False """.strip() assert expected in result.output @@ -318,8 +319,9 @@ def test_list(pm_runner, integ_project): dependency.install() expected = """ -NAME VERSION INSTALLED COMPILED -dependency-in-project-only local True False +NAME VERSION INSTALLED COMPILED +apedependencythatisnotinstalledape False False +dependency-in-project-only local True False """.strip() result = pm_runner.invoke("list") assert result.exit_code == 0, result.output