From cf8e0951bfb65116a84b448b72dc6e878fb8dfbc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicolas=20M=2E=20Thi=C3=A9ry?=
 <nicolas.thiery@universite-paris-saclay.fr>
Date: Tue, 4 Feb 2025 00:51:41 +0100
Subject: [PATCH] Substitutions: refactoring and availability for C++; misc
 minor changes

Protocol change:
- Substitutions defined by a cell are sent to jupylates by printing a
  json dict rather than through the cell's value
- The cell may print any number of such json dicts
- Refinement of the spec: the json dict should map strings to string,
  to be substituted. Building on top of that to substitute literals is
  responsability of the language helpers / author.

Misc:
- Some more documentation for the examples
- Fixed examples/04-guess_output_cpp.md
- Two additional examples
---
 examples/01_pandas_select_columns.md        |  3 +-
 examples/02-interpret_code.md               | 16 ++++--
 examples/02-interpret_code_cpp.md           | 59 +++++++++++++++++++++
 examples/03-write_code.md                   |  2 +-
 examples/04-guess_output_cpp.md             |  4 +-
 examples/05-substitutions.md                | 51 ++++++++++++++++++
 install_files/include/jupylates_helpers.hpp | 19 +++++++
 jupylates/jupylates.py                      | 17 ++++--
 jupylates/jupylates_helpers.py              |  4 +-
 9 files changed, 160 insertions(+), 15 deletions(-)
 create mode 100644 examples/02-interpret_code_cpp.md
 create mode 100644 examples/05-substitutions.md

diff --git a/examples/01_pandas_select_columns.md b/examples/01_pandas_select_columns.md
index be42327..78119c9 100644
--- a/examples/01_pandas_select_columns.md
+++ b/examples/01_pandas_select_columns.md
@@ -25,8 +25,9 @@ This exercise illustrates:
 - the use of arbitrary features of the underlying language and
   libraries to generate random values (here a Pandas DataFrame) and
   test them.
-- specifying learning objectives as a narrative
+- specifying learning objectives as notebook metadata and/or as narrative
 - the rich display of values
+- requesting an expression as answer
 - one approach to structure the solution and answer enabling testing
   and displaying the solution
 
diff --git a/examples/02-interpret_code.md b/examples/02-interpret_code.md
index 7465218..4ec90f3 100644
--- a/examples/02-interpret_code.md
+++ b/examples/02-interpret_code.md
@@ -10,16 +10,24 @@ kernelspec:
   name: python3
 ---
 
++++ {"tags": ["instructors"]}
+
+:::{hint} About this demo
+:class: dropdown
+
+This exercise illustrates:
+- the use of substitutions
+- requesting an integer as answer
+:::
+
 ```{code-cell}
 :tags: [hide-cell, substitutions]
 
 import random
 from jupylates.jupylates_helpers import SUBSTITUTE, INPUT_INT, assertEqual
 
-SUBSTITUTE(
-	I1 = random.randint(1, 7),
-	I2 = random.randint(8, 15)
-)
+SUBSTITUTE(I1 = random.randint(1, 7))
+SUBSTITUTE(I2 = random.randint(8, 15))
 ```
 
 :::{admonition} Instructions
diff --git a/examples/02-interpret_code_cpp.md b/examples/02-interpret_code_cpp.md
new file mode 100644
index 0000000..0663cf6
--- /dev/null
+++ b/examples/02-interpret_code_cpp.md
@@ -0,0 +1,59 @@
+---
+jupytext:
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+kernelspec:
+  display_name: C++17
+  language: C++17
+  name: xcpp17
+---
+
++++ {"tags": ["instructors"]}
+
+:::{hint} About this demo
+:class: dropdown
+
+This exercise illustrates:
+- an exercise in C++
+- substitutions of random literals in C++
+- requesting an integer as answer
+:::
+
+```{code-cell}
+:tags: [substitutions, hide-cell]
+
+#include <jupylates_helpers.hpp>
+
+int I1 = RANDOM_INT(1, 7);
+int I2 = RANDOM_INT(8, 15);
+
+SUBSTITUTE_LITERAL("I1", I1);
+SUBSTITUTE_LITERAL("I2", I2);
+```
+
+:::{admonition} Instructions
+What's the value of `r` after executing the following code?
+:::
+
+```{code-cell}
+int X = I1;
+int Y = I2;
+
+int Z;
+
+Z = X;
+X = Y;
+Y = Z;
+int r = Y;
+```
+
+```{code-cell}
+:tags: [answer, solution, test, hide-output]
+
+int answer, solution;
+INPUT_INT("r", solution, answer, r);
+
+CHECK(answer == solution)
+```
diff --git a/examples/03-write_code.md b/examples/03-write_code.md
index b8efd64..ed254c0 100644
--- a/examples/03-write_code.md
+++ b/examples/03-write_code.md
@@ -16,7 +16,7 @@ kernelspec:
 :class: dropdown
 
 This exercise illustrates:
-- requesting a code answer
+- requesting a code answer in Python
 :::
 
 :::{admonition} Learning objective
diff --git a/examples/04-guess_output_cpp.md b/examples/04-guess_output_cpp.md
index c94e253..8e5cdb8 100644
--- a/examples/04-guess_output_cpp.md
+++ b/examples/04-guess_output_cpp.md
@@ -30,7 +30,7 @@ Understand the step by step execution of a for loop.
 :tags: [hide-cell]
 
 #include <sstream>
-#include "jupylates_helpers.hpp"
+#include <jupylates_helpers.hpp>
 
 CONST I1 = RANDOM_INT(0, 6);
 CONST I2 = I1 + RANDOM_INT(1, 4);
@@ -59,5 +59,5 @@ for (int I = I1; I <= I2 ; I = I + 1 ) {
 std::string answer, solution;
 INPUT_TEXT("", solution, answer, cout.str());
 
-assertEqual(answer, solution);
+CHECK(answer == solution);
 ```
diff --git a/examples/05-substitutions.md b/examples/05-substitutions.md
new file mode 100644
index 0000000..7551daf
--- /dev/null
+++ b/examples/05-substitutions.md
@@ -0,0 +1,51 @@
+---
+jupytext:
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+kernelspec:
+  display_name: C++17
+  language: C++17
+  name: xcpp17
+---
+
++++ {"tags": ["learning objectives"]}
+
+:::{hint} About this demo
+:class: dropdown
+
+This exercise illustrates:
+- an exercise in C++
+- substitutions of random code
+:::
+
+```{code-cell}
+:tags: [substitutions, hide-cell]
+
+#include <jupylates_helpers.hpp>
+
+SUBSTITUTE("VAL_OR_REF ", RANDOM_CHOICE("", "&"));
+SUBSTITUTE("PLUS_OR_MINUS", RANDOM_CHOICE("+", "-"));
+SUBSTITUTE("A", RANDOM_CHOICE("a", "b", "c"));
+SUBSTITUTE("D", RANDOM_CHOICE("d", "e", "f"));
+```
+
+:::{admonition} Instructions
+What's the value of `A` after executing the following code:
+:::
+
+```{code-cell}
+int A = 1 PLUS_OR_MINUS 1;
+int VAL_OR_REF D = A;
+D = 4;
+```
+
+```{code-cell}
+:tags: [answer, solution, test, hide-output]
+
+int answer, solution;
+INPUT_INT("A", solution, answer, A);
+
+CHECK(answer == solution);
+```
diff --git a/install_files/include/jupylates_helpers.hpp b/install_files/include/jupylates_helpers.hpp
index c29fc12..105e094 100644
--- a/install_files/include/jupylates_helpers.hpp
+++ b/install_files/include/jupylates_helpers.hpp
@@ -5,6 +5,7 @@
 #include <cstdlib>
 #include <vector>
 #include <functional>
+#include <sstream>
 
 /** Infrastructure minimale de test **/
 #ifndef ASSERT
@@ -101,4 +102,22 @@ void INPUT_EXPR(std::string description, T& answer, T& solution, T solution_valu
 #define INPUT_FLOAT INPUT_EXPR<float>
 #define INPUT_BOOL INPUT_EXPR<bool>
 
+template<class T>
+std::string to_literal(T i) {
+    std::ostringstream s;
+    s << i;
+    return s.str();
+}
+// Will need to be overriden for vectors, ...
+
+
+void SUBSTITUTE(std::string name, std::string value) {
+    std::cout << "{\"" << name << "\": \"" << value << "\"}" << std::endl;
+}
+
+template<class T>
+void SUBSTITUTE_LITERAL(std::string name, T value) {
+    SUBSTITUTE(name, to_literal(value));
+}
+
 #endif
diff --git a/jupylates/jupylates.py b/jupylates/jupylates.py
index 428e4c6..bd2d5aa 100644
--- a/jupylates/jupylates.py
+++ b/jupylates/jupylates.py
@@ -88,7 +88,7 @@ def execute_code(
                 assert False
         if msg["msg_type"] == "execute_input":
             continue
-        if msg["msg_type"] in ["execute_result", "error"]:
+        if msg["msg_type"] in ["stream", "execute_result", "error"]:
             content = copy.copy(msg["content"])
             content["output_type"] = msg["msg_type"]
             outputs.append(content)
@@ -99,6 +99,8 @@ def display_outputs(outputs: List[dict]) -> None:
     for output in outputs:
         if output["output_type"] == "error":
             display(output["ename"])
+        elif output["output_type"] == "stream":
+            print(output["text"], end='')
         elif "text/html" in output["data"]:
             display(HTML(output["data"]["text/plain"]))
         elif "text/plain" in output["data"]:
@@ -1043,10 +1045,15 @@ class Exerciser(ipywidgets.HBox):
                     else:
                         outputs = []
                     if "substitutions" in cell_tags:
-                        assert len(outputs) == 1
-                        output = outputs[0]["data"]["text/plain"]
-                        self.substitutions.update(json.loads(output[1:-1]))
-                        outputs = []
+                        decoder = json.JSONDecoder()
+                        for output in outputs:
+                            if output.get('name') != 'stdout':
+                                continue
+                            text = output['text']
+                            while text:
+                                d, pos = decoder.raw_decode(text)
+                                text = text[pos:].strip()
+                                self.substitutions.update(d)
                     if "hide-cell" not in cell_tags and "hide-input" not in cell_tags:
                         display(Code(source, language=lexer[language]))
                     if "hide-cell" not in cell_tags and "hide-output" not in cell_tags:
diff --git a/jupylates/jupylates_helpers.py b/jupylates/jupylates_helpers.py
index 660be1b..036781e 100644
--- a/jupylates/jupylates_helpers.py
+++ b/jupylates/jupylates_helpers.py
@@ -27,11 +27,11 @@ def RANDOM_CHOICE(*args: Any) -> Any:
     return choice(args)
 
 
-def SUBSTITUTE(**args: Any) -> str:
+def SUBSTITUTE(**args: Any) -> None:
     import __main__
 
     __main__.__dict__.update(args)
-    return json.dumps({key: str(value) for key, value in args.items()})
+    print(json.dumps({key: str(value) for key, value in args.items()}))
 
 
 T = TypeVar("T", int, float, str, Any)
-- 
GitLab