Repository: ApeWorX/ape

Issue:
After re-installing compiled dependency, can't access contracts
### Have you ensured that all of these are up to date?

- [ ] Ape
- [ ] Ape plugins

### What version of Ape are you on?

_No response_

### What command(s) is the bug in?

_No response_

### Operating System

macOS (Apple Silicon)

### Ape Configuration

_No response_

### Description

Setup is a bit complicated but

1. Download a dependency
2. Compile and use contracts from it
3. Re-install dependency (to get latest changes)
4. Attempt to access contract again... It fails for me with Solidity saying No input and does not return any contracts. Doing `ape pm list` shows it as compiled already... not sure what is going, but it ain't right

### Reproduction code

_No response_

Hints from issue comments:
@linear: <p><a href="https://linear.app/apeworx/issue/APE-1951/after-re-installing-compiled-dependency-cant-access-contracts">APE-1951 After re-installing compiled dependency, can't access contracts</a></p>

Solution patch:
diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py
index 533e9e362..1be7b358d 100644
--- a/src/ape/managers/project.py
+++ b/src/ape/managers/project.py
@@ -1206,7 +1206,7 @@ class DependencyManager(BaseManager):
         return result
 
     def __contains__(self, name: str) -> bool:
-        for dependency in self.installed:
+        for dependency in self.all:
             if name == dependency.name:
                 return True
 
@@ -1214,17 +1214,17 @@ class DependencyManager(BaseManager):
 
     def keys(self) -> Iterator[str]:
         _ = [x for x in self.specified]  # Install specified if needed.
-        for dependency in self.installed:
+        for dependency in self.all:
             yield dependency.name
 
     def items(self) -> Iterator[tuple[str, dict[str, "ProjectManager"]]]:
         _ = [x for x in self.specified]  # Install specified if needed.
-        for dependency in self.installed:
+        for dependency in self.all:
             yield dependency.name, {dependency.version: dependency.project}
 
     def values(self) -> Iterator[dict[str, "ProjectManager"]]:
         _ = [x for x in self.specified]  # Install specified if needed.
-        for dependency in self.installed:
+        for dependency in self.all:
             yield {dependency.version: dependency.project}
 
     @property
@@ -1319,12 +1319,10 @@ class DependencyManager(BaseManager):
         for data in self.config.dependencies:
             yield self.decode_dependency(**data)
 
-    # TODO: We may want to discern between dependencies where their API files are known
-    #   versus dependencies where their projects are cached, as there is a difference.
     @property
-    def installed(self) -> Iterator[Dependency]:
+    def all(self) -> Iterator[Dependency]:
         """
-        All installed dependencies, regardless of their project
+        All dependencies known by Ape, regardless of their project
         affiliation. NOTE: By "installed" here, we simply
         mean the API files are cached and known by Ape.
         However, it does not guarantee the project is
@@ -1341,7 +1339,7 @@ class DependencyManager(BaseManager):
                 if not api_file.is_file():
                     continue
 
-                data = json.loads(api_file.read_text())
+                data = json.loads(api_file.read_text(encoding="utf-8"))
                 api = self.decode_dependency(**data)
                 if api.name == self.project.name:
                     # Don't include self as a dependency
@@ -1350,6 +1348,16 @@ class DependencyManager(BaseManager):
 
                 yield self._create_dependency(api)
 
+    # TODO: Remove this in 0.9.
+    @property
+    def installed(self) -> Iterator[Dependency]:
+        """
+        DEPRECATED: Use ``.all``. Deprecated because of confusion
+        between this and uninstalled dependencies Ape still nows about
+        but require an extra install step, such as fetching from GitHub.
+        """
+        yield from self.all
+
     @property
     def uri_map(self) -> dict[str, Url]:
         """
@@ -1415,7 +1423,7 @@ class DependencyManager(BaseManager):
 
         # Check already-installed first to prevent having to install anything.
         name_matches = []
