-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add command to compile python to TEAL with Puyapy
- Loading branch information
1 parent
1a15d5d
commit 1030799
Showing
12 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import logging | ||
|
||
import click | ||
|
||
from algokit.core.compile.python import find_valid_puyapy_command | ||
from algokit.core.proc import run | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@click.group("compile", hidden=True) | ||
@click.option( | ||
"-v", | ||
"--version", | ||
"version", | ||
required=False, | ||
default=None, | ||
help=( | ||
"Compiler version, for example, 1.0.0. " | ||
"If the version isn't specified, AlgoKit will check if the compiler is installed locally, and execute that. " | ||
"If the compiler is not found, it will install the latest version. " | ||
"If the version is specified, AlgoKit will check if the local compiler's version satisfies, and execute that. " | ||
"Otherwise, AlgoKit will install the specifed compiler version." | ||
), | ||
) | ||
@click.pass_context | ||
def compile_group(context: click.Context, version: str | None) -> None: | ||
"""Compile high level language smart contracts to TEAL""" | ||
context.ensure_object(dict) | ||
context.obj["version"] = version | ||
|
||
|
||
@click.command( | ||
context_settings={ | ||
"ignore_unknown_options": True, | ||
}, | ||
add_help_option=False, | ||
) | ||
@click.argument("puya_args", nargs=-1, type=click.UNPROCESSED) | ||
@click.pass_context | ||
def compile_py_command(context: click.Context, puya_args: list[str]) -> None: | ||
""" | ||
Compile Python contract(s) to TEAL with PuyaPy | ||
""" | ||
version = str(context.obj["version"]) if context.obj["version"] else None | ||
|
||
puya_command = find_valid_puyapy_command(version) | ||
|
||
run_result = run( | ||
[ | ||
*puya_command, | ||
*puya_args, | ||
], | ||
) | ||
click.echo(run_result.output) | ||
|
||
if run_result.exit_code != 0: | ||
click.secho( | ||
"An error occurred during compile. Ensure supplied files are valid PuyaPy code before retrying.", | ||
err=True, | ||
fg="red", | ||
) | ||
raise click.exceptions.Exit(run_result.exit_code) | ||
|
||
|
||
compile_group.add_command(compile_py_command, "python") | ||
compile_group.add_command(compile_py_command, "py") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
from collections.abc import Iterator | ||
|
||
from algokit.core.proc import run | ||
from algokit.core.utils import extract_version_triple, find_valid_pipx_command | ||
|
||
|
||
def find_valid_puyapy_command(version: str | None) -> list[str]: | ||
return _find_puya_command_at_version(version) if version is not None else _find_puya_command() | ||
|
||
|
||
def _find_puya_command_at_version(version: str) -> list[str]: | ||
""" | ||
Find puya command with a specific version. | ||
If the puya version isn't installed, install it with pipx run. | ||
""" | ||
for puyapy_command in _get_candidates_puyapy_commands(): | ||
try: | ||
puyapy_version_result = run([*puyapy_command, "--version"]) | ||
except OSError: | ||
pass # in case of path/permission issues, go to next candidate | ||
else: | ||
if puyapy_version_result.exit_code == 0 and ( | ||
extract_version_triple(version) == extract_version_triple(puyapy_version_result.output) | ||
): | ||
return puyapy_command | ||
|
||
pipx_command = find_valid_pipx_command( | ||
"Unable to find pipx install so that the `PuyaPy` compiler can be installed; " | ||
"please install pipx via https://pypa.github.io/pipx/ " | ||
"and then try `algokit compile py ...` again." | ||
) | ||
|
||
return [ | ||
*pipx_command, | ||
"run", | ||
f"puya=={version}", | ||
] | ||
|
||
|
||
def _find_puya_command() -> list[str]: | ||
""" | ||
Find puya command. | ||
If puya isn't installed, install the latest version with pipx. | ||
""" | ||
for puyapy_command in _get_candidates_puyapy_commands(): | ||
try: | ||
puyapy_help_result = run([*puyapy_command, "-h"]) | ||
except OSError: | ||
pass # in case of path/permission issues, go to next candidate | ||
else: | ||
if puyapy_help_result.exit_code == 0: | ||
return puyapy_command | ||
|
||
pipx_command = find_valid_pipx_command( | ||
"Unable to find pipx install so that the `PuyaPy` compiler can be installed; " | ||
"please install pipx via https://pypa.github.io/pipx/ " | ||
"and then try `algokit compile py ...` again." | ||
) | ||
_install_puyapy_with_pipx(pipx_command) | ||
return ["puyapy"] | ||
|
||
|
||
def _install_puyapy_with_pipx(pipx_command: list[str]) -> None: | ||
run( | ||
[ | ||
*pipx_command, | ||
"install", | ||
"puya", | ||
], | ||
bad_return_code_error_message=( | ||
"Unable to install puya via pipx; please install puya manually and try `algokit compile py ...` again." | ||
), | ||
) | ||
|
||
|
||
def _get_candidates_puyapy_commands() -> Iterator[list[str]]: | ||
# when puya is installed at the project level | ||
yield ["poetry", "run", "puyapy"] | ||
# when puya is installed at the global level | ||
yield ["puyapy"] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
VALID_PUYA_CONTRACT_FILE_CONTENT = """ | ||
from puyapy import Contract, Txn, log | ||
class HelloWorldContract(Contract): | ||
def approval_program(self) -> bool: | ||
name = Txn.application_args(0) | ||
log(b"Hello, " + name) | ||
return True | ||
def clear_state_program(self) -> bool: | ||
return True | ||
""" | ||
|
||
INVALID_PUYA_CONTRACT_FILE_CONTENT = """ | ||
from puyapy import Contract, Txn, log | ||
class HelloWorldContract(Contract): | ||
def approval_program(self) -> bool: | ||
name = Txn.application_args_invalid(0) | ||
log(b"Hello, " + name) | ||
return True | ||
def clear_state_program(self) -> bool: | ||
return True | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import os | ||
import sys | ||
from pathlib import Path | ||
|
||
import pytest | ||
from pytest_mock import MockerFixture | ||
|
||
from tests.compile.conftest import INVALID_PUYA_CONTRACT_FILE_CONTENT, VALID_PUYA_CONTRACT_FILE_CONTENT | ||
from tests.utils.approvals import verify | ||
from tests.utils.click_invoker import invoke | ||
from tests.utils.proc_mock import ProcMock | ||
|
||
|
||
def _normalize_path(path: Path) -> str: | ||
return str(path.absolute()).replace("\\", r"\\") | ||
|
||
|
||
@pytest.fixture() | ||
def dummy_contract_path() -> Path: | ||
return Path(__file__).parent / "dummy_contract.py" | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def cwd(tmp_path_factory: pytest.TempPathFactory) -> Path: | ||
return tmp_path_factory.mktemp("cwd", numbered=True) | ||
|
||
|
||
@pytest.fixture() | ||
def output_path(cwd: Path) -> Path: | ||
return cwd / "output" | ||
|
||
|
||
def test_compile_py_help(mocker: MockerFixture) -> None: | ||
proc_mock = ProcMock() | ||
proc_mock.set_output(["poetry", "run", "puyapy", "-h"], output=["Puyapy help"]) | ||
|
||
mocker.patch("algokit.core.proc.Popen").side_effect = proc_mock.popen | ||
result = invoke("compile python -h") | ||
|
||
assert result.exit_code == 0 | ||
verify(result.output) | ||
|
||
|
||
def test_puyapy_is_not_installed_anywhere(dummy_contract_path: Path, mocker: MockerFixture) -> None: | ||
proc_mock = ProcMock() | ||
proc_mock.should_bad_exit_on(["poetry", "run", "puyapy", "-h"], exit_code=1, output=["Puyapy not found"]) | ||
proc_mock.should_bad_exit_on(["puyapy", "-h"], exit_code=1, output=["Puyapy not found"]) | ||
|
||
proc_mock.set_output(["pipx", "--version"], ["1.0.0"]) | ||
|
||
proc_mock.set_output(["pipx", "install", "puya"], ["Puyapy is installed"]) | ||
proc_mock.set_output(["puyapy", str(dummy_contract_path)], ["Done"]) | ||
|
||
mocker.patch("algokit.core.proc.Popen").side_effect = proc_mock.popen | ||
|
||
result = invoke(f"compile python {_normalize_path(dummy_contract_path)}") | ||
|
||
assert result.exit_code == 0 | ||
verify(result.output) | ||
|
||
|
||
def test_specificed_puyapy_version_is_not_installed(dummy_contract_path: Path, mocker: MockerFixture) -> None: | ||
current_version = "1.0.0" | ||
target_version = "1.1.0" | ||
|
||
proc_mock = ProcMock() | ||
proc_mock.set_output(["poetry", "run", "puyapy", "--version"], output=[current_version]) | ||
proc_mock.should_bad_exit_on(["puyapy", "--version"], exit_code=1, output=["Puyapy not found"]) | ||
|
||
proc_mock.set_output(["pipx", "--version"], ["1.0.0"]) | ||
proc_mock.set_output(["pipx", "run", f"puya=={target_version}", str(dummy_contract_path)], ["Done"]) | ||
|
||
mocker.patch("algokit.core.proc.Popen").side_effect = proc_mock.popen | ||
|
||
result = invoke(f"compile --version {target_version} py {_normalize_path(dummy_contract_path)}") | ||
|
||
assert result.exit_code == 0 | ||
verify(result.output) | ||
|
||
|
||
def test_puyapy_is_installed_in_project(dummy_contract_path: Path, mocker: MockerFixture) -> None: | ||
proc_mock = ProcMock() | ||
proc_mock.set_output(["poetry", "run", "puyapy", "-h"], output=["Puyapy help"]) | ||
proc_mock.set_output(["poetry", "run", "puyapy", str(dummy_contract_path)], ["Done"]) | ||
|
||
mocker.patch("algokit.core.proc.Popen").side_effect = proc_mock.popen | ||
|
||
result = invoke(f"compile python {_normalize_path(dummy_contract_path)}") | ||
|
||
assert result.exit_code == 0 | ||
verify(result.output) | ||
|
||
|
||
def test_puyapy_is_installed_globally(dummy_contract_path: Path, mocker: MockerFixture) -> None: | ||
proc_mock = ProcMock() | ||
proc_mock.should_bad_exit_on(["poetry", "run", "puyapy", "-h"], exit_code=1, output=["Puyapy not found"]) | ||
|
||
proc_mock.set_output(["puyapy", "-h"], output=["Puyapy help"]) | ||
proc_mock.set_output(["puyapy", str(dummy_contract_path)], ["Done"]) | ||
|
||
mocker.patch("algokit.core.proc.Popen").side_effect = proc_mock.popen | ||
|
||
result = invoke(f"compile python {_normalize_path(dummy_contract_path)}") | ||
|
||
assert result.exit_code == 0 | ||
verify(result.output) | ||
|
||
|
||
@pytest.mark.skipif(sys.version_info < (3, 12), reason="PuyaPy requires python3.12 or higher") | ||
def test_valid_contract(cwd: Path, output_path: Path) -> None: | ||
# Set NO_COLOR to 1 to avoid requirements for colorama on Windows | ||
os.environ["NO_COLOR"] = "1" | ||
|
||
contract_path = cwd / "contract.py" | ||
contract_path.write_text(VALID_PUYA_CONTRACT_FILE_CONTENT) | ||
|
||
result = invoke(f"compile python {_normalize_path(contract_path)} --out-dir {_normalize_path(output_path)}") | ||
|
||
# Only check for the exit code, don't check the results from PuyaPy | ||
assert result.exit_code == 0 | ||
|
||
|
||
@pytest.mark.skipif(sys.version_info < (3, 12), reason="PuyaPy requires python3.12 or higher") | ||
def test_invalid_contract(cwd: Path, output_path: Path) -> None: | ||
# Set NO_COLOR to 1 to avoid requirements for colorama on Windows | ||
os.environ["NO_COLOR"] = "1" | ||
|
||
contract_path = cwd / "contract.py" | ||
contract_path.write_text(INVALID_PUYA_CONTRACT_FILE_CONTENT) | ||
result = invoke(f"compile python {_normalize_path(contract_path)} --out-dir {_normalize_path(output_path)}") | ||
|
||
# Only check for the exit code and the error message from AlgoKit CLI | ||
assert result.exit_code == 1 | ||
result.output.endswith( | ||
"An error occurred during compile. Ensure supplied files are valid PuyaPy code before retrying." | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
DEBUG: Running 'poetry run puyapy -h' in '{current_working_directory}' | ||
DEBUG: poetry: Puyapy help | ||
DEBUG: Running 'poetry run puyapy -h' in '{current_working_directory}' | ||
DEBUG: poetry: Puyapy help | ||
Puyapy help |
7 changes: 7 additions & 0 deletions
7
tests/compile/test_python.test_puyapy_is_installed_globally.approved.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
DEBUG: Running 'poetry run puyapy -h' in '{current_working_directory}' | ||
DEBUG: poetry: Puyapy not found | ||
DEBUG: Running 'puyapy -h' in '{current_working_directory}' | ||
DEBUG: puyapy: Puyapy help | ||
DEBUG: Running 'puyapy {current_working_directory}/tests/compile/dummy_contract.py' in '{current_working_directory}' | ||
DEBUG: puyapy: Done | ||
Done |
5 changes: 5 additions & 0 deletions
5
tests/compile/test_python.test_puyapy_is_installed_in_project.approved.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
DEBUG: Running 'poetry run puyapy -h' in '{current_working_directory}' | ||
DEBUG: poetry: Puyapy help | ||
DEBUG: Running 'poetry run puyapy {current_working_directory}/tests/compile/dummy_contract.py' in '{current_working_directory}' | ||
DEBUG: poetry: Done | ||
Done |
11 changes: 11 additions & 0 deletions
11
tests/compile/test_python.test_puyapy_is_not_installed_anywhere.approved.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
DEBUG: Running 'poetry run puyapy -h' in '{current_working_directory}' | ||
DEBUG: poetry: Puyapy not found | ||
DEBUG: Running 'puyapy -h' in '{current_working_directory}' | ||
DEBUG: puyapy: Puyapy not found | ||
DEBUG: Running 'pipx --version' in '{current_working_directory}' | ||
DEBUG: pipx: 1.0.0 | ||
DEBUG: Running 'pipx install puya' in '{current_working_directory}' | ||
DEBUG: pipx: Puyapy is installed | ||
DEBUG: Running 'puyapy {current_working_directory}/tests/compile/dummy_contract.py' in '{current_working_directory}' | ||
DEBUG: puyapy: Done | ||
Done |
9 changes: 9 additions & 0 deletions
9
tests/compile/test_python.test_specificed_puyapy_version_is_not_installed.approved.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
DEBUG: Running 'poetry run puyapy --version' in '{current_working_directory}' | ||
DEBUG: poetry: 1.0.0 | ||
DEBUG: Running 'puyapy --version' in '{current_working_directory}' | ||
DEBUG: puyapy: Puyapy not found | ||
DEBUG: Running 'pipx --version' in '{current_working_directory}' | ||
DEBUG: pipx: 1.0.0 | ||
DEBUG: Running 'pipx run puya==1.1.0 {current_working_directory}/tests/compile/dummy_contract.py' in '{current_working_directory}' | ||
DEBUG: pipx: Done | ||
Done |
1030799
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coverage Report