Skip to content

Commit 7070b95

Browse files
committed
rolled back change that made tar its own filter type -- big misunderstanding
1 parent bbc1f78 commit 7070b95

6 files changed

Lines changed: 32 additions & 158 deletions

File tree

src/runloop_api_client/lib/_ignore.py

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from __future__ import annotations
22

33
import os
4-
import tarfile
54
from abc import ABC, abstractmethod
6-
from typing import Callable, Iterable, Optional, Sequence
5+
from typing import Iterable, Optional, Sequence
76
from pathlib import Path, PurePosixPath
87
from dataclasses import dataclass
98

@@ -12,7 +11,6 @@
1211
"IgnoreMatcher",
1312
"DockerIgnoreMatcher",
1413
"FilePatternMatcher",
15-
"TarFilterMatcher",
1614
"read_ignorefile",
1715
"compile_ignore",
1816
"path_match",
@@ -290,74 +288,6 @@ def iter_included_files(
290288
yield file_path
291289

292290

293-
TarFilter = Callable[[tarfile.TarInfo], Optional[tarfile.TarInfo]]
294-
295-
296-
def _compute_included_dirs_from_files(included_files: set[str]) -> set[str]:
297-
"""Return all directory ancestors (plus ``'.'``) for a set of file paths."""
298-
299-
included_dirs: set[str] = {"."}
300-
for rel in included_files:
301-
parent = PurePosixPath(rel).parent
302-
while True:
303-
as_posix = parent.as_posix() or "."
304-
included_dirs.add(as_posix)
305-
if as_posix == ".":
306-
break
307-
parent = parent.parent
308-
return included_dirs
309-
310-
311-
class TarFilterMatcher:
312-
"""Adapt an :class:`IgnoreMatcher` to a :class:`TarFilter`-compatible callable.
313-
314-
This helper precomputes the set of included files under ``root`` using the
315-
provided :class:`IgnoreMatcher` and converts that into a simple tar filter:
316-
317-
- Only files returned by ``matcher.iter_paths(root)`` are included.
318-
- Directory entries are included only when they are ancestors of at least
319-
one included file (plus the root ``'.'`` entry).
320-
321-
Member names passed to ``__call__`` are expected to be relative to
322-
``root`` and to use POSIX ``'/'`` separators, matching the behaviour of
323-
``build_directory_tar`` in :mod:`runloop_api_client.lib.context_loader`.
324-
"""
325-
326-
def __init__(self, root: Path, matcher: IgnoreMatcher) -> None:
327-
self._root = root.resolve()
328-
329-
# Compute the set of included files as relative POSIX paths.
330-
# Note: the majority of the work being performed here is simply to deal with the path to the root.
331-
included_files: set[str] = set()
332-
for path in matcher.iter_paths(self._root):
333-
rel = path.resolve().relative_to(self._root)
334-
rel_posix = PurePosixPath(rel).as_posix()
335-
included_files.add(rel_posix)
336-
337-
included_dirs = _compute_included_dirs_from_files(included_files)
338-
339-
self._included_files = included_files
340-
self._included_dirs = included_dirs
341-
342-
def __call__(self, ti: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
343-
name = ti.name
344-
345-
# The root of the archive is always kept.
346-
if name == ".":
347-
return ti
348-
349-
if ti.isdir():
350-
if name in self._included_dirs:
351-
return ti
352-
return None
353-
354-
# Non-directory entries (files, symlinks, etc.) are kept only if their
355-
# relative path is in the included file set.
356-
if name in self._included_files:
357-
return ti
358-
return None
359-
360-
361291
class IgnoreMatcher(ABC):
362292
"""Abstract interface for ignore matchers like .dockerignore and .gitignore.
363293

src/runloop_api_client/sdk/async_.py

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6-
from typing import Dict, Mapping, Optional, Sequence
6+
from typing import Dict, Mapping, Optional
77
from pathlib import Path
88
from datetime import timedelta
99
from typing_extensions import Unpack
@@ -24,11 +24,10 @@
2424
from .._types import Timeout, NotGiven, not_given
2525
from .._client import DEFAULT_MAX_RETRIES, AsyncRunloop
2626
from ._helpers import detect_content_type
27-
from ..lib._ignore import IgnoreMatcher, TarFilterMatcher, FilePatternMatcher
2827
from .async_devbox import AsyncDevbox
2928
from .async_snapshot import AsyncSnapshot
3029
from .async_blueprint import AsyncBlueprint
31-
from ..lib.context_loader import build_directory_tar
30+
from ..lib.context_loader import TarFilter, build_directory_tar
3231
from .async_storage_object import AsyncStorageObject
3332
from ..types.object_create_params import ContentType
3433

@@ -376,7 +375,7 @@ async def upload_from_dir(
376375
name: Optional[str] = None,
377376
metadata: Optional[Dict[str, str]] = None,
378377
ttl: Optional[timedelta] = None,
379-
ignore: IgnoreMatcher | Sequence[str] | str | None = None,
378+
ignore: TarFilter | None = None,
380379
**options: Unpack[LongRequestOptions],
381380
) -> AsyncStorageObject:
382381
"""Create and upload an object from a local directory.
@@ -391,17 +390,10 @@ async def upload_from_dir(
391390
:type metadata: Optional[Dict[str, str]]
392391
:param ttl: Optional Time-To-Live, after which the object is automatically deleted
393392
:type ttl: Optional[timedelta]
394-
:param ignore: Optional ignore configuration controlling which files from
395-
``dir_path`` are included in the uploaded tarball. This may be:
396-
397-
- An :class:`~runloop_api_client.lib._ignore.IgnoreMatcher`
398-
implementation such as :class:`~runloop_api_client.lib._ignore.DockerIgnoreMatcher`
399-
or :class:`~runloop_api_client.lib._ignore.FilePatternMatcher`.
400-
- A single pattern string.
401-
- A sequence of pattern strings.
402-
403-
Patterns follow Docker-style semantics (``!`` negation, ``**`` support).
404-
:type ignore: Optional[IgnoreMatcher | Sequence[str] | str]
393+
:param ignore: Optional tar filter function compatible with
394+
:meth:`tarfile.TarFile.add`. If provided, it will be called for each
395+
member to allow modification or exclusion (by returning ``None``).
396+
:type ignore: Optional[TarFilter]
405397
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions`
406398
for available options
407399
:return: Wrapper for the uploaded object
@@ -416,17 +408,7 @@ async def upload_from_dir(
416408
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
417409

418410
def synchronous_io() -> bytes:
419-
matcher: IgnoreMatcher | None
420-
if ignore is None:
421-
matcher = None
422-
elif isinstance(ignore, IgnoreMatcher):
423-
matcher = ignore
424-
else:
425-
matcher = FilePatternMatcher(ignore) # type: ignore[arg-type]
426-
427-
if matcher is None:
428-
return build_directory_tar(path)
429-
return build_directory_tar(path, tar_filter=TarFilterMatcher(path, matcher))
411+
return build_directory_tar(path, tar_filter=ignore)
430412

431413
tar_bytes = await asyncio.to_thread(synchronous_io)
432414

src/runloop_api_client/sdk/sync.py

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import Dict, Mapping, Optional, Sequence
5+
from typing import Dict, Mapping, Optional
66
from pathlib import Path
77
from datetime import timedelta
88
from typing_extensions import Unpack
@@ -26,9 +26,8 @@
2626
from ._helpers import detect_content_type
2727
from .snapshot import Snapshot
2828
from .blueprint import Blueprint
29-
from ..lib._ignore import IgnoreMatcher, TarFilterMatcher, FilePatternMatcher
3029
from .storage_object import StorageObject
31-
from ..lib.context_loader import build_directory_tar
30+
from ..lib.context_loader import TarFilter, build_directory_tar
3231
from ..types.object_create_params import ContentType
3332

3433

@@ -375,7 +374,7 @@ def upload_from_dir(
375374
name: Optional[str] = None,
376375
metadata: Optional[Dict[str, str]] = None,
377376
ttl: Optional[timedelta] = None,
378-
ignore: IgnoreMatcher | Sequence[str] | str | None = None,
377+
ignore: TarFilter | None = None,
379378
**options: Unpack[LongRequestOptions],
380379
) -> StorageObject:
381380
"""Create and upload an object from a local directory.
@@ -390,17 +389,10 @@ def upload_from_dir(
390389
:type metadata: Optional[Dict[str, str]]
391390
:param ttl: Optional Time-To-Live, after which the object is automatically deleted
392391
:type ttl: Optional[timedelta]
393-
:param ignore: Optional ignore configuration controlling which files from
394-
``dir_path`` are included in the uploaded tarball. This may be:
395-
396-
- An :class:`~runloop_api_client.lib._ignore.IgnoreMatcher`
397-
implementation such as :class:`~runloop_api_client.lib._ignore.DockerIgnoreMatcher`
398-
or :class:`~runloop_api_client.lib._ignore.FilePatternMatcher`.
399-
- A single pattern string.
400-
- A sequence of pattern strings.
401-
402-
Patterns follow Docker-style semantics (``!`` negation, ``**`` support).
403-
:type ignore: Optional[IgnoreMatcher | Sequence[str] | str]
392+
:param ignore: Optional tar filter function compatible with
393+
:meth:`tarfile.TarFile.add`. If provided, it will be called for each
394+
member to allow modification or exclusion (by returning ``None``).
395+
:type ignore: Optional[TarFilter]
404396
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions`
405397
for available options
406398
:return: Wrapper for the uploaded object
@@ -414,19 +406,7 @@ def upload_from_dir(
414406
name = name or f"{path.name}.tar.gz"
415407
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
416408

417-
# Pick the right matcher
418-
matcher: IgnoreMatcher | None
419-
if ignore is None:
420-
matcher = None
421-
elif isinstance(ignore, IgnoreMatcher):
422-
matcher = ignore
423-
else:
424-
matcher = FilePatternMatcher(ignore) # type: ignore[arg-type]
425-
426-
if matcher is None:
427-
tar_bytes = build_directory_tar(path)
428-
else:
429-
tar_bytes = build_directory_tar(path, tar_filter=TarFilterMatcher(path, matcher))
409+
tar_bytes = build_directory_tar(path, tar_filter=ignore)
430410

431411
obj = self.create(name=name, content_type="tgz", metadata=metadata, ttl_ms=ttl_ms, **options)
432412
obj.upload_content(tar_bytes)

tests/sdk/test_async_clients.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,13 @@ async def test_upload_from_dir_with_inline_ignore_patterns(
425425

426426
client = AsyncStorageObjectOps(mock_async_client)
427427

428-
# Inline patterns: drop logs and anything under build/
429-
obj = await client.upload_from_dir(test_dir, ignore=["*.log", "build/"])
428+
# Tar filter: drop logs and anything under build/
429+
def ignore_logs_and_build(ti: tarfile.TarInfo) -> tarfile.TarInfo | None:
430+
if ti.name.endswith(".log") or ti.name.startswith("build/"):
431+
return None
432+
return ti
433+
434+
obj = await client.upload_from_dir(test_dir, ignore=ignore_logs_and_build)
430435

431436
assert isinstance(obj, AsyncStorageObject)
432437
uploaded_content = http_client.put.call_args[1]["content"]

tests/sdk/test_clients.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
BlueprintOps,
2626
StorageObjectOps,
2727
)
28-
from runloop_api_client.lib._ignore import FilePatternMatcher
2928
from runloop_api_client.lib.polling import PollingConfig
3029

3130

@@ -486,7 +485,7 @@ def test_upload_from_dir_with_string_path(
486485
def test_upload_from_dir_respects_filter(
487486
self, mock_client: Mock, object_view: MockObjectView, tmp_path: Path
488487
) -> None:
489-
"""upload_from_dir should respect ignore patterns when provided."""
488+
"""upload_from_dir should respect a tar filter when provided."""
490489
mock_client.objects.create.return_value = object_view
491490

492491
test_dir = tmp_path / "ctx"
@@ -504,10 +503,13 @@ def test_upload_from_dir_respects_filter(
504503

505504
client = StorageObjectOps(mock_client)
506505

507-
# Ignore patterns: drop logs and anything under build/
508-
matcher = FilePatternMatcher(["*.log", "build/"])
506+
# Tar filter: drop logs and anything under build/
507+
def ignore_logs_and_build(ti: tarfile.TarInfo) -> tarfile.TarInfo | None:
508+
if ti.name.endswith(".log") or ti.name.startswith("build/"):
509+
return None
510+
return ti
509511

510-
obj = client.upload_from_dir(test_dir, ignore=matcher)
512+
obj = client.upload_from_dir(test_dir, ignore=ignore_logs_and_build)
511513

512514
assert isinstance(obj, StorageObject)
513515
uploaded_content = http_client.put.call_args[1]["content"]

tests/test_utils/test_context_loader.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,14 @@
44

55
from runloop_api_client.lib._ignore import (
66
IgnorePattern,
7-
TarFilterMatcher,
87
FilePatternMatcher,
98
is_ignored,
109
path_match,
1110
compile_ignore,
1211
read_ignorefile,
1312
iter_included_files,
1413
)
15-
from runloop_api_client.lib.context_loader import (
16-
build_directory_tar,
17-
build_docker_context_tar,
18-
)
14+
from runloop_api_client.lib.context_loader import build_docker_context_tar
1915

2016

2117
def test_segment_match_basic_globs():
@@ -184,24 +180,3 @@ def test_build_docker_context_tar_supports_file_pattern_matcher(tmp_path: Path)
184180

185181
assert "keep.bin" in names
186182
assert "ignore.txt" not in names
187-
188-
189-
def test_tar_filter_matcher_respects_patterns(tmp_path: Path) -> None:
190-
"""TarFilterMatcher should apply FilePatternMatcher patterns at tar level."""
191-
192-
root = tmp_path
193-
(root / "keep.txt").write_text("keep", encoding="utf-8")
194-
(root / "ignore.log").write_text("ignore", encoding="utf-8")
195-
build_dir = root / "build"
196-
build_dir.mkdir()
197-
(build_dir / "ignored.txt").write_text("ignored", encoding="utf-8")
198-
199-
matcher = FilePatternMatcher(["*.log", "build/"])
200-
tar_bytes = build_directory_tar(root, tar_filter=TarFilterMatcher(root, matcher))
201-
202-
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:gz") as tf:
203-
names = {m.name for m in tf.getmembers()}
204-
205-
assert "keep.txt" in names
206-
assert "ignore.log" not in names
207-
assert not any(name.startswith("build/") for name in names)

0 commit comments

Comments
 (0)