Skip to content

Commit

Permalink
fix: bath path stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed May 24, 2024
1 parent 37eb57b commit e2f01b8
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 85 deletions.
53 changes: 22 additions & 31 deletions src/ape/cli/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ape.cli.choices import _ACCOUNT_TYPE_FILTER, Alias
from ape.logging import logger
from ape.utils.basemodel import ManagerAccessMixin
from ape.utils.os import get_full_extension, path_match
from ape.utils.os import get_full_extension
from ape.utils.validators import _validate_account_alias

if TYPE_CHECKING:
Expand Down Expand Up @@ -53,9 +53,7 @@ class _ContractPaths(ManagerAccessMixin):

def __init__(self, value, project: Optional["ProjectManager"] = None):
self.value = value
self._path_set: set[Path] = set()
self.missing_compilers: set[str] = set() # set of .ext
self.exclude_list: dict[str, bool] = {}
self.project = project or ManagerAccessMixin.local_project

@classmethod
Expand All @@ -74,22 +72,18 @@ def filtered_paths(self) -> set[Path]:
value = self.value
contract_paths: Iterable[Path]

if value and isinstance(value, (list, tuple, set)):
# Given a single list of paths.
contract_paths = value

elif value and isinstance(value, (Path, str)):
if value and isinstance(value, (Path, str)):
# Given single path.
contract_paths = (Path(value),)

elif not value or value == "*":
# Get all file paths in the project.
return {p for p in self.project.sources.paths}

else:
raise ValueError(f"Unknown contracts-paths value '{value}'.")
# Given a sequence of paths.
contract_paths = value

self.lookup(contract_paths)
# Convert source IDs or relative paths to absolute paths.
path_set = self.lookup(contract_paths)

# Handle missing compilers.
if self.missing_compilers:
Expand All @@ -107,51 +101,46 @@ def filtered_paths(self) -> set[Path]:

logger.warning(message)

return self._path_set
return path_set

@property
def exclude_patterns(self) -> set[str]:
return self.config_manager.get_config("compile").exclude or set()

def do_exclude(self, path: Union[Path, str]) -> bool:
name = path if isinstance(path, str) else str(path)
if name not in self.exclude_list:
self.exclude_list[name] = path_match(name, *self.exclude_patterns)

return self.exclude_list[name]
return self.project.sources.is_excluded(path)

def compiler_is_unknown(self, path: Union[Path, str]) -> bool:
path = Path(path)
if self.do_exclude(path):
return False

ext = get_full_extension(path)
unknown_compiler = ext and ext not in self.compiler_manager.registered_compilers
if unknown_compiler and ext not in self.missing_compilers:
self.missing_compilers.add(ext)

return bool(unknown_compiler)

def lookup(self, path_iter: Iterable):
for path in path_iter:
path = Path(path)
if self.do_exclude(path):
continue
def lookup(self, path_iter: Iterable, path_set: Optional[set] = None) -> set[Path]:
path_set = path_set or set()

for path_id in path_iter:
path = Path(path_id)
contracts_folder = self.project.contracts_folder
if (
self.project.path / path.name
) == contracts_folder or path.name == contracts_folder.name:
# Was given the path to the contracts folder.
self.lookup(self.project.sources.paths)
return {p for p in self.project.sources.paths}

elif (self.project.path / path).is_dir():
# Was given sub-dir in the project folder.
self.lookup(p for p in (self.project.path / path).iterdir())
return self.lookup(
(p for p in (self.project.path / path).iterdir()), path_set=path_set
)

elif (contracts_folder / path.name).is_dir():
# Was given sub-dir in the contracts folder.
self.lookup(p for p in (contracts_folder / path.name).iterdir())
return self.lookup(
(p for p in (contracts_folder / path.name).iterdir()), path_set=path_set
)

elif resolved_path := self.project.sources.lookup(path):
# Check compiler missing.
Expand All @@ -162,14 +151,16 @@ def lookup(self, path_iter: Iterable):
suffix = get_full_extension(resolved_path)
if suffix in self.compiler_manager.registered_compilers:
# File exists and is compile-able.
self._path_set.add(resolved_path)
path_set.add(resolved_path)

elif suffix:
raise BadArgumentUsage(f"Source file '{resolved_path.name}' not found.")

else:
raise BadArgumentUsage(f"Source file '{path.name}' not found.")

return path_set


