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 support for PEP658 core metadata #133

Merged
merged 2 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,20 @@ format listed above of one filename per line, format your file with one JSON
object per line, like this:

```json
{"filename": "dumb-init-1.1.2.tar.gz", "hash": "md5=<hash>", "requires_dist": ["cfgv"], "requires_python": ">=3.6", "uploaded_by": "ckuehl", "upload_timestamp": 1512539924, "yanked_reason": null}
{"filename": "dumb-init-1.1.2.tar.gz", "hash": "sha256=<hash>", "requires_python": ">=3.6", "uploaded_by": "ckuehl", "upload_timestamp": 1512539924, "yanked_reason": null, "core_metadata": "sha256=<hash>"}
```

| Key | Required? | Description |
| -------------------- | --------- | ----------- |
| `filename` | Yes | Name of the file |
| `hash` | No | Hash of the file in the format `<hashalgo>=<hashvalue>` |
| `requires_python` | No | Python requirement string for the package ([PEP345](https://peps.python.org/pep-0345/#requires-python)) |
| `core_metadata` | No | Either string `"true"` or a string in the format `<hashalgo>=<hashvalue>` to indicate metadata is available for this file by appending `.metadata` to the file URL ([PEP658](https://peps.python.org/pep-0658/), [PEP714](https://peps.python.org/pep-0714/)) |
| `uploaded_by` | No | Freeform text to indicate an uploader of the package; only shown on web UI |
| `upload_timestamp` | No | UNIX timestamp to indicate upload time of the package |
| `yanked_reason` | No | Freeform text to indicate the package is yanked for the given reason ([PEP592](https://peps.python.org/pep-0592/)) |
| `requires_dist` | No | _(Deprecated)_ Array of requires_dist dependencies ([PEP345](https://peps.python.org/pep-0345/#requires-python)), used only in the JSON API; consider using `core_metadata` instead |

The `filename` key is required. All other keys are optional and will be used to
provide additional information in your generated repository. This extended
information can be useful to determine, for example, who uploaded a package.
Expand Down
3 changes: 3 additions & 0 deletions dumb_pypi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class Package(NamedTuple):
hash: str | None
requires_dist: tuple[str, ...] | None
requires_python: str | None
core_metadata: str | None
upload_timestamp: int | None
uploaded_by: str | None
yanked_reason: str | None
Expand Down Expand Up @@ -197,6 +198,7 @@ def create(
upload_timestamp: int | None = None,
uploaded_by: str | None = None,
yanked_reason: str | None = None,
core_metadata: str | None = None,
) -> Package:
if not re.match(r'[a-zA-Z0-9_\-\.\+]+$', filename) or '..' in filename:
raise ValueError(f'Unsafe package name: {filename}')
Expand All @@ -210,6 +212,7 @@ def create(
hash=hash,
requires_dist=tuple(requires_dist) if requires_dist is not None else None,
requires_python=requires_python,
core_metadata=core_metadata,
upload_timestamp=upload_timestamp,
uploaded_by=uploaded_by,
yanked_reason=yanked_reason,
Expand Down
3 changes: 3 additions & 0 deletions dumb_pypi/templates/package.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ <h1>{{package_name}}</h1>
{%- if file.requires_python %}
data-requires-python="{{file.requires_python}}"
{%- endif %}
{%- if file.core_metadata %}
data-core-metadata="{{file.core_metadata}}"
{%- endif %}
{%- if file.yanked_reason %}
data-yanked="{{file.yanked_reason}}"
{%- endif %}
Expand Down
16 changes: 15 additions & 1 deletion testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess
import sys
import tempfile
import zipfile
from typing import NamedTuple

from dumb_pypi import main
Expand All @@ -26,6 +27,7 @@ def as_json(self):
return json.dumps({
'filename': self.filename,
'requires_python': self.requires_python,
'core_metadata': 'true' if self.filename.endswith('.whl') else None,
})


Expand All @@ -50,4 +52,16 @@ def make_package(package: FakePackage, path: str) -> None:

subprocess.check_call((sys.executable, setup_py) + args, cwd=td)
created, = os.listdir(os.path.join(td, 'dist'))
shutil.move(os.path.join(td, 'dist', created), os.path.join(path, package.filename))
dest = os.path.join(path, package.filename)
shutil.move(os.path.join(td, 'dist', created), dest)

# Extract PEP658 metadata.
if dest.endswith('.whl'):
with zipfile.ZipFile(dest) as zf:
metadata_path, = (
name
for name in zf.namelist()
if name.endswith('.dist-info/METADATA')
)
with open(f'{dest}.metadata', 'wb') as f:
f.write(zf.read(metadata_path))
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import requests


PIP_TEST_VERSION = '23.1.2'
PIP_TEST_VERSION = '23.2.1'


UrlAndPath = collections.namedtuple('UrlAndPath', ('url', 'path'))
Expand Down
19 changes: 19 additions & 0 deletions tests/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,22 @@ def test_pip_respects_requires_python(tmpdir, tmpweb, pip):
)
downloaded_package, = tmpdir.listdir(fil=os.path.isfile)
assert downloaded_package.basename == 'foo-2.tar.gz'


def test_pip_uses_core_metadata(capfd, tmpdir, tmpweb, pip):
install_packages(
tmpweb.path,
(FakePackage('foo-1-py2.py3-none-any.whl'),)
)
pip_download(
pip,
tmpweb.url + '/simple',
tmpdir.strpath,
'foo',
)
downloaded_package, = tmpdir.listdir(fil=os.path.isfile)
assert downloaded_package.basename == 'foo-1-py2.py3-none-any.whl'
assert (
f'Obtaining dependency information for foo from {tmpweb.url}/pool/foo-1-py2.py3-none-any.whl.metadata'
in capfd.readouterr().out
)
2 changes: 2 additions & 0 deletions tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def test_input_json_all_info():
hash='sha256=deadbeef',
requires_dist=['aspy.yaml'],
requires_python='>=3.6',
core_metadata="sha256=badc0ffee",
uploaded_by='asottile',
upload_timestamp=1528586805,
yanked_reason='Wrong Python Pinning',
Expand All @@ -177,6 +178,7 @@ def test_input_json_all_info():
'hash': 'sha256=deadbeef',
'requires_dist': ('aspy.yaml',),
'requires_python': '>=3.6',
'core_metadata': 'sha256=badc0ffee',
'uploaded_by': 'asottile',
'upload_timestamp': 1528586805,
'yanked_reason': 'Wrong Python Pinning',
Expand Down