Skip to content

Commit

Permalink
feat: Make the parse function part of the public API (#40)
Browse files Browse the repository at this point in the history
<!-- readthedocs-preview pep610 start -->
----
πŸ“š Documentation preview πŸ“š:
https://pep610--40.org.readthedocs.build/en/40/

<!-- readthedocs-preview pep610 end -->
  • Loading branch information
edgarrmondragon authored Feb 6, 2024
1 parent dddc740 commit f1e32e8
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 33 deletions.
8 changes: 8 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"sphinx.ext.intersphinx",
"sphinx.ext.napoleon",
"myst_parser",
"sphinx_copybutton",
"sphinx_design",
]

Expand Down Expand Up @@ -74,9 +75,16 @@
intersphinx_mapping = {
"metadata": ("https://importlib-metadata.readthedocs.io/en/latest", None),
"packaging": ("https://packaging.python.org/en/latest", None),
"pip": ("https://pip.pypa.io/en/stable/", None),
"python": ("https://docs.python.org/3/", None),
}

# -- Options for Sphinx Copybutton ---------------------------------------------------
# https://sphinx-copybutton.readthedocs.io/en/latest/use.html

# Skip all prompt characters generated by pygments, including all console outputs.
copybutton_exclude = ".linenos, .gp, .go"

# -- Options for Myst Parser -------------------------------------------------------
# https://myst-parser.readthedocs.io/en/latest/configuration.html
myst_enable_extensions = ["colon_fence"]
34 changes: 34 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@ else:
:::
::::

It can also be used to parse the direct URL download info in pip's {external:doc}`reference/installation-report`:

```python
import json
import subprocess

import pep610

report = json.loads(
subprocess.run(
[
"pip",
"install",
"--quiet",
"--report",
"-",
"--dry-run",
"git+https://github.com/pypa/packaging@main",
],
capture_output=True,
text=True,
).stdout
)

for package in report["install"]:
if package["is_direct"]:
data = pep610.parse(package["download_info"])
print(data)
```

## Supported formats

```{eval-rst}
Expand Down Expand Up @@ -80,6 +110,10 @@ else:

## Functions

```{eval-rst}
.. autofunction:: pep610.parse
```

```{eval-rst}
.. autofunction:: pep610.read_from_distribution
```
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ optional-dependencies.docs = [
"furo==2024.1.29",
"myst-parser==2",
"sphinx==7.2.6",
"sphinx-copybutton==0.5.2",
"sphinx_design==0.5",
]
urls.Documentation = "https://pep610.readthedocs.io"
Expand Down
96 changes: 63 additions & 33 deletions src/pep610/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,17 @@ def has_valid_algorithms(self: ArchiveInfo) -> bool:
Returns:
Whether the archive has valid hashes.
>>> archive_info = ArchiveInfo(
... hashes={
... "sha256": "1dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db9",
... "md5": "c4e0f0a1e0a5e708c8e3e3c4cbe2e85f",
... },
... )
>>> archive_info.has_valid_algorithms()
True
"""
.. code-block:: pycon
>>> archive_info = ArchiveInfo(
... hashes={
... "sha256": "1dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db9",
... "md5": "c4e0f0a1e0a5e708c8e3e3c4cbe2e85f",
... },
... )
>>> archive_info.has_valid_algorithms()
True
""" # noqa: E501
return set(self.all_hashes).intersection(hashlib.algorithms_guaranteed) != set()

@property
Expand All @@ -147,18 +149,20 @@ def all_hashes(self: Self) -> dict[str, str]:
Returns:
All archive hashes.
>>> archive_info = ArchiveInfo(
... hash=HashData(
... "sha256",
... "2dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db8",
... ),
... hashes={
... "sha256": "1dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db9",
... "md5": "c4e0f0a1e0a5e708c8e3e3c4cbe2e85f",
... },
... )
>>> archive_info.all_hashes
{'sha256': '1dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db9', 'md5': 'c4e0f0a1e0a5e708c8e3e3c4cbe2e85f'}
.. code-block:: pycon
>>> archive_info = ArchiveInfo(
... hash=HashData(
... "sha256",
... "2dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db8",
... ),
... hashes={
... "sha256": "1dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db9",
... "md5": "c4e0f0a1e0a5e708c8e3e3c4cbe2e85f",
... },
... )
>>> archive_info.all_hashes
{'sha256': '1dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db9', 'md5': 'c4e0f0a1e0a5e708c8e3e3c4cbe2e85f'}
""" # noqa: E501
hashes = {}
if self.hash is not None:
Expand Down Expand Up @@ -203,17 +207,23 @@ def is_editable(self: Self) -> bool:
Returns:
Whether the distribution is installed in editable mode.
>>> dir_info = DirInfo(editable=True)
>>> dir_info.is_editable()
True
.. code-block:: pycon
>>> dir_info = DirInfo(editable=True)
>>> dir_info.is_editable()
True
.. code-block:: pycon
>>> dir_info = DirInfo(editable=False)
>>> dir_info.is_editable()
False
>>> dir_info = DirInfo(editable=False)
>>> dir_info.is_editable()
False
>>> dir_info = DirInfo(editable=None)
>>> dir_info.is_editable()
False
.. code-block:: pycon
>>> dir_info = DirInfo(editable=None)
>>> dir_info.is_editable()
False
"""
return self.editable is True

Expand Down Expand Up @@ -280,9 +290,29 @@ def _(data: DirData) -> DirectoryDict:
return {"url": data.url, "dir_info": dir_info}


def _parse(content: str) -> VCSData | ArchiveData | DirData | None:
data = json.loads(content)
def parse(data: dict) -> VCSData | ArchiveData | DirData | None:
"""Parse the direct URL data.
Args:
data: The direct URL data.
Returns:
The parsed direct URL data.
.. code-block:: pycon
>>> parse(
... {
... "url": "https://github.com/pypa/packaging",
... "vcs_info": {
... "vcs": "git",
... "requested_revision": "main",
... "commit_id": "4f42225e91a0be634625c09e84dd29ea82b85e27"
... }
... }
... )
VCSData(url='https://github.com/pypa/packaging', vcs_info=VCSInfo(vcs='git', commit_id='4f42225e91a0be634625c09e84dd29ea82b85e27', requested_revision='main', resolved_revision=None, resolved_revision_type=None))
""" # noqa: E501
if "archive_info" in data:
hashes = data["archive_info"].get("hashes")
hash_data = None
Expand Down Expand Up @@ -332,7 +362,7 @@ def read_from_distribution(dist: Distribution) -> VCSData | ArchiveData | DirDat
DirData(url='file:///home/user/pep610', dir_info=DirInfo(editable=False))
"""
if contents := dist.read_text("direct_url.json"):
return _parse(contents)
return parse(json.loads(contents))

return None

Expand Down
120 changes: 120 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from __future__ import annotations

import json

import pytest

PIP_INSTALL_REPORT = """\
{
"version": "1",
"pip_version": "22.2",
"install": [
{
"download_info": {
"url": "https://files.pythonhosted.org/packages/a4/0c/fbaa7319dcb5eecd3484686eb5a5c5702a6445adb566f01aee6de3369bc4/pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"archive_info": {
"hashes": {
"sha256": "18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"
}
}
},
"is_direct": false,
"is_yanked": false,
"requested": true,
"metadata": {
"name": "pydantic",
"version": "1.9.1",
"requires_dist": [
"typing-extensions (>=3.7.4.3)",
"dataclasses (>=0.6) ; python_version < \\"3.7\\"",
"python-dotenv (>=0.10.4) ; extra == 'dotenv'",
"email-validator (>=1.0.3) ; extra == 'email'"
],
"requires_python": ">=3.6.1",
"provides_extra": [
"dotenv",
"email"
]
}
},
{
"download_info": {
"url": "https://github.com/pypa/packaging",
"vcs_info": {
"vcs": "git",
"requested_revision": "main",
"commit_id": "4f42225e91a0be634625c09e84dd29ea82b85e27"
}
},
"is_direct": true,
"is_yanked": false,
"requested": true,
"metadata": {
"name": "packaging",
"version": "21.4.dev0",
"requires_dist": [
"pyparsing (!=3.0.5,>=2.0.2)"
],
"requires_python": ">=3.7"
}
},
{
"download_info": {
"url": "https://files.pythonhosted.org/packages/6c/10/a7d0fa5baea8fe7b50f448ab742f26f52b80bfca85ac2be9d35cdd9a3246/pyparsing-3.0.9-py3-none-any.whl",
"archive_info": {
"hashes": {
"sha256": "5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
}
}
},
"is_direct": false,
"requested": false,
"metadata": {
"name": "pyparsing",
"version": "3.0.9",
"requires_dist": [
"railroad-diagrams ; extra == \\"diagrams\\"",
"jinja2 ; extra == \\"diagrams\\""
],
"requires_python": ">=3.6.8"
}
},
{
"download_info": {
"url": "https://files.pythonhosted.org/packages/75/e1/932e06004039dd670c9d5e1df0cd606bf46e29a28e65d5bb28e894ea29c9/typing_extensions-4.2.0-py3-none-any.whl",
"archive_info": {
"hashes": {
"sha256": "6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"
}
}
},
"is_direct": false,
"requested": false,
"metadata": {
"name": "typing_extensions",
"version": "4.2.0",
"requires_python": ">=3.7"
}
}
],
"environment": {
"implementation_name": "cpython",
"implementation_version": "3.10.5",
"os_name": "posix",
"platform_machine": "x86_64",
"platform_release": "5.13-generic",
"platform_system": "Linux",
"platform_version": "...",
"python_full_version": "3.10.5",
"platform_python_implementation": "CPython",
"python_version": "3.10",
"sys_platform": "linux"
}
}
"""


@pytest.fixture()
def pip_install_report() -> dict:
"""Return the parsed JSON report of a pip install command."""
return json.loads(PIP_INSTALL_REPORT)
26 changes: 26 additions & 0 deletions tests/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
VCSData,
VCSInfo,
is_editable,
parse,
read_from_distribution,
to_dict,
write_to_distribution,
Expand Down Expand Up @@ -324,6 +325,31 @@ def test_no_file(tmp_path: Path):
assert read_from_distribution(dist) is None


def _get_direct_url_packages(report: dict) -> dict:
"""Get direct URL packages from a pip install report."""
return {
package["metadata"]["name"]: parse(package["download_info"])
for package in report["install"]
if package["is_direct"]
}


def test_parse_pip_install_report(pip_install_report: dict):
"""Test parsing a pip install report."""
packages = _get_direct_url_packages(pip_install_report)

assert packages == {
"packaging": VCSData(
url="https://github.com/pypa/packaging",
vcs_info=VCSInfo(
vcs="git",
requested_revision="main",
commit_id="4f42225e91a0be634625c09e84dd29ea82b85e27",
),
),
}


@pytest.mark.parametrize(
("data", "expected"),
[
Expand Down

0 comments on commit f1e32e8

Please sign in to comment.