def contract_file_paths_argument():
"""
Expand Down
112 changes: 61 additions & 51 deletions src/ape/managers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
)


def path_to_source_id(path: Path, root_path: Path) -> str:
def _path_to_source_id(path: Path, root_path: Path) -> str:
return f"{get_relative_path(path.absolute(), root_path.absolute())}"


Expand All @@ -65,6 +65,7 @@ def __init__(
self.get_contracts_path = get_contracts_path
self.exclude_globs = exclude_globs or set()
self._sources: dict[str, Source] = {}
self._exclude_cache: dict[str, bool] = {}

@log_instead_of_fail(default="<LocalSources>")
def __repr__(self) -> str:
Expand Down Expand Up @@ -155,39 +156,29 @@ def __contains_path(self, source_path: Path) -> bool:

return False

@cached_property
def _all_files(self) -> list[Path]:
contracts_folder = self.get_contracts_path()
if not contracts_folder.is_dir():
return []

return get_all_files_in_directory(contracts_folder)

@property
def paths(self) -> Iterator[Path]:
"""
All contract sources paths.
"""
if self._path_cache is not None:
yield from [p for p in self._path_cache if p.is_file()]
return

cache: list[Path] = []
for path in self._get_paths():
cache.append(path)
yield path

self._path_cache = cache

def _get_paths(self) -> Iterator[Path]:
# Yield for first time.
contracts_folder = self.get_contracts_path()
cache: list[Path] = []
if not contracts_folder.is_dir():
return

all_files = get_all_files_in_directory(contracts_folder)
for path in all_files:
for path in self._all_files:
if self.is_excluded(path):
continue

cache.append(path)
yield path

# Helps `self.paths` be faster.
self._path_cache = cache
# Caching is only for during this calculation.
# We can't cache after in case files change.
if "_all_files" in self.__dict__:
del self.__dict__["_all_files"]

def is_excluded(self, path: Path) -> bool:
"""
Expand All @@ -200,25 +191,46 @@ def is_excluded(self, path: Path) -> bool:
Returns:
bool
"""
source_id = self._get_source_id(path)
if source_id in self._exclude_cache:
return self._exclude_cache[source_id]

# Non-files and hidden files are ignored.
is_file = path.is_file()
if not is_file or path.name.startswith("."):
# Ignore random hidden files if they are known source types.
self._exclude_cache[source_id] = True
return True

# Files with missing compiler extensions are also ignored.
suffix = get_full_extension(path)
registered = self.compiler_manager.registered_compilers
if suffix not in registered:
self._exclude_cache[source_id] = True
return True

# If we get here, we have a matching compiler and this source exists.
# Check if is excluded.
source_id = self._get_source_id(path)
options = (str(path), path.name, source_id)
parent_dir_name = path.parent.name

for excl in self.exclude_globs:
source_id = self._get_source_id(path)
options = (str(path), path.name, source_id)
# perf: Check parent directory first to exclude faster by marking them all.
if path_match(parent_dir_name, excl):
self._exclude_cache[source_id] = True
for sub in get_all_files_in_directory(path.parent):
sub_source_id = self._get_source_id(sub)
self._exclude_cache[sub_source_id] = True

return True

for opt in options:
if path_match(opt, excl):
self._exclude_cache[source_id] = True
return True

registered = self.compiler_manager.registered_compilers
is_file = path.is_file()
suffix = get_full_extension(path)
if is_file and suffix in registered:
return False

elif is_file and path.name.startswith("."):
# Ignore random hidden files if they are known source types.
return True

# Likely from a source that doesn't have an installed compiler.
self._exclude_cache[source_id] = False
return False

def lookup(self, path_id: Union[str, Path]) -> Optional[Path]:
Expand Down Expand Up @@ -290,7 +302,7 @@ def find_in_dir(dir_path: Path, path: Path) -> Optional[Path]:
return find_in_dir(self.root_path, relative_path)

def _get_source_id(self, path: Path) -> str:
return path_to_source_id(path, self.root_path)
return _path_to_source_id(path, self.root_path)

def _get_path(self, source_id: str) -> Path:
return self.root_path / source_id
Expand Down Expand Up @@ -362,7 +374,7 @@ def get(self, name: str, compile_missing: bool = True) -> Optional[ContractConta

if compile_missing:
# Try again after compiling all missing.
self._compile_missing_contracts(self.sources._get_paths())
self._compile_missing_contracts(self.sources.paths)
return self.get(name, compile_missing=False)

return None
Expand Down Expand Up @@ -411,7 +423,7 @@ def _load_contracts(self, use_cache: bool = True) -> dict[str, ContractContainer

def _compile_all(self, use_cache: bool = True) -> Iterator[ContractContainer]:
if sources := self.sources:
paths = sources._get_paths()
paths = sources.paths
yield from self._compile(paths, use_cache=use_cache)

def _compile(
Expand All @@ -424,12 +436,11 @@ def _compile(
path_ls_final = []
for path in path_ls:
path = Path(path)
if path.is_absolute():
if path.is_file() and path.is_absolute():
path_ls_final.append(path)
elif (self.project.path / path).exists():
elif (self.project.path / path).is_file():
path_ls_final.append(self.project.path / path)
else:
raise FileNotFoundError(str(path))
# else: is no longer a file (deleted).

# Compile necessary contracts.
if needs_compile := list(
Expand All @@ -438,7 +449,7 @@ def _compile(
self._compile_contracts(needs_compile)

src_ids = [
(f"{get_relative_path(Path(p).absolute(), self.project.path)}") for p in path_ls_final
f"{get_relative_path(Path(p).absolute(), self.project.path)}" for p in path_ls_final
]
for contract_type in (self.project.manifest.contract_types or {}).values():
if contract_type.source_id and contract_type.source_id in src_ids:
Expand All @@ -461,6 +472,9 @@ def _detect_change(self, path: Union[Path, str]) -> bool:
):
return True # New file.

elif not path.is_file():
return False # No longer exists.

# ethpm_types strips trailing white space and ensures
# a newline at the end so content so `splitlines()` works.
# We need to do the same here for to prevent the endless recompiling bug.
Expand Down Expand Up @@ -620,6 +634,7 @@ def install(

# Either never fetched, it is missing but present in manifest, or we are forcing.
if not unpacked:
logger.info(f"Fetching {self.api.package_id} {self.api.version_id}")
# No sources found! Fetch the project.
shutil.rmtree(self.project_path, ignore_errors=True)
self.project_path.parent.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -2135,9 +2150,7 @@ def load_contracts(
self, *source_ids: Union[str, Path], use_cache: bool = True
) -> dict[str, ContractContainer]:
paths = (
[(self.path / src_id) for src_id in source_ids]
if source_ids
else self.sources._get_paths()
[(self.path / src_id) for src_id in source_ids] if source_ids else self.sources.paths
)
return {
c.contract_type.name: c
Expand Down Expand Up @@ -2206,9 +2219,6 @@ def _create_contract_source(self, contract_type: ContractType) -> Optional[Contr
return None

def _update_contract_types(self, contract_types: dict[str, ContractType]):
# First, ensure paths are up to date.
_ = self.sources._get_paths()

super()._update_contract_types(contract_types)

if "ABI" in [x.value for x in self.config.compile.output_extra]:
Expand Down
9 changes: 8 additions & 1 deletion src/ape/utils/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,19 @@ def __exit__(self, *exc):
sys.path.append(path)


def get_full_extension(path: Path) -> str:
def get_full_extension(path: Union[Path, str]) -> str:
"""
For a path like ``Path("Contract.t.sol")``,
returns ``.t.sol``, unlike the regular Path
property ``.suffix`` which returns ``.sol``.
Args:
path (Path | str): The path with an extension.
Returns:
str: The full suffix
"""
path = Path(path)
if path.is_dir():
return ""

Expand Down
5 changes: 4 additions & 1 deletion src/ape_compile/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ def cli(
project.dependencies
) > 0:
for dependency in project.dependencies:
# Even if compiling we failed, we at least tried
# and so we don't need to warn "Nothing to compile".
compiled = True

try:
contract_types = dependency.project.load_contracts(use_cache=use_cache)
except Exception as err:
Expand All @@ -84,7 +88,6 @@ def cli(
continue

cli_ctx.logger.success(f"'{dependency.project.name}' compiled.")
compiled = True
if display_size:
_display_byte_code_sizes(cli_ctx, contract_types)

Expand Down
2 changes: 2 additions & 0 deletions tests/functional/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ def test_load_contracts_after_deleting_same_named_contract(tmp_project, compiler

result = tmp_project.load_contracts()
assert "foo" not in result # Was deleted.
# Also ensure it is gone from paths.
assert "foo.__mock__" not in [x.name for x in tmp_project.sources.paths]

# Create a new contract with the same name.
new_contract = tmp_project.contracts_folder / "bar.__mock__"
Expand Down
Loading

0 comments on commit e2f01b8

Please sign in to comment.