-        for dependency in self.installed:
+        for dependency in self.all:
             if dependency.package_id == name and dependency.version == version:
                 # If matching package-id, use that no matter what.
                 return dependency
@@ -1435,7 +1443,7 @@ class DependencyManager(BaseManager):
         # Was not found in this project's dependencies.
         checked.add(self.project.project_id)
 
-        deps = [*self.installed]
+        deps = [*self.all]
         if allow_install:
             deps.extend([*self.specified])
 
@@ -1481,7 +1489,7 @@ class DependencyManager(BaseManager):
 
         # Yield any remaining installed.
         using_package_id = False
-        for dependency in self.installed:
+        for dependency in self.all:
             if dependency.package_id != name:
                 continue
 
@@ -1497,7 +1505,7 @@ class DependencyManager(BaseManager):
             return
 
         # Never yield. Check if using short-name.
-        for dependency in self.installed:
+        for dependency in self.all:
             if dependency.name != name:
                 continue
 
@@ -1508,19 +1516,25 @@ class DependencyManager(BaseManager):
             versions_yielded.add(dependency.version)
 
     def _create_dependency(self, api: DependencyAPI) -> Dependency:
+        attempt_cache = True
         try:
             is_cached = self._cache.__contains__(api)
         except ProjectError:
             # Certain kinds of dependencies have no version ID
             # when uninstalled, and it will cause the hash error.
             is_cached = False
+            attempt_cache = False
 
         if is_cached:
             return self._cache[api]
 
         # Create new instance.
         dependency = Dependency(api, project=self.project)
-        self._cache[api] = dependency
+
+        # Only attempt cache if we are not getting an error hitting it.
+        if attempt_cache:
+            self._cache[api] = dependency
+
         return dependency
 
     def get_dependency(


Test patch:
diff --git a/tests/functional/test_dependencies.py b/tests/functional/test_dependencies.py
index 66686552d..860c1cc9b 100644
--- a/tests/functional/test_dependencies.py
+++ b/tests/functional/test_dependencies.py
@@ -281,7 +281,7 @@ def test_uninstall(name, project_with_downloaded_dependencies):
     dependency = dm.get_dependency(name, version)
     dependency.uninstall()
     assert not any(
-        (d.name == name or d.package_id == name) and d.version == version for d in dm.installed
+        (d.name == name or d.package_id == name) and d.version == version for d in dm.all
     )
 
 
@@ -314,6 +314,35 @@ def test_unpack_no_contracts_folder(project, with_dependencies_project_path):
         assert "empty-dependency" in subdirs
 
 
+def test_all(project):
+    # Set up an installed dependency.
+    packages_cache = project.dependencies.packages_cache
+    corrupt_file = packages_cache.api_folder / "dep123" / "0_4_0.json"
+    corrupt_file.parent.mkdir(exist_ok=True, parents=True)
+    corrupt_api = {"name": "dep123", "local": "."}
+    corrupt_file.write_text(json.dumps(corrupt_api), encoding="utf8")
+
+    # Iterate through the installed list.
+    deps = [x for x in project.dependencies.all if x.name == "dep123"]
+
+    # Show it is there.
+    assert len(deps) >= 1
+    assert deps[0].name == "dep123"
+    assert not deps[0].installed
+
+
+def test_all_when_contains_corrupt_dependency(project):
+    packages_cache = project.dependencies.packages_cache
+    name = "anothermissingpackagefromanapietest"
+    corrupt_file = packages_cache.api_folder / name / "0_1_0.json"
+    corrupt_file.parent.mkdir(exist_ok=True, parents=True)
+    corrupt_api = {"name": name, "site_package": name}
+    corrupt_file.write_text(json.dumps(corrupt_api), encoding="utf8")
+    deps = [x for x in project.dependencies.all if x.name == name]
+    assert len(deps) > -1
+    assert not deps[0].installed
+
+
 class TestPackagesCache:
     @pytest.fixture
     def cache(self):