Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add cph list subcommand #236

Merged
merged 13 commits into from
Apr 18, 2024
19 changes: 19 additions & 0 deletions news/236-list-subcommand
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Add `cph list` to report artifact contents without prior extraction. (#236)

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
10 changes: 10 additions & 0 deletions src/conda_package_handling/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,13 @@ def get_pkg_details(in_file):
else:
raise ValueError(f"Don't know what to do with file {in_file}")
return details


def list_contents(in_file, verbose=False):
for format in SUPPORTED_EXTENSIONS.values():
if format.supported(in_file):
details = format.list_contents(in_file, verbose=verbose)
break
else:
raise ValueError(f"Don't know what to do with file {in_file}")
return details
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 10 additions & 0 deletions src/conda_package_handling/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ def build_parser():
type=int,
default=1,
)
list_parser = sp.add_parser("list", help="list package contents", aliases=["l"])
list_parser.add_argument("archive_path", help="path to archive to inspect")
list_parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Increase verbosity of output",
)

return parser

Expand Down Expand Up @@ -135,6 +143,8 @@ def main(args=None):
print("failed files:")
pprint(failed_files)
sys.exit(1)
elif args.subcommand in ("list", "l"):
api.list_contents(args.archive_path, verbose=args.verbose)


if __name__ == "__main__": # pragma: no cover
Expand Down
9 changes: 8 additions & 1 deletion src/conda_package_handling/conda_fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from . import utils
from .interface import AbstractBaseFormat
from .streaming import _extract
from .streaming import _extract, _list

CONDA_PACKAGE_FORMAT_VERSION = 2
DEFAULT_COMPRESSION_TUPLE = (".tar.zst", "zstd", "zstd:compression-level=19")
Expand Down Expand Up @@ -142,3 +142,10 @@ def get_pkg_details(in_file):
size = stat_result.st_size
md5, sha256 = utils.checksums(in_file, ("md5", "sha256"))
return {"size": size, "md5": md5, "sha256": sha256}

@staticmethod
def list_contents(fn, verbose=False, **kw):
components = utils.ensure_list(kw.get("components")) or ("info", "pkg")
kenodegard marked this conversation as resolved.
Show resolved Hide resolved
if not os.path.isabs(fn):
fn = os.path.normpath(os.path.join(os.getcwd(), fn))
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
_list(fn, components=components, verbose=verbose)
5 changes: 5 additions & 0 deletions src/conda_package_handling/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ def create(prefix, file_list, out_fn, out_folder=os.getcwd(), **kw): # pragma:
@abc.abstractmethod
def get_pkg_details(in_file): # pragma: no cover
raise NotImplementedError

@staticmethod
@abc.abstractmethod
kenodegard marked this conversation as resolved.
Show resolved Hide resolved
def list_contents(in_file, verbose=False, **kw): # pragma: no cover
raise NotImplementedError
57 changes: 42 additions & 15 deletions src/conda_package_handling/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

from __future__ import annotations

from tarfile import TarError
import io
from contextlib import redirect_stdout
from tarfile import TarError, TarFile, TarInfo
from typing import Generator
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
from zipfile import BadZipFile

from conda_package_streaming.extract import exceptions as cps_exceptions
Expand All @@ -13,30 +16,54 @@
from . import exceptions


def _extract(filename: str, dest_dir: str, components: list[str]):
"""
Extract .conda or .tar.bz2 package to dest_dir.

If it's a conda package, components may be ["pkg", "info"]

If it's a .tar.bz2 package, components must equal ["pkg"]

Internal. Skip directly to conda-package-streaming if you don't need
exception compatibility.
"""

def _stream_components(
filename: str,
components: list[str],
dest_dir: str = "",
) -> Generator[tuple[TarFile, TarInfo], None, None]:
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
if str(filename).endswith(".tar.bz2"):
assert components == ["pkg"]

try:
with open(filename, "rb") as fileobj:
for component in components:
# will parse zipfile twice
stream = package_streaming.stream_conda_component(
yield package_streaming.stream_conda_component(
filename, fileobj, component=component
)
extract_stream(stream, dest_dir)
except cps_exceptions.CaseInsensitiveFileSystemError as e:
raise exceptions.CaseInsensitiveFileSystemError(filename, dest_dir) from e
except (OSError, TarError, BadZipFile) as e:
raise exceptions.InvalidArchiveError(filename, f"failed with error: {str(e)}") from e


def _extract(filename: str, dest_dir: str, components: list[str]):
"""
Extract .conda or .tar.bz2 package to dest_dir.

If it's a conda package, components may be ["pkg", "info"]

If it's a .tar.bz2 package, components must equal ["pkg"]

Internal. Skip directly to conda-package-streaming if you don't need
exception compatibility.
"""
for stream in _stream_components(filename, components, dest_dir=dest_dir):
try:
extract_stream(stream, dest_dir)
except cps_exceptions.CaseInsensitiveFileSystemError as e:
raise exceptions.CaseInsensitiveFileSystemError(filename, dest_dir) from e
except (OSError, TarError, BadZipFile) as e:
raise exceptions.InvalidArchiveError(filename, f"failed with error: {str(e)}") from e


def _list(filename: str, components: list[str], verbose=True):
memfile = io.StringIO()
for component in _stream_components(filename, components):
for tar, _ in component:
with redirect_stdout(memfile):
tar.list(verbose=verbose)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, such re-use.

component.close()
kenodegard marked this conversation as resolved.
Show resolved Hide resolved
memfile.seek(0)
lines = sorted(memfile.readlines(), key=lambda line: line.split(None, 5)[-1])
print("".join(lines))
6 changes: 6 additions & 0 deletions src/conda_package_handling/tarball.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,9 @@ def get_pkg_details(in_file):
size = stat_result.st_size
md5, sha256 = utils.checksums(in_file, ("md5", "sha256"))
return {"size": size, "md5": md5, "sha256": sha256}

@staticmethod
def list_contents(fn, verbose=False, **kw):
if not os.path.isabs(fn):
fn = os.path.normpath(os.path.join(os.getcwd(), fn))
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
streaming._list(str(fn), components=["pkg"], verbose=verbose)
16 changes: 16 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import io
import os
from contextlib import redirect_stdout
from pathlib import Path

import pytest
Expand Down Expand Up @@ -46,3 +49,16 @@ def test_import_main():
"""
with pytest.raises(SystemExit):
import conda_package_handling.__main__ # noqa


@pytest.mark.parametrize(
"artifact,n_files",
[("mock-2.0.0-py37_1000.conda", 43), ("mock-2.0.0-py37_1000.tar.bz2", 43)],
)
def test_list(artifact, n_files):
"Integration test to ensure `cph list` works correctly."
memfile = io.StringIO()
with redirect_stdout(memfile):
cli.main(["list", os.path.join(data_dir, artifact)])
memfile.seek(0)
assert n_files == sum(1 for line in memfile.readlines() if line.strip())
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
Loading