From 136dbfa5b5d76fe3b6f106f0e6f9726862c8ad2f Mon Sep 17 00:00:00 2001 From: Samraat Pawar Date: Sun, 11 Oct 2026 17:29:31 +0100 Subject: [PATCH 1/2] docs(python): run R subprocess workflows from project root --- content/code/fmr.R | 11 +++-- content/code/test_r.py | 19 ++++++-- content/notebooks/python.ipynb | 31 ++++++------- scripts/runtime_path_exceptions.yml | 70 ----------------------------- 4 files changed, 39 insertions(+), 92 deletions(-) diff --git a/content/code/fmr.R b/content/code/fmr.R index 6f2d3fa1..f5a89494 100755 --- a/content/code/fmr.R +++ b/content/code/fmr.R @@ -1,13 +1,16 @@ -# plots log(field metabolic rate) against log(body mass) for the Nagy et al -# 1999 dataset to a file fmr.pdf. +# Plot log(field metabolic rate) against log(body mass) for the Nagy et al. +# 1999 dataset. Run from the coursework project root with: +# Rscript code/fmr.R cat("reading CSV\n") -nagy <- read.csv('../data/nagy_et_al_1999.csv', stringsAsFactors = FALSE) +nagy <- read.csv('data/nagy_et_al_1999.csv', stringsAsFactors = FALSE) cat("Creating graph\n") -pdf('../results/fmr_plot.pdf', 11, 8.5) +dir.create('results', showWarnings = FALSE) +pdf('results/fmr_plot.pdf', 11, 8.5) col <- c(Aves='purple3', Mammalia='red3', Reptilia='green3') +stopifnot(all(unique(nagy$Class) %in% names(col))) plot(log10(nagy$M.g), log10(nagy$FMR.kJ.day.1), pch=19, col=col[nagy$Class], xlab=~log[10](M), ylab=~log[10](FMR)) for(class in unique(nagy$Class)){ diff --git a/content/code/test_r.py b/content/code/test_r.py index cb7cadf5..73c539ae 100755 --- a/content/code/test_r.py +++ b/content/code/test_r.py @@ -1,5 +1,18 @@ +#!/usr/bin/env python3 + +"""Run the small R example and save its output from the project root.""" + import subprocess -subprocess.Popen("rscript --verbose test_r.r > \ -../results/test_r.rout 2> ../results/test_r_errFile.rout",\ - shell=True).wait() + +with open("results/test_r.rout", "w") as output_file, \ + open("results/test_r_errFile.rout", "w") as error_file: + result = subprocess.run( + ["Rscript", "--verbose", "code/test_r.R"], + stdout=output_file, + stderr=error_file, + check=False, + ) + +print("Return code:", result.returncode) +raise SystemExit(result.returncode) diff --git a/content/notebooks/python.ipynb b/content/notebooks/python.ipynb index 23da1f49..50a5df66 100755 --- a/content/notebooks/python.ipynb +++ b/content/notebooks/python.ipynb @@ -8964,16 +8964,16 @@ "print(\"Hello, this is R!\")\n", "```\n", "\n", - "Then try running it from your bash terminal (*not* from the Python terminal) (e.g., using `Rscript test_r.R`). This should return in your terminal:\n", + "Then run it from your bash terminal (*not* from the Python terminal), starting at the coursework project root: `Rscript code/test_r.R`. This should return in your terminal:\n", "\n", "```bash\n", "[1] \"Hello, this is R!\"\n", "```\n", "\n", - "Then try `Rscript --verbose test_r.R`. This should return something like:\n", + "Then try `Rscript --verbose code/test_r.R`. This should return something like:\n", "\n", "```bash\n", - " '/usr/lib/r/bin/r --no-echo --no-restore --file=test_r.R'\n", + " '/usr/lib/r/bin/r --no-echo --no-restore --file=code/test_r.R'\n", "\n", "[1] \"Hello, this is R!\"\n", "```\n", @@ -8987,10 +8987,10 @@ "Now try:\n", "\n", "```bash\n", - "Rscript --verbose test_r.R > ../results/test_r.rout 2> ../results/test_r_errFile.rout\n", + "Rscript --verbose code/test_r.R > results/test_r.rout 2> results/test_r_errFile.rout\n", "```\n", "\n", - "Then check the contents of `test_r.rout` and `test_r_errFile.rout`. The former should contain the output of the R script, and the latter the additional information produced by the `--verbose` flag. \n", + "Then check `results/test_r.rout` and `results/test_r_errFile.rout`. The former should contain the output of the R script, and the latter the additional information produced by the `--verbose` flag. \n", "\n", "Now try running this using subprocess instead:" ] @@ -9001,10 +9001,10 @@ "metadata": {}, "outputs": [], "source": [ - "with open(\"../results/test_r.rout\", \"w\") as output_file, \\\n", - " open(\"../results/test_r_errFile.rout\", \"w\") as error_file:\n", + "with open(\"results/test_r.rout\", \"w\") as output_file, \\\n", + " open(\"results/test_r_errFile.rout\", \"w\") as error_file:\n", " result = subprocess.run(\n", - " [\"Rscript\", \"--verbose\", \"test_r.R\"],\n", + " [\"Rscript\", \"--verbose\", \"code/test_r.R\"],\n", " stdout=output_file,\n", " stderr=error_file,\n", " check=False,\n", @@ -9034,16 +9034,17 @@ "source": [ "import subprocess\n", "\n", - "with open(\"../results/test_r.rout\", \"w\") as output_file, \\\n", - " open(\"../results/test_r_errFile.rout\", \"w\") as error_file:\n", + "with open(\"results/test_r.rout\", \"w\") as output_file, \\\n", + " open(\"results/test_r_errFile.rout\", \"w\") as error_file:\n", " result = subprocess.run(\n", - " [\"Rscript\", \"--verbose\", \"test_r.R\"],\n", + " [\"Rscript\", \"--verbose\", \"code/test_r.R\"],\n", " stdout=output_file,\n", " stderr=error_file,\n", " check=False,\n", " )\n", "\n", - "print(\"Return code:\", result.returncode)" + "print(\"Return code:\", result.returncode)\n", + "raise SystemExit(result.returncode)" ] }, { @@ -9051,7 +9052,7 @@ "metadata": {}, "source": [ "\n", - "Now run `test_r.py` (or `%run test_r.py` in IPython) and check `test_r.rout` and `test_r_errFile.rout`.\n", + "From the project root, run `python3 code/test_r.py` (or `%run code/test_r.py` in IPython) and check `results/test_r.rout` and `results/test_r_errFile.rout`.\n", "\n", "Also check what happens if you run (type directly in `ipython` or `python` console):" ] @@ -9063,7 +9064,7 @@ "outputs": [], "source": [ "result = subprocess.run(\n", - " [\"Rscript\", \"--verbose\", \"NonExistScript.R\"],\n", + " [\"Rscript\", \"--verbose\", \"code/NonExistScript.R\"],\n", " capture_output=True,\n", " text=True,\n", ")\n", @@ -9138,7 +9139,7 @@ "\n", "#### Run an R analysis from Python\n", "\n", - "Download {download}`fmr.R <../code/fmr.R>` into `code/` and {download}`nagy_et_al_1999.csv <../data/nagy_et_al_1999.csv>` into `data/`. Read the R script, then write `run_fmr_r.py` that runs `Rscript fmr.R` from the coursework project's `code/` directory using `subprocess.run()` without `shell=True`.\n", + "Download {download}`fmr.R <../code/fmr.R>` into `code/` and {download}`nagy_et_al_1999.csv <../data/nagy_et_al_1999.csv>` into `data/`. Read the R script, then write `run_fmr_r.py` that runs `Rscript code/fmr.R` from the coursework project root using `subprocess.run()` without `shell=True`.\n", "\n", "Capture standard output and standard error as text, print both streams with labels, and print whether the command succeeded based on its return code. On success, verify that `results/fmr_plot.pdf` exists and is non-empty; on failure, exit with a non-zero status. Your wrapper is complete when a normal run reports success and deliberately using a nonexistent R script reports failure without a Python traceback.\n", "\n", diff --git a/scripts/runtime_path_exceptions.yml b/scripts/runtime_path_exceptions.yml index 0909edad..23efa834 100644 --- a/scripts/runtime_path_exceptions.yml +++ b/scripts/runtime_path_exceptions.yml @@ -33,20 +33,6 @@ exceptions: reason: Known statistics workflow path awaiting dependency-aware chapter migration. owner: Statistics teaching owner target_phase: Phase 5 -- location: code/fmr.R - rule: parent_project_path - value: ../data/nagy_et_al_1999.csv - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 -- location: code/fmr.R - rule: parent_project_path - value: ../results/fmr_plot.pdf - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 - location: code/glm.R rule: parent_project_path value: ../data/prac_data.csv @@ -103,20 +89,6 @@ exceptions: reason: Known statistics workflow path awaiting dependency-aware chapter migration. owner: Statistics teaching owner target_phase: Phase 5 -- location: code/test_r.py - rule: parent_project_path - value: ../results/test_r.rout - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 -- location: code/test_r.py - rule: parent_project_path - value: ../results/test_r_errFile.rout - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 - location: notebooks/anova.ipynb:cell 45 rule: parent_project_path value: ../data/mammals.rdata @@ -222,48 +194,6 @@ exceptions: reason: Known statistics workflow path awaiting dependency-aware chapter migration. owner: Statistics teaching owner target_phase: Phase 5 -- location: notebooks/python.ipynb:cell 587:fence 4 - rule: parent_project_path - value: ../results/test_r.rout - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 -- location: notebooks/python.ipynb:cell 587:fence 4 - rule: parent_project_path - value: ../results/test_r_errFile.rout - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 -- location: notebooks/python.ipynb:cell 588 - rule: parent_project_path - value: ../results/test_r.rout - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 -- location: notebooks/python.ipynb:cell 588 - rule: parent_project_path - value: ../results/test_r_errFile.rout - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 -- location: notebooks/python.ipynb:cell 591 - rule: parent_project_path - value: ../results/test_r.rout - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 -- location: notebooks/python.ipynb:cell 591 - rule: parent_project_path - value: ../results/test_r_errFile.rout - count: 1 - reason: Known Python workflow path awaiting chapter and supplied-script migration. - owner: Python teaching owner - target_phase: Phase 4 - location: notebooks/regress.ipynb:cell 6 rule: parent_project_path value: ../data/genome_size.csv From 1d0eee5b54ddc045cc823a595d4f5eb7d54d8ae1 Mon Sep 17 00:00:00 2001 From: Samraat Pawar Date: Sun, 11 Oct 2026 17:29:31 +0100 Subject: [PATCH 2/2] test(r): verify Python-to-R file workflows --- scripts/check_r_file_workflows.R | 42 ++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/scripts/check_r_file_workflows.R b/scripts/check_r_file_workflows.R index 6b0e80d7..0755f2ec 100644 --- a/scripts/check_r_file_workflows.R +++ b/scripts/check_r_file_workflows.R @@ -6,7 +6,11 @@ repository_root <- normalizePath(".") required_files <- c( "content/code/basic_io.R", "content/code/data_wrang.R", + "content/code/fmr.R", + "content/code/test_r.R", + "content/code/test_r.py", "content/data/trees.csv", + "content/data/nagy_et_al_1999.csv", "content/data/pound_hill_data.csv", "content/data/pound_hill_meta_data.csv" ) @@ -29,7 +33,11 @@ on.exit(unlink(project_root, recursive = TRUE), add = TRUE) stopifnot( all(file.copy( - file.path(repository_root, "content/code", c("basic_io.R", "data_wrang.R")), + file.path( + repository_root, + "content/code", + c("basic_io.R", "data_wrang.R", "fmr.R", "test_r.R", "test_r.py") + ), file.path(project_root, "code"), overwrite = TRUE )), @@ -37,7 +45,12 @@ stopifnot( file.path( repository_root, "content/data", - c("trees.csv", "pound_hill_data.csv", "pound_hill_meta_data.csv") + c( + "trees.csv", + "nagy_et_al_1999.csv", + "pound_hill_data.csv", + "pound_hill_meta_data.csv" + ) ), file.path(project_root, "data"), overwrite = TRUE @@ -65,6 +78,31 @@ run_script("code/basic_io.R") basic_output <- file.path("results", "MyData.csv") stopifnot(file.exists(basic_output), file.info(basic_output)$size > 0) +run_script("code/fmr.R") +fmr_output <- file.path("results", "fmr_plot.pdf") +stopifnot(file.exists(fmr_output), file.info(fmr_output)$size > 0) + +python_output <- system2( + Sys.which("python3"), + "code/test_r.py", + stdout = TRUE, + stderr = TRUE +) +python_status <- attr(python_output, "status") +if (!is.null(python_status) && python_status != 0) { + stop(paste(python_output, collapse = "\n"), call. = FALSE) +} +stopifnot( + any(grepl("Return code: 0", python_output, fixed = TRUE)), + file.exists(file.path("results", "test_r.rout")), + file.exists(file.path("results", "test_r_errFile.rout")), + grepl( + "Hello, this is R!", + paste(readLines(file.path("results", "test_r.rout")), collapse = "\n"), + fixed = TRUE + ) +) + run_script("code/data_wrang.R") wrangled_output <- read.csv(file.path("results", "pound_hill_long.csv")) stopifnot(