Skip to content

Commit

Permalink
Merge pull request #9 from jaraco/feature/native-tarfile
Browse files Browse the repository at this point in the history
Provide native tarfile functionality
  • Loading branch information
jaraco authored Apr 6, 2024
2 parents 0a1e149 + 92cd4b7 commit 44fbe82
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 7 deletions.
32 changes: 32 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import http.server
import io
import functools
import tarfile
import threading

import portend
import pytest


@pytest.fixture
def tarfile_served(tmp_path_factory):
"""
Start an HTTP server serving a tarfile.
"""
tmp_path = tmp_path_factory.mktemp('www')
fn = tmp_path / 'served.tgz'
tf = tarfile.open(fn, mode='w:gz')
info = tarfile.TarInfo('served/contents.txt')
tf.addfile(info, io.BytesIO('hello, contents'.encode()))
tf.close()
httpd, url = start_server(tmp_path)
with httpd:
yield url + '/served.tgz'


def start_server(path):
_host, port = addr = ('', portend.find_available_local_port())
Handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=path)
httpd = http.server.HTTPServer(addr, Handler)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
return httpd, f'http://localhost:{port}'
37 changes: 30 additions & 7 deletions jaraco/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@
import os
import shutil
import subprocess
import sys
import tempfile
import urllib.request
import warnings
from typing import Iterator


if sys.version_info < (3, 12):
from backports import tarfile
else:
import tarfile


@contextlib.contextmanager
def pushd(dir: str | os.PathLike) -> Iterator[str | os.PathLike]:
"""
Expand All @@ -34,23 +42,38 @@ def tarball(
) -> Iterator[str | os.PathLike]:
"""
Get a tarball, extract it, yield, then clean up.
>>> import urllib.request
>>> url = getfixture('tarfile_served')
>>> target = getfixture('tmp_path') / 'out'
>>> tb = tarball(url, target_dir=target)
>>> import pathlib
>>> with tb as extracted:
... contents = pathlib.Path(extracted, 'contents.txt').read_text()
>>> assert not os.path.exists(extracted)
"""
if target_dir is None:
target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '')
runner = functools.partial(subprocess.check_call, shell=True)
# In the tar command, use --strip-components=1 to strip the first path and
# then
# use -C to cause the files to be extracted to {target_dir}. This ensures
# that we always know where the files were extracted.
runner('mkdir {target_dir}'.format(**vars()))
os.mkdir(target_dir)
try:
getter = 'wget {url} -O -'
extract = 'tar x{compression} --strip-components=1 -C {target_dir}'
cmd = ' | '.join((getter, extract))
runner(cmd.format(compression=infer_compression(url), **vars()))
req = urllib.request.urlopen(url)
with tarfile.open(fileobj=req, mode='r|gz') as tf:
tf.extractall(path=target_dir, filter=strip_first_component)
yield target_dir
finally:
runner('rm -Rf {target_dir}'.format(**vars()))
shutil.rmtree(target_dir)


def strip_first_component(
member: tarfile.TarInfo,
path,
) -> tarfile.TarInfo:
_, member.name = member.name.split('/', 1)
return member


def _compose(*cmgrs):
Expand Down
1 change: 1 addition & 0 deletions newsfragments/5.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implemented tarfile using native functionality and avoiding subprocessing, making it portable.
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ classifiers =
include_package_data = true
python_requires = >=3.8
install_requires =
backports.tarfile; python_version < "3.12"

[options.extras_require]
testing =
Expand All @@ -28,6 +29,7 @@ testing =
pytest-ruff >= 0.2.1

# local
portend

docs =
# upstream
Expand Down

0 comments on commit 44fbe82

Please sign in to comment.