From 9e2826a83dc565a5a05d3055831730738a7c9db3 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 23 Jun 2025 12:32:27 -0700 Subject: [PATCH 1/3] Remove unneeded lines check that prevented exercises with multiple functions/classes --- dist/code-exercise.umd.js | 4 ++-- package.json | 2 +- src/code-exercise.js | 7 +++---- src/doctest-grader.js | 28 +--------------------------- 4 files changed, 7 insertions(+), 34 deletions(-) diff --git a/dist/code-exercise.umd.js b/dist/code-exercise.umd.js index ac17d38..e897e94 100644 --- a/dist/code-exercise.umd.js +++ b/dist/code-exercise.umd.js @@ -58,7 +58,7 @@ const J=2; transform: rotate(360deg); } } - `;render(){return Y`
`}}customElements.define("loader-element",e$);class i$ extends L{static properties={starterCode:{type:String},exerciseName:{type:String,attribute:"name"},isLoading:{type:Boolean},runStatus:{type:String},testResultsStatus:{type:String},testResultsHeader:{type:String},testResultsDetails:{type:String},runOutput:{type:String},runStdout:{type:String},showTests:{type:Boolean,attribute:"show-tests"}};editorRef=(()=>new lt)();editor=null;createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),!this.starterCode&&this.innerHTML.trim()&&(this.starterCode=this.innerHTML.trim().replace(/>/g,">"),this.innerHTML=""),this.style.whiteSpace="normal",this.style.fontFamily="inherit"}render(){return Y` + `;render(){return Y`
`}}customElements.define("loader-element",e$);class i$ extends L{static properties={starterCode:{type:String},exerciseName:{type:String,attribute:"name"},isLoading:{type:Boolean},runStatus:{type:String},testResultsStatus:{type:String},testResultsHeader:{type:String},testResultsDetails:{type:String},runOutput:{type:String},runStdout:{type:String},showTests:{type:Boolean,attribute:"show-tests"}};editorRef=(()=>new lt)();editor=null;createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),!this.starterCode&&this.innerHTML.trim()&&(this.starterCode=this.innerHTML.trim().replace(/>/g,">").replace(/</g,"<"),this.innerHTML=""),this.style.whiteSpace="normal",this.style.fontFamily="inherit"}render(){return Y`
@@ -118,4 +118,4 @@ const J=2; `:""}
- `}getStorageKey(){return this.exerciseName?`${this.exerciseName}-repr`:null}firstUpdated(){const t=this.getStorageKey(),e=t?function(t,e){let i;return i=Jd()?localStorage.getItem(t):Kd[t],i??e}(t):null;e?console.log(`Loading stored code in localStorage from ${t}`):t?console.log(`No stored code found for ${t}, using starter code. Your code changes will be stored in localStorage.`):console.log("No exercise name provided, code will not be stored");const i=Xe.create({doc:e||this.starterCode||"",extensions:[rd,Gd(),Nn.of([Zf]),zl.of(" "),jn.lineWrapping,jn.updateListener.of((t=>{const e=this.getStorageKey();t.docChanged&&e&&t$(e,t.state.doc.toString())}))]});this.editor=new jn({state:i,parent:this.editorRef.value})}async onRunCode(){this.runStatus="Running code...",this.testResultsStatus="";const t=this.editor.state.doc.toString();try{const{results:e,error:i,stdout:s}=await new Ld(t);this.runOutput=i?.message||e||"",this.runStdout=s||"",this.runOutput||this.runStdout||(this.runOutput="No output from code execution.\n",this.showTests&&(this.runOutput+='To check if your function code is correct, select "Run Tests" button instead.'))}catch(t){console.warn(`Error in pyodideWorker at ${t.filename}, Line: ${t.lineno}, ${t.message}`),this.runOutput=`Error: ${t.message}`}this.runStatus=""}async onRunTests(){this.runStatus="Running tests...",this.runOutput="",this.runStdout="";let t=function(t){const e=t.split("\n");if(!e[0].includes("def")&&!e[0].includes("class"))return{status:"fail",header:"Error running tests",details:"First code line must be `def` or `class` declaration"};if(function(t){let e=1;for(;e{t.startsWith('File "__main__"')?i=!0:((t.startsWith("Trying:")||t.startsWith("1 items had no tests:"))&&(i=!1),i&&(t=t.replace("Failed example:","\n❌ Failed test:"),e.push(t)))})),e.join("\n").trim()}(t)}}}(i):function(t,e){return t.message.startsWith("Traceback")?{status:"fail",header:"Syntax error",details:Vd(t.message,e)}:"Infinite loop"==t.message?{status:"fail",header:"Infinite loop",details:"Your code did not finish executing within 60 seconds. Please look to see if you accidentally coded an infinite loop."}:{status:"fail",header:"Unexpected error occurred",details:`Error: ${t.message}`}}(s,t.startLine),this.runStdout=r||""}catch(t){console.warn(`Error in pyodideWorker at ${t.filename}, Line: ${t.lineno}, ${t.message}`)}this.runStatus="",this.testResultsStatus=t.status,this.testResultsHeader=t.header,this.testResultsDetails=t.details}async resetCode(){if(confirm("Are you sure you want to reset your code to the starter code? This cannot be undone.")){const t=Xe.create({doc:this.starterCode||"",extensions:[rd,Gd(),jn.lineWrapping]});this.editor.setState(t);const e=this.getStorageKey();e&&t$(e,this.starterCode)}}}window.customElements.define("code-exercise-element",i$),t.CodeExerciseElement=i$,Object.defineProperty(t,"t",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).CodeExerciseElement={}); + `}getStorageKey(){return this.exerciseName?`${this.exerciseName}-repr`:null}firstUpdated(){const t=this.getStorageKey(),e=t?function(t,e){let i;return i=Jd()?localStorage.getItem(t):Kd[t],i??e}(t):null;e?console.log(`Loading stored code in localStorage from ${t}`):t?console.log(`No stored code found for ${t}, using starter code. Your code changes will be stored in localStorage.`):console.log("No exercise name provided, code will not be stored");const i=Xe.create({doc:e||this.starterCode||"",extensions:[rd,Gd(),Nn.of([Zf]),zl.of(" "),jn.lineWrapping,jn.updateListener.of((t=>{const e=this.getStorageKey();t.docChanged&&e&&t$(e,t.state.doc.toString())}))]});this.editor=new jn({state:i,parent:this.editorRef.value})}async onRunCode(){this.runStatus="Running code...",this.testResultsStatus="";const t=this.editor.state.doc.toString();try{const{results:e,error:i,stdout:s}=await new Ld(t);this.runOutput=i?.message||e||"",this.runStdout=s||"",this.runOutput||this.runStdout||(this.runOutput="No output from code execution.\n",this.showTests&&(this.runOutput+='To check if your function code is correct, select "Run Tests" button instead.'))}catch(t){console.warn(`Error in pyodideWorker at ${t.filename}, Line: ${t.lineno}, ${t.message}`),this.runOutput=`Error: ${t.message}`}this.runStatus=""}async onRunTests(){this.runStatus="Running tests...",this.runOutput="",this.runStdout="";let t=function(t){const e=t.split("\n");if(!e[0].includes("def")&&!e[0].includes("class"))return{status:"fail",header:"Error running tests",details:"First code line must be `def` or `class` declaration"};let i=[...e];return i.push("import sys"),i.push("import io"),i.push("sys.stdout = io.StringIO()"),i.push("import doctest"),i.push("doctest.testmod(verbose=True)"),i.push("sys.stdout.getvalue()"),i=i.join("\n"),{status:"success",header:"Running tests...",code:i,startLine:0}}(this.editor.state.doc.toString());if(t.code)try{const{results:e,error:i,stdout:s}=await new Ld(t.code);t=e?function(t){const e=t.match(/(\d+)\spassed\sand\s(\d+)\sfailed./);if(e){const i=parseInt(e[1],10),s=i+parseInt(e[2],10);return{status:i==s?"pass":"fail",header:`${i} of ${s} tests passed`,details:function(t){let e=[],i=!1;return t.split("\n").forEach((t=>{t.startsWith('File "__main__"')?i=!0:((t.startsWith("Trying:")||t.startsWith("1 items had no tests:"))&&(i=!1),i&&(t=t.replace("Failed example:","\n❌ Failed test:"),e.push(t)))})),e.join("\n").trim()}(t)}}}(e):function(t,e){return t.message.startsWith("Traceback")?{status:"fail",header:"Syntax error",details:Vd(t.message,e)}:"Infinite loop"==t.message?{status:"fail",header:"Infinite loop",details:"Your code did not finish executing within 60 seconds. Please look to see if you accidentally coded an infinite loop."}:{status:"fail",header:"Unexpected error occurred",details:`Error: ${t.message}`}}(i,t.startLine),this.runStdout=s||""}catch(t){console.warn(`Error in pyodideWorker at ${t.filename}, Line: ${t.lineno}, ${t.message}`)}this.runStatus="",this.testResultsStatus=t.status,this.testResultsHeader=t.header,this.testResultsDetails=t.details}async resetCode(){if(confirm("Are you sure you want to reset your code to the starter code? This cannot be undone.")){const t=Xe.create({doc:this.starterCode||"",extensions:[rd,Gd(),jn.lineWrapping]});this.editor.setState(t);const e=this.getStorageKey();e&&t$(e,this.starterCode)}}}window.customElements.define("code-exercise-element",i$),t.CodeExerciseElement=i$,Object.defineProperty(t,"t",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).CodeExerciseElement={}); diff --git a/package.json b/package.json index 06a9cdc..392e436 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "python-code-exercise-element", - "version": "0.1.14", + "version": "0.1.17", "description": "A web component for Python coding exercises with doctest runner", "repository": { "type": "git", diff --git a/src/code-exercise.js b/src/code-exercise.js index cae52c9..8d72675 100644 --- a/src/code-exercise.js +++ b/src/code-exercise.js @@ -36,8 +36,8 @@ export class CodeExerciseElement extends LitElement { connectedCallback() { super.connectedCallback(); if (!this.starterCode && this.innerHTML.trim()) { - // Unescape the > signs in doctest output that get escaped by HTML parser - this.starterCode = this.innerHTML.trim().replace(/>/g, '>'); + // Unescape the HTML entities in doctest output that get escaped by HTML parser + this.starterCode = this.innerHTML.trim().replace(/>/g, '>').replace(/</g, '<'); // Clear the innerHTML since it will be displayed in the editor this.innerHTML = ''; } @@ -191,8 +191,7 @@ export class CodeExerciseElement extends LitElement { if (testResults.code) { try { - const code = testResults.code + '\nsys.stdout.getvalue()'; - const {results, error, stdout} = await new FiniteWorker(code); + const {results, error, stdout} = await new FiniteWorker(testResults.code); if (results) { testResults = processTestResults(results); } else { diff --git a/src/doctest-grader.js b/src/doctest-grader.js index c520f58..787e468 100644 --- a/src/doctest-grader.js +++ b/src/doctest-grader.js @@ -1,19 +1,3 @@ -function findNextUnindentedLine(lines, start) { - /* - Finds the next piece of unindented code in the file. Ignores empty lines and lines - that start with a space or tab. Returns len(lines) if no unindented line found. - */ - let lineNum = start; - while (lineNum < lines.length) { - const line = lines[lineNum]; - if (!(line == '' || line[0] == ' ' || line[0] == '\t' || line[0] == '\n')) { - break; - } - lineNum++; - } - return lineNum; -} - function extractError(error, numDocstringLines) { let startI = -1; let endI = -1; @@ -65,17 +49,6 @@ export function prepareCode(code) { }; } - // Find any code lines that aren't properly indented - let line = findNextUnindentedLine(lines, 1); // Start after def/class line - if (line != lines.length) { - return { - status: 'fail', - header: 'Error running tests', - details: - 'All lines in a function or class definition should be indented at least once. It looks like you have a line that has no indentation.', - }; - } - let finalCode = [...lines]; // Copy the lines array // Redirects stdout so we can return it @@ -85,6 +58,7 @@ export function prepareCode(code) { // Runs the doctests finalCode.push('import doctest'); finalCode.push('doctest.testmod(verbose=True)'); + finalCode.push('sys.stdout.getvalue()'); finalCode = finalCode.join('\n'); return { From b9a7fb820a14089af7e1c26151dedf1b0ff0488f Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 23 Jun 2025 12:48:18 -0700 Subject: [PATCH 2/3] Remove inital check for def/class since some code starts with imports --- dist/code-exercise.umd.js | 2 +- src/doctest-grader.js | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/dist/code-exercise.umd.js b/dist/code-exercise.umd.js index e897e94..264b6c1 100644 --- a/dist/code-exercise.umd.js +++ b/dist/code-exercise.umd.js @@ -118,4 +118,4 @@ const J=2; `:""} - `}getStorageKey(){return this.exerciseName?`${this.exerciseName}-repr`:null}firstUpdated(){const t=this.getStorageKey(),e=t?function(t,e){let i;return i=Jd()?localStorage.getItem(t):Kd[t],i??e}(t):null;e?console.log(`Loading stored code in localStorage from ${t}`):t?console.log(`No stored code found for ${t}, using starter code. Your code changes will be stored in localStorage.`):console.log("No exercise name provided, code will not be stored");const i=Xe.create({doc:e||this.starterCode||"",extensions:[rd,Gd(),Nn.of([Zf]),zl.of(" "),jn.lineWrapping,jn.updateListener.of((t=>{const e=this.getStorageKey();t.docChanged&&e&&t$(e,t.state.doc.toString())}))]});this.editor=new jn({state:i,parent:this.editorRef.value})}async onRunCode(){this.runStatus="Running code...",this.testResultsStatus="";const t=this.editor.state.doc.toString();try{const{results:e,error:i,stdout:s}=await new Ld(t);this.runOutput=i?.message||e||"",this.runStdout=s||"",this.runOutput||this.runStdout||(this.runOutput="No output from code execution.\n",this.showTests&&(this.runOutput+='To check if your function code is correct, select "Run Tests" button instead.'))}catch(t){console.warn(`Error in pyodideWorker at ${t.filename}, Line: ${t.lineno}, ${t.message}`),this.runOutput=`Error: ${t.message}`}this.runStatus=""}async onRunTests(){this.runStatus="Running tests...",this.runOutput="",this.runStdout="";let t=function(t){const e=t.split("\n");if(!e[0].includes("def")&&!e[0].includes("class"))return{status:"fail",header:"Error running tests",details:"First code line must be `def` or `class` declaration"};let i=[...e];return i.push("import sys"),i.push("import io"),i.push("sys.stdout = io.StringIO()"),i.push("import doctest"),i.push("doctest.testmod(verbose=True)"),i.push("sys.stdout.getvalue()"),i=i.join("\n"),{status:"success",header:"Running tests...",code:i,startLine:0}}(this.editor.state.doc.toString());if(t.code)try{const{results:e,error:i,stdout:s}=await new Ld(t.code);t=e?function(t){const e=t.match(/(\d+)\spassed\sand\s(\d+)\sfailed./);if(e){const i=parseInt(e[1],10),s=i+parseInt(e[2],10);return{status:i==s?"pass":"fail",header:`${i} of ${s} tests passed`,details:function(t){let e=[],i=!1;return t.split("\n").forEach((t=>{t.startsWith('File "__main__"')?i=!0:((t.startsWith("Trying:")||t.startsWith("1 items had no tests:"))&&(i=!1),i&&(t=t.replace("Failed example:","\n❌ Failed test:"),e.push(t)))})),e.join("\n").trim()}(t)}}}(e):function(t,e){return t.message.startsWith("Traceback")?{status:"fail",header:"Syntax error",details:Vd(t.message,e)}:"Infinite loop"==t.message?{status:"fail",header:"Infinite loop",details:"Your code did not finish executing within 60 seconds. Please look to see if you accidentally coded an infinite loop."}:{status:"fail",header:"Unexpected error occurred",details:`Error: ${t.message}`}}(i,t.startLine),this.runStdout=s||""}catch(t){console.warn(`Error in pyodideWorker at ${t.filename}, Line: ${t.lineno}, ${t.message}`)}this.runStatus="",this.testResultsStatus=t.status,this.testResultsHeader=t.header,this.testResultsDetails=t.details}async resetCode(){if(confirm("Are you sure you want to reset your code to the starter code? This cannot be undone.")){const t=Xe.create({doc:this.starterCode||"",extensions:[rd,Gd(),jn.lineWrapping]});this.editor.setState(t);const e=this.getStorageKey();e&&t$(e,this.starterCode)}}}window.customElements.define("code-exercise-element",i$),t.CodeExerciseElement=i$,Object.defineProperty(t,"t",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).CodeExerciseElement={}); + `}getStorageKey(){return this.exerciseName?`${this.exerciseName}-repr`:null}firstUpdated(){const t=this.getStorageKey(),e=t?function(t,e){let i;return i=Jd()?localStorage.getItem(t):Kd[t],i??e}(t):null;e?console.log(`Loading stored code in localStorage from ${t}`):t?console.log(`No stored code found for ${t}, using starter code. Your code changes will be stored in localStorage.`):console.log("No exercise name provided, code will not be stored");const i=Xe.create({doc:e||this.starterCode||"",extensions:[rd,Gd(),Nn.of([Zf]),zl.of(" "),jn.lineWrapping,jn.updateListener.of((t=>{const e=this.getStorageKey();t.docChanged&&e&&t$(e,t.state.doc.toString())}))]});this.editor=new jn({state:i,parent:this.editorRef.value})}async onRunCode(){this.runStatus="Running code...",this.testResultsStatus="";const t=this.editor.state.doc.toString();try{const{results:e,error:i,stdout:s}=await new Ld(t);this.runOutput=i?.message||e||"",this.runStdout=s||"",this.runOutput||this.runStdout||(this.runOutput="No output from code execution.\n",this.showTests&&(this.runOutput+='To check if your function code is correct, select "Run Tests" button instead.'))}catch(t){console.warn(`Error in pyodideWorker at ${t.filename}, Line: ${t.lineno}, ${t.message}`),this.runOutput=`Error: ${t.message}`}this.runStatus=""}async onRunTests(){this.runStatus="Running tests...",this.runOutput="",this.runStdout="";let t=function(t){let e=[...t.split("\n")];return e.push("import sys"),e.push("import io"),e.push("sys.stdout = io.StringIO()"),e.push("import doctest"),e.push("doctest.testmod(verbose=True)"),e.push("sys.stdout.getvalue()"),e=e.join("\n"),{status:"success",header:"Running tests...",code:e,startLine:0}}(this.editor.state.doc.toString());if(t.code)try{const{results:e,error:i,stdout:s}=await new Ld(t.code);t=e?function(t){const e=t.match(/(\d+)\spassed\sand\s(\d+)\sfailed./);if(e){const i=parseInt(e[1],10),s=i+parseInt(e[2],10);return{status:i==s?"pass":"fail",header:`${i} of ${s} tests passed`,details:function(t){let e=[],i=!1;return t.split("\n").forEach((t=>{t.startsWith('File "__main__"')?i=!0:((t.startsWith("Trying:")||t.startsWith("1 items had no tests:"))&&(i=!1),i&&(t=t.replace("Failed example:","\n❌ Failed test:"),e.push(t)))})),e.join("\n").trim()}(t)}}}(e):function(t,e){return t.message.startsWith("Traceback")?{status:"fail",header:"Syntax error",details:Vd(t.message,e)}:"Infinite loop"==t.message?{status:"fail",header:"Infinite loop",details:"Your code did not finish executing within 60 seconds. Please look to see if you accidentally coded an infinite loop."}:{status:"fail",header:"Unexpected error occurred",details:`Error: ${t.message}`}}(i,t.startLine),this.runStdout=s||""}catch(t){console.warn(`Error in pyodideWorker at ${t.filename}, Line: ${t.lineno}, ${t.message}`)}this.runStatus="",this.testResultsStatus=t.status,this.testResultsHeader=t.header,this.testResultsDetails=t.details}async resetCode(){if(confirm("Are you sure you want to reset your code to the starter code? This cannot be undone.")){const t=Xe.create({doc:this.starterCode||"",extensions:[rd,Gd(),jn.lineWrapping]});this.editor.setState(t);const e=this.getStorageKey();e&&t$(e,this.starterCode)}}}window.customElements.define("code-exercise-element",i$),t.CodeExerciseElement=i$,Object.defineProperty(t,"t",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).CodeExerciseElement={}); diff --git a/src/doctest-grader.js b/src/doctest-grader.js index 787e468..41f295f 100644 --- a/src/doctest-grader.js +++ b/src/doctest-grader.js @@ -41,20 +41,13 @@ function cleanupDoctestResults(resultsStr) { export function prepareCode(code) { const lines = code.split('\n'); - if (!(lines[0].includes('def') || lines[0].includes('class'))) { - return { - status: 'fail', - header: 'Error running tests', - details: 'First code line must be `def` or `class` declaration', - }; - } - - let finalCode = [...lines]; // Copy the lines array + let finalCode = [...lines]; // Redirects stdout so we can return it finalCode.push('import sys'); finalCode.push('import io'); finalCode.push('sys.stdout = io.StringIO()'); + // Runs the doctests finalCode.push('import doctest'); finalCode.push('doctest.testmod(verbose=True)'); From c022ecef4d5a08deea36c2e1ce9edc981a296543 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 30 Jun 2025 06:38:10 -0700 Subject: [PATCH 3/3] Update version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 392e436..5f0fedb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "python-code-exercise-element", - "version": "0.1.17", + "version": "0.1.18", "description": "A web component for Python coding exercises with doctest runner", "repository": { "type": "git",