Skip to content

Commit

Permalink
fix: issues with pattern matching for source file exclusions (#2081)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored May 9, 2024
1 parent 2fd4240 commit d2cdb0d
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 12 deletions.
5 changes: 2 additions & 3 deletions src/ape/cli/arguments.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from fnmatch import fnmatch
from functools import cached_property
from pathlib import Path
from typing import Iterable, List, Set, Union
Expand All @@ -9,7 +8,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_all_files_in_directory, get_full_extension
from ape.utils.os import get_all_files_in_directory, get_full_extension, path_match
from ape.utils.validators import _validate_account_alias


Expand Down Expand Up @@ -111,7 +110,7 @@ def exclude_patterns(self) -> List[str]:
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] = any(fnmatch(name, p) for p in self.exclude_patterns)
self.exclude_list[name] = path_match(name, *self.exclude_patterns)

return self.exclude_list[name]

Expand Down
9 changes: 2 additions & 7 deletions src/ape/managers/project/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
from fnmatch import fnmatch
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence

Expand All @@ -10,7 +9,7 @@
from ape.api import ProjectAPI
from ape.logging import logger
from ape.managers.config import CONFIG_FILE_NAME as APE_CONFIG_FILE_NAME
from ape.utils import cached_property, get_all_files_in_directory, get_relative_path
from ape.utils import cached_property, get_all_files_in_directory, get_relative_path, path_match


class _ProjectSources:
Expand Down Expand Up @@ -180,11 +179,7 @@ def create_manifest(
set(
[p for p in self.source_paths if p in file_paths]
if file_paths
else [
p
for p in self.source_paths
if not any(fnmatch(p, e) for e in compile_config.exclude)
]
else [p for p in self.source_paths if not path_match(p, *compile_config.exclude)]
)
)

Expand Down
2 changes: 2 additions & 0 deletions src/ape/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
get_all_files_in_directory,
get_full_extension,
get_relative_path,
path_match,
run_in_tempdir,
use_temp_sys_path,
)
Expand Down Expand Up @@ -114,6 +115,7 @@
"only_raise_attribute_error",
"parse_coverage_tables",
"parse_gas_table",
"path_match",
"raises_not_implemented",
"returns_array",
"run_in_tempdir",
Expand Down
4 changes: 2 additions & 2 deletions src/ape/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
DEFAULT_TRANSACTION_TYPE = 0
DEFAULT_MAX_RETRIES_TX = 20
SOURCE_EXCLUDE_PATTERNS = (
"**/.cache/**",
".cache",
".DS_Store",
".gitkeep",
"**/.build/**/*.json",
".build",
"*.md",
"*.rst",
"*.txt",
Expand Down
39 changes: 39 additions & 0 deletions src/ape/utils/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re
import sys
from contextlib import contextmanager
from fnmatch import fnmatch
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any, Callable, Iterator, List, Optional, Pattern, Union
Expand Down Expand Up @@ -203,9 +204,47 @@ def run_in_tempdir(
Args:
fn (Callable): A function that takes a path. It gets called
with the resolved path to the temporary directory.
name (str): Optionally name the temporary directory.
Returns:
Any: The result of the function call.
"""
with create_tempdir(name=name) as temp_dir:
return fn(temp_dir)


def path_match(path: Union[str, Path], *exclusions: str) -> bool:
"""
A better glob-matching function. For example:
>>> from pathlib import Path
>>> p = Path("test/to/.build/me/2/file.json")
>>> p.match("**/.build/**")
False
>>> from ape.utils.os import path_match
>>> path_match(p, "**/.build/**")
True
"""
path_str = str(path)
path_path = Path(path)

for excl in exclusions:
if fnmatch(path_str, excl):
return True

elif fnmatch(path_path.name, excl):
return True

else:
# If the exclusion is he full name of any of the parents
# (e.g. ".cache", it is a match).
for parent in path_path.parents:
if parent.name == excl:
return True

# Walk the path recursively.
relative_str = path_str.replace(str(parent), "").strip(os.path.sep)
if fnmatch(relative_str, excl):
return True

return False
3 changes: 3 additions & 0 deletions src/ape_compile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class Config(PluginConfig):
exclude: Set[str] = set()
"""
Source exclusion globs across all file types.
**NOTE**: ``ape.utils.misc.SOURCE_EXCLUDE_PATTERNS`` are automatically
included in this set.
"""

cache_folder: Optional[Path] = None
Expand Down
92 changes: 92 additions & 0 deletions tests/functional/utils/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import pytest

from ape.utils.misc import SOURCE_EXCLUDE_PATTERNS
from ape.utils.os import (
create_tempdir,
get_all_files_in_directory,
get_full_extension,
get_relative_path,
path_match,
run_in_tempdir,
)

Expand Down Expand Up @@ -117,3 +119,93 @@ def fn(p):
assert arguments[0].resolve() == arguments[0]
if name is not None:
assert arguments[0].name == name


@pytest.mark.parametrize(
"path",
(
"path/to/.cache/file/1.json",
Path("path/to/.cache/file/1.json"),
".cache",
Path(".cache"),
Path("contracts/.cache/TEST.json"),
),
)
def test_path_match_cache_folder(path):
assert path_match(path, *SOURCE_EXCLUDE_PATTERNS)


@pytest.mark.parametrize(
"path",
(
"path/to/.build/__local__.json",
Path("path/to/.build/__local__.json"),
".build",
Path(".build"),
Path("contracts/.build/TEST.json"),
),
)
def test_path_match_build_folder(path):
assert path_match(path, *SOURCE_EXCLUDE_PATTERNS)


@pytest.mark.parametrize(
"path",
(
"path/to/MyFile.build.json/__local__.json",
Path("path/to/MyFile.build.json/__local__.json"),
".builder",
Path(".cacher"),
Path("contracts/.builder/TEST.json"),
),
)
def test_pat_match_does_not_match_but_close(path):
"""
Ensures we don't get false positives.
"""
assert not path_match(path, *SOURCE_EXCLUDE_PATTERNS)


@pytest.mark.parametrize(
"path",
(
".gitkeep",
"path/to/.gitkeep",
"index.html",
"path/to/index.css",
"py.typed",
),
)
def test_path_match(path):
assert path_match(path, *SOURCE_EXCLUDE_PATTERNS)


@pytest.mark.parametrize(
"path,exc",
[
("path/to/MyContract.t.sol", "*t.sol"),
("contracts/mocks/MyContract.sol", "mocks"),
("mocks/MyContract.sol", "mocks"),
("MockContract.sol", "Mock*"),
],
)
def test_path_match_test_contracts(path, exc):
assert not path_match(path, *SOURCE_EXCLUDE_PATTERNS)
exclusions = [*SOURCE_EXCLUDE_PATTERNS, exc]
assert path_match(path, *exclusions)


@pytest.mark.parametrize(
"path",
(
Path("path/to/contracts/exclude_dir/subdir/MyContract.json"),
Path("exclude_dir/subdir/MyContract.json"),
Path("exclude_dir/MyContract.json"),
),
)
def test_path_match_recurse_dir(path):
"""
Testing a specific way of excluding all the files in a directory.
"""
excl = "exclude_dir/**"
assert path_match(path, excl)

0 comments on commit d2cdb0d

Please sign in to comment.