Skip to content

Commit 324b4e5

Browse files
committed
Add support for browsing tarballs
1 parent c5a5083 commit 324b4e5

File tree

6 files changed

+47
-15
lines changed

6 files changed

+47
-15
lines changed

README.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ You can see all uploaded package archives for a given package.
3333

3434
You can inspect a package archive's metadata and its contents.
3535

36-
Note that this is currently only supported for `.zip` and `.whl` files, but
37-
support for tarballs is planned.
38-
3936

4037
### Easily view files from package archives
4138

@@ -80,12 +77,3 @@ $ make start-dev
8077
```
8178

8279
to run a copy of the application locally with hot reloading enabled.
83-
84-
85-
## Roadmap
86-
87-
* Add support for non-ZIP files (e.g. `.tar.gz` source distributions).
88-
89-
This shouldn't be too hard, but it may be slow at runtime since unlike zip,
90-
tarballs don't contain an index or support extracting individual files
91-
without reading the entire tarball.

pypi_browser/app.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import pypi_browser.packaging
2828
from pypi_browser import pypi
29+
from pypi_browser.packaging import PackageFormat
2930

3031

3132
PACKAGE_TYPE_NOT_SUPPORTED_ERROR = (
@@ -207,6 +208,7 @@ async def package_file(request: Request) -> Response:
207208
{
208209
'request': request,
209210
'package': package_name,
211+
'package_is_tarball': package.package_format is PackageFormat.TARBALL,
210212
'filename': file_name,
211213
'entries': entries,
212214
'metadata_path': metadata_path,
@@ -319,6 +321,7 @@ async def transfer_file():
319321
{
320322
'request': request,
321323
'package': package_name,
324+
'package_is_tarball': package.package_format is PackageFormat.TARBALL,
322325
'filename': file_name,
323326
'archive_path': archive_path,
324327
'rendered_text': fluffy_code.code.render(
@@ -341,6 +344,7 @@ async def transfer_file():
341344
{
342345
'request': request,
343346
'package': package_name,
347+
'package_is_tarball': package.package_format is PackageFormat.TARBALL,
344348
'filename': file_name,
345349
'archive_path': archive_path,
346350
'metadata': metadata,
@@ -354,6 +358,7 @@ async def transfer_file():
354358
{
355359
'request': request,
356360
'package': package_name,
361+
'package_is_tarball': package.package_format is PackageFormat.TARBALL,
357362
'filename': file_name,
358363
'archive_path': archive_path,
359364
'metadata': metadata,

pypi_browser/packaging.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os.path
66
import re
77
import stat
8+
import tarfile
89
import typing
910
import zipfile
1011
from dataclasses import dataclass
@@ -27,8 +28,6 @@ class PackageType(enum.Enum):
2728
class PackageFormat(enum.Enum):
2829
ZIPFILE = enum.auto()
2930
TARBALL = enum.auto()
30-
TARBALL_GZ = enum.auto()
31-
TARBALL_BZ2 = enum.auto()
3231

3332

3433
@dataclass(frozen=True)
@@ -51,6 +50,19 @@ def _package_entries_from_zipfile(path: str) -> typing.Set[PackageEntry]:
5150
}
5251

5352

53+
def _package_entries_from_tarball(path: str) -> typing.Set[PackageEntry]:
54+
with tarfile.open(path) as tf:
55+
return {
56+
PackageEntry(
57+
path=entry.name,
58+
size=entry.size,
59+
mode=stat.filemode(entry.mode),
60+
)
61+
for entry in tf.getmembers()
62+
if not entry.isdir()
63+
}
64+
65+
5466
ArchiveFile = typing.Union[zipfile.ZipExtFile]
5567

5668

@@ -90,8 +102,10 @@ def from_path(cls, path: str) -> 'Package':
90102
elif name.endswith('.egg'):
91103
package_type = PackageType.EGG
92104
package_format = PackageFormat.ZIPFILE
105+
elif name.endswith(('.tar', '.tar.gz', '.tgz', '.tar.bz2')):
106+
package_type = PackageType.SDIST
107+
package_format = PackageFormat.TARBALL
93108
else:
94-
# TODO: Add support for tarballs
95109
raise UnsupportedPackageType(name)
96110

97111
return cls(
@@ -103,6 +117,8 @@ def from_path(cls, path: str) -> 'Package':
103117
async def entries(self) -> typing.Set[PackageEntry]:
104118
if self.package_format is PackageFormat.ZIPFILE:
105119
return await asyncio.to_thread(_package_entries_from_zipfile, self.path)
120+
elif self.package_format is PackageFormat.TARBALL:
121+
return await asyncio.to_thread(_package_entries_from_tarball, self.path)
106122
else:
107123
raise AssertionError(self.package_format)
108124

@@ -116,5 +132,13 @@ async def open_from_archive(self, path: str) -> str:
116132
yield zip_archive_file
117133
finally:
118134
await asyncio.to_thread(zf.close)
135+
elif self.package_format is PackageFormat.TARBALL:
136+
tf = await asyncio.to_thread(tarfile.open, self.path)
137+
archive_file = await asyncio.to_thread(tf.extractfile, path)
138+
try:
139+
async with AsyncArchiveFile(archive_file) as zip_archive_file:
140+
yield zip_archive_file
141+
finally:
142+
await asyncio.to_thread(tf.close)
119143
else:
120144
raise AssertionError(self.package_format)

pypi_browser/templates/_macros.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,15 @@
88
{% endif %}
99
{% endmacro %}
1010

11+
{% macro tarball_warning(is_tarball) %}
12+
{% if is_tarball %}
13+
<div class="alert alert-warning" role="alert">
14+
You're browsing the contents of a tarball which may be slow as
15+
tarballs do not support random access. Browse a <tt>.whl</tt> or
16+
<tt>.zip</tt> file for best performance.
17+
</div>
18+
{% endif %}
19+
{% endmacro %}
20+
1121
{# vim: ft=jinja
1222
#}

pypi_browser/templates/package_file.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ <h1 class="font-monospace">{{filename}}</h1>
2424

2525
<h5>{{macros.package_type_pill(filename)}}</h5>
2626

27+
{{macros.tarball_warning(package_is_tarball)}}
28+
2729
{% if metadata_path %}
2830
<h4>Package Metadata</h4>
2931
<table class="table table-bordered table-sm small metadata">

pypi_browser/templates/package_file_archive_path.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{% set page = 'package-file-archive-path' %}
22
{% set selected_package_name = package %}
33
{% extends '_base.html' %}
4+
{% import '_macros.html' as macros %}
45

56
{% block title %}{{archive_path}} | {{filename}} | {{package}} | PyPI Browser{% endblock %}
67

@@ -40,6 +41,8 @@ <h1 class="font-monospace">{{archive_path}}</h1>
4041
</ol>
4142
</nav>
4243

44+
{{macros.tarball_warning(package_is_tarball)}}
45+
4346
<div class="row mb-6">
4447
<div class="col col-sm-9">
4548
{% if cannot_render_error %}

0 commit comments

Comments
 (0)