Skip to content

Commit

Permalink
Add pyodide skeleton enable and disable commands (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodmane authored Jan 27, 2025
1 parent 4931c09 commit dcdf3da
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 18 deletions.
77 changes: 66 additions & 11 deletions pyodide_build/cli/skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,70 @@ def callback() -> None:
return


def get_recipe_dir(recipe_dir: str | None) -> Path:
if recipe_dir:
return Path(recipe_dir)
cwd = Path.cwd()
root = build_env.search_pyodide_root(curdir=cwd)

if not root:
root = cwd

return root / "packages"


@app.command("enable")
def enable(
names: list[str],
recipe_dir: str | None = typer.Option(
None,
help="The directory containing the recipe of packages."
"If not specified, the default is ``<cwd>/packages``.",
),
):
recipe_dir_ = get_recipe_dir(recipe_dir)
status = 0
for name in names:
try:
skeleton.enable_package(
recipe_dir_,
name,
)
except skeleton.MkpkgFailedException as e:
status = -1
logger.error("%s update failed: %s", name, e)
except Exception:
print(name)
raise
sys.exit(status)


@app.command("disable")
def disable(
names: list[str],
message: str = typer.Option(
"", "--message", "-m", help="Comment to explain why it was disabled"
),
recipe_dir: str | None = typer.Option(
None,
help="The directory containing the recipe of packages."
"If not specified, the default is ``<cwd>/packages``.",
),
) -> int:
recipe_dir_ = get_recipe_dir(recipe_dir)
status = 0
for name in names:
try:
skeleton.disable_package(recipe_dir_, name, message)
except skeleton.MkpkgFailedException as e:
status = -1
logger.error("%s update failed: %s", name, e)
except Exception:
print(name)
raise
sys.exit(status)


@app.command("pypi")
def new_recipe_pypi(
name: str,
Expand Down Expand Up @@ -47,7 +111,7 @@ def new_recipe_pypi(
help="Which source format is preferred. Options are wheel or sdist. "
"If not specified, then either a wheel or an sdist will be used. ",
),
recipe_dir: str = typer.Option(
recipe_dir: str | None = typer.Option(
None,
help="The directory containing the recipe of packages."
"If not specified, the default is ``<cwd>/packages``.",
Expand All @@ -65,16 +129,7 @@ def new_recipe_pypi(
# It is unlikely that a user will run this command outside of the Pyodide
# tree, so we do not need to initialize the environment at this stage.

if recipe_dir:
recipe_dir_ = Path(recipe_dir)
else:
cwd = Path.cwd()
root = build_env.search_pyodide_root(curdir=cwd)

if not root:
root = cwd

recipe_dir_ = root / "packages"
recipe_dir_ = get_recipe_dir(recipe_dir)

if update or update_patched or update_pinned:
try:
Expand Down
74 changes: 67 additions & 7 deletions pyodide_build/recipe/skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ def _download_wheel(pypi_metadata: URLDict) -> Iterator[Path]:


def run_prettier(meta_path: str | Path) -> None:
subprocess.run(["npx", "prettier", "-w", meta_path], check=True)
try:
subprocess.run(["npx", "prettier", "-w", meta_path], check=True)
except FileNotFoundError:
warnings.warn(
"'npx' executable missing, output has not been prettified.", stacklevel=1
)


def make_package(
Expand Down Expand Up @@ -203,12 +208,7 @@ def make_package(

yaml.representer.ignore_aliases = lambda *_: True
yaml.dump(yaml_content, meta_path)
try:
run_prettier(meta_path)
except FileNotFoundError:
warnings.warn(
"'npx' executable missing, output has not been prettified.", stacklevel=1
)
run_prettier(meta_path)

logger.success(f"Output written to {meta_path}")

Expand Down Expand Up @@ -325,3 +325,63 @@ def update_package(
run_prettier(meta_path)

logger.success(f"Updated {package} from {local_ver} to {pypi_ver}.")


def disable_package(recipe_dir: Path, package: str, message: str) -> None:
yaml = YAML()

meta_path = recipe_dir / package / "meta.yaml"
if not meta_path.exists():
logger.error("%s does not exist", meta_path)
raise MkpkgFailedException(f"{package} recipe not found at {meta_path}")

yaml_content = yaml.load(meta_path.read_bytes())
pkg = yaml_content["package"]
pkg_keys = list(pkg)
# Insert after the version key
version_idx = pkg_keys.index("version") + 1
pkg.insert(version_idx, "_disabled", True)
# Add message above it
if message:
pkg.yaml_set_comment_before_after_key("_disabled", before=message)
yaml.dump(yaml_content, meta_path)
run_prettier(meta_path)


def remove_comment_on_line(pkg: Any, line: int):
# Search for comment on the right line. It's probably after the version key
# where we put it, but this will find it as long as it isn't directly after
# top_level (we don't traverse the tree to look for it).
if pkg.ca.comment and pkg.ca.comment.line == line:
pkg.ca.comment = None
return
for cmts in pkg.ca.items.values():
for idx, cmt in enumerate(cmts):
if cmt and cmt.start_mark.line == line:
cmts[idx] = None
return


def enable_package(recipe_dir: Path, package: str) -> None:
yaml = YAML()

meta_path = recipe_dir / package / "meta.yaml"
if not meta_path.exists():
logger.error("%s does not exist", meta_path)
raise MkpkgFailedException(f"{package} recipe not found at {meta_path}")

text_lines = meta_path.read_text().splitlines()
for idx, line in enumerate(text_lines): # noqa: B007
if line.strip().startswith("_disabled"):
break
else:
# Not disabled, let's return
return
yaml_content = yaml.load(meta_path.read_bytes())
pkg = yaml_content["package"]
if text_lines[idx - 1].strip().startswith("#"):
# There's a comment to remove, we have to hunt it down...
remove_comment_on_line(pkg, idx - 1)
del pkg["_disabled"]
yaml.dump(yaml_content, meta_path)
run_prettier(meta_path)
40 changes: 40 additions & 0 deletions pyodide_build/tests/recipe/test_skeleton.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from pathlib import Path
from textwrap import dedent

import pytest
from packaging import version
Expand Down Expand Up @@ -70,6 +71,45 @@ def test_mkpkg_update(tmpdir, old_dist_type, new_dist_type):
assert db.source.url.endswith(old_ext)


def test_enable_disable(tmpdir):
base_dir = Path(str(tmpdir))

disabled = dedent(
"""\
package:
name: jedi
version: 0.19.1
# Here is some information
_disabled: true
top-level:
- jedi
source:
sha256: shasum
url: aurlhere
requirements:
run:
- parso
about:
home: https://github.com/davidhalter/jedi
PyPI: https://pypi.org/project/jedi
summary: An autocompletion tool for Python that can be used for text editors.
license: MIT
"""
).strip()
enabled_lines = disabled.splitlines()
del enabled_lines[3:5]
enabled = "\n".join(enabled_lines)

package_dir = base_dir / "jedi"
package_dir.mkdir(parents=True)
meta_path = package_dir / "meta.yaml"
meta_path.write_text(disabled)
skeleton.enable_package(base_dir, "jedi")
assert meta_path.read_text().strip() == enabled
skeleton.disable_package(base_dir, "jedi", "Here is some information")
assert meta_path.read_text().strip() == disabled


def test_mkpkg_update_pinned(tmpdir):
base_dir = Path(str(tmpdir))

Expand Down

0 comments on commit dcdf3da

Please sign in to comment.