Skip to content

Commit 534b8ad

Browse files
tcosentinoclaude
andcommitted
fix: allow pathspec 1.x alongside 0.x
Widen the pathspec constraint to <2 so fal co-installs with black 26 (requires pathspec>=1.0.0). Excludes 1.0.0-1.0.3, which each ship a crash bug fixed by 1.0.4. Python 3.8 keeps resolving pathspec 0.12.x. On 1.x, pin the simple regex backend for sync ignore matching so results do not depend on re2/hyperscan being importable, build the spec once per directory walk, and suppress the deprecated-alias DeprecationWarnings at both call sites. Adds matching-behavior tests that run against 0.12.x (py3.8 lane) and 1.x (py3.9+ lanes) in CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 95bc7eb commit 534b8ad

5 files changed

Lines changed: 104 additions & 11 deletions

File tree

projects/fal/pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ dependencies = [
4545
"argcomplete>=3.1.0,<4; python_version >= '3.10'",
4646
"argcomplete>=3.1.0,!=3.7.1,<4; python_version < '3.10'",
4747
"packaging>=21.3",
48-
"pathspec>=0.11.1,<1",
48+
# 1.0.0-1.0.3 each ship a crash bug, all fixed by 1.0.4 (#100, #102, #103):
49+
# https://github.com/cpburnz/python-pathspec/blob/master/CHANGES.rst
50+
# 1.x needs Python >=3.9, so 3.8 installs keep resolving 0.12.x.
51+
"pathspec>=0.11.1,!=1.0.0,!=1.0.1,!=1.0.2,!=1.0.3,<2",
4952
"pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*",
5053
# serve=True dependencies
5154
# FastAPI 0.123.1 fixes the OpenAPI schema remapping issue that blocked 0.119+.

projects/fal/src/fal/container.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44
import shlex
55
import sys
6+
import warnings
67
from dataclasses import dataclass, field
78
from pathlib import Path
89
from typing import Dict, List, Literal, Optional, Union
@@ -244,15 +245,17 @@ def get_patterns(self) -> List[str]:
244245
return DEFAULT_DOCKERIGNORE_PATTERNS
245246

246247
def get_regex_patterns(self) -> List[str]:
248+
# Deprecated alias on pathspec 1.x; kept deliberately (see fal/sync.py).
247249
from pathspec.patterns.gitwildmatch import GitWildMatchPattern # noqa: PLC0415
248250

249251
patterns = self.get_patterns()
250252
regex_patterns = []
251-
for pattern in patterns:
252-
# Convert ignore patterns to regex, this way we can use `re` at runtime.
253-
regex, _ = GitWildMatchPattern.pattern_to_regex(pattern)
254-
if regex:
255-
regex_patterns.append(regex)
253+
with warnings.catch_warnings():
254+
warnings.simplefilter("ignore", DeprecationWarning)
255+
for pattern in patterns:
256+
regex, _ = GitWildMatchPattern.pattern_to_regex(pattern)
257+
if regex:
258+
regex_patterns.append(regex)
256259
return regex_patterns
257260

258261

projects/fal/src/fal/sync.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
import hashlib
44
import os
5+
import warnings
56
import zipfile
67
from pathlib import Path
78
from typing import TYPE_CHECKING
89

910
if TYPE_CHECKING:
1011
from openapi_fal_rest.client import Client
1112

13+
import pathspec
14+
from packaging.version import Version
1215
from pathspec import PathSpec
1316

17+
_PATHSPEC_IS_1X = Version(pathspec.__version__).major >= 1
18+
1419

1520
def _check_hash(client: Client, target_path: str, hash_string: str) -> bool:
1621
import openapi_fal_rest.api.files.check_dir_hash as check_dir_hash_api
@@ -80,21 +85,29 @@ def _load_gitignore_patterns(dir_path: str) -> list:
8085
return gitignore_patterns
8186

8287

83-
def _is_ignored(file_path: str, gitignore_patterns: list[str]) -> bool:
84-
pathspec = PathSpec.from_lines("gitwildmatch", gitignore_patterns)
85-
return pathspec.match_file(file_path)
88+
def _build_ignore_spec(gitignore_patterns: list[str]) -> PathSpec:
89+
# "gitwildmatch" is deprecated on pathspec 1.x, but do not rename it to
90+
# "gitignore": that name binds different pattern classes on 0.x vs 1.x.
91+
if _PATHSPEC_IS_1X:
92+
# Pin the stdlib backend; 1.x auto-picks re2/hyperscan when installed.
93+
with warnings.catch_warnings():
94+
warnings.simplefilter("ignore", DeprecationWarning)
95+
return PathSpec.from_lines(
96+
"gitwildmatch", gitignore_patterns, backend="simple"
97+
)
98+
return PathSpec.from_lines("gitwildmatch", gitignore_patterns)
8699

87100

88101
def _zip_directory(dir_path: str, zip_path: str) -> None:
89-
gitignore_patterns = _load_gitignore_patterns(dir_path)
102+
ignore_spec = _build_ignore_spec(_load_gitignore_patterns(dir_path))
90103

91104
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
92105
for root, _, files in os.walk(dir_path):
93106
for file in files:
94107
file_path = os.path.join(root, file)
95108
relative_path = os.path.relpath(file_path, dir_path)
96109

97-
if not _is_ignored(relative_path, gitignore_patterns):
110+
if not ignore_spec.match_file(relative_path):
98111
arcname = relative_path
99112
zipf.write(file_path, arcname)
100113

projects/fal/tests/unit/test_container.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for fal.container module."""
22

3+
import re
4+
import warnings
35
from pathlib import Path
46

57
import pytest
@@ -594,6 +596,32 @@ def test_no_context_dir_returns_defaults(self):
594596
# Verify they are regex patterns (contain regex syntax)
595597
assert any("\\.git" in p or "\\.pyc" in p for p in patterns)
596598

599+
def test_regex_patterns_match_ignored_paths(self):
600+
"""Compiled patterns should match ignored paths and skip kept ones."""
601+
img = ContainerImage(
602+
dockerfile_str="FROM python:3.11",
603+
dockerignore=["*.pyc", "node_modules/"],
604+
)
605+
compiled = [re.compile(p) for p in img._dockerignore]
606+
607+
def is_ignored(path: str) -> bool:
608+
# Mirrors how file_sync.py applies these patterns at upload time.
609+
return any(c.search(path) for c in compiled)
610+
611+
assert is_ignored("x/y.pyc")
612+
assert is_ignored("node_modules/pkg/index.js")
613+
assert not is_ignored("src/main.py")
614+
615+
def test_regex_pattern_generation_emits_no_warnings(self):
616+
"""The deprecated pathspec alias must not leak warnings to -W error users."""
617+
with warnings.catch_warnings():
618+
warnings.simplefilter("error")
619+
img = ContainerImage(
620+
dockerfile_str="FROM python:3.11",
621+
dockerignore=["*.pyc"],
622+
)
623+
assert img._dockerignore
624+
597625
def test_loads_dockerignore_file(self, tmp_path: Path):
598626
"""Should load and convert patterns from .dockerignore to regex."""
599627
(tmp_path / ".dockerignore").write_text("*.pyc\n__pycache__/\n.git/")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Tests for fal.sync ignore handling."""
2+
3+
import warnings
4+
5+
import pytest
6+
7+
from fal.sync import _build_ignore_spec
8+
9+
IGNORE_PATTERNS = [
10+
"# comment",
11+
"",
12+
"*.pyc",
13+
"!keep.pyc",
14+
"node_modules/",
15+
"/env",
16+
"build/**",
17+
]
18+
19+
20+
@pytest.mark.parametrize(
21+
"path, expected",
22+
[
23+
("src/main.py", False),
24+
("x/y.pyc", True),
25+
("keep.pyc", False),
26+
("node_modules/pkg/index.js", True),
27+
("env", True),
28+
("sub/env", False),
29+
("build/out/app.bin", True),
30+
],
31+
)
32+
def test_ignore_spec_matches_gitignore_semantics(path, expected):
33+
spec = _build_ignore_spec(IGNORE_PATTERNS)
34+
assert spec.match_file(path) is expected
35+
36+
37+
def test_empty_gitignore_ignores_nothing():
38+
spec = _build_ignore_spec([])
39+
assert not spec.match_file("anything.py")
40+
41+
42+
def test_building_and_matching_emits_no_warnings():
43+
with warnings.catch_warnings():
44+
warnings.simplefilter("error")
45+
spec = _build_ignore_spec(IGNORE_PATTERNS)
46+
assert spec.match_file("x/y.pyc")

0 commit comments

Comments
 (0)