Skip to content

Commit

Permalink
feat: support PEP 723 run requirements
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii committed Nov 3, 2023
1 parent ce27a75 commit 7e9fab4
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## dev

- Support PEP 723 run requirements in `pipx run`.
- Make usage message in `pipx run` show `package_or_url`, so extra will be printed out as well
- Add `--force-reinstall` to pip arguments when `--force` was passed
- Use the py launcher, if available, to select Python version with the `--python` option
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies = [
"importlib-metadata>=3.3.0; python_version < '3.8'",
"packaging>=20.0",
"platformdirs>=2.1.0",
"tomli; python_version < '3.11'",
"userpath>=1.6.0,!=1.9.0",
]
dynamic = ["version"]
Expand Down
61 changes: 34 additions & 27 deletions src/pipx/commands/run.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import datetime
import hashlib
import logging
import re
import sys
import time
import urllib.parse
import urllib.request
Expand All @@ -24,6 +26,11 @@
)
from pipx.venv import Venv

if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -319,41 +326,41 @@ def _http_get_request(url: str) -> str:
raise PipxError(str(e)) from e


PEP723 = re.compile(
r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$"
)


def _get_requirements_from_script(content: str) -> Optional[List[str]]:
# An iterator over the lines in the script. We will
# read through this in sections, so it needs to be an
# iterator, not just a list.
lines = iter(content.splitlines())

for line in lines:
if not line.startswith("#"):
continue
line_content = line[1:].strip()
if line_content == "Requirements:":
break
else:
# No "Requirements:" line in the file
"""
Supports PEP 723.
"""

name = "pyproject"
matches = [m for m in PEP723.finditer(content) if m.group("type") == name]

if not matches:
return None

# We are now at the first requirement
requirements = []
for line in lines:
# Stop at the end of the comment block
if not line.startswith("#"):
break
line_content = line[1:].strip()
# Stop at a blank comment line
if not line_content:
break
if len(matches) > 1:
raise ValueError(f"Multiple {name} blocks found")

content = "".join(
line[2:] if line.startswith("# ") else line[1:]
for line in matches[0].group("content").splitlines(keepends=True)
)

pyproject = tomllib.loads(content)

requirements = []
for requirement in pyproject.get("run", {}).get("requirements", []):
# Validate the requirement
try:
req = Requirement(line_content)
req = Requirement(requirement)
except InvalidRequirement as e:
raise PipxError(f"Invalid requirement {line_content}: {str(e)}") from e
raise PipxError(f"Invalid requirement {requirement}: {e}") from e

# Use the normalised form of the requirement,
# not the original line.
# Use the normalised form of the requirement
requirements.append(str(req))

return requirements
16 changes: 9 additions & 7 deletions tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,9 @@ def test_run_with_requirements(caplog, pipx_temp_env, tmp_path):
script.write_text(
textwrap.dedent(
f"""
# Requirements:
# requests==2.28.1
# /// pyproject
# run.requirements = ["requests==2.28.1"]
# ///
# Check requests can be imported
import requests
Expand Down Expand Up @@ -247,9 +248,9 @@ def test_run_with_requirements_and_args(caplog, pipx_temp_env, tmp_path):
script.write_text(
textwrap.dedent(
f"""
# Requirements:
# packaging
# /// pyproject
# run.requirements = ["packaging"]
# ///
import packaging
import sys
from pathlib import Path
Expand All @@ -267,8 +268,9 @@ def test_run_with_invalid_requirement(capsys, pipx_temp_env, tmp_path):
script.write_text(
textwrap.dedent(
"""
# Requirements:
# this is an invalid requirement
# /// pyproject
# run.requirements = ["this is an invalid requirement"]
# ///
print()
"""
).strip()
Expand Down

0 comments on commit 7e9fab4

Please sign in to comment.