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
5 changes: 3 additions & 2 deletions rsconnect/environment_r.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,9 @@ def _remote_repo_url(remote_type: str, pkg_ref: str) -> str:
return f"{host}{pkg_ref}" if host else ""


def _join_list(value: Optional[Sequence[str]]) -> Optional[str]:
return ", ".join(value) if value else None
def _join_list(value: Optional[Sequence[Optional[str]]]) -> Optional[str]:
items = [v for v in value if v is not None] if value else []
return ", ".join(items) if items else None


def _is_url(value: str) -> bool:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_environment_r.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,38 @@ def test_null_r_section_raises(tmp_path):
REnvironment.create(str(tmp_path))


def test_null_entries_in_linking_fields_are_tolerated(tmp_path):
# rpy2 and similar packages can produce renv.lock entries like
# "LinkingTo": [null], which caused _join_list to raise TypeError
# because str.join rejects None elements.
write_lockfile(
tmp_path,
{
"R": {"Version": "4.3.1", "Repositories": [{"Name": "CRAN", "URL": "https://cloud.r-project.org"}]},
"Packages": {
"rpy2": {
"Package": "rpy2",
"Version": "1.0.0",
"Source": "Repository",
"Repository": "CRAN",
"Imports": ["R6", None],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use Null to match the exact case that this is intended to fix?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm following the code correctly, this is actually all a Python dict representation that will then get written to JSON at

json.dump(lockfile, f)
at that point it will be JSON null and then it will get read in and yadda yadda...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh .py my bad!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tricky cause they look sooooo close ... and then they are not 🫠

"Suggests": [None],
"LinkingTo": [None],
},
},
},
)

env = REnvironment.create(str(tmp_path))
assert env is not None
description = env.packages["rpy2"]["description"]
# Null entries are dropped; non-null strings are kept.
assert description.get("Imports") == "R6"
# Fields that are all-null collapse to absent rather than empty.
assert "Suggests" not in description
assert "LinkingTo" not in description


MINIMAL_LOCKFILE = {
"R": {"Version": "4.3.1", "Repositories": [{"Name": "CRAN", "URL": "https://cloud.r-project.org"}]},
"Packages": {},
Expand Down
Loading