from __future__ import annotations
from pathlib import Path
from typing import Any
def _dedupe(items: list[str]) -> list[str]:
seen = set()
result = []
for item in items:
if item not in seen:
result.append(item)
seen.add(item)
return result
from typing import Any
def default_rproject(
project_name: str,
r_version: str,
packages: list[str],
git_dependencies: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
"""Return the default rv project configuration.
Parameters
----------
project_name
Name of the project.
r_version
Required R version.
packages
CRAN-style package dependencies.
git_dependencies
Optional git dependencies.
library
Project-local rv library location. Relative paths are resolved
relative to the project directory by rv.
"""
dependencies: list[Any] = _dedupe(packages)
if git_dependencies:
dependencies.extend(git_dependencies)
return {
"project": {
"name": project_name,
"r_version": r_version,
"repositories": [
{
"alias": "PPM",
"url": "https://packagemanager.posit.co/cran/latest",
"force_source": True
}
],
"dependencies": dependencies,
},
}
def default_pyproject(
project_name: str,
python_version: str,
packages: list[str],
) -> dict[str, Any]:
"""Return the default uv project file data."""
return {
"project": {
"name": project_name,
"version": "0.1.0",
"requires-python": f">={python_version}",
"dependencies": _dedupe(packages),
}
}
def _toml_value(value: Any) -> str:
if isinstance(value, str):
return '"' + value.replace('"', '\\"') + '"'
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, list):
if not value:
return "[]"
if all(not isinstance(item, dict) for item in value):
return "[" + ", ".join(_toml_value(item) for item in value) + "]"
raise TypeError(f"Unsupported TOML value: {value!r}")
def _fallback_toml_dumps(data: dict[str, Any]) -> str:
lines: list[str] = []
def write_table(prefix: list[str], table: dict[str, Any]) -> None:
if prefix:
lines.append(f"[{'.'.join(prefix)}]")
deferred: list[tuple[str, dict[str, Any]]] = []
arrays: list[tuple[str, list[dict[str, Any]]]] = []
for key, value in table.items():
if isinstance(value, dict):
deferred.append((key, value))
elif isinstance(value, list) and value and all(isinstance(item, dict) for item in value):
arrays.append((key, value))
else:
lines.append(f"{key} = {_toml_value(value)}")
lines.append("")
for key, value in deferred:
write_table(prefix + [key], value)
for key, values in arrays:
for item in values:
lines.append(f"[[{'.'.join(prefix + [key])}]]")
for item_key, item_value in item.items():
lines.append(f"{item_key} = {_toml_value(item_value)}")
lines.append("")
write_table([], data)
return "\n".join(lines).rstrip() + "\n"
def _toml_dumps(data: dict[str, Any]) -> str:
try:
import tomli_w
except ImportError:
return _fallback_toml_dumps(data)
return tomli_w.dumps(data)
def write_rproject_toml(path: Path, rproject: dict[str, Any]) -> Path:
"""Write rproject.toml idempotently."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(_toml_dumps(rproject))
return path
def write_pyproject_toml(path: Path, pyproject: dict[str, Any]) -> Path:
"""Write pyproject.toml idempotently."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(_toml_dumps(pyproject))
return path23 Data Science Language Package Layer
This notebook writes rproject.toml and pyproject.toml.
Do not run rv sync or uv sync here.
Here’s the default set of R and Python packages I’ll typically have; make it leaner for faster builds, and add more as needed for your project. Bounce between this notebook and the Spack environment notebook to add any system dependencies you need for your R and Python packages.
DEFAULT_R_PACKAGES = [
"box", # we use this for namespacing and dependency injection
"remotes",
"rprojroot", # always travel with this
"stringi",
"targets",
"tarchetypes",
"usethis",
"httpgd",
"httpuv",
"parsermd",
"rmarkdown",
"skimr",
"DiagrammeR",
"svglite",
"later",
"rstudioapi",
"dplyr",
"languageserver",
"here",
"fledge",
"readr",
"stringr",
"withr",
"readxl",
"tidyverse",
"websocket",
"codetools",
"quarto",
"rlang",
"purrr",
"tibble",
"devtools",
"jsonlite",
]
DEFAULT_R_GIT_DEPENDENCIES = [
{
"name": "sess",
"git": "https://github.com/REditorSupport/vscode-R",
"branch": "master",
"directory": "sess",
}
]
DEFAULT_PYTHON_PACKAGES = [
"numpy",
"pandas",
"ipykernel",
"jupyterlab",
]from rse_workbench.datascience import (
default_pyproject,
default_rproject,
write_pyproject_toml,
write_rproject_toml,
)rproject = default_rproject(
project_name=project_contract.project_name,
r_version=project_contract.r_version,
packages=DEFAULT_R_PACKAGES,
git_dependencies=DEFAULT_R_GIT_DEPENDENCIES,
)
pyproject = default_pyproject(
project_name=project_contract.project_name,
python_version=project_contract.python_version,
packages=DEFAULT_PYTHON_PACKAGES,
)
write_rproject_toml(project_contract.rproject_toml, rproject)
write_pyproject_toml(project_contract.pyproject_toml, pyproject)print(project_contract.rproject_toml.read_text())
print(project_contract.pyproject_toml.read_text())Stop here. The next notebook writes the runtime activation contract.