Skip to content
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
10 changes: 6 additions & 4 deletions rsconnect/subprocesses/inspect_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import subprocess
import sys
from dataclasses import asdict, dataclass, replace
from typing import Callable, Optional
from typing import Callable, List, Optional, cast

try:
import tomllib
Expand Down Expand Up @@ -287,11 +287,13 @@ def pyproject_dependencies(dirname: str, pyproject_filename: str):
raise EnvironmentException(f"Error reading {pyproject_filename}: {exception}")

project = pyproject.get("project", {})
dependencies: list[object] = project.get("dependencies", [])
if not isinstance(dependencies, list):
dependencies: List[object] = project.get("dependencies", [])
if not isinstance(dependencies, list) or not all(isinstance(dep, str) for dep in dependencies):
raise EnvironmentException(f"Invalid project.dependencies in {pyproject_filename}: expected a list of strings.")

requirements = filter_pip_freeze_output("\n".join(str(dep) for dep in dependencies))
requirements = filter_pip_freeze_output("\n".join(cast(List[str], dependencies)))
if requirements and not requirements.endswith("\n"):
requirements += "\n"
requirements = (
f"# requirements.txt generated from pyproject.toml by rsconnect-python on "
f"{datetime.datetime.now(datetime.timezone.utc)}\n"
Expand Down
40 changes: 39 additions & 1 deletion tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,49 @@ def test_pyproject_dependencies(tmp_path):
assert "annotated-types>=0.7.0" in env.contents
# rsconnect dependency lines must be stripped, matching the behavior of output_file() and pip_freeze()
dep_lines = [line for line in env.contents.splitlines() if line and not line.startswith("#")]
assert not any("rsconnect" in line for line in dep_lines)
assert dep_lines == ["aiofiles==24.1.0", "annotated-types>=0.7.0"]
assert env.contents.endswith("\n")
assert env.source == "pyproject_toml"
assert env.package_manager == "pip"


@pytest.mark.parametrize(
"dependencies",
[
"3",
"[3]",
"[{a = 'b'}]",
pytest.param(
"['flask', 3]",
marks=pytest.mark.skipif(sys.version_info < (3, 11), reason="toml fallback rejects heterogeneous arrays"),
),
pytest.param(
"['flask', {a = 'b'}]",
marks=pytest.mark.skipif(sys.version_info < (3, 11), reason="toml fallback rejects heterogeneous arrays"),
),
"[true]",
"[['flask']]",
],
)
def test_pyproject_dependencies_invalid(tmp_path, dependencies):
(tmp_path / "pyproject.toml").write_text(f"[project]\ndependencies = {dependencies}\n", encoding="utf-8")

with pytest.raises(
EnvironmentException, match=r"Invalid project\.dependencies in pyproject\.toml: expected a list of strings\."
):
detect_environment(str(tmp_path), requirements_file="pyproject.toml")


@pytest.mark.parametrize("dependencies", ["[]", "['rsconnect-python']"])
def test_pyproject_dependencies_empty(tmp_path, dependencies):
(tmp_path / "pyproject.toml").write_text(f"[project]\ndependencies = {dependencies}\n", encoding="utf-8")

env = detect_environment(str(tmp_path), requirements_file="pyproject.toml")

assert len(env.contents.splitlines()) == 1
assert env.contents.endswith("\n")


def test_requirements_file_keeps_crlf_line_endings(tmp_path: Path):
(tmp_path / "requirements.txt").write_bytes(b"numpy\r\npandas\r\n")

Expand Down
28 changes: 28 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,34 @@ def test_explicit_requirements_file(self, tmp_path):
assert requirements.read_text() == "example-package==1.0\n"
assert manifest["files"][requirements.name]["checksum"] == file_checksum(requirements)

@pytest.mark.parametrize(
"dependencies",
[
pytest.param(
"['flask', 3, {a = 'b'}]",
marks=pytest.mark.skipif(
sys.version_info < (3, 11), reason="toml fallback rejects heterogeneous arrays"
),
),
"[3]",
"[{a = 'b'}]",
"['flask']",
],
)
def test_pyproject_requirements_file(self, tmp_path, dependencies):
(tmp_path / "app.py").write_text("app = object()\n")
(tmp_path / "pyproject.toml").write_text(f"[project]\ndependencies = {dependencies}\n")

result = CliRunner().invoke(cli, ["write-manifest", "api", "-r", "pyproject.toml", str(tmp_path)])
requirements = tmp_path / "requirements.txt"
if dependencies == "['flask']":
assert result.exit_code == 0, result.output
assert requirements.read_text().endswith("\nflask\n")
else:
assert result.exit_code != 0
assert "Invalid project.dependencies in pyproject.toml: expected a list of strings" in result.output
assert not requirements.exists()


class TestWriteManifestExcludeRenv:
def test_exclude_renv_omits_r_dependencies(self, tmp_path):
Expand Down
Loading