diff --git a/_config.yml b/_config.yml index d2e3536d..1e1a45eb 100644 --- a/_config.yml +++ b/_config.yml @@ -116,4 +116,6 @@ execute: - 'short_courses/r_unit_testing.ipynb' - 'short_courses/r_functions.ipynb' - 'short_courses/r_functions.ipynb' - - 'individual_modules/markdown_with_python/ch2_liveOutput.ipynb' + - 'short_courses/python_language_features.ipynb' + - 'short_courses/python_testing.ipynb' + - 'individual_modules/markdown_with_python/liveOutput.ipynb' diff --git a/_static/multiple_regression_hyperplane.html b/_static/multiple_regression_hyperplane.html index f80e3ff0..e657a871 100644 --- a/_static/multiple_regression_hyperplane.html +++ b/_static/multiple_regression_hyperplane.html @@ -2,6 +2,6 @@
-
+
\ No newline at end of file diff --git a/_static/multiple_regression_hyperplane_2.html b/_static/multiple_regression_hyperplane_2.html index 27f5a15e..eac56f47 100644 --- a/_static/multiple_regression_hyperplane_2.html +++ b/_static/multiple_regression_hyperplane_2.html @@ -2,6 +2,6 @@
-
+
\ No newline at end of file diff --git a/_static/workshop_prereqs_advanced_regression_analysis_with_r_.html b/_static/workshop_prereqs_advanced_regression_analysis_with_r_.html index 0d276aff..1c628a68 100644 --- a/_static/workshop_prereqs_advanced_regression_analysis_with_r_.html +++ b/_static/workshop_prereqs_advanced_regression_analysis_with_r_.html @@ -282,8 +282,8 @@

// parsing and collecting nodes and edges from the python - nodes = new vis.DataSet([{"color": "#B0B0B0", "id": "Regression Analysis with R Adapting to Varied Data Types", "label": "Regression Analysis with R Adapting to Varied Data Types", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Regression Analysis with R Adapting to Varied Data Types\nCourse Pre-reqs: Introduction to Regression with R\nSubsequent Courses: Advanced Regression Analysis With R "}, {"color": "#B0B0B0", "id": "Advanced Regression Analysis With R ", "label": "Advanced Regression Analysis With R ", "level": 4, "shape": "dot", "size": 10, "title": "Course Name: Advanced Regression Analysis With R \nCourse Pre-reqs: Regression Analysis with R Adapting to Varied Data Types\nSubsequent Courses: None"}, {"color": "#B0B0B0", "id": "Introduction to R", "label": "Introduction to R", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to R\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Regression with R"}, {"color": "#B0B0B0", "id": "Introduction to Regression with R", "label": "Introduction to Regression with R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Regression with R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: Regression Analysis with R Adapting to Varied Data Types"}]); - edges = new vis.DataSet([{"arrows": "to", "from": "Regression Analysis with R Adapting to Varied Data Types", "to": "Advanced Regression Analysis With R ", "width": 1}, {"arrows": "to", "from": "Introduction to R", "to": "Introduction to Regression with R", "width": 1}, {"arrows": "to", "from": "Introduction to Regression with R", "to": "Regression Analysis with R Adapting to Varied Data Types", "width": 1}]); + nodes = new vis.DataSet([{"color": "#B0B0B0", "id": "Regression Analysis with R Adapting to Varied Data Types", "label": "Regression Analysis with R Adapting to Varied Data Types", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Regression Analysis with R Adapting to Varied Data Types\nCourse Pre-reqs: Introduction to Regression with R\nSubsequent Courses: Advanced Regression Analysis With R "}, {"color": "#B0B0B0", "id": "Advanced Regression Analysis With R ", "label": "Advanced Regression Analysis With R ", "level": 4, "shape": "dot", "size": 10, "title": "Course Name: Advanced Regression Analysis With R \nCourse Pre-reqs: Regression Analysis with R Adapting to Varied Data Types\nSubsequent Courses: None"}, {"color": "#B0B0B0", "id": "Introduction to Regression with R", "label": "Introduction to Regression with R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Regression with R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: Regression Analysis with R Adapting to Varied Data Types"}, {"color": "#B0B0B0", "id": "Introduction to R", "label": "Introduction to R", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to R\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Regression with R"}]); + edges = new vis.DataSet([{"arrows": "to", "from": "Regression Analysis with R Adapting to Varied Data Types", "to": "Advanced Regression Analysis With R ", "width": 1}, {"arrows": "to", "from": "Introduction to Regression with R", "to": "Regression Analysis with R Adapting to Varied Data Types", "width": 1}, {"arrows": "to", "from": "Introduction to R", "to": "Introduction to Regression with R", "width": 1}]); nodeColors = {}; allNodes = nodes.get({ returnType: "Object" }); diff --git a/_static/workshop_prereqs_intermediate_version_control.html b/_static/workshop_prereqs_intermediate_version_control.html index 76de6db0..4f50c7e5 100644 --- a/_static/workshop_prereqs_intermediate_version_control.html +++ b/_static/workshop_prereqs_intermediate_version_control.html @@ -282,8 +282,8 @@

// parsing and collecting nodes and edges from the python - nodes = new vis.DataSet([{"color": "#B0B0B0", "id": "Introduction to Version Control with Git and GitHub", "label": "Introduction to Version Control with Git and GitHub", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Version Control with Git and GitHub\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Intermediate Version Control"}, {"color": "#B0B0B0", "id": "Intermediate Version Control", "label": "Intermediate Version Control", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Intermediate Version Control\nCourse Pre-reqs: Introduction to Version Control with Git and GitHub\nSubsequent Courses: None"}, {"color": "#B0B0B0", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Version Control with Git and GitHub"}]); - edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to Version Control with Git and GitHub", "to": "Intermediate Version Control", "width": 1}, {"arrows": "to", "from": "Introduction to Unix", "to": "Introduction to Version Control with Git and GitHub", "width": 1}]); + nodes = new vis.DataSet([{"color": "#B0B0B0", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Version Control with Git and GitHub"}, {"color": "#B0B0B0", "id": "Introduction to Version Control with Git and GitHub", "label": "Introduction to Version Control with Git and GitHub", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Version Control with Git and GitHub\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Intermediate Version Control"}, {"color": "#B0B0B0", "id": "Intermediate Version Control", "label": "Intermediate Version Control", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Intermediate Version Control\nCourse Pre-reqs: Introduction to Version Control with Git and GitHub\nSubsequent Courses: None"}]); + edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to Unix", "to": "Introduction to Version Control with Git and GitHub", "width": 1}, {"arrows": "to", "from": "Introduction to Version Control with Git and GitHub", "to": "Intermediate Version Control", "width": 1}]); nodeColors = {}; allNodes = nodes.get({ returnType: "Object" }); diff --git a/_static/workshop_prereqs_parallel_computing.html b/_static/workshop_prereqs_parallel_computing.html index fe8a9c41..54885b27 100644 --- a/_static/workshop_prereqs_parallel_computing.html +++ b/_static/workshop_prereqs_parallel_computing.html @@ -282,8 +282,8 @@

// parsing and collecting nodes and edges from the python - nodes = new vis.DataSet([{"color": "#B0B0B0", "id": "Introduction to HPC", "label": "Introduction to HPC", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to HPC\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Parallel Computing"}, {"color": "#B0B0B0", "id": "Parallel Computing", "label": "Parallel Computing", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Parallel Computing\nCourse Pre-reqs: Introduction to HPC\nSubsequent Courses: None"}, {"color": "#B0B0B0", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to HPC"}]); - edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to HPC", "to": "Parallel Computing", "width": 1}, {"arrows": "to", "from": "Introduction to Unix", "to": "Introduction to HPC", "width": 1}]); + nodes = new vis.DataSet([{"color": "#B0B0B0", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to HPC"}, {"color": "#B0B0B0", "id": "Introduction to HPC", "label": "Introduction to HPC", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to HPC\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Parallel Computing"}, {"color": "#B0B0B0", "id": "Parallel Computing", "label": "Parallel Computing", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Parallel Computing\nCourse Pre-reqs: Introduction to HPC\nSubsequent Courses: None"}]); + edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to Unix", "to": "Introduction to HPC", "width": 1}, {"arrows": "to", "from": "Introduction to HPC", "to": "Parallel Computing", "width": 1}]); nodeColors = {}; allNodes = nodes.get({ returnType: "Object" }); diff --git a/_static/workshop_prereqs_regression_analysis_with_r_adapting_to_varied_data_types.html b/_static/workshop_prereqs_regression_analysis_with_r_adapting_to_varied_data_types.html index ebaa7777..c2443843 100644 --- a/_static/workshop_prereqs_regression_analysis_with_r_adapting_to_varied_data_types.html +++ b/_static/workshop_prereqs_regression_analysis_with_r_adapting_to_varied_data_types.html @@ -282,8 +282,8 @@

// parsing and collecting nodes and edges from the python - nodes = new vis.DataSet([{"color": "#B0B0B0", "id": "Introduction to R", "label": "Introduction to R", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to R\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Regression with R"}, {"color": "#B0B0B0", "id": "Introduction to Regression with R", "label": "Introduction to Regression with R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Regression with R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: Regression Analysis with R Adapting to Varied Data Types"}, {"color": "#B0B0B0", "id": "Regression Analysis with R Adapting to Varied Data Types", "label": "Regression Analysis with R Adapting to Varied Data Types", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Regression Analysis with R Adapting to Varied Data Types\nCourse Pre-reqs: Introduction to Regression with R\nSubsequent Courses: None"}]); - edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to R", "to": "Introduction to Regression with R", "width": 1}, {"arrows": "to", "from": "Introduction to Regression with R", "to": "Regression Analysis with R Adapting to Varied Data Types", "width": 1}]); + nodes = new vis.DataSet([{"color": "#B0B0B0", "id": "Introduction to Regression with R", "label": "Introduction to Regression with R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Regression with R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: Regression Analysis with R Adapting to Varied Data Types"}, {"color": "#B0B0B0", "id": "Regression Analysis with R Adapting to Varied Data Types", "label": "Regression Analysis with R Adapting to Varied Data Types", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Regression Analysis with R Adapting to Varied Data Types\nCourse Pre-reqs: Introduction to Regression with R\nSubsequent Courses: None"}, {"color": "#B0B0B0", "id": "Introduction to R", "label": "Introduction to R", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to R\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Regression with R"}]); + edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to Regression with R", "to": "Regression Analysis with R Adapting to Varied Data Types", "width": 1}, {"arrows": "to", "from": "Introduction to R", "to": "Introduction to Regression with R", "width": 1}]); nodeColors = {}; allNodes = nodes.get({ returnType: "Object" }); diff --git a/_static/workshops_network_hpc.html b/_static/workshops_network_hpc.html index a8d85a26..a33a5c53 100644 --- a/_static/workshops_network_hpc.html +++ b/_static/workshops_network_hpc.html @@ -282,7 +282,7 @@

// parsing and collecting nodes and edges from the python - nodes = new vis.DataSet([{"color": "#4682B4", "id": "Introduction to HPC", "label": "Introduction to HPC", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to HPC\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Parallel Computing"}, {"color": "#4682B4", "id": "Parallel Computing", "label": "Parallel Computing", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Parallel Computing\nCourse Pre-reqs: Introduction to HPC\nSubsequent Courses: None"}, {"color": "#4682B4", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to HPC, Introduction to Version Control with Git and GitHub"}, {"color": "#4682B4", "id": "Introduction to Version Control with Git and GitHub", "label": "Introduction to Version Control with Git and GitHub", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Version Control with Git and GitHub\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Intermediate Version Control"}, {"color": "#4682B4", "id": "Intermediate Version Control", "label": "Intermediate Version Control", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Intermediate Version Control\nCourse Pre-reqs: Introduction to Version Control with Git and GitHub\nSubsequent Courses: None"}, {"color": "#4682B4", "id": "Introduction to Python", "label": "Introduction to Python", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Python\nCourse Pre-reqs: None\nSubsequent Courses: None"}]); + nodes = new vis.DataSet([{"color": "#FF6347", "id": "Introduction to HPC", "label": "Introduction to HPC", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to HPC\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Parallel Computing"}, {"color": "#FF6347", "id": "Parallel Computing", "label": "Parallel Computing", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Parallel Computing\nCourse Pre-reqs: Introduction to HPC\nSubsequent Courses: None"}, {"color": "#FF6347", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to HPC, Introduction to Version Control with Git and GitHub"}, {"color": "#FF6347", "id": "Introduction to Version Control with Git and GitHub", "label": "Introduction to Version Control with Git and GitHub", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Version Control with Git and GitHub\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Intermediate Version Control"}, {"color": "#FF6347", "id": "Intermediate Version Control", "label": "Intermediate Version Control", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Intermediate Version Control\nCourse Pre-reqs: Introduction to Version Control with Git and GitHub\nSubsequent Courses: None"}, {"color": "#FF6347", "id": "Introduction to Python", "label": "Introduction to Python", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Python\nCourse Pre-reqs: None\nSubsequent Courses: None"}]); edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to HPC", "to": "Parallel Computing", "width": 1}, {"arrows": "to", "from": "Introduction to Unix", "to": "Introduction to HPC", "width": 1}, {"arrows": "to", "from": "Introduction to Unix", "to": "Introduction to Version Control with Git and GitHub", "width": 1}, {"arrows": "to", "from": "Introduction to Version Control with Git and GitHub", "to": "Intermediate Version Control", "width": 1}]); nodeColors = {}; diff --git a/_static/workshops_network_python_ds.html b/_static/workshops_network_python_ds.html index fb1a00d9..d430b69f 100644 --- a/_static/workshops_network_python_ds.html +++ b/_static/workshops_network_python_ds.html @@ -282,7 +282,7 @@

// parsing and collecting nodes and edges from the python - nodes = new vis.DataSet([{"color": "#FF6347", "id": "Introduction to Version Control with Git and GitHub", "label": "Introduction to Version Control with Git and GitHub", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Version Control with Git and GitHub\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Intermediate Version Control"}, {"color": "#FF6347", "id": "Intermediate Version Control", "label": "Intermediate Version Control", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Intermediate Version Control\nCourse Pre-reqs: Introduction to Version Control with Git and GitHub\nSubsequent Courses: None"}, {"color": "#FF6347", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Version Control with Git and GitHub"}, {"color": "#FF6347", "id": "Introduction to Python", "label": "Introduction to Python", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Python\nCourse Pre-reqs: None\nSubsequent Courses: Python for Data Analysis, Using Markdown for Python"}, {"color": "#FF6347", "id": "Python for Data Analysis", "label": "Python for Data Analysis", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Python for Data Analysis\nCourse Pre-reqs: Introduction to Python\nSubsequent Courses: Introduction to Machine Learning"}, {"color": "#FF6347", "id": "Using Markdown for Python", "label": "Using Markdown for Python", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Using Markdown for Python\nCourse Pre-reqs: Introduction to Python\nSubsequent Courses: None"}, {"color": "#FF6347", "id": "Introduction to Machine Learning", "label": "Introduction to Machine Learning", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Machine Learning\nCourse Pre-reqs: Python for Data Analysis\nSubsequent Courses: None"}]); + nodes = new vis.DataSet([{"color": "#FFD700", "id": "Introduction to Version Control with Git and GitHub", "label": "Introduction to Version Control with Git and GitHub", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Version Control with Git and GitHub\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Intermediate Version Control"}, {"color": "#FFD700", "id": "Intermediate Version Control", "label": "Intermediate Version Control", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Intermediate Version Control\nCourse Pre-reqs: Introduction to Version Control with Git and GitHub\nSubsequent Courses: None"}, {"color": "#FFD700", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Version Control with Git and GitHub"}, {"color": "#FFD700", "id": "Introduction to Python", "label": "Introduction to Python", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Python\nCourse Pre-reqs: None\nSubsequent Courses: Python for Data Analysis, Using Markdown for Python"}, {"color": "#FFD700", "id": "Python for Data Analysis", "label": "Python for Data Analysis", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Python for Data Analysis\nCourse Pre-reqs: Introduction to Python\nSubsequent Courses: Introduction to Machine Learning"}, {"color": "#FFD700", "id": "Using Markdown for Python", "label": "Using Markdown for Python", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Using Markdown for Python\nCourse Pre-reqs: Introduction to Python\nSubsequent Courses: None"}, {"color": "#FFD700", "id": "Introduction to Machine Learning", "label": "Introduction to Machine Learning", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Machine Learning\nCourse Pre-reqs: Python for Data Analysis\nSubsequent Courses: None"}]); edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to Version Control with Git and GitHub", "to": "Intermediate Version Control", "width": 1}, {"arrows": "to", "from": "Introduction to Unix", "to": "Introduction to Version Control with Git and GitHub", "width": 1}, {"arrows": "to", "from": "Introduction to Python", "to": "Python for Data Analysis", "width": 1}, {"arrows": "to", "from": "Introduction to Python", "to": "Using Markdown for Python", "width": 1}, {"arrows": "to", "from": "Python for Data Analysis", "to": "Introduction to Machine Learning", "width": 1}]); nodeColors = {}; diff --git a/_static/workshops_network_r_ds.html b/_static/workshops_network_r_ds.html index 73fcc6fe..fc6ca45f 100644 --- a/_static/workshops_network_r_ds.html +++ b/_static/workshops_network_r_ds.html @@ -282,7 +282,7 @@

// parsing and collecting nodes and edges from the python - nodes = new vis.DataSet([{"color": "#FFD700", "id": "Introduction to R", "label": "Introduction to R", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to R\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Regression with R, Working With Data In R, Improve Your R Code, Introduction to Markdown in R"}, {"color": "#FFD700", "id": "Introduction to Regression with R", "label": "Introduction to Regression with R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Regression with R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: Regression Analysis with R Adapting to Varied Data Types"}, {"color": "#FFD700", "id": "Working With Data In R", "label": "Working With Data In R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Working With Data In R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: None"}, {"color": "#FFD700", "id": "Improve Your R Code", "label": "Improve Your R Code", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Improve Your R Code\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: None"}, {"color": "#FFD700", "id": "Introduction to Markdown in R", "label": "Introduction to Markdown in R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Markdown in R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: None"}, {"color": "#FFD700", "id": "Regression Analysis with R Adapting to Varied Data Types", "label": "Regression Analysis with R Adapting to Varied Data Types", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Regression Analysis with R Adapting to Varied Data Types\nCourse Pre-reqs: Introduction to Regression with R\nSubsequent Courses: Advanced Regression Analysis With R "}, {"color": "#FFD700", "id": "Advanced Regression Analysis With R ", "label": "Advanced Regression Analysis With R ", "level": 4, "shape": "dot", "size": 10, "title": "Course Name: Advanced Regression Analysis With R \nCourse Pre-reqs: Regression Analysis with R Adapting to Varied Data Types\nSubsequent Courses: None"}, {"color": "#FFD700", "id": "Introduction to Version Control with Git and GitHub", "label": "Introduction to Version Control with Git and GitHub", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Version Control with Git and GitHub\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Intermediate Version Control"}, {"color": "#FFD700", "id": "Intermediate Version Control", "label": "Intermediate Version Control", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Intermediate Version Control\nCourse Pre-reqs: Introduction to Version Control with Git and GitHub\nSubsequent Courses: None"}, {"color": "#FFD700", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Version Control with Git and GitHub"}]); + nodes = new vis.DataSet([{"color": "#4682B4", "id": "Introduction to R", "label": "Introduction to R", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to R\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Regression with R, Working With Data In R, Improve Your R Code, Introduction to Markdown in R"}, {"color": "#4682B4", "id": "Introduction to Regression with R", "label": "Introduction to Regression with R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Regression with R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: Regression Analysis with R Adapting to Varied Data Types"}, {"color": "#4682B4", "id": "Working With Data In R", "label": "Working With Data In R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Working With Data In R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: None"}, {"color": "#4682B4", "id": "Improve Your R Code", "label": "Improve Your R Code", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Improve Your R Code\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: None"}, {"color": "#4682B4", "id": "Introduction to Markdown in R", "label": "Introduction to Markdown in R", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Markdown in R\nCourse Pre-reqs: Introduction to R\nSubsequent Courses: None"}, {"color": "#4682B4", "id": "Regression Analysis with R Adapting to Varied Data Types", "label": "Regression Analysis with R Adapting to Varied Data Types", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Regression Analysis with R Adapting to Varied Data Types\nCourse Pre-reqs: Introduction to Regression with R\nSubsequent Courses: Advanced Regression Analysis With R "}, {"color": "#4682B4", "id": "Advanced Regression Analysis With R ", "label": "Advanced Regression Analysis With R ", "level": 4, "shape": "dot", "size": 10, "title": "Course Name: Advanced Regression Analysis With R \nCourse Pre-reqs: Regression Analysis with R Adapting to Varied Data Types\nSubsequent Courses: None"}, {"color": "#4682B4", "id": "Introduction to Version Control with Git and GitHub", "label": "Introduction to Version Control with Git and GitHub", "level": 2, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Version Control with Git and GitHub\nCourse Pre-reqs: Introduction to Unix\nSubsequent Courses: Intermediate Version Control"}, {"color": "#4682B4", "id": "Intermediate Version Control", "label": "Intermediate Version Control", "level": 3, "shape": "dot", "size": 10, "title": "Course Name: Intermediate Version Control\nCourse Pre-reqs: Introduction to Version Control with Git and GitHub\nSubsequent Courses: None"}, {"color": "#4682B4", "id": "Introduction to Unix", "label": "Introduction to Unix", "level": 1, "shape": "dot", "size": 10, "title": "Course Name: Introduction to Unix\nCourse Pre-reqs: None\nSubsequent Courses: Introduction to Version Control with Git and GitHub"}]); edges = new vis.DataSet([{"arrows": "to", "from": "Introduction to R", "to": "Introduction to Regression with R", "width": 1}, {"arrows": "to", "from": "Introduction to R", "to": "Working With Data In R", "width": 1}, {"arrows": "to", "from": "Introduction to R", "to": "Improve Your R Code", "width": 1}, {"arrows": "to", "from": "Introduction to R", "to": "Introduction to Markdown in R", "width": 1}, {"arrows": "to", "from": "Introduction to Regression with R", "to": "Regression Analysis with R Adapting to Varied Data Types", "width": 1}, {"arrows": "to", "from": "Regression Analysis with R Adapting to Varied Data Types", "to": "Advanced Regression Analysis With R ", "width": 1}, {"arrows": "to", "from": "Introduction to Version Control with Git and GitHub", "to": "Intermediate Version Control", "width": 1}, {"arrows": "to", "from": "Introduction to Unix", "to": "Introduction to Version Control with Git and GitHub", "width": 1}]); nodeColors = {}; diff --git a/_toc.yml b/_toc.yml index 8568c9ce..2c759b51 100644 --- a/_toc.yml +++ b/_toc.yml @@ -161,13 +161,13 @@ parts: - file: individual_modules/introduction_to_machine_learning/resources - file: individual_modules/section_landing_pages/markdown_with_python sections: - - file: individual_modules/markdown_with_python/2-Motivation-and-Background - - file: individual_modules/markdown_with_python/3-Markdown-Fundamentals - - file: individual_modules/markdown_with_python/4-Markdown-Task-1 - - file: individual_modules/markdown_with_python/5-How-to-embed-Python-Code-in-Markdown - - file: individual_modules/markdown_with_python/6-Export-the-Document - - file: individual_modules/markdown_with_python/7-Markdown-Task-2 - - file: individual_modules/markdown_with_python/8-Further-Knowledge + - file: individual_modules/markdown_with_python/Motivation-and-Background + - file: individual_modules/markdown_with_python/Markdown-Fundamentals + - file: individual_modules/markdown_with_python/Markdown-Task-1 + - file: individual_modules/markdown_with_python/How-to-embed-Python-Code-in-Markdown + - file: individual_modules/markdown_with_python/Export-the-Document + - file: individual_modules/markdown_with_python/Markdown-Task-2 + - file: individual_modules/markdown_with_python/Further-Knowledge - file: individual_modules/markdown_with_python/resources - file: course_homepages/R sections: @@ -335,6 +335,8 @@ parts: - file: short_courses/python sections: - file: short_courses/python_environments + - file: short_courses/python_language_features + - file: short_courses/python_testing - file: short_courses/r sections: - file: short_courses/r_environments diff --git a/individual_modules/improve_your_r_code/microbenchmark.ipynb b/individual_modules/improve_your_r_code/microbenchmark.ipynb index 31d3c127..c9aa3833 100644 --- a/individual_modules/improve_your_r_code/microbenchmark.ipynb +++ b/individual_modules/improve_your_r_code/microbenchmark.ipynb @@ -172,7 +172,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Exercise\n", + "## Exercise\n", "Try microbenchmarking one of your own functions." ] } diff --git a/individual_modules/improve_your_r_code/style.ipynb b/individual_modules/improve_your_r_code/style.ipynb index fed25287..9b005838 100644 --- a/individual_modules/improve_your_r_code/style.ipynb +++ b/individual_modules/improve_your_r_code/style.ipynb @@ -492,7 +492,7 @@ "\n", "## The Styler and lintr packages\n", "The `styler` and `lintr` packages are two useful packages for formatting your code according to the Tidyverse style guide. You can install them from CRAN in the usual way:\n", - "```{r eval=FALSE, include=TRUE}\n", + "```r\n", "install.packages(\"styler\")\n", "install.packages(\"lintr\")\n", "```\n", diff --git a/individual_modules/introduction_to_julia/functions.ipynb b/individual_modules/introduction_to_julia/functions.ipynb index d5bbae51..35437c29 100644 --- a/individual_modules/introduction_to_julia/functions.ipynb +++ b/individual_modules/introduction_to_julia/functions.ipynb @@ -369,51 +369,61 @@ ] }, { - "cell_type": "code", - "execution_count": 6, - "id": "ba047e98-2bcf-4e07-a71b-48868ae04f69", + "cell_type": "markdown", + "id": "7d0c0b2a-8214-42a7-8ab8-29525cdfb025", "metadata": {}, - "outputs": [ - { - "ename": "LoadError", - "evalue": "MethodError: no method matching add(::Int64, ::String)\nThe function `add` exists, but no method is defined for this combination of argument types.\n\n\u001b[0mClosest candidates are:\n\u001b[0m add(\u001b[91m::String\u001b[39m, ::String)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[35mMain\u001b[39m \u001b[90m\u001b[4mIn[5]:7\u001b[24m\u001b[39m\n\u001b[0m add(::Int64, \u001b[91m::Int64\u001b[39m)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[35mMain\u001b[39m \u001b[90m\u001b[4mIn[5]:2\u001b[24m\u001b[39m\n", - "output_type": "error", - "traceback": [ - "MethodError: no method matching add(::Int64, ::String)\nThe function `add` exists, but no method is defined for this combination of argument types.\n\n\u001b[0mClosest candidates are:\n\u001b[0m add(\u001b[91m::String\u001b[39m, ::String)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[35mMain\u001b[39m \u001b[90m\u001b[4mIn[5]:7\u001b[24m\u001b[39m\n\u001b[0m add(::Int64, \u001b[91m::Int64\u001b[39m)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[35mMain\u001b[39m \u001b[90m\u001b[4mIn[5]:2\u001b[24m\u001b[39m\n", - "", - "Stacktrace:", - " [1] top-level scope", - "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[6]:2\u001b[24m\u001b[39m" - ] - } - ], "source": [ + "```\n", "# Error: not defined for Int, String\n", - "add(1, \"hello\")" + "add(1, \"hello\")\n", + "```\n", + "\n", + "produces: \n", + "\n", + "```\n", + "MethodError: no method matching add(::Int64, ::String)\n", + "The function `add` exists, but no method is defined for this combination of argument types.\n", + "\n", + "Closest candidates are:\n", + " add(::String, ::String)\n", + " @ Main In[5]:7\n", + " add(::Int64, ::Int64)\n", + " @ Main In[5]:2\n", + "\n", + "\n", + "Stacktrace:\n", + " [1] top-level scope\n", + " @ In[6]:2\n", + "```" ] }, { - "cell_type": "code", - "execution_count": 7, - "id": "203e17c6-f031-4458-9f68-0a9d0682ccab", + "cell_type": "markdown", + "id": "2ddb2f55-54f6-44a7-b619-fef08840a779", "metadata": {}, - "outputs": [ - { - "ename": "LoadError", - "evalue": "MethodError: no method matching add(::Int64, ::Int64, ::Int64)\nThe function `add` exists, but no method is defined for this combination of argument types.\n\n\u001b[0mClosest candidates are:\n\u001b[0m add(::Int64, ::Int64)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[35mMain\u001b[39m \u001b[90m\u001b[4mIn[5]:2\u001b[24m\u001b[39m\n\u001b[0m add(\u001b[91m::String\u001b[39m, \u001b[91m::String\u001b[39m)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[35mMain\u001b[39m \u001b[90m\u001b[4mIn[5]:7\u001b[24m\u001b[39m\n", - "output_type": "error", - "traceback": [ - "MethodError: no method matching add(::Int64, ::Int64, ::Int64)\nThe function `add` exists, but no method is defined for this combination of argument types.\n\n\u001b[0mClosest candidates are:\n\u001b[0m add(::Int64, ::Int64)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[35mMain\u001b[39m \u001b[90m\u001b[4mIn[5]:2\u001b[24m\u001b[39m\n\u001b[0m add(\u001b[91m::String\u001b[39m, \u001b[91m::String\u001b[39m)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[35mMain\u001b[39m \u001b[90m\u001b[4mIn[5]:7\u001b[24m\u001b[39m\n", - "", - "Stacktrace:", - " [1] top-level scope", - "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[7]:2\u001b[24m\u001b[39m" - ] - } - ], "source": [ + "```\n", "# Error: not defined for more than two Ints\n", - "add(1, 2, 3)" + "add(1, 2, 3)\n", + "```\n", + "\n", + "produces: \n", + "\n", + "```\n", + "MethodError: no method matching add(::Int64, ::Int64, ::Int64)\n", + "The function `add` exists, but no method is defined for this combination of argument types.\n", + "\n", + "Closest candidates are:\n", + " add(::Int64, ::Int64)\n", + " @ Main In[5]:2\n", + " add(::String, ::String)\n", + " @ Main In[5]:7\n", + "\n", + "\n", + "Stacktrace:\n", + " [1] top-level scope\n", + " @ In[7]:2\n", + "```" ] }, { @@ -426,7 +436,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "3f2a79ca-6003-4607-b60b-8119890fe2e4", "metadata": {}, "outputs": [ @@ -436,7 +446,7 @@ "add (generic function with 4 methods)" ] }, - "execution_count": 8, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -470,7 +480,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "6076fdcd-7e84-4ff7-8eac-cfc65b2b34bc", "metadata": {}, "outputs": [], @@ -497,7 +507,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "id": "0535dc96-91cc-41c1-a2db-86638c7d7a49", "metadata": {}, "outputs": [ @@ -507,7 +517,7 @@ "add (generic function with 5 methods)" ] }, - "execution_count": 10, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -552,7 +562,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "id": "91b257a3-0a45-4c51-840d-ef6e6fdecba9", "metadata": {}, "outputs": [ @@ -595,7 +605,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "id": "6fae928e-0537-4112-976f-f47b7dd8be26", "metadata": {}, "outputs": [ @@ -622,7 +632,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 11, "id": "dc6bdf2e-d5ad-4bd0-ae93-f6679e270cd0", "metadata": {}, "outputs": [ @@ -632,7 +642,7 @@ "add (generic function with 6 methods)" ] }, - "execution_count": 13, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -654,7 +664,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 12, "id": "abab1a36-1024-417e-a3d7-f5b6bfba5df2", "metadata": {}, "outputs": [ @@ -683,38 +693,38 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 13, "id": "68d60f05-7d23-465f-aba7-7669a70a93b1", "metadata": {}, "outputs": [ { "data": { "text/html": [ - "# 6 methods for generic function add from \u001b[35mMain\u001b[39m:" + "# 6 methods for generic function add from \u001b[35mMain\u001b[39m:" ], "text/plain": [ "# 6 methods for generic function \"add\" from \u001b[35mMain\u001b[39m:\n", " [1] add(\u001b[90mx\u001b[39m::\u001b[1mString\u001b[22m, \u001b[90my\u001b[39m::\u001b[1mInt64\u001b[22m)\n", - "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[8]:5\u001b[24m\u001b[39m\n", + "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[6]:5\u001b[24m\u001b[39m\n", " [2] add(\u001b[90mx\u001b[39m::\u001b[1mInt64\u001b[22m, \u001b[90my\u001b[39m::\u001b[1mString\u001b[22m)\n", - "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[8]:2\u001b[24m\u001b[39m\n", + "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[6]:2\u001b[24m\u001b[39m\n", " [3] add(\u001b[90mx\u001b[39m::\u001b[1mString\u001b[22m, \u001b[90my\u001b[39m::\u001b[1mString\u001b[22m)\n", "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[5]:7\u001b[24m\u001b[39m\n", " [4] add(\u001b[90mx\u001b[39m::\u001b[1mInt64\u001b[22m, \u001b[90my\u001b[39m::\u001b[1mInt64\u001b[22m)\n", "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[5]:2\u001b[24m\u001b[39m\n", " [5] add(\u001b[90mx\u001b[39m::\u001b[1mNumber\u001b[22m, \u001b[90my\u001b[39m::\u001b[1mNumber\u001b[22m)\n", - "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[13]:2\u001b[24m\u001b[39m\n", + "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[11]:2\u001b[24m\u001b[39m\n", " [6] add(\u001b[90mx\u001b[39m, \u001b[90my\u001b[39m)\n", - "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[10]:2\u001b[24m\u001b[39m" + "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[8]:2\u001b[24m\u001b[39m" ] }, - "execution_count": 15, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# See all methods for `add`\n", + "# See all methods for add\n", "methods(add)" ] }, @@ -781,7 +791,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 14, "id": "c42e667f-ddee-4cc5-b8de-a725cac617e2", "metadata": { "editable": true, @@ -855,7 +865,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 15, "id": "1fa474f1-9085-4fa3-80e9-90211ea59713", "metadata": { "editable": true, @@ -876,7 +886,7 @@ " 25" ] }, - "execution_count": 17, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -904,7 +914,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 16, "id": "3e9cc925-d1a8-4a8e-8347-fbf8d5a62c47", "metadata": { "editable": true, @@ -923,7 +933,7 @@ "", "Stacktrace:", " [1] top-level scope", - "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[18]:2\u001b[24m\u001b[39m" + "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[16]:2\u001b[24m\u001b[39m" ] } ], @@ -955,7 +965,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 17, "id": "5b6de371-efc6-471c-a2b8-3630e1af0749", "metadata": {}, "outputs": [], @@ -982,7 +992,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 18, "id": "56924f81-38ed-4d0e-bbb9-1bded88a1757", "metadata": {}, "outputs": [], @@ -1032,7 +1042,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 19, "id": "3ba0a139-9e29-41c8-ae56-8da9c479ca41", "metadata": { "editable": true, diff --git a/individual_modules/introduction_to_julia/variables.ipynb b/individual_modules/introduction_to_julia/variables.ipynb index ffa364b4..b2fbf903 100644 --- a/individual_modules/introduction_to_julia/variables.ipynb +++ b/individual_modules/introduction_to_julia/variables.ipynb @@ -785,27 +785,35 @@ ] }, { - "cell_type": "code", - "execution_count": 17, - "id": "2f47d33d-a24d-4d80-bfa0-0029b5ed5f07", + "cell_type": "markdown", + "id": "0a6b87d1-14bb-4a7a-bd1a-3fdac5435915", "metadata": {}, - "outputs": [ - { - "ename": "LoadError", - "evalue": "MethodError: \u001b[0mCannot `convert` an object of type \u001b[92mFloat64\u001b[39m\u001b[0m to an object of type \u001b[91mString\u001b[39m\nThe function `convert` exists, but no method is defined for this combination of argument types.\n\n\u001b[0mClosest candidates are:\n\u001b[0m convert(::Type{String}, \u001b[91m::Base.JuliaSyntax.Kind\u001b[39m)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[90mBase\u001b[39m \u001b[90m/Users/julia/.julia/scratchspaces/a66863c6-20e8-4ff4-8a62-49f30b1f605e/agent-cache/default-honeycrisp-HL2F7YQ3XH.0/build/default-honeycrisp-HL2F7YQ3XH-0/julialang/julia-release-1-dot-11/base/JuliaSyntax/src/\u001b[39m\u001b[90m\u001b[4mkinds.jl:975\u001b[24m\u001b[39m\n\u001b[0m convert(::Type{String}, \u001b[91m::String\u001b[39m)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[90mBase\u001b[39m \u001b[90m\u001b[4messentials.jl:461\u001b[24m\u001b[39m\n\u001b[0m convert(::Type{T}, \u001b[91m::T\u001b[39m) where T<:AbstractString\n\u001b[0m\u001b[90m @\u001b[39m \u001b[90mBase\u001b[39m \u001b[90mstrings/\u001b[39m\u001b[90m\u001b[4mbasic.jl:231\u001b[24m\u001b[39m\n\u001b[0m ...\n", - "output_type": "error", - "traceback": [ - "MethodError: \u001b[0mCannot `convert` an object of type \u001b[92mFloat64\u001b[39m\u001b[0m to an object of type \u001b[91mString\u001b[39m\nThe function `convert` exists, but no method is defined for this combination of argument types.\n\n\u001b[0mClosest candidates are:\n\u001b[0m convert(::Type{String}, \u001b[91m::Base.JuliaSyntax.Kind\u001b[39m)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[90mBase\u001b[39m \u001b[90m/Users/julia/.julia/scratchspaces/a66863c6-20e8-4ff4-8a62-49f30b1f605e/agent-cache/default-honeycrisp-HL2F7YQ3XH.0/build/default-honeycrisp-HL2F7YQ3XH-0/julialang/julia-release-1-dot-11/base/JuliaSyntax/src/\u001b[39m\u001b[90m\u001b[4mkinds.jl:975\u001b[24m\u001b[39m\n\u001b[0m convert(::Type{String}, \u001b[91m::String\u001b[39m)\n\u001b[0m\u001b[90m @\u001b[39m \u001b[90mBase\u001b[39m \u001b[90m\u001b[4messentials.jl:461\u001b[24m\u001b[39m\n\u001b[0m convert(::Type{T}, \u001b[91m::T\u001b[39m) where T<:AbstractString\n\u001b[0m\u001b[90m @\u001b[39m \u001b[90mBase\u001b[39m \u001b[90mstrings/\u001b[39m\u001b[90m\u001b[4mbasic.jl:231\u001b[24m\u001b[39m\n\u001b[0m ...\n", - "", - "Stacktrace:", - " [1] top-level scope", - "\u001b[90m @\u001b[39m \u001b[90m\u001b[4mIn[17]:2\u001b[24m\u001b[39m" - ] - } - ], "source": [ + "```\n", "# Error -- cannot convert 1.0 to a string\n", - "name = 1.0" + "name = 1.0\n", + "```\n", + "\n", + "produces: \n", + "\n", + "```\n", + "MethodError: Cannot `convert` an object of type Float64 to an object of type String\n", + "The function `convert` exists, but no method is defined for this combination of argument types.\n", + "\n", + "Closest candidates are:\n", + " convert(::Type{String}, ::Base.JuliaSyntax.Kind)\n", + " @ Base /Users/julia/.julia/scratchspaces/a66863c6-20e8-4ff4-8a62-49f30b1f605e/agent-cache/default-honeycrisp-HL2F7YQ3XH.0/build/default-honeycrisp-HL2F7YQ3XH-0/julialang/julia-release-1-dot-11/base/JuliaSyntax/src/kinds.jl:975\n", + " convert(::Type{String}, ::String)\n", + " @ Base essentials.jl:461\n", + " convert(::Type{T}, ::T) where T<:AbstractString\n", + " @ Base strings/basic.jl:231\n", + " ...\n", + "\n", + "\n", + "Stacktrace:\n", + " [1] top-level scope\n", + " @ In[17]:2\n", + "```" ] }, { diff --git a/individual_modules/markdown_with_python/6-Export-the-Document.md b/individual_modules/markdown_with_python/Export-the-Document.md similarity index 100% rename from individual_modules/markdown_with_python/6-Export-the-Document.md rename to individual_modules/markdown_with_python/Export-the-Document.md diff --git a/individual_modules/markdown_with_python/8-Further-Knowledge.md b/individual_modules/markdown_with_python/Further-Knowledge.md similarity index 100% rename from individual_modules/markdown_with_python/8-Further-Knowledge.md rename to individual_modules/markdown_with_python/Further-Knowledge.md diff --git a/individual_modules/markdown_with_python/5-How-to-embed-Python-Code-in-Markdown.md b/individual_modules/markdown_with_python/How-to-embed-Python-Code-in-Markdown.md similarity index 100% rename from individual_modules/markdown_with_python/5-How-to-embed-Python-Code-in-Markdown.md rename to individual_modules/markdown_with_python/How-to-embed-Python-Code-in-Markdown.md diff --git a/individual_modules/markdown_with_python/3-Markdown-Fundamentals.md b/individual_modules/markdown_with_python/Markdown-Fundamentals.md similarity index 99% rename from individual_modules/markdown_with_python/3-Markdown-Fundamentals.md rename to individual_modules/markdown_with_python/Markdown-Fundamentals.md index 711f6d97..8d8473d7 100644 --- a/individual_modules/markdown_with_python/3-Markdown-Fundamentals.md +++ b/individual_modules/markdown_with_python/Markdown-Fundamentals.md @@ -122,7 +122,7 @@ Here are the main text styles: - ***Bold and italic text***: `***Bold and italic text***` - - ~~Strikethrough~~: `~~Strikethrough text~~` + - **Strikethrough**: `~~Strikethrough text~~` These styles help make your writing easier to read. Use them to highlight important points, but keep it simple so your document stays clean and easy to follow. diff --git a/individual_modules/markdown_with_python/4-Markdown-Task-1.md b/individual_modules/markdown_with_python/Markdown-Task-1.md similarity index 100% rename from individual_modules/markdown_with_python/4-Markdown-Task-1.md rename to individual_modules/markdown_with_python/Markdown-Task-1.md diff --git a/individual_modules/markdown_with_python/7-Markdown-Task-2.md b/individual_modules/markdown_with_python/Markdown-Task-2.md similarity index 100% rename from individual_modules/markdown_with_python/7-Markdown-Task-2.md rename to individual_modules/markdown_with_python/Markdown-Task-2.md diff --git a/individual_modules/markdown_with_python/2-Motivation-and-Background.md b/individual_modules/markdown_with_python/Motivation-and-Background.md similarity index 100% rename from individual_modules/markdown_with_python/2-Motivation-and-Background.md rename to individual_modules/markdown_with_python/Motivation-and-Background.md diff --git a/individual_modules/markdown_with_python/ch0_outline.md b/individual_modules/markdown_with_python/ch0_outline.md deleted file mode 100644 index afec3034..00000000 --- a/individual_modules/markdown_with_python/ch0_outline.md +++ /dev/null @@ -1,55 +0,0 @@ -# Course Outline: Introduction to Markdown with Python - -## 1. Formatting in Markdown - -### 1. Headings - -### 2. Paragraphs & Line Breaks - -### 3. Text Styling - -### 4. Lists -- Unordered Lists (Bullets) -- Ordered Lists (Numbered) - -### 5. Task Lists - -### 6. Blockquotes - -### 7. Code Formatting -- Inline Code -- Code Blocks - -### 8. Tables -- Links -- Images - -### 9. Horizontal Lines - -### 10. Links and Images - -### Summary of Formatting Features - -## Exercise 1 - -## 2. Embedding Python Code in Markdown -- Inline code snippets -- Executable Python code blocks in Jupyter Notebook -- Running Python within Markdown cells - -## Exercise 2 -- Create a structured Markdown document -- Add formatted text and lists -- Embed and execute Python code in Markdown -- Save and export your document - -## 3. Exporting Dynamic Documents -- Exporting documents to different formats (HTML, PDF, etc.) - -## 4. Q&A -- Key takeaways -- Additional resources and documentation -- Ask away! - - - \ No newline at end of file diff --git a/individual_modules/markdown_with_python/ch2_liveOutput.ipynb b/individual_modules/markdown_with_python/ch2_liveOutput.ipynb deleted file mode 100644 index 2e5638bf..00000000 --- a/individual_modules/markdown_with_python/ch2_liveOutput.ipynb +++ /dev/null @@ -1,1983 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "240dfb4a-2234-489b-89ed-1d60c07dfe5c", - "metadata": {}, - "source": [ - "# Introduction" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "df768551-afd7-436a-9c60-457f60ffcc9e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This is my Research Report!\n" - ] - } - ], - "source": [ - "print(\"This is my Research Report!\")" - ] - }, - { - "cell_type": "markdown", - "id": "e3e439fd-c956-4afd-ae0b-a037d3f42297", - "metadata": {}, - "source": [ - "# Methods" - ] - }, - { - "cell_type": "markdown", - "id": "be441b6f-e7de-437d-8e75-e607f7267eaf", - "metadata": {}, - "source": [ - "1. Open JupyterLab\n", - "2. Creade a Jupyter Notebook\n", - "3. Start typing" - ] - }, - { - "cell_type": "markdown", - "id": "5bf438c9-aa9e-4c88-8663-dcc45c79a7d3", - "metadata": {}, - "source": [ - "# Results" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "16fdfc09-84e6-4a5a-9bea-ec24e7378bc7", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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g4waNQrTp0+Xn1OEmePHj5fjkRJReWGQISJFHDhwAHPnzsXGjRvx1FNPYdiwYXJ9y8cff4yoqKg7D+HgwYNyUXCXLl3k68VMi9jdJFpDd8vJycGxY8dgYWGBuLi4h379119/HefPn7/T2mrSpImcxSEibWGQIaIKJwLHK6+8grfeeku2e77//nscPnwYX331Fdzd3VG3bt07j6LQ4+PjI8OLCBxipkUs1r3fuHHjYG5uji1btsi1Mrt27XpoHWK7t5gBWrt2rfxYsYuKiLSFQYaIKtyECRPk7ItoAQm1atWSrSCxcPfChQv/er0ILqKNJNbEnDt3ToYUsVD4bps2bZKtqqVLl6J9+/YYP348hg4dKtfEPIhYR7Nt2zbZ1hLrc/7880+5XoeItIWLfYmoQu3Zswdt27aVi29btWp1z5+J7dOFhYXYsWOH3Il0NxFyRFDJy8tD165d8fTTT2PKlClyO/W1a9fkgmGxW0mEJEFszW7evDl8fX3l+pr7F/uKXVNi5kaszXF2dkanTp1kq6ty5coV+v0goifDIENERESaxdYSERERaRaDDBEREWkWgwwRERFpFoMMERERaRaDDBEREWkWgwwRERFpFoMMERERaRaDDBEREWkWgwwRERFpFoMMERERaRaDDBEREWkWgwwRERFBq/4vsQ3g12ndukIAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "# might have to run: pip install matplotlib in the terminal\n", - "\n", - "# Sample data\n", - "x = [1, 2, 3, 4, 5]\n", - "y = [2, 4, 6, 8, 10]\n", - "\n", - "# Create a line plot\n", - "plt.plot(x, y)\n", - "\n", - "# Add labels and title\n", - "plt.xlabel('X-axis')\n", - "plt.ylabel('Y-axis')\n", - "plt.title('Basic Line Plot')\n", - "\n", - "# Show the plot\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "322eb9dc-5f35-454d-9e71-19ad249f859a", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "hovertemplate": "Year=%{x}
Sales=%{y}", - "legendgroup": "", - "line": { - "color": "#636efa", - "dash": "solid" - }, - "marker": { - "symbol": "circle" - }, - "mode": "lines+markers", - "name": "", - "orientation": "v", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "4gfjB+QH5QfmBw==", - "dtype": "i2" - }, - "xaxis": "x", - "y": { - "bdata": "ZACWAMgAtADcAA==", - "dtype": "i2" - }, - "yaxis": "y" - } - ], - "layout": { - "legend": { - "tracegroupgap": 0 - }, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermap": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermap" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Sales Over Time" - }, - "xaxis": { - "anchor": "y", - "domain": [ - 0, - 1 - ], - "title": { - "text": "Year" - } - }, - "yaxis": { - "anchor": "x", - "domain": [ - 0, - 1 - ], - "title": { - "text": "Sales" - } - } - } - }, - "image/png": "iVBORw0KGgoAAAANSUhEUgAACMgAAAFoCAYAAACo8FWBAAAAAXNSR0IArs4c6QAAIABJREFUeF7s3QeUZFWd+PHfq849XV0dBhDFwLIuBkBxTbCLkkTJQUCCqCRhQTIKgggIuAIKCJJMgKgIoiigiAFR/wICgijioosoSxChY/V07qr/+d33qsPMdFf1va9uhfetczjT01P33fc+t1g529+5N8jn83nhhQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAnQoEBDJ1urI8FgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggIARIJDhg4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBQ1wIEMnW9vDwcAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAIEMnwEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBuhYgkKnr5eXhEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBAhk+AwggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII1LUAgUxdLy8PhwACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIEAgw2cAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoK4FCGTqenl5OAQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAECGT4DCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAnUtQCBT18vLwyGAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggQyPAZQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEKhrAQKZul5eHg4BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECAQIbPAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBdCxDI1PXy8nAIIIAAAggggAACCCCAAAIIIIAAAggggAAC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" - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import plotly.express as px\n", - "import pandas as pd\n", - "\n", - "# Sample data\n", - "df = pd.DataFrame({\n", - " \"Year\": [2018, 2019, 2020, 2021, 2022],\n", - " \"Sales\": [100, 150, 200, 180, 220]\n", - "})\n", - "\n", - "# Create interactive line plot\n", - "fig = px.line(df, x=\"Year\", y=\"Sales\", title=\"Sales Over Time\", markers=True)\n", - "fig.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "0489633f-4d95-4cf9-9ffc-0846672df1ac", - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "6de39751-c3a8-497f-9e5d-1999c53f1b77", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: plotly in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (6.2.0)\n", - "Requirement already satisfied: pandas in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (2.3.1)\n", - "Requirement already satisfied: narwhals>=1.15.1 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from plotly) (2.0.1)\n", - "Requirement already satisfied: packaging in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from plotly) (25.0)\n", - "Requirement already satisfied: numpy>=1.23.2 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from pandas) (1.26.4)\n", - "Requirement already satisfied: python-dateutil>=2.8.2 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from pandas) (2.9.0.post0)\n", - "Requirement already satisfied: pytz>=2020.1 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from pandas) (2025.2)\n", - "Requirement already satisfied: tzdata>=2022.7 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from pandas) (2025.2)\n", - "Requirement already satisfied: six>=1.5 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\n" - ] - } - ], - "source": [ - "!pip install plotly pandas" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "ae1068cb-1ceb-44f9-89fc-5e64a7b38cf5", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "hovertemplate": "Year=%{x}
Sales=%{y}", - "legendgroup": "", - "line": { - "color": "#636efa", - "dash": "solid" - }, - "marker": { - "symbol": "circle" - }, - "mode": "lines+markers", - "name": "", - "orientation": "v", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "4gfjB+QH5QfmBw==", - "dtype": "i2" - }, - "xaxis": "x", - "y": { - "bdata": "ZACWAMgAtADcAA==", - "dtype": "i2" - }, - "yaxis": "y" - } - ], - "layout": { - "legend": { - "tracegroupgap": 0 - }, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermap": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermap" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Sales Over Time" - }, - "xaxis": { - "anchor": "y", - "domain": [ - 0, - 1 - ], - "title": { - "text": "Year" - } - }, - "yaxis": { - "anchor": "x", - "domain": [ - 0, - 1 - ], - "title": { - "text": "Sales" - } - } - } - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import plotly.express as px\n", - "import pandas as pd\n", - "\n", - "# Sample data\n", - "df = pd.DataFrame({\n", - " \"Year\": [2018, 2019, 2020, 2021, 2022],\n", - " \"Sales\": [100, 150, 200, 180, 220]\n", - "})\n", - "\n", - "# Create interactive line plot\n", - "fig = px.line(df, x=\"Year\", y=\"Sales\", title=\"Sales Over Time\", markers=True)\n", - "fig.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "a95fea8f-a39c-4a10-8302-a08beeb31181", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: ipywidgets in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (8.1.7)\n", - "Requirement already satisfied: comm>=0.1.3 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipywidgets) (0.2.3)\n", - "Requirement already satisfied: ipython>=6.1.0 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipywidgets) (9.4.0)\n", - "Requirement already satisfied: traitlets>=4.3.1 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipywidgets) (5.14.3)\n", - "Requirement already satisfied: widgetsnbextension~=4.0.14 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipywidgets) (4.0.14)\n", - "Requirement already satisfied: jupyterlab_widgets~=3.0.15 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipywidgets) (3.0.15)\n", - "Requirement already satisfied: decorator in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (5.2.1)\n", - "Requirement already satisfied: ipython-pygments-lexers in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (1.1.1)\n", - "Requirement already satisfied: jedi>=0.16 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.19.2)\n", - "Requirement already satisfied: matplotlib-inline in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.1.7)\n", - "Requirement already satisfied: pexpect>4.3 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (4.9.0)\n", - "Requirement already satisfied: prompt_toolkit<3.1.0,>=3.0.41 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (3.0.51)\n", - "Requirement already satisfied: pygments>=2.4.0 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (2.19.2)\n", - "Requirement already satisfied: stack_data in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.6.3)\n", - "Requirement already satisfied: typing_extensions>=4.6 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (4.14.1)\n", - "Requirement already satisfied: wcwidth in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from prompt_toolkit<3.1.0,>=3.0.41->ipython>=6.1.0->ipywidgets) (0.2.13)\n", - "Requirement already satisfied: parso<0.9.0,>=0.8.4 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets) (0.8.4)\n", - "Requirement already satisfied: ptyprocess>=0.5 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets) (0.7.0)\n", - "Requirement already satisfied: executing>=1.2.0 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from stack_data->ipython>=6.1.0->ipywidgets) (2.2.0)\n", - "Requirement already satisfied: asttokens>=2.1.0 in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from stack_data->ipython>=6.1.0->ipywidgets) (3.0.0)\n", - "Requirement already satisfied: pure-eval in /Users/lb788/miniconda3/envs/CfRR-py311/lib/python3.11/site-packages (from stack_data->ipython>=6.1.0->ipywidgets) (0.2.3)\n" - ] - } - ], - "source": [ - "!pip install ipywidgets" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "0769a722-1a50-449d-9126-c7293af1a62a", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9120cef866864dc792e9c93f38a184c4", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "interactive(children=(IntSlider(value=10, description='x_max', max=20, min=1), Output()), _dom_classes=('widge…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import ipywidgets as widgets\n", - "from ipywidgets import interact\n", - "\n", - "# Function to plot data\n", - "def plot_graph(x_max):\n", - " # Generate x values and compute y values\n", - " x = np.linspace(0, x_max, 100)\n", - " y = np.sin(x)\n", - "\n", - " # Create the plot\n", - " plt.plot(x, y, label=f\"sin(x), x_max = {x_max}\")\n", - " plt.xlabel('X')\n", - " plt.ylabel('Y')\n", - " plt.title('Interactive Plot of sin(x)')\n", - " plt.legend()\n", - " plt.grid(True)\n", - " plt.show()\n", - "\n", - "# Create an interactive slider widget for adjusting the x_max value\n", - "interact(plot_graph, x_max=widgets.IntSlider(min=1, max=20, step=1, value=10))\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b30d8410-4569-422a-9f11-597331f6d6dd", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/individual_modules/python_for_data_analysis/Python_Language_features.ipynb b/individual_modules/python_for_data_analysis/Python_Language_features.ipynb deleted file mode 100644 index 0a5ab7f0..00000000 --- a/individual_modules/python_for_data_analysis/Python_Language_features.ipynb +++ /dev/null @@ -1,2100 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "584fd347-192b-4b4f-af38-264c5132e694", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "# Advanced Language Features \n", - "## Learning Objectives\n", - "- Understand the use and significance of docstrings and type hints in Python for better code documentation and type-checking\n", - "- Utilize introspection to inspect objects, functions, and modules\n", - "- Apply decorators to modify the behaviour of functions and methods\n", - "- Implement useful techniques such as casting and error handling with try-except blocks\n", - "- Create and use lambda functions, list comprehensions, and generator expressions for efficient Python programming\n", - "- Define and use classes in Python, including understanding inheritance and operator overloading\n", - "\n", - "\n", - "\n", - " " - ] - }, - { - "cell_type": "markdown", - "id": "5a6452ca", - "metadata": {}, - "source": [ - "## Annotating code\n", - "\n", - "Although Python code is readable by humans, software documentation is always required. This can take many forms and serve many different users.\n", - "\n", - "Good choices of names for modules, variables and functions can help to communicate the purpose, and limitations, of your code. But if you want to share your code, or even come back to it in a few months time, it's worth taking the trouble to include comments and *docstrings*.\n", - "\n", - "### Docstrings\n", - "\n", - "A *docstring* is a string literal that occurs as the first statement in a module, function, class, or method definition. Such a *docstring* becomes the ```__doc__``` special attribute of that object.\n", - "\n", - "All modules should normally have *docstrings*, and all functions and classes exported by a module should also have *docstrings*. Public methods (including the ```__init__``` constructor) should also have *docstrings*. A package may be documented in the module *docstring* of the ```__init__.py``` file in the package directory.\n", - "\n", - "Ref \n", - "\n", - "\n", - "### Type-hints\n", - "\n", - "Here's an example from the Azure Cognitive Services documentation.\n", - "\n", - "```python\n", - "can_read_data(requested_bytes: int, pos: int | None = None) -> bool\n", - "```\n", - "\n", - "Type-hints are not enforced by Python, which can seem strange if you are more familiar with compiled languages.\n", - "\n", - "Also, type-hints are evolving, although they have been available since Python 3.5 (2015) new features have been added since, with a ```type``` statement added in Python 3.12 (2023). " - ] - }, - { - "cell_type": "markdown", - "id": "8abdd292", - "metadata": {}, - "source": [ - "### Docstring Exercise\n", - "\n", - "All standard Python modules have docstrings that describe the module and exported classes and functions. They can be accessed using the built-in ```help()``` function.\n", - "\n", - "Here is an example docstring for the random module. Find the Python code for this module and compare the text with the help() output. \n", - "\n", - "Hint: ```python -m site``` " - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b4da2058", - "metadata": {}, - "outputs": [], - "source": [ - "import random\n", - "\n", - "# help(random)" - ] - }, - { - "cell_type": "markdown", - "id": "6dd38e30", - "metadata": {}, - "source": [ - "## Introspection\n", - "\n", - "The help() function uses introspection to establish the signature of functions.\n", - "\n", - "For example" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5a6ccb58", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on function my_add_two in module __main__:\n", - "\n", - "my_add_two(a)\n", - " Add the value 2.0\n", - " \n", - " argument:\n", - " a -- input value\n", - " \n", - " return:\n", - " a + 2.0\n", - "\n" - ] - } - ], - "source": [ - "def my_add_two(a):\n", - " \"\"\"\n", - " Add the value 2.0\n", - "\n", - " argument:\n", - " a -- input value\n", - "\n", - " return:\n", - " a + 2.0\n", - "\n", - " \"\"\"\n", - " return a + 2.0\n", - "\n", - "help(my_add_two)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "0cb8fd40", - "metadata": {}, - "outputs": [], - "source": [ - "## Other introspection functions\n", - "\n", - "# help(dir) \n", - "\n", - "# help(type)\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "f39f84fa", - "metadata": {}, - "source": [ - "### Typing exercise\n", - "\n", - "Copy the following code fragment into your IDE. If type checking is enabled you should see a warning message.\n", - "\n", - "![Screenshot of a Python code snippet in Visual Studio Code showing type hints and type checking errors. A dictionary word_countis annotated asdict[str, int]. Assigning a string key 'the'with integer value1is valid, but assigning an integer key2with a string value'error' triggers type errors. The editor highlights mismatched key and value types using Pylance.](images/type-checking.png)\n", - "\n", - "Without making any changes, try running the code." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "6b463bc2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'the': 1, 2: 'error'}\n" - ] - } - ], - "source": [ - "word_count: \"dict[str,int]\" = {}\n", - "\n", - "word_count['the'] = 1\n", - "word_count[2] = 'error'\n", - "\n", - "print(word_count)\n" - ] - }, - { - "cell_type": "markdown", - "id": "441d71df", - "metadata": {}, - "source": [ - "For some projects it can be important that typing, and other, errors are caught before software is deployed.\n", - "Tools such as **mypy** can be used to enforce these rules.\n", - "\n", - "```text\n", - "$ mypy type_hints.py\n", - "type_hints.py:4: error: Invalid index type \"int\" for \"dict[str, int]\"; expected type \"str\" [index]\n", - "type_hints.py:4: error: Incompatible types in assignment (expression has type \"str\", target has type \"int\") [assignment]\n", - "Found 2 errors in 1 file (checked 1 source file)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "7f40b3cb", - "metadata": {}, - "source": [ - "## Decorators\n", - "\n", - "Another form of Python code annotation is decorators. Though there is an important distinction to be made. Decorators modify the behavior of code.\n", - "\n", - "Although you can create your own decorators - they are really just functions that call other functions - you will most often encounter them as part of a toolkit or framework, i.e. software that simplifies the creation of certain types of application.\n", - "\n", - "For example, the Flask web framework uses a 'route' decorator to associate functions that you supply with particular URLs.\n", - "\n", - "```python\n", - "@app.route('/')\n", - "def index():\n", - " return render_template('index.html')\n", - "\n", - "@app.route('/about')\n", - "def about():\n", - " return render_template('about.html')\n", - "```\n" - ] - }, - { - "cell_type": "markdown", - "id": "86379c8b", - "metadata": {}, - "source": [ - "## Useful techniques\n", - "\n", - "We have already seen dir() and type() for **introspection**.\n", - "\n", - "Another useful technique is:\n", - "\n", - "### Casting\n", - "\n", - "```\n", - "float(\"6.4\")\n", - "list((1,2,3))\n", - "int(\"12\")\n", - "str() # Though print() does this by default\n", - "```\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "b4689e96", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "mon\n", - "['Mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']\n" - ] - } - ], - "source": [ - "days = ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')\n", - "print(days[0])\n", - "# days[0] = 'Mon'\n", - "days = list(days)\n", - "days[0] = 'Mon'\n", - "print(days)\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "53626169", - "metadata": {}, - "source": [ - "### try-except (and raise)\n", - "\n", - "```python\n", - "my_num = float(input('type a number '))\n", - "```\n", - "\n", - "If the user types text that cannot be converted to a number, then an error, or exception, occurs.\n", - "\n", - "\n", - "```text\n", - "---------------------------------------------------------------------------\n", - "ValueError Traceback (most recent call last)\n", - "Cell In [9], line 1\n", - "----> 1 my_num = float(input('type a number '))\n", - " 2 print(my_num)\n", - "\n", - "ValueError: could not convert string to float: 'one'\n", - "```\n", - "\n", - "These run-time errors can be caught with a 'try-except' clause.\n", - "\n", - "## Exercise\n", - "\n", - "Uncomment the following code.\n", - "\n", - "Add a try-except clause to this code sample." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "4e1db602", - "metadata": {}, - "outputs": [], - "source": [ - "# my_num = float(input('type a number '))\n", - "# print(my_num)" - ] - }, - { - "cell_type": "markdown", - "id": "2d82f46e", - "metadata": {}, - "source": [ - "It can be tempting to wrap parts of a program that might fail in a try-except that simply ignores the exception. This can cause many problems, and be a nightmare for others trying to use your code.\n", - "\n", - "For example you might have a loop opening files, some of which contain invalid data. Using a 'bare' or 'catch-all' exception would also mask file permission errors, incorrect paths, and much more.\n", - "\n", - "This doesn't mean you shouldn't write code that continues after an exception, just be aware that it has downsides and take the trouble to take appropriate action, such as logging the error." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "8b80a85a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Traceback (most recent call last):\n", - " File \"/var/folders/79/nkc7chg17vz5kdlf2mdlh8zw0000gq/T/ipykernel_66791/2841111012.py\", line 11, in \n", - " always_bad()\n", - " File \"/var/folders/79/nkc7chg17vz5kdlf2mdlh8zw0000gq/T/ipykernel_66791/2841111012.py\", line 8, in always_bad\n", - " raise Exception(\"Never happy\")\n", - "Exception: Never happy\n", - "\n", - "Carry on regardless...\n" - ] - } - ], - "source": [ - "import traceback\n", - "import sys\n", - "\n", - "def always_bad():\n", - " ''' \n", - " Throw an exception.\n", - " '''\n", - " raise Exception(\"Never happy\")\n", - "\n", - "try:\n", - " always_bad()\n", - "except Exception as e:\n", - " traceback.print_exception(type(e), value=e, tb=e.__traceback__, limit=2, file=sys.stdout)\n", - " ## It is easier to use the following in most situations -\n", - " # traceback.print_exc()\n", - "\n", - "print(\"\\nCarry on regardless...\")" - ] - }, - { - "cell_type": "markdown", - "id": "ac925ec1", - "metadata": {}, - "source": [ - "## Lambda functions\n", - "\n", - "A lambda function is a small function that is defined with the ```lambda``` keyword. It can take any number of arguments, but can only have one expression. In other programming languages these are sometimes called anonymous functions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10fbc05b", - "metadata": {}, - "outputs": [], - "source": [ - "add = lambda x, y: x + y\n", - "print(add(5, 3))" - ] - }, - { - "cell_type": "markdown", - "id": "d882da43", - "metadata": {}, - "source": [ - "### Exercise\n", - "\n", - "Write a lambda function that returns the value of its only argument if it is an odd number,or the next odd number if it is an even number." - ] - }, - { - "cell_type": "markdown", - "id": "3290125d", - "metadata": {}, - "source": [ - "## List comprehensions\n", - "\n", - "A list comprehension is shorthand for\n", - "\n", - "```python\n", - " result = []\n", - " for x in some_list:\n", - " result.append[some_function(x)]\n", - "```\n", - "\n", - "The input to the for statement does not need to be a list, any 'iterable' will do. For example a tuple, or a file." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "c5dd17b6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']\n", - "['Hello', 'World']\n" - ] - } - ], - "source": [ - "days = [d.upper() for d in ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')]\n", - "print(days)\n", - "\n", - "with open('data/rows.txt') as myFile:\n", - " lines = [x.strip().capitalize() for x in myFile]\n", - "print(lines)" - ] - }, - { - "cell_type": "markdown", - "id": "c4c6592d", - "metadata": {}, - "source": [ - "## Generators\n", - "\n", - "In the previous example we used a list comprehension to capitalise all the lines in a file.\n", - "\n", - "An alternative approach is to use a generator comprehension.\n", - "\n", - "The syntax is almost identical, but the result seems quite different.\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "5519293c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " at 0x107110ee0>\n" - ] - } - ], - "source": [ - "with open('data/rows.txt') as myFile:\n", - " lines = (x.strip().capitalize() for x in myFile)\n", - "print(lines)" - ] - }, - { - "cell_type": "markdown", - "id": "0207fdbc", - "metadata": {}, - "source": [ - "### Question\n", - "\n", - "What do we need to do to get the lines from the generator?" - ] - }, - { - "cell_type": "markdown", - "id": "615acc5d", - "metadata": {}, - "source": [ - "## Consuming generator output\n", - "Although generator output can converted to a list, the intended use is with next()" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "57b85794-9225-488f-9cc8-fb2771e146a2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Hello\n", - "World\n" - ] - } - ], - "source": [ - "with open('data/rows.txt') as data:\n", - " lines = (x.strip().capitalize() for x in data)\n", - " while True:\n", - " line = next(lines, None)\n", - " if line is None:\n", - " break\n", - " print(line)" - ] - }, - { - "cell_type": "markdown", - "id": "2e6c886f", - "metadata": {}, - "source": [ - "## Or better - use a for loop" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "63ec21eb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Hello\n", - "World\n" - ] - } - ], - "source": [ - "with open('data/rows.txt') as data:\n", - " lines = (x.strip().capitalize() for x in data)\n", - " for line in lines:\n", - " print(line)" - ] - }, - { - "cell_type": "markdown", - "id": "acba2626", - "metadata": {}, - "source": [ - "### yield and next()\n", - "\n", - "Although the generator comprehension and list comprehension syntax is almost identical, it is clear that the generator mechanism is quite different.\n", - "\n", - "Generators in their long form are functions rather than a loop. However rather than a return statement there is a yield statement. Yield may be called as many times as you wish, indeed it could yield - FOREVER\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "0ed2e755", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "hello\n", - "None\n", - "0\n", - "1\n", - "2\n", - "1.414\n" - ] - } - ], - "source": [ - "def my_gen():\n", - " yield \"hello\"\n", - " yield None\n", - " # Calling a generator function from within a generator function is allowed\n", - " for i in range(3):\n", - " yield i\n", - " yield 1.414\n", - "\n", - "for item in my_gen():\n", - " print(item)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "9a8d1051", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Traceback (most recent call last):\n", - " File \"/var/folders/79/nkc7chg17vz5kdlf2mdlh8zw0000gq/T/ipykernel_66791/679916601.py\", line 5, in \n", - " print(len(data))\n", - " ^^^^^^^^^\n", - "TypeError: object of type '_io.TextIOWrapper' has no len()\n" - ] - } - ], - "source": [ - "# Why does this fail?\n", - "# How could we fix it?\n", - "try:\n", - " with open('data/rows.txt') as data:\n", - " print(len(data))\n", - "except Exception as e:\n", - " traceback.print_exc()" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "9a084827", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['hello\\n', 'world\\n']\n" - ] - } - ], - "source": [ - "# Why does this work?\n", - "# What is happening here that is different?\n", - "with open('data/rows.txt') as data:\n", - " print(list(data))" - ] - }, - { - "cell_type": "markdown", - "id": "703c575f", - "metadata": {}, - "source": [ - "## Classes\n", - "\n", - "This course module does not cover Object Oriented Programming, or Object Oriented Design as those are both big topics. However, it is important to know that as well as the procedural style of programming the Python language also supports object orientation.\n", - "\n", - "Key concepts in OOP are - objects, classes, properties, methods, constructors and inheritance. So we will take a quick look at those.\n", - "\n", - "### classes and objects\n", - "\n", - "A class is a user defined data type.\n", - "\n", - "An object is an instance of a class.\n", - "\n", - "Rather than consider the finer details of what this means, it is easier to take a look at already familiar built-in types.\n", - "\n", - "### the string class\n", - "\n", - "Typically we create a string with\n", - "\n", - "```\n", - "my_string = \"Hello world!\"\n", - "```\n", - "\n", - "This is actually shorthand for\n", - "```\n", - "my_string = str(\"Hello world!\")\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "a73b9b72", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "my_string = 'Hello'\n", - "print(type(my_string))" - ] - }, - { - "cell_type": "markdown", - "id": "ca41580a", - "metadata": {}, - "source": [ - "### The file class\n", - "\n", - "Whenever we open a file in Python, for reading or writing, we are using a file object. However, this is a little more complicated. So let's check the type of a file object. " - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "37e9a8cd", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n" - ] - } - ], - "source": [ - "my_file = open('data/rows.txt')\n", - "print(type(my_file))\n", - "my_file.close()\n", - "\n", - "my_binary_file = open('data/rows.txt', 'rb')\n", - "print(type(my_binary_file))\n", - "my_binary_file.close()\n" - ] - }, - { - "cell_type": "markdown", - "id": "27d89181", - "metadata": {}, - "source": [ - "This might seem surprising. On reflection, from mistakes we have all made, and learned from, when working with files - it is to be expected that there are different types of file object for text and binary files, and also for reading and writing.\n", - "\n", - "So here we see the principle benefit of object oriented programming - information hiding. Generally coders want a simple and consistent interface to things that are conceptually similar, such as files." - ] - }, - { - "cell_type": "markdown", - "id": "5fae2074", - "metadata": {}, - "source": [ - "## Defining our own classes\n", - "\n", - "Typical reasons for defining new classes are -\n", - "\n", - "* Encapsulation (information hiding)\n", - "\n", - "* Easier code re-use\n" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "d16a9a88", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MICHAEL\n", - "michael\n", - "MICHAEL\n" - ] - } - ], - "source": [ - "my_name = \"michael\"\n", - "a = my_name.upper()\n", - "print(a)\n", - "print(my_name)\n", - "\n", - "# or\n", - "my_name = \"michael\"\n", - "my_name = my_name.upper()\n", - "print(my_name)\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "120164a6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MICHAEL\n" - ] - } - ], - "source": [ - "# or\n", - "def upper(input:str):\n", - " return input.upper()\n", - "\n", - "my_name = upper(\"michael\")\n", - "print(my_name)" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "d35c6ac7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MICHAEL\n", - "\n" - ] - } - ], - "source": [ - "class Upper(str):\n", - " def __new__(cls, text=\"\"):\n", - " return super().__new__(cls, text.upper())\n", - "\n", - "my_name = Upper(\"michael\")\n", - "print(my_name)\n", - "print(type(my_name))\n" - ] - }, - { - "cell_type": "markdown", - "id": "e83fbbe4", - "metadata": {}, - "source": [ - "## Features of our new class\n", - "\n", - "Our Upper() class is an example of code re-use. We have re-used the str() class to provide the implementation of Upper(). So we gain all the features of str().\n", - "\n", - "Let's see what it can do:" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "60e18cd3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " HELLO\n", - " hello\n", - " HELLO\n" - ] - } - ], - "source": [ - "class Upper(str):\n", - " def __new__(cls, text=\"\"):\n", - " return super().__new__(cls, text.upper())\n", - "\n", - "## Alternative (factory) implementation \n", - "class UpperFactory(str):\n", - " def __init__(self, text=\"\"):\n", - " str.__init__(text.upper())\n", - "\n", - "\n", - "hello = Upper(\"hello\")\n", - "print(type(hello), hello)\n", - "\n", - "lh = hello.lower()\n", - "print(type(lh), lh)\n", - "\n", - "uh = hello.upper()\n", - "print(type(uh), uh)\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "f5d1f988", - "metadata": {}, - "source": [ - "## Pros and cons of inheritance\n", - "\n", - "* We get all the methods of the super-class - GOOD, simplifies implementation\n", - "\n", - "* We get all the methods of the super-class - BAD, methods not adapted to features of our new class\n", - "\n", - "For now, just note that designing and implementing well behaved classes is not trivial, and filling your code with custom classes might not be what others (or future you) will appreciate. \n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "102b87f2", - "metadata": {}, - "source": [ - "## Operator overloading\n", - "\n", - "Although a separate concept to classes, and object orientation, a common feature of object oriented programming languages is operator overloading. This is something that you will already be familiar with from Python strings." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "9407e7f7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "hello world\n", - "___________\n" - ] - } - ], - "source": [ - "print('hello' + ' ' + 'world')\n", - "print(11 * '_')\n" - ] - }, - { - "cell_type": "markdown", - "id": "843c65af", - "metadata": {}, - "source": [ - "Let's implement something similar for our Upper class -" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "63921d3b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HELLO_WORLD\n" - ] - } - ], - "source": [ - "class Upper(str):\n", - " def __new__(cls, text=\"\"):\n", - " return super().__new__(cls, text.upper())\n", - " \n", - " def __add__(self, x):\n", - " return Upper(str(self) + \"_\" + x)\n", - "\n", - "print(Upper(\"hello\") + \"world\")" - ] - }, - { - "cell_type": "markdown", - "id": "dcbf16d4", - "metadata": {}, - "source": [ - "How about this -" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "0c7eff02", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "helloWORLD\n" - ] - } - ], - "source": [ - "print(\"hello\" + Upper(\"world\"))" - ] - }, - { - "cell_type": "markdown", - "id": "69c8f60e", - "metadata": {}, - "source": [ - "Oh no!!\n", - "\n", - "### Exercise\n", - "\n", - "Fix it!" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "7c7eb2f0-d2ba-4c68-9ac4-67660e50b968", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [ - "remove-input" - ] - }, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "var questionslidjmjkIrCfN=[\n", - " {\n", - " \"question\": \"What is the purpose of type hints in Python?\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"To enforce type checking at runtime\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"To provide hints for IDEs and linters\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"To automatically convert types\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"To improve code execution speed\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " },\n", - " {\n", - " \"question\": \"Which of the following best describes a decorator in python?\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"A function that returns another function\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"A function that modifies the behaviour of another function\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"A function that creates new types\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"A function that adds type hints to another function\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " },\n", - " {\n", - " \"question\": \"What is the advantage of using generator expression in python?\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"They store all values in memory\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"They provide a concise way to create generators without using def and yield\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"They automatically sort the values\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"They ensure type safety\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " }\n", - "];\n", - " // Make a random ID\n", - "function makeid(length) {\n", - " var result = [];\n", - " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", - " var charactersLength = characters.length;\n", - " for (var i = 0; i < length; i++) {\n", - " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", - " }\n", - " return result.join('');\n", - "}\n", - "\n", - "// Choose a random subset of an array. Can also be used to shuffle the array\n", - "function getRandomSubarray(arr, size) {\n", - " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", - " while (i--) {\n", - " index = Math.floor((i + 1) * Math.random());\n", - " temp = shuffled[index];\n", - " shuffled[index] = shuffled[i];\n", - " shuffled[i] = temp;\n", - " }\n", - " return shuffled.slice(0, size);\n", - "}\n", - "\n", - "function printResponses(responsesContainer) {\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
  1. Copy the text in this cell below \"Answer String\"
  2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
  3. Select the whole \"Replace Me\" text
  4. Paste in your answer string and press shift-Enter.
  5. Save the notebook using the save icon or File->Save Notebook menu item



  6. Answer String:
    ';\n", - " console.log(responses);\n", - " responses.forEach((response, index) => {\n", - " if (response) {\n", - " console.log(index + ': ' + response);\n", - " stringResponses+= index + ': ' + response +\"
    \";\n", - " }\n", - " });\n", - " responsesContainer.innerHTML=stringResponses;\n", - "}\n", - "function check_mc() {\n", - " var id = this.id.split('-')[0];\n", - " //var response = this.id.split('-')[1];\n", - " //console.log(response);\n", - " //console.log(\"In check_mc(), id=\"+id);\n", - " //console.log(event.srcElement.id) \n", - " //console.log(event.srcElement.dataset.correct) \n", - " //console.log(event.srcElement.dataset.feedback)\n", - "\n", - " var label = event.srcElement;\n", - " //console.log(label, label.nodeName);\n", - " var depth = 0;\n", - " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", - " label = label.parentElement;\n", - " console.log(depth, label);\n", - " depth++;\n", - " }\n", - "\n", - "\n", - "\n", - " var answers = label.parentElement.children;\n", - "\n", - " //console.log(answers);\n", - "\n", - "\n", - " // Split behavior based on multiple choice vs many choice:\n", - " var fb = document.getElementById(\"fb\" + id);\n", - "\n", - "\n", - "\n", - "\n", - " if (fb.dataset.numcorrect == 1) {\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " //console.log(responsesContainer);\n", - " var response = label.firstChild.innerText;\n", - " if (label.querySelector(\".QuizCode\")){\n", - " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", - " }\n", - " console.log(response);\n", - " //console.log(document.getElementById(\"quizWrap\"+id));\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " console.log(responses);\n", - " responses[qnum]= response;\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End code to preserve responses\n", - " \n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " //console.log(child);\n", - " child.className = \"MCButton\";\n", - " }\n", - "\n", - "\n", - "\n", - " if (label.dataset.correct == \"true\") {\n", - " // console.log(\"Correct action\");\n", - " if (\"feedback\" in label.dataset) {\n", - " fb.textContent = jaxify(label.dataset.feedback);\n", - " } else {\n", - " fb.textContent = \"Correct!\";\n", - " }\n", - " label.classList.add(\"correctButton\");\n", - "\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - "\n", - " } else {\n", - " if (\"feedback\" in label.dataset) {\n", - " fb.textContent = jaxify(label.dataset.feedback);\n", - " } else {\n", - " fb.textContent = \"Incorrect -- try again.\";\n", - " }\n", - " //console.log(\"Error action\");\n", - " label.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - " }\n", - " else {\n", - " var reset = false;\n", - " var feedback;\n", - " if (label.dataset.correct == \"true\") {\n", - " if (\"feedback\" in label.dataset) {\n", - " feedback = jaxify(label.dataset.feedback);\n", - " } else {\n", - " feedback = \"Correct!\";\n", - " }\n", - " if (label.dataset.answered <= 0) {\n", - " if (fb.dataset.answeredcorrect < 0) {\n", - " fb.dataset.answeredcorrect = 1;\n", - " reset = true;\n", - " } else {\n", - " fb.dataset.answeredcorrect++;\n", - " }\n", - " if (reset) {\n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " child.className = \"MCButton\";\n", - " child.dataset.answered = 0;\n", - " }\n", - " }\n", - " label.classList.add(\"correctButton\");\n", - " label.dataset.answered = 1;\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - "\n", - " }\n", - " } else {\n", - " if (\"feedback\" in label.dataset) {\n", - " feedback = jaxify(label.dataset.feedback);\n", - " } else {\n", - " feedback = \"Incorrect -- try again.\";\n", - " }\n", - " if (fb.dataset.answeredcorrect > 0) {\n", - " fb.dataset.answeredcorrect = -1;\n", - " reset = true;\n", - " } else {\n", - " fb.dataset.answeredcorrect--;\n", - " }\n", - "\n", - " if (reset) {\n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " child.className = \"MCButton\";\n", - " child.dataset.answered = 0;\n", - " }\n", - " }\n", - " label.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " //console.log(responsesContainer);\n", - " var response = label.firstChild.innerText;\n", - " if (label.querySelector(\".QuizCode\")){\n", - " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", - " }\n", - " console.log(response);\n", - " //console.log(document.getElementById(\"quizWrap\"+id));\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " if (label.dataset.correct == \"true\") {\n", - " if (typeof(responses[qnum]) == \"object\"){\n", - " if (!responses[qnum].includes(response))\n", - " responses[qnum].push(response);\n", - " } else{\n", - " responses[qnum]= [ response ];\n", - " }\n", - " } else {\n", - " responses[qnum]= response;\n", - " }\n", - " console.log(responses);\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End save responses stuff\n", - "\n", - "\n", - "\n", - " var numcorrect = fb.dataset.numcorrect;\n", - " var answeredcorrect = fb.dataset.answeredcorrect;\n", - " if (answeredcorrect >= 0) {\n", - " fb.textContent = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", - " } else {\n", - " fb.textContent = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", - " }\n", - "\n", - "\n", - " }\n", - "\n", - " if (typeof MathJax != 'undefined') {\n", - " var version = MathJax.version;\n", - " console.log('MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([fb]);\n", - " }\n", - " } else {\n", - " console.log('MathJax not detected');\n", - " }\n", - "\n", - "}\n", - "\n", - "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", - " var shuffled;\n", - " if (shuffle_answers == \"True\") {\n", - " //console.log(shuffle_answers+\" read as true\");\n", - " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", - " } else {\n", - " //console.log(shuffle_answers+\" read as false\");\n", - " shuffled = qa.answers;\n", - " }\n", - "\n", - "\n", - " var num_correct = 0;\n", - "\n", - "\n", - "\n", - " shuffled.forEach((item, index, ans_array) => {\n", - " //console.log(answer);\n", - "\n", - " // Make input element\n", - " var inp = document.createElement(\"input\");\n", - " inp.type = \"radio\";\n", - " inp.id = \"quizo\" + id + index;\n", - " inp.style = \"display:none;\";\n", - " aDiv.append(inp);\n", - "\n", - " //Make label for input element\n", - " var lab = document.createElement(\"label\");\n", - " lab.className = \"MCButton\";\n", - " lab.id = id + '-' + index;\n", - " lab.onclick = check_mc;\n", - " var aSpan = document.createElement('span');\n", - " aSpan.classsName = \"\";\n", - " //qDiv.id=\"quizQn\"+id+index;\n", - " if (\"answer\" in item) {\n", - " aSpan.innerHTML = jaxify(item.answer);\n", - " //aSpan.innerHTML=item.answer;\n", - " }\n", - " lab.append(aSpan);\n", - "\n", - " // Create div for code inside question\n", - " var codeSpan;\n", - " if (\"code\" in item) {\n", - " codeSpan = document.createElement('span');\n", - " codeSpan.id = \"code\" + id + index;\n", - " codeSpan.className = \"QuizCode\";\n", - " var codePre = document.createElement('pre');\n", - " codeSpan.append(codePre);\n", - " var codeCode = document.createElement('code');\n", - " codePre.append(codeCode);\n", - " codeCode.innerHTML = item.code;\n", - " lab.append(codeSpan);\n", - " //console.log(codeSpan);\n", - " }\n", - "\n", - " //lab.textContent=item.answer;\n", - "\n", - " // Set the data attributes for the answer\n", - " lab.setAttribute('data-correct', item.correct);\n", - " if (item.correct) {\n", - " num_correct++;\n", - " }\n", - " if (\"feedback\" in item) {\n", - " lab.setAttribute('data-feedback', item.feedback);\n", - " }\n", - " lab.setAttribute('data-answered', 0);\n", - "\n", - " aDiv.append(lab);\n", - "\n", - " });\n", - "\n", - " if (num_correct > 1) {\n", - " outerqDiv.className = \"ManyChoiceQn\";\n", - " } else {\n", - " outerqDiv.className = \"MultipleChoiceQn\";\n", - " }\n", - "\n", - " return num_correct;\n", - "\n", - "}\n", - "function check_numeric(ths, event) {\n", - "\n", - " if (event.keyCode === 13) {\n", - " ths.blur();\n", - "\n", - " var id = ths.id.split('-')[0];\n", - "\n", - " var submission = ths.value;\n", - " if (submission.indexOf('/') != -1) {\n", - " var sub_parts = submission.split('/');\n", - " //console.log(sub_parts);\n", - " submission = sub_parts[0] / sub_parts[1];\n", - " }\n", - " //console.log(\"Reader entered\", submission);\n", - "\n", - " if (\"precision\" in ths.dataset) {\n", - " var precision = ths.dataset.precision;\n", - " // console.log(\"1:\", submission)\n", - " submission = Math.round((1 * submission + Number.EPSILON) * 10 ** precision) / 10 ** precision;\n", - " // console.log(\"Rounded to \", submission, \" precision=\", precision );\n", - " }\n", - "\n", - "\n", - " //console.log(\"In check_numeric(), id=\"+id);\n", - " //console.log(event.srcElement.id) \n", - " //console.log(event.srcElement.dataset.feedback)\n", - "\n", - " var fb = document.getElementById(\"fb\" + id);\n", - " fb.style.display = \"none\";\n", - " fb.textContent = \"Incorrect -- try again.\";\n", - "\n", - " var answers = JSON.parse(ths.dataset.answers);\n", - " //console.log(answers);\n", - "\n", - " var defaultFB = \"\";\n", - " var correct;\n", - " var done = false;\n", - " answers.every(answer => {\n", - " //console.log(answer.type);\n", - "\n", - " correct = false;\n", - " // if (answer.type==\"value\"){\n", - " if ('value' in answer) {\n", - " if (submission == answer.value) {\n", - " if (\"feedback\" in answer) {\n", - " fb.textContent = jaxify(answer.feedback);\n", - " } else {\n", - " fb.textContent = jaxify(\"Correct\");\n", - " }\n", - " correct = answer.correct;\n", - " //console.log(answer.correct);\n", - " done = true;\n", - " }\n", - " // } else if (answer.type==\"range\") {\n", - " } else if ('range' in answer) {\n", - " //console.log(answer.range);\n", - " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", - " fb.textContent = jaxify(answer.feedback);\n", - " correct = answer.correct;\n", - " //console.log(answer.correct);\n", - " done = true;\n", - " }\n", - " } else if (answer.type == \"default\") {\n", - " defaultFB = answer.feedback;\n", - " }\n", - " if (done) {\n", - " return false; // Break out of loop if this has been marked correct\n", - " } else {\n", - " return true; // Keep looking for case that includes this as a correct answer\n", - " }\n", - " });\n", - "\n", - " if ((!done) && (defaultFB != \"\")) {\n", - " fb.innerHTML = jaxify(defaultFB);\n", - " //console.log(\"Default feedback\", defaultFB);\n", - " }\n", - "\n", - " fb.style.display = \"block\";\n", - " if (correct) {\n", - " ths.className = \"Input-text\";\n", - " ths.classList.add(\"correctButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - " } else {\n", - " ths.className = \"Input-text\";\n", - " ths.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - "\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " console.log(submission);\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " //console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " console.log(responses);\n", - " if (submission == ths.value){\n", - " responses[qnum]= submission;\n", - " } else {\n", - " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", - " }\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End code to preserve responses\n", - "\n", - " if (typeof MathJax != 'undefined') {\n", - " var version = MathJax.version;\n", - " console.log('MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([fb]);\n", - " }\n", - " } else {\n", - " console.log('MathJax not detected');\n", - " }\n", - " return false;\n", - " }\n", - "\n", - "}\n", - "\n", - "function isValid(el, charC) {\n", - " //console.log(\"Input char: \", charC);\n", - " if (charC == 46) {\n", - " if (el.value.indexOf('.') === -1) {\n", - " return true;\n", - " } else if (el.value.indexOf('/') != -1) {\n", - " var parts = el.value.split('/');\n", - " if (parts[1].indexOf('.') === -1) {\n", - " return true;\n", - " }\n", - " }\n", - " else {\n", - " return false;\n", - " }\n", - " } else if (charC == 47) {\n", - " if (el.value.indexOf('/') === -1) {\n", - " if ((el.value != \"\") && (el.value != \".\")) {\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " return false;\n", - " }\n", - " } else if (charC == 45) {\n", - " var edex = el.value.indexOf('e');\n", - " if (edex == -1) {\n", - " edex = el.value.indexOf('E');\n", - " }\n", - "\n", - " if (el.value == \"\") {\n", - " return true;\n", - " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else if (charC == 101) { // \"e\"\n", - " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", - " // Prev symbol must be digit or decimal point:\n", - " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", - " return true;\n", - " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " if (charC > 31 && (charC < 48 || charC > 57))\n", - " return false;\n", - " }\n", - " return true;\n", - "}\n", - "\n", - "function numeric_keypress(evnt) {\n", - " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", - "\n", - " if (charC == 13) {\n", - " check_numeric(this, evnt);\n", - " } else {\n", - " return isValid(this, charC);\n", - " }\n", - "}\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", - "\n", - "\n", - "\n", - " //console.log(answer);\n", - "\n", - "\n", - " outerqDiv.className = \"NumericQn\";\n", - " aDiv.style.display = 'block';\n", - "\n", - " var lab = document.createElement(\"label\");\n", - " lab.className = \"InpLabel\";\n", - " lab.textContent = \"Type numeric answer here:\";\n", - " aDiv.append(lab);\n", - "\n", - " var inp = document.createElement(\"input\");\n", - " inp.type = \"text\";\n", - " //inp.id=\"input-\"+id;\n", - " inp.id = id + \"-0\";\n", - " inp.className = \"Input-text\";\n", - " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", - " if (\"precision\" in qa) {\n", - " inp.setAttribute('data-precision', qa.precision);\n", - " }\n", - " aDiv.append(inp);\n", - " //console.log(inp);\n", - "\n", - " //inp.addEventListener(\"keypress\", check_numeric);\n", - " //inp.addEventListener(\"keypress\", numeric_keypress);\n", - " /*\n", - " inp.addEventListener(\"keypress\", function(event) {\n", - " return numeric_keypress(this, event);\n", - " }\n", - " );\n", - " */\n", - " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", - " inp.onkeypress = numeric_keypress;\n", - " inp.onpaste = event => false;\n", - "\n", - " inp.addEventListener(\"focus\", function (event) {\n", - " this.value = \"\";\n", - " return false;\n", - " }\n", - " );\n", - "\n", - "\n", - "}\n", - "function jaxify(string) {\n", - " var mystring = string;\n", - "\n", - " var count = 0;\n", - " var loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", - "\n", - " var count2 = 0;\n", - " var loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", - "\n", - " //console.log(loc);\n", - "\n", - " while ((loc >= 0) || (loc2 >= 0)) {\n", - "\n", - " /* Have to replace all the double $$ first with current implementation */\n", - " if (loc2 >= 0) {\n", - " if (count2 % 2 == 0) {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, \"$1\\\\[\");\n", - " } else {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, \"$1\\\\]\");\n", - " }\n", - " count2++;\n", - " } else {\n", - " if (count % 2 == 0) {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, \"$1\\\\(\");\n", - " } else {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, \"$1\\\\)\");\n", - " }\n", - " count++;\n", - " }\n", - " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", - " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", - " //console.log(mystring,\", loc:\",loc,\", loc2:\",loc2);\n", - " }\n", - "\n", - " //console.log(mystring);\n", - " return mystring;\n", - "}\n", - "\n", - "\n", - "function show_questions(json, mydiv) {\n", - " console.log('show_questions');\n", - " //var mydiv=document.getElementById(myid);\n", - " var shuffle_questions = mydiv.dataset.shufflequestions;\n", - " var num_questions = mydiv.dataset.numquestions;\n", - " var shuffle_answers = mydiv.dataset.shuffleanswers;\n", - " var max_width = mydiv.dataset.maxwidth;\n", - "\n", - " if (num_questions > json.length) {\n", - " num_questions = json.length;\n", - " }\n", - "\n", - " var questions;\n", - " if ((num_questions < json.length) || (shuffle_questions == \"True\")) {\n", - " //console.log(num_questions+\",\"+json.length);\n", - " questions = getRandomSubarray(json, num_questions);\n", - " } else {\n", - " questions = json;\n", - " }\n", - "\n", - " //console.log(\"SQ: \"+shuffle_questions+\", NQ: \" + num_questions + \", SA: \", shuffle_answers);\n", - "\n", - " // Iterate over questions\n", - " questions.forEach((qa, index, array) => {\n", - " //console.log(qa.question); \n", - "\n", - " var id = makeid(8);\n", - " //console.log(id);\n", - "\n", - "\n", - " // Create Div to contain question and answers\n", - " var iDiv = document.createElement('div');\n", - " //iDiv.id = 'quizWrap' + id + index;\n", - " iDiv.id = 'quizWrap' + id;\n", - " iDiv.className = 'Quiz';\n", - " iDiv.setAttribute('data-qnum', index);\n", - " iDiv.style.maxWidth =max_width+\"px\";\n", - " mydiv.appendChild(iDiv);\n", - " // iDiv.innerHTML=qa.question;\n", - " \n", - " var outerqDiv = document.createElement('div');\n", - " outerqDiv.id = \"OuterquizQn\" + id + index;\n", - " // Create div to contain question part\n", - " var qDiv = document.createElement('div');\n", - " qDiv.id = \"quizQn\" + id + index;\n", - " \n", - " if (qa.question) {\n", - " iDiv.append(outerqDiv);\n", - "\n", - " //qDiv.textContent=qa.question;\n", - " qDiv.innerHTML = jaxify(qa.question);\n", - " outerqDiv.append(qDiv);\n", - " }\n", - "\n", - " // Create div for code inside question\n", - " var codeDiv;\n", - " if (\"code\" in qa) {\n", - " codeDiv = document.createElement('div');\n", - " codeDiv.id = \"code\" + id + index;\n", - " codeDiv.className = \"QuizCode\";\n", - " var codePre = document.createElement('pre');\n", - " codeDiv.append(codePre);\n", - " var codeCode = document.createElement('code');\n", - " codePre.append(codeCode);\n", - " codeCode.innerHTML = qa.code;\n", - " outerqDiv.append(codeDiv);\n", - " //console.log(codeDiv);\n", - " }\n", - "\n", - "\n", - " // Create div to contain answer part\n", - " var aDiv = document.createElement('div');\n", - " aDiv.id = \"quizAns\" + id + index;\n", - " aDiv.className = 'Answer';\n", - " iDiv.append(aDiv);\n", - "\n", - " //console.log(qa.type);\n", - "\n", - " var num_correct;\n", - " if ((qa.type == \"multiple_choice\") || (qa.type == \"many_choice\") ) {\n", - " num_correct = make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id);\n", - " if (\"answer_cols\" in qa) {\n", - " //aDiv.style.gridTemplateColumns = 'auto '.repeat(qa.answer_cols);\n", - " aDiv.style.gridTemplateColumns = 'repeat(' + qa.answer_cols + ', 1fr)';\n", - " }\n", - " } else if (qa.type == \"numeric\") {\n", - " //console.log(\"numeric\");\n", - " make_numeric(qa, outerqDiv, qDiv, aDiv, id);\n", - " }\n", - "\n", - "\n", - " //Make div for feedback\n", - " var fb = document.createElement(\"div\");\n", - " fb.id = \"fb\" + id;\n", - " //fb.style=\"font-size: 20px;text-align:center;\";\n", - " fb.className = \"Feedback\";\n", - " fb.setAttribute(\"data-answeredcorrect\", 0);\n", - " fb.setAttribute(\"data-numcorrect\", num_correct);\n", - " iDiv.append(fb);\n", - "\n", - "\n", - " });\n", - " var preserveResponses = mydiv.dataset.preserveresponses;\n", - " console.log(preserveResponses);\n", - " console.log(preserveResponses == \"true\");\n", - " if (preserveResponses == \"true\") {\n", - " console.log(preserveResponses);\n", - " // Create Div to contain record of answers\n", - " var iDiv = document.createElement('div');\n", - " iDiv.id = 'responses' + mydiv.id;\n", - " iDiv.className = 'JCResponses';\n", - " // Create a place to store responses as an empty array\n", - " iDiv.setAttribute('data-responses', '[]');\n", - "\n", - " // Dummy Text\n", - " iDiv.innerHTML=\"Select your answers and then follow the directions that will appear here.\"\n", - " //iDiv.className = 'Quiz';\n", - " mydiv.appendChild(iDiv);\n", - " }\n", - "//console.log(\"At end of show_questions\");\n", - " if (typeof MathJax != 'undefined') {\n", - " console.log(\"MathJax version\", MathJax.version);\n", - " var version = MathJax.version;\n", - " setTimeout(function(){\n", - " var version = MathJax.version;\n", - " console.log('After sleep, MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([mydiv]);\n", - " }\n", - " }, 500);\n", - "if (typeof version == 'undefined') {\n", - " } else\n", - " {\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([mydiv]);\n", - " } else {\n", - " console.log(\"MathJax not found\");\n", - " }\n", - " }\n", - " }\n", - " return false;\n", - "}\n", - "/* This is to handle asynchrony issues in loading Jupyter notebooks\n", - " where the quiz has been previously run. The Javascript was generally\n", - " being run before the div was added to the DOM. I tried to do this\n", - " more elegantly using Mutation Observer, but I didn't get it to work.\n", - "\n", - " Someone more knowledgeable could make this better ;-) */\n", - "\n", - " function try_show() {\n", - " if(document.getElementById(\"lidjmjkIrCfN\")) {\n", - " show_questions(questionslidjmjkIrCfN, lidjmjkIrCfN); \n", - " } else {\n", - " setTimeout(try_show, 200);\n", - " }\n", - " };\n", - " \n", - " {\n", - " // console.log(element);\n", - "\n", - " //console.log(\"lidjmjkIrCfN\");\n", - " // console.log(document.getElementById(\"lidjmjkIrCfN\"));\n", - "\n", - " try_show();\n", - " }\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from jupyterquiz import display_quiz\n", - "display_quiz(\"questions/summary_language_features.json\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b0c5e7e3-989b-4a8f-83d2-7238e5d551be", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.19" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/individual_modules/python_for_data_analysis/Python_Testing.ipynb b/individual_modules/python_for_data_analysis/Python_Testing.ipynb deleted file mode 100644 index 4585a682..00000000 --- a/individual_modules/python_for_data_analysis/Python_Testing.ipynb +++ /dev/null @@ -1,1784 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "# Checking and Testing Code\n", - "## Learning Objectives\n", - "- Understand the importance and limitations of software testing\n", - "- Recognize different types of testing such as system-level and defect testing\n", - "- Learn how to implement and interpret assert statement in Python code\n", - "- Understand when and why to use assert statements\n", - "- Understand the concept of unit testing and Test Driven Development (TDD)\n", - "- Write and execute unit tests using the unit test module\n", - "- Learn the purpose and implementation of fixtures and mocks in testing\n", - "- Apply these concepts to test code that depends on external resources\n", - "- Understand the role of code linting and type checking\n", - "- Use tools like Flake8 and my[py] to ensure code quality and adherence to style guidelines" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Testing software\n", - "\n", - "Although we cannot prove our code is free of defects or bugs, we can, and should, establish that it behaves as intended.\n", - "\n", - "\n", - "\n", - "### System level testing\n", - "\n", - "Once we have completed our software, we should ensure that it works as intended. This is referred to as validation testing. Typically, this will involve taking a sample of input data and ensuring that the output of our software is as expected for the given input.\n", - "\n", - "This *validation* testing will tell us if the overall system runs and produces valid results. Such tests should be repeated when changes are made to the software to ensure the changes have not introduced errors.\n", - "\n", - "Changes that can impact your software are diverse and include:\n", - "\n", - " * new Python language releases\n", - " \n", - " * upgrades to imported libraries\n", - " \n", - " * operating system updates.\n", - "\n", - "### Defect testing\n", - "\n", - "In a research environment it is often the case that there is no explicit specification for the software we create.\n", - "\n", - "By specification we mean something like:\n", - "\n", - "* Written statement of user requirements - typically \"user stories\"\n", - "\n", - "* Functional requirements - e.g what file formats are to be supported\n", - "\n", - "* Non-functional requirements - e.g. subject data must be encrypted.\n", - "\n", - "### Discussion\n", - "\n", - "If you don't have a specification for your software, how might you establish suitable tests to find and resolve defects?\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Assert statement\n", - "\n", - "The built-in Python assert statement looks like this -" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "# Try modifying this code to deliberately fail the assert statements\n", - "\n", - "def my_add_two(a):\n", - " return a + 2.0\n", - "\n", - "assert my_add_two(1) == 3\n", - "# Better to include a message in case of failure\n", - "assert my_add_two(3) == 5, f\"my_add_two(3) failed with {my_add_two(3)}, expected 5\"" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## When to use assert\n", - "\n", - "```assert``` should never be used to modify control flow.\n", - "\n", - "Assertions allow you to verify that parts of your program are correct, but are only applied if the internal constant ```__debug__``` is ```True```. Although ```__debug__``` is usually set to True, it is not guaranteed.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "## This is approximate what the assert statement does \n", - "\n", - "def my_assert(condition, message):\n", - " if __debug__ and not condition:\n", - " raise AssertionError(message)\n", - "\n", - "my_assert(my_add_two(1) == 3, \"my_add_two(1) failed\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "### Why might we want different behaviour from our assert statements?\n", - "\n", - "### What would you want your assert statements to do?" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Unit-tests\n", - "\n", - "Unit-tests are small tests that test the behaviours of our functions and classes.\n", - "\n", - "Unit-tests are typically run within a testing framework or test-runner that automates testing, often inside our IDE.\n", - "\n", - "### Test Driven Development (TDD)\n", - "\n", - "TDD is an approach to software design, it is not software testing. TDD uses unit-tests to create a software design, especially when the design is created incrementally, as with Agile.\n", - "\n", - "### Refactoring\n", - "\n", - "Whether or not you adopt TDD, refactoring - changing the implementation of your code without changing its behaviour, is something that you are certain to do. If only to remove print statements, or change the names of variables.\n", - "\n", - "Refactoring code without appropriate tests can easily introduce new errors." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## unittest\n", - "\n", - "Python 3 distributions include the unittest module. See https://docs.python.org/3/library/unittest.html\n" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "...\n", - "----------------------------------------------------------------------\n", - "Ran 3 tests in 0.005s\n", - "\n", - "OK\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import unittest\n", - "\n", - "class TestMyAddTwo(unittest.TestCase):\n", - " def test_my_add_two(self):\n", - " self.assertEqual(my_add_two(1), 3)\n", - " def test_my_add_two_3(self):\n", - " self.assertEqual(my_add_two(3), 5)\n", - "\n", - "# unittest.main(argv=[''], exit=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Fixtures and mocks\n", - "\n", - "Ideally each unit of code should be tested independently.\n", - "\n", - "### Why is this?\n", - "\n", - "However, there are situations where testing code might require data read from a file or a database connection. If only one test requires this external data, then opening the file and reading the data will be part of the test. If several tests require this data, then we use a fixture. \n" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "...\n", - "----------------------------------------------------------------------\n", - "Ran 3 tests in 0.003s\n", - "\n", - "OK\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Module level fixture setup and teardown\n", - "def setUpModule():\n", - " global sample_data\n", - " sample_data = open(\"data/rows.txt\", \"r\")\n", - "\n", - "def tearDownModule():\n", - " sample_data.close()\n", - "\n", - "# unittest.main(argv=[''], exit=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [], - "source": [ - "# Class level fixture setup and teardown\n", - "class TestMyAddTwo(unittest.TestCase):\n", - " @classmethod\n", - " def setUpClass(cls):\n", - " cls.sample_data = open(\"data/rows.txt\", \"r\")\n", - "\n", - " @classmethod\n", - " def tearDownClass(cls):\n", - " cls.sample_data.close()\n", - "\n", - " def test_file_parsing(self):\n", - " for line in self.sample_data:\n", - " # Do something with the line\n", - " pass\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Mocks\n", - "\n", - "Mock and MagicMock objects create all attributes and methods as you access them and store details of how they have been used. You can configure them to specify return values or limit what attributes are available.\n", - "\n", - "See https://docs.python.org/3/library/unittest.mock.html\n", - "\n", - "\n", - "### How can we test a function that does not return a value?\n" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[1, 2, 3]\n", - "\n" - ] - } - ], - "source": [ - "\n", - "def show_results():\n", - " arr = [1, 2, 3]\n", - " print(arr)\n", - " print()\n", - "\n", - "show_results()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Here is a possible test\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "..\n", - "----------------------------------------------------------------------\n", - "Ran 2 tests in 0.003s\n", - "\n", - "OK\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from unittest.mock import MagicMock\n", - "\n", - "class TestShowResults(unittest.TestCase):\n", - " def setUp(self):\n", - " global print\n", - " print = MagicMock()\n", - " def tearDown(self):\n", - " global print\n", - " print = __builtins__.print\n", - " def test_show_results(self):\n", - " show_results()\n", - " self.assertEqual(print.call_count, 2)\n", - "\n", - "# unittest.main(argv=[''], exit=False)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## How does this test work?" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## 'Linting' code with Flake8 and my[py]\n", - "\n", - "There are various tools that can analyse Python code and suggest fixes or improvements without running the code.\n", - "\n", - "These are 'static code checkers' or 'linters' - because they help you remove fluff!\n", - "\n", - "### my[py]\n", - "We saw my[py] briefly before. It is used to find mistakes in type hints, and can even be used to enforce type hints if desired.\n", - "\n", - "https://mypy.readthedocs.io/en/stable/getting_started.html\n", - "\n", - "### Flake8\n", - "Flake8 runs a variety of checks on your Python scripts, and can be used with IDEs such as VS Code to help you write clearer, more readable, code. The, optional, but highly recommended style guide for Python is PEP 8.\n", - "\n", - "https://peps.python.org/pep-0008/\n", - "\n", - "https://flake8.pycqa.org/en/latest/index.html\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Test coverage\n", - "\n", - "### Coverage.py works in three phases:\n", - "\n", - "* Execution: Coverage.py runs your code, and monitors it to see what lines were executed.\n", - "\n", - "* Analysis: Coverage.py examines your code to determine what lines could have run.\n", - "\n", - "* Reporting: Coverage.py combines the results of execution and analysis to produce a coverage number and an indication of missing execution.\n", - "\n", - "See https://coverage.readthedocs.io/en/7.5.3/api.html" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Pytest\n", - "\n", - "### Introduction to pytest\n", - "\n", - "Pytest is another popular tool for testing in Python which makes it easier to write and run tests. \n", - "\n", - "Pytest uses file and function naming conventions to discover test. You will rarely need to run a test directly as the framework will find and run tests for you when you modify your code.\n", - "\n", - "Pytest is a package that you install into your environment from conda or PyPI. For example:\n", - "\n", - "```sh\n", - "pip install pytest\n", - "```\n", - "\n", - "You should then create a directory containing your tests called `tests`.\n", - "\n", - "```sh\n", - "mkdir tests\n", - "```\n", - "\n", - "Within tests, you can create Python scripts containing tests - e.g. `example_test.py`.\n", - "\n", - "### Example test: comparing csv files\n", - "\n", - "There are a wide variety of applications for testing. Below is an example of a test where we are running some code to generate a `.csv` file, and then confirming if the results are as expected.\n", - "\n", - "This could come in handy if you have produced code for a model, and are concerned that others running on the model on a different machine could be getting slightly different results. \n", - "\n", - "In this example, we start the `.py` file by importing:\n", - "\n", - "* `pytest` (to run the tests)\n", - "* `pandas` (to manage the csv files)\n", - "* Our model (imagining a scenario where we have a file `model_code` inside a folder `scripts/`, which is a sister folder to `tests/`)\n", - "* `tempfile` (to save our model results to a temporary directory)\n", - "\n", - "```python\n", - "import pytest\n", - "import pandas as pd\n", - "from scripts import model_code\n", - "import tempfile\n", - "```\n", - "\n", - "We then provide file paths at the start of our script:\n", - "\n", - "```python\n", - "EXP_FOLDER = 'exp_results'\n", - "TEMP_FOLDER = tempfile.mkdtemp()\n", - "```\n", - "\n", - "Assuming we might be running the model with two different parameters (so producing two `.csv` files, and comparing each of those), we can use `parametrise` to run the same test with two different inputs. First, we can define our parameters for the model:\n", - "\n", - "```python\n", - "parameters = [\n", - " {\n", - " 'arrivals': 100,\n", - " 'file': 'result100.csv'\n", - " },\n", - " {\n", - " 'arrivals': 150,\n", - " 'file': 'result150.csv'\n", - " }\n", - "]\n", - "```\n", - "\n", - "For the file paths, we should set these up as fixtures:\n", - "\n", - "```python\n", - "@pytest.fixture\n", - "def exp_folder():\n", - " return EXP_FOLDER\n", - "\n", - "\n", - "@pytest.fixture\n", - "def temp_folder():\n", - " return TEMP_FOLDER\n", - "```\n", - "\n", - "We can then write our test function, and use the file names from parameters as the ID for each test:\n", - "\n", - "```python\n", - "@pytest.mark.parametrize('param', parameters,\n", - " ids=[d['file'] for d in parameters])\n", - "def test_equal_df(param, temp_folder, exp_folder):\n", - " '''\n", - " Test that model results are consistent with the expected\n", - " results (which are saved in the EXP_FOLDER)\n", - " '''\n", - " # Run the model (assuming the function has inputs for our\n", - " # parameter dictionary and for a save location for the .csv file)\n", - " model_code.main(**param, temp_folder)\n", - "\n", - " # Import the test and expected results (we can use the filename from the\n", - " # parameter dictionary, and then the folder name)\n", - " test_result = import_xls(temp_folder, param['file'])\n", - " exp_result = import_xls(exp_folder, param['file'])\n", - "\n", - " # Check that the dataframes are equal\n", - " pd.testing.assert_frame_equal(test_result, exp_result)\n", - "```\n", - "\n", - "With our `.py` file now complete, we can run our tests from the terminal. Ensuring you are located in the parent folder to `tests/`, run the command:\n", - "\n", - "```sh\n", - "pytest\n", - "```\n", - "\n", - "If your tests take a long time to run, you may want to explore parallelising them. You can install `pytest-xdist`, which is a package that parallelises your pytests using multiple CPUs. With this package installed, you can run the command:\n", - "\n", - "```sh\n", - "pytest -n auto\n", - "```\n", - "\n", - "### Coverage\n", - "\n", - "https://pypi.org/project/pytest-cov/\n", - "\n", - "### pytest-notebook\n", - "\n", - "See https://pytest-notebook.readthedocs.io/en/latest/" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Resources\n", - "\n", - "See the testing section of https://alan-turing-institute.github.io/rse-course/html/module01_introduction_to_python/index.html" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## Testing Practical Exercise\n", - "\n", - "Python 3 distributions include the unittest module. See https://docs.python.org/3/library/unittest.html\n", - "\n", - "Here is the example included in the Python documentation." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "\n", - "## Exercise 1\n", - "\n", - "Using the above as a template, create a test class for the Upper class we used earlier.\n", - "\n", - "```python\n", - "class Upper(str):\n", - " def __new__(cls, text=\"\"):\n", - " return super().__new__(cls, text.upper())\n", - "```\n", - "\n", - "### Important\n", - "\n", - "What should (and can) be tested?\n", - "\n", - "See https://docs.python.org/3/library/unittest.html\n", - "\n", - "\n", - "## Exercise 2\n", - "\n", - "Design a new capability for the class using TDD.\n", - "\n", - "Here are some suggestions -\n", - "\n", - "* Do not allow strings without at least one letter\n", - "\n", - "* Only allow strings that begin with a letter\n", - "\n", - "* Limit the length of the string to 10 characters." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [ - "remove-input" - ] - }, - "outputs": [ - { - "data": { - "text/html": [ - "
    " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "var questionsaOPZkuwGBEWd=[\n", - " {\n", - " \"question\": \"What is the primary goal of software testing?\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"To prove that the code is free of defects\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"To establish that the osftware behave as intended\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"To increase the execution speed of the software\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"To ensure the software uses minimal memory\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " },\n", - " {\n", - " \"question\": \"Which statement correctly describes the Python assert statement\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"It is used tohandle exceptions\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"It verifies that a condition is true and raises an error if it is not\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"It is used for logging messages\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"It is a replacement for the if-else statement\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " },\n", - " {\n", - " \"question\": \"What is Test Driven Development (TDD)?\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"A software testing technique to find bugs in the code\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"An approach to software designw here tests are written before the code\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"A method to enhance the performance of the software\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"A tool used for automating software development\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " }\n", - "];\n", - " // Make a random ID\n", - "function makeid(length) {\n", - " var result = [];\n", - " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", - " var charactersLength = characters.length;\n", - " for (var i = 0; i < length; i++) {\n", - " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", - " }\n", - " return result.join('');\n", - "}\n", - "\n", - "// Choose a random subset of an array. Can also be used to shuffle the array\n", - "function getRandomSubarray(arr, size) {\n", - " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", - " while (i--) {\n", - " index = Math.floor((i + 1) * Math.random());\n", - " temp = shuffled[index];\n", - " shuffled[index] = shuffled[i];\n", - " shuffled[i] = temp;\n", - " }\n", - " return shuffled.slice(0, size);\n", - "}\n", - "\n", - "function printResponses(responsesContainer) {\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
    1. Copy the text in this cell below \"Answer String\"
    2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
    3. Select the whole \"Replace Me\" text
    4. Paste in your answer string and press shift-Enter.
    5. Save the notebook using the save icon or File->Save Notebook menu item



    6. Answer String:
      ';\n", - " console.log(responses);\n", - " responses.forEach((response, index) => {\n", - " if (response) {\n", - " console.log(index + ': ' + response);\n", - " stringResponses+= index + ': ' + response +\"
      \";\n", - " }\n", - " });\n", - " responsesContainer.innerHTML=stringResponses;\n", - "}\n", - "function check_mc() {\n", - " var id = this.id.split('-')[0];\n", - " //var response = this.id.split('-')[1];\n", - " //console.log(response);\n", - " //console.log(\"In check_mc(), id=\"+id);\n", - " //console.log(event.srcElement.id) \n", - " //console.log(event.srcElement.dataset.correct) \n", - " //console.log(event.srcElement.dataset.feedback)\n", - "\n", - " var label = event.srcElement;\n", - " //console.log(label, label.nodeName);\n", - " var depth = 0;\n", - " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", - " label = label.parentElement;\n", - " console.log(depth, label);\n", - " depth++;\n", - " }\n", - "\n", - "\n", - "\n", - " var answers = label.parentElement.children;\n", - "\n", - " //console.log(answers);\n", - "\n", - "\n", - " // Split behavior based on multiple choice vs many choice:\n", - " var fb = document.getElementById(\"fb\" + id);\n", - "\n", - "\n", - "\n", - "\n", - " if (fb.dataset.numcorrect == 1) {\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " //console.log(responsesContainer);\n", - " var response = label.firstChild.innerText;\n", - " if (label.querySelector(\".QuizCode\")){\n", - " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", - " }\n", - " console.log(response);\n", - " //console.log(document.getElementById(\"quizWrap\"+id));\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " console.log(responses);\n", - " responses[qnum]= response;\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End code to preserve responses\n", - " \n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " //console.log(child);\n", - " child.className = \"MCButton\";\n", - " }\n", - "\n", - "\n", - "\n", - " if (label.dataset.correct == \"true\") {\n", - " // console.log(\"Correct action\");\n", - " if (\"feedback\" in label.dataset) {\n", - " fb.textContent = jaxify(label.dataset.feedback);\n", - " } else {\n", - " fb.textContent = \"Correct!\";\n", - " }\n", - " label.classList.add(\"correctButton\");\n", - "\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - "\n", - " } else {\n", - " if (\"feedback\" in label.dataset) {\n", - " fb.textContent = jaxify(label.dataset.feedback);\n", - " } else {\n", - " fb.textContent = \"Incorrect -- try again.\";\n", - " }\n", - " //console.log(\"Error action\");\n", - " label.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - " }\n", - " else {\n", - " var reset = false;\n", - " var feedback;\n", - " if (label.dataset.correct == \"true\") {\n", - " if (\"feedback\" in label.dataset) {\n", - " feedback = jaxify(label.dataset.feedback);\n", - " } else {\n", - " feedback = \"Correct!\";\n", - " }\n", - " if (label.dataset.answered <= 0) {\n", - " if (fb.dataset.answeredcorrect < 0) {\n", - " fb.dataset.answeredcorrect = 1;\n", - " reset = true;\n", - " } else {\n", - " fb.dataset.answeredcorrect++;\n", - " }\n", - " if (reset) {\n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " child.className = \"MCButton\";\n", - " child.dataset.answered = 0;\n", - " }\n", - " }\n", - " label.classList.add(\"correctButton\");\n", - " label.dataset.answered = 1;\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - "\n", - " }\n", - " } else {\n", - " if (\"feedback\" in label.dataset) {\n", - " feedback = jaxify(label.dataset.feedback);\n", - " } else {\n", - " feedback = \"Incorrect -- try again.\";\n", - " }\n", - " if (fb.dataset.answeredcorrect > 0) {\n", - " fb.dataset.answeredcorrect = -1;\n", - " reset = true;\n", - " } else {\n", - " fb.dataset.answeredcorrect--;\n", - " }\n", - "\n", - " if (reset) {\n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " child.className = \"MCButton\";\n", - " child.dataset.answered = 0;\n", - " }\n", - " }\n", - " label.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " //console.log(responsesContainer);\n", - " var response = label.firstChild.innerText;\n", - " if (label.querySelector(\".QuizCode\")){\n", - " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", - " }\n", - " console.log(response);\n", - " //console.log(document.getElementById(\"quizWrap\"+id));\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " if (label.dataset.correct == \"true\") {\n", - " if (typeof(responses[qnum]) == \"object\"){\n", - " if (!responses[qnum].includes(response))\n", - " responses[qnum].push(response);\n", - " } else{\n", - " responses[qnum]= [ response ];\n", - " }\n", - " } else {\n", - " responses[qnum]= response;\n", - " }\n", - " console.log(responses);\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End save responses stuff\n", - "\n", - "\n", - "\n", - " var numcorrect = fb.dataset.numcorrect;\n", - " var answeredcorrect = fb.dataset.answeredcorrect;\n", - " if (answeredcorrect >= 0) {\n", - " fb.textContent = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", - " } else {\n", - " fb.textContent = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", - " }\n", - "\n", - "\n", - " }\n", - "\n", - " if (typeof MathJax != 'undefined') {\n", - " var version = MathJax.version;\n", - " console.log('MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([fb]);\n", - " }\n", - " } else {\n", - " console.log('MathJax not detected');\n", - " }\n", - "\n", - "}\n", - "\n", - "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", - " var shuffled;\n", - " if (shuffle_answers == \"True\") {\n", - " //console.log(shuffle_answers+\" read as true\");\n", - " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", - " } else {\n", - " //console.log(shuffle_answers+\" read as false\");\n", - " shuffled = qa.answers;\n", - " }\n", - "\n", - "\n", - " var num_correct = 0;\n", - "\n", - "\n", - "\n", - " shuffled.forEach((item, index, ans_array) => {\n", - " //console.log(answer);\n", - "\n", - " // Make input element\n", - " var inp = document.createElement(\"input\");\n", - " inp.type = \"radio\";\n", - " inp.id = \"quizo\" + id + index;\n", - " inp.style = \"display:none;\";\n", - " aDiv.append(inp);\n", - "\n", - " //Make label for input element\n", - " var lab = document.createElement(\"label\");\n", - " lab.className = \"MCButton\";\n", - " lab.id = id + '-' + index;\n", - " lab.onclick = check_mc;\n", - " var aSpan = document.createElement('span');\n", - " aSpan.classsName = \"\";\n", - " //qDiv.id=\"quizQn\"+id+index;\n", - " if (\"answer\" in item) {\n", - " aSpan.innerHTML = jaxify(item.answer);\n", - " //aSpan.innerHTML=item.answer;\n", - " }\n", - " lab.append(aSpan);\n", - "\n", - " // Create div for code inside question\n", - " var codeSpan;\n", - " if (\"code\" in item) {\n", - " codeSpan = document.createElement('span');\n", - " codeSpan.id = \"code\" + id + index;\n", - " codeSpan.className = \"QuizCode\";\n", - " var codePre = document.createElement('pre');\n", - " codeSpan.append(codePre);\n", - " var codeCode = document.createElement('code');\n", - " codePre.append(codeCode);\n", - " codeCode.innerHTML = item.code;\n", - " lab.append(codeSpan);\n", - " //console.log(codeSpan);\n", - " }\n", - "\n", - " //lab.textContent=item.answer;\n", - "\n", - " // Set the data attributes for the answer\n", - " lab.setAttribute('data-correct', item.correct);\n", - " if (item.correct) {\n", - " num_correct++;\n", - " }\n", - " if (\"feedback\" in item) {\n", - " lab.setAttribute('data-feedback', item.feedback);\n", - " }\n", - " lab.setAttribute('data-answered', 0);\n", - "\n", - " aDiv.append(lab);\n", - "\n", - " });\n", - "\n", - " if (num_correct > 1) {\n", - " outerqDiv.className = \"ManyChoiceQn\";\n", - " } else {\n", - " outerqDiv.className = \"MultipleChoiceQn\";\n", - " }\n", - "\n", - " return num_correct;\n", - "\n", - "}\n", - "function check_numeric(ths, event) {\n", - "\n", - " if (event.keyCode === 13) {\n", - " ths.blur();\n", - "\n", - " var id = ths.id.split('-')[0];\n", - "\n", - " var submission = ths.value;\n", - " if (submission.indexOf('/') != -1) {\n", - " var sub_parts = submission.split('/');\n", - " //console.log(sub_parts);\n", - " submission = sub_parts[0] / sub_parts[1];\n", - " }\n", - " //console.log(\"Reader entered\", submission);\n", - "\n", - " if (\"precision\" in ths.dataset) {\n", - " var precision = ths.dataset.precision;\n", - " // console.log(\"1:\", submission)\n", - " submission = Math.round((1 * submission + Number.EPSILON) * 10 ** precision) / 10 ** precision;\n", - " // console.log(\"Rounded to \", submission, \" precision=\", precision );\n", - " }\n", - "\n", - "\n", - " //console.log(\"In check_numeric(), id=\"+id);\n", - " //console.log(event.srcElement.id) \n", - " //console.log(event.srcElement.dataset.feedback)\n", - "\n", - " var fb = document.getElementById(\"fb\" + id);\n", - " fb.style.display = \"none\";\n", - " fb.textContent = \"Incorrect -- try again.\";\n", - "\n", - " var answers = JSON.parse(ths.dataset.answers);\n", - " //console.log(answers);\n", - "\n", - " var defaultFB = \"\";\n", - " var correct;\n", - " var done = false;\n", - " answers.every(answer => {\n", - " //console.log(answer.type);\n", - "\n", - " correct = false;\n", - " // if (answer.type==\"value\"){\n", - " if ('value' in answer) {\n", - " if (submission == answer.value) {\n", - " if (\"feedback\" in answer) {\n", - " fb.textContent = jaxify(answer.feedback);\n", - " } else {\n", - " fb.textContent = jaxify(\"Correct\");\n", - " }\n", - " correct = answer.correct;\n", - " //console.log(answer.correct);\n", - " done = true;\n", - " }\n", - " // } else if (answer.type==\"range\") {\n", - " } else if ('range' in answer) {\n", - " //console.log(answer.range);\n", - " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", - " fb.textContent = jaxify(answer.feedback);\n", - " correct = answer.correct;\n", - " //console.log(answer.correct);\n", - " done = true;\n", - " }\n", - " } else if (answer.type == \"default\") {\n", - " defaultFB = answer.feedback;\n", - " }\n", - " if (done) {\n", - " return false; // Break out of loop if this has been marked correct\n", - " } else {\n", - " return true; // Keep looking for case that includes this as a correct answer\n", - " }\n", - " });\n", - "\n", - " if ((!done) && (defaultFB != \"\")) {\n", - " fb.innerHTML = jaxify(defaultFB);\n", - " //console.log(\"Default feedback\", defaultFB);\n", - " }\n", - "\n", - " fb.style.display = \"block\";\n", - " if (correct) {\n", - " ths.className = \"Input-text\";\n", - " ths.classList.add(\"correctButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - " } else {\n", - " ths.className = \"Input-text\";\n", - " ths.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - "\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " console.log(submission);\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " //console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " console.log(responses);\n", - " if (submission == ths.value){\n", - " responses[qnum]= submission;\n", - " } else {\n", - " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", - " }\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End code to preserve responses\n", - "\n", - " if (typeof MathJax != 'undefined') {\n", - " var version = MathJax.version;\n", - " console.log('MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([fb]);\n", - " }\n", - " } else {\n", - " console.log('MathJax not detected');\n", - " }\n", - " return false;\n", - " }\n", - "\n", - "}\n", - "\n", - "function isValid(el, charC) {\n", - " //console.log(\"Input char: \", charC);\n", - " if (charC == 46) {\n", - " if (el.value.indexOf('.') === -1) {\n", - " return true;\n", - " } else if (el.value.indexOf('/') != -1) {\n", - " var parts = el.value.split('/');\n", - " if (parts[1].indexOf('.') === -1) {\n", - " return true;\n", - " }\n", - " }\n", - " else {\n", - " return false;\n", - " }\n", - " } else if (charC == 47) {\n", - " if (el.value.indexOf('/') === -1) {\n", - " if ((el.value != \"\") && (el.value != \".\")) {\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " return false;\n", - " }\n", - " } else if (charC == 45) {\n", - " var edex = el.value.indexOf('e');\n", - " if (edex == -1) {\n", - " edex = el.value.indexOf('E');\n", - " }\n", - "\n", - " if (el.value == \"\") {\n", - " return true;\n", - " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else if (charC == 101) { // \"e\"\n", - " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", - " // Prev symbol must be digit or decimal point:\n", - " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", - " return true;\n", - " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " if (charC > 31 && (charC < 48 || charC > 57))\n", - " return false;\n", - " }\n", - " return true;\n", - "}\n", - "\n", - "function numeric_keypress(evnt) {\n", - " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", - "\n", - " if (charC == 13) {\n", - " check_numeric(this, evnt);\n", - " } else {\n", - " return isValid(this, charC);\n", - " }\n", - "}\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", - "\n", - "\n", - "\n", - " //console.log(answer);\n", - "\n", - "\n", - " outerqDiv.className = \"NumericQn\";\n", - " aDiv.style.display = 'block';\n", - "\n", - " var lab = document.createElement(\"label\");\n", - " lab.className = \"InpLabel\";\n", - " lab.textContent = \"Type numeric answer here:\";\n", - " aDiv.append(lab);\n", - "\n", - " var inp = document.createElement(\"input\");\n", - " inp.type = \"text\";\n", - " //inp.id=\"input-\"+id;\n", - " inp.id = id + \"-0\";\n", - " inp.className = \"Input-text\";\n", - " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", - " if (\"precision\" in qa) {\n", - " inp.setAttribute('data-precision', qa.precision);\n", - " }\n", - " aDiv.append(inp);\n", - " //console.log(inp);\n", - "\n", - " //inp.addEventListener(\"keypress\", check_numeric);\n", - " //inp.addEventListener(\"keypress\", numeric_keypress);\n", - " /*\n", - " inp.addEventListener(\"keypress\", function(event) {\n", - " return numeric_keypress(this, event);\n", - " }\n", - " );\n", - " */\n", - " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", - " inp.onkeypress = numeric_keypress;\n", - " inp.onpaste = event => false;\n", - "\n", - " inp.addEventListener(\"focus\", function (event) {\n", - " this.value = \"\";\n", - " return false;\n", - " }\n", - " );\n", - "\n", - "\n", - "}\n", - "function jaxify(string) {\n", - " var mystring = string;\n", - "\n", - " var count = 0;\n", - " var loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", - "\n", - " var count2 = 0;\n", - " var loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", - "\n", - " //console.log(loc);\n", - "\n", - " while ((loc >= 0) || (loc2 >= 0)) {\n", - "\n", - " /* Have to replace all the double $$ first with current implementation */\n", - " if (loc2 >= 0) {\n", - " if (count2 % 2 == 0) {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, \"$1\\\\[\");\n", - " } else {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, \"$1\\\\]\");\n", - " }\n", - " count2++;\n", - " } else {\n", - " if (count % 2 == 0) {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, \"$1\\\\(\");\n", - " } else {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, \"$1\\\\)\");\n", - " }\n", - " count++;\n", - " }\n", - " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", - " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", - " //console.log(mystring,\", loc:\",loc,\", loc2:\",loc2);\n", - " }\n", - "\n", - " //console.log(mystring);\n", - " return mystring;\n", - "}\n", - "\n", - "\n", - "function show_questions(json, mydiv) {\n", - " console.log('show_questions');\n", - " //var mydiv=document.getElementById(myid);\n", - " var shuffle_questions = mydiv.dataset.shufflequestions;\n", - " var num_questions = mydiv.dataset.numquestions;\n", - " var shuffle_answers = mydiv.dataset.shuffleanswers;\n", - " var max_width = mydiv.dataset.maxwidth;\n", - "\n", - " if (num_questions > json.length) {\n", - " num_questions = json.length;\n", - " }\n", - "\n", - " var questions;\n", - " if ((num_questions < json.length) || (shuffle_questions == \"True\")) {\n", - " //console.log(num_questions+\",\"+json.length);\n", - " questions = getRandomSubarray(json, num_questions);\n", - " } else {\n", - " questions = json;\n", - " }\n", - "\n", - " //console.log(\"SQ: \"+shuffle_questions+\", NQ: \" + num_questions + \", SA: \", shuffle_answers);\n", - "\n", - " // Iterate over questions\n", - " questions.forEach((qa, index, array) => {\n", - " //console.log(qa.question); \n", - "\n", - " var id = makeid(8);\n", - " //console.log(id);\n", - "\n", - "\n", - " // Create Div to contain question and answers\n", - " var iDiv = document.createElement('div');\n", - " //iDiv.id = 'quizWrap' + id + index;\n", - " iDiv.id = 'quizWrap' + id;\n", - " iDiv.className = 'Quiz';\n", - " iDiv.setAttribute('data-qnum', index);\n", - " iDiv.style.maxWidth =max_width+\"px\";\n", - " mydiv.appendChild(iDiv);\n", - " // iDiv.innerHTML=qa.question;\n", - " \n", - " var outerqDiv = document.createElement('div');\n", - " outerqDiv.id = \"OuterquizQn\" + id + index;\n", - " // Create div to contain question part\n", - " var qDiv = document.createElement('div');\n", - " qDiv.id = \"quizQn\" + id + index;\n", - " \n", - " if (qa.question) {\n", - " iDiv.append(outerqDiv);\n", - "\n", - " //qDiv.textContent=qa.question;\n", - " qDiv.innerHTML = jaxify(qa.question);\n", - " outerqDiv.append(qDiv);\n", - " }\n", - "\n", - " // Create div for code inside question\n", - " var codeDiv;\n", - " if (\"code\" in qa) {\n", - " codeDiv = document.createElement('div');\n", - " codeDiv.id = \"code\" + id + index;\n", - " codeDiv.className = \"QuizCode\";\n", - " var codePre = document.createElement('pre');\n", - " codeDiv.append(codePre);\n", - " var codeCode = document.createElement('code');\n", - " codePre.append(codeCode);\n", - " codeCode.innerHTML = qa.code;\n", - " outerqDiv.append(codeDiv);\n", - " //console.log(codeDiv);\n", - " }\n", - "\n", - "\n", - " // Create div to contain answer part\n", - " var aDiv = document.createElement('div');\n", - " aDiv.id = \"quizAns\" + id + index;\n", - " aDiv.className = 'Answer';\n", - " iDiv.append(aDiv);\n", - "\n", - " //console.log(qa.type);\n", - "\n", - " var num_correct;\n", - " if ((qa.type == \"multiple_choice\") || (qa.type == \"many_choice\") ) {\n", - " num_correct = make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id);\n", - " if (\"answer_cols\" in qa) {\n", - " //aDiv.style.gridTemplateColumns = 'auto '.repeat(qa.answer_cols);\n", - " aDiv.style.gridTemplateColumns = 'repeat(' + qa.answer_cols + ', 1fr)';\n", - " }\n", - " } else if (qa.type == \"numeric\") {\n", - " //console.log(\"numeric\");\n", - " make_numeric(qa, outerqDiv, qDiv, aDiv, id);\n", - " }\n", - "\n", - "\n", - " //Make div for feedback\n", - " var fb = document.createElement(\"div\");\n", - " fb.id = \"fb\" + id;\n", - " //fb.style=\"font-size: 20px;text-align:center;\";\n", - " fb.className = \"Feedback\";\n", - " fb.setAttribute(\"data-answeredcorrect\", 0);\n", - " fb.setAttribute(\"data-numcorrect\", num_correct);\n", - " iDiv.append(fb);\n", - "\n", - "\n", - " });\n", - " var preserveResponses = mydiv.dataset.preserveresponses;\n", - " console.log(preserveResponses);\n", - " console.log(preserveResponses == \"true\");\n", - " if (preserveResponses == \"true\") {\n", - " console.log(preserveResponses);\n", - " // Create Div to contain record of answers\n", - " var iDiv = document.createElement('div');\n", - " iDiv.id = 'responses' + mydiv.id;\n", - " iDiv.className = 'JCResponses';\n", - " // Create a place to store responses as an empty array\n", - " iDiv.setAttribute('data-responses', '[]');\n", - "\n", - " // Dummy Text\n", - " iDiv.innerHTML=\"Select your answers and then follow the directions that will appear here.\"\n", - " //iDiv.className = 'Quiz';\n", - " mydiv.appendChild(iDiv);\n", - " }\n", - "//console.log(\"At end of show_questions\");\n", - " if (typeof MathJax != 'undefined') {\n", - " console.log(\"MathJax version\", MathJax.version);\n", - " var version = MathJax.version;\n", - " setTimeout(function(){\n", - " var version = MathJax.version;\n", - " console.log('After sleep, MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([mydiv]);\n", - " }\n", - " }, 500);\n", - "if (typeof version == 'undefined') {\n", - " } else\n", - " {\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([mydiv]);\n", - " } else {\n", - " console.log(\"MathJax not found\");\n", - " }\n", - " }\n", - " }\n", - " return false;\n", - "}\n", - "/* This is to handle asynchrony issues in loading Jupyter notebooks\n", - " where the quiz has been previously run. The Javascript was generally\n", - " being run before the div was added to the DOM. I tried to do this\n", - " more elegantly using Mutation Observer, but I didn't get it to work.\n", - "\n", - " Someone more knowledgeable could make this better ;-) */\n", - "\n", - " function try_show() {\n", - " if(document.getElementById(\"aOPZkuwGBEWd\")) {\n", - " show_questions(questionsaOPZkuwGBEWd, aOPZkuwGBEWd); \n", - " } else {\n", - " setTimeout(try_show, 200);\n", - " }\n", - " };\n", - " \n", - " {\n", - " // console.log(element);\n", - "\n", - " //console.log(\"aOPZkuwGBEWd\");\n", - " // console.log(document.getElementById(\"aOPZkuwGBEWd\"));\n", - "\n", - " try_show();\n", - " }\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from jupyterquiz import display_quiz\n", - "display_quiz(\"questions/summary_testing.json\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.19" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/individual_modules/python_for_data_analysis/Python_Virtual_environments.ipynb b/individual_modules/python_for_data_analysis/Python_Virtual_environments.ipynb deleted file mode 100644 index fc216e62..00000000 --- a/individual_modules/python_for_data_analysis/Python_Virtual_environments.ipynb +++ /dev/null @@ -1,1305 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "# Virtual environments\n", - "\n", - "## Learning Objectives\n", - "- Understand the importance of using virtual environments in Python development\n", - "- Differentiate between various tools for managing virtual environments: `venv`, `Conda`, `Pipenv`, and `Poetry`\n", - "- Create and activate a virtual environment using `venv`\n", - "- Use `Conda` to manage environments and packages.\n", - "- Utilize `Pipenv` and `Poetry` for dependency management and virtual environments\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Why do we need virtual environments?\n", - "\n", - "### Control\n", - "\n", - "Virtual environments give you control over the version of Python and the versions of installed libraries (modules).\n", - "Typically this means you will be using the latest versions of Python and libraries for your latest project.\n", - "\n", - "### Freedom to update\n", - "\n", - "By using virtual environments you are free to update to new versions of libraries for your latest project, without breaking earlier projects.\n", - "\n", - "### Reliable deployment\n", - "\n", - "It is far easier to create reproducible and portable software with virtual environments.\n", - "\n", - "### System Python!\n", - "\n", - "Use of Python is so widespread that some operating systems include Python, for a long time this was Python 2.7 though more recently Python 3 (3.8,3.9) has become a popular default. It is never a good idea to change this configuration, except with the usual system upgrade tools. \n", - "\n", - "## Why not use virtual machines or containers?\n", - "\n", - "These have their own place in development and deployment of software. We are not going to cover them here, except to note the following. \n", - "\n", - "If you find yourself having to do complicated or unusual things with virtual environments, or virtual environments do not provide the facilities or security your software requires, then consider alternatives such as Docker.\n", - "\n", - "Yes, *but*\n", - "\n", - "**I have admin rights so I can install libraries and a new Python version for all users.**\n", - "\n", - "**Just don't!**\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "## venv\n", - "\n", - "Tutorial https://docs.python.org/3/tutorial/venv.html\n", - "\n", - "```sh\n", - "python -m venv .venv\n", - "\n", - "source .venv/bin/activate\n", - "```\n", - "\n", - "Though in practice your IDE, e.g. VS Code will typically activate environments for you.\n", - "\n", - "## Virtualenv\n", - "\n", - "Tutorial: \n", - "\n", - "Virtualenv is a tool that can be used to set up a virtual environment. If it is not already on your machine, you'll need to install it such as by:\n", - "\n", - "```sh\n", - "pip install virtualenv\n", - "```\n", - "\n", - "To create a new environment:\n", - "\n", - "```sh\n", - "virtualenv env_name\n", - "```\n", - "\n", - "To activate and deactivate the environment:\n", - "\n", - "```sh\n", - "source env_name/bin/activate\n", - "deactivate\n", - "```\n", - "\n", - "To install packages into the environment, a great method is to store all dependencies listed in a `requirements.txt` (rather than directly installing them with `pip install package_name`). This is because you can save and share this file alongside your repository, and it allows others to easily see and make a copy of the environment you used for your analysis - and for yourself to reproduce that environment, when you return to your code years later!\n", - "\n", - "An example `requirements.txt` file:\n", - "\n", - "```sh\n", - "jupyter==1.0.0\n", - "pandas==2.2.2\n", - "```\n", - "\n", - "To install the packages from `requirements.txt` into your environment:\n", - "\n", - "```sh\n", - "pip install -r requirements.txt\n", - "```\n", - "\n", - "To update your environment (such as if you to have add a new package to the requirements file):\n", - "\n", - "```sh\n", - "pip install -r requirements.txt --upgrade\n", - "```\n", - "\n", - "To delete your environment, use the command below - but be careful! Do not name your environment with the same name as a folder in your current location. If so, you could accidentally permanently delete that folder rather than your environment...\n", - "\n", - "```sh\n", - "rm -r env_name\n", - "```\n", - "\n", - "## Conda\n", - "\n", - "Tutorial: \n", - "\n", - "Conda is another popular tool for environment management in Python. If not already on your machine, follow these [installation instructions](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) to install conda.\n", - "\n", - "You can save the dependencies needed for your Python environment using an `environment.yml` file. Other people can then build an environment with the same dependencies based on that file. Within this file, you can just list the packages needed, or you can include specific versions (if you want people to use the same environment as you).\n", - "\n", - "Example file:\n", - "\n", - "```sh\n", - "name: shoaib2022\n", - "channels:\n", - " - defaults\n", - "dependencies:\n", - " - matplotlib=3.3.4\n", - " - pytest=7.4.4\n", - " - pip:\n", - " - pytest-xdist==3.6.1\n", - "```\n", - "\n", - "To create environment from the file:\n", - "\n", - "```sh\n", - "conda env create --name env_name --file environment.yml\n", - "```\n", - "\n", - "To activate the environment:\n", - "\n", - "```sh\n", - "conda activate env_name\n", - "```\n", - "\n", - "To see packages in the current environment:\n", - "\n", - "```sh\n", - "conda list\n", - "```\n", - "\n", - "To see the conda environments on your machine:\n", - "\n", - "```sh\n", - "conda env list\n", - "```\n", - "\n", - "To update the current environment from a `.yml` file (such as if you have changed the dependencies or versions listed):\n", - "\n", - "```sh\n", - "conva env update --file environment.yml --prune\n", - "```\n", - "\n", - "To delete the environment:\n", - "\n", - "```sh\n", - "conda remove -n env_name --all\n", - "```\n", - "\n", - "## Pipenv\n", - "\n", - "```sh\n", - "mkdir .venv\n", - "pipenv install numpy\n", - "pipenv shell\n", - "```\n", - "\n", - "## Poetry\n", - "\n", - "https://python-poetry.org/docs/\n", - "\n", - "```sh\n", - "pipx install poetry\n", - "```\n", - "\n", - "https://python-poetry.org/docs/basic-usage/" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [ - "remove-input" - ] - }, - "outputs": [ - { - "data": { - "text/html": [ - "
      " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "var questionsqukeMLYgLRMA=[\n", - " {\n", - " \"question\": \"What is one of the main benefits of using virtual environments in Python?\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"Improved performance of the Python interpreter\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"Control over the version of Python and installed libraries\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"Automatic code optimization\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"Built-in version control for the code\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " },\n", - " {\n", - " \"question\": \"Why is it recommended not to modify the sytsem Python installation directly?\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"It is protected by the operating system and cannot be changed\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"Modifying system Python can break system tools that depends on it\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"System Python is always the latest version and does not need changes\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"It is automatically updated and does not require manual changes\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " },\n", - " {\n", - " \"question\": \"Which of the following commands is used to create a virtual environment using venv?\",\n", - " \"type\": \"many_choice\",\n", - " \"answers\": [\n", - " {\n", - " \"answer\": \"python -m venv env\",\n", - " \"correct\": true,\n", - " \"feedback\": \"Correct\"\n", - " },\n", - " {\n", - " \"answer\": \"conda create --name env\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"pipenv install\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " },\n", - " {\n", - " \"answer\": \"poetry new\",\n", - " \"correct\": false,\n", - " \"feedback\": \"Incorrect\"\n", - " }\n", - " ]\n", - " }\n", - "];\n", - " // Make a random ID\n", - "function makeid(length) {\n", - " var result = [];\n", - " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", - " var charactersLength = characters.length;\n", - " for (var i = 0; i < length; i++) {\n", - " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", - " }\n", - " return result.join('');\n", - "}\n", - "\n", - "// Choose a random subset of an array. Can also be used to shuffle the array\n", - "function getRandomSubarray(arr, size) {\n", - " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", - " while (i--) {\n", - " index = Math.floor((i + 1) * Math.random());\n", - " temp = shuffled[index];\n", - " shuffled[index] = shuffled[i];\n", - " shuffled[i] = temp;\n", - " }\n", - " return shuffled.slice(0, size);\n", - "}\n", - "\n", - "function printResponses(responsesContainer) {\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
      1. Copy the text in this cell below \"Answer String\"
      2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
      3. Select the whole \"Replace Me\" text
      4. Paste in your answer string and press shift-Enter.
      5. Save the notebook using the save icon or File->Save Notebook menu item



      6. Answer String:
        ';\n", - " console.log(responses);\n", - " responses.forEach((response, index) => {\n", - " if (response) {\n", - " console.log(index + ': ' + response);\n", - " stringResponses+= index + ': ' + response +\"
        \";\n", - " }\n", - " });\n", - " responsesContainer.innerHTML=stringResponses;\n", - "}\n", - "function check_mc() {\n", - " var id = this.id.split('-')[0];\n", - " //var response = this.id.split('-')[1];\n", - " //console.log(response);\n", - " //console.log(\"In check_mc(), id=\"+id);\n", - " //console.log(event.srcElement.id) \n", - " //console.log(event.srcElement.dataset.correct) \n", - " //console.log(event.srcElement.dataset.feedback)\n", - "\n", - " var label = event.srcElement;\n", - " //console.log(label, label.nodeName);\n", - " var depth = 0;\n", - " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", - " label = label.parentElement;\n", - " console.log(depth, label);\n", - " depth++;\n", - " }\n", - "\n", - "\n", - "\n", - " var answers = label.parentElement.children;\n", - "\n", - " //console.log(answers);\n", - "\n", - "\n", - " // Split behavior based on multiple choice vs many choice:\n", - " var fb = document.getElementById(\"fb\" + id);\n", - "\n", - "\n", - "\n", - "\n", - " if (fb.dataset.numcorrect == 1) {\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " //console.log(responsesContainer);\n", - " var response = label.firstChild.innerText;\n", - " if (label.querySelector(\".QuizCode\")){\n", - " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", - " }\n", - " console.log(response);\n", - " //console.log(document.getElementById(\"quizWrap\"+id));\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " console.log(responses);\n", - " responses[qnum]= response;\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End code to preserve responses\n", - " \n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " //console.log(child);\n", - " child.className = \"MCButton\";\n", - " }\n", - "\n", - "\n", - "\n", - " if (label.dataset.correct == \"true\") {\n", - " // console.log(\"Correct action\");\n", - " if (\"feedback\" in label.dataset) {\n", - " fb.textContent = jaxify(label.dataset.feedback);\n", - " } else {\n", - " fb.textContent = \"Correct!\";\n", - " }\n", - " label.classList.add(\"correctButton\");\n", - "\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - "\n", - " } else {\n", - " if (\"feedback\" in label.dataset) {\n", - " fb.textContent = jaxify(label.dataset.feedback);\n", - " } else {\n", - " fb.textContent = \"Incorrect -- try again.\";\n", - " }\n", - " //console.log(\"Error action\");\n", - " label.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - " }\n", - " else {\n", - " var reset = false;\n", - " var feedback;\n", - " if (label.dataset.correct == \"true\") {\n", - " if (\"feedback\" in label.dataset) {\n", - " feedback = jaxify(label.dataset.feedback);\n", - " } else {\n", - " feedback = \"Correct!\";\n", - " }\n", - " if (label.dataset.answered <= 0) {\n", - " if (fb.dataset.answeredcorrect < 0) {\n", - " fb.dataset.answeredcorrect = 1;\n", - " reset = true;\n", - " } else {\n", - " fb.dataset.answeredcorrect++;\n", - " }\n", - " if (reset) {\n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " child.className = \"MCButton\";\n", - " child.dataset.answered = 0;\n", - " }\n", - " }\n", - " label.classList.add(\"correctButton\");\n", - " label.dataset.answered = 1;\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - "\n", - " }\n", - " } else {\n", - " if (\"feedback\" in label.dataset) {\n", - " feedback = jaxify(label.dataset.feedback);\n", - " } else {\n", - " feedback = \"Incorrect -- try again.\";\n", - " }\n", - " if (fb.dataset.answeredcorrect > 0) {\n", - " fb.dataset.answeredcorrect = -1;\n", - " reset = true;\n", - " } else {\n", - " fb.dataset.answeredcorrect--;\n", - " }\n", - "\n", - " if (reset) {\n", - " for (var i = 0; i < answers.length; i++) {\n", - " var child = answers[i];\n", - " child.className = \"MCButton\";\n", - " child.dataset.answered = 0;\n", - " }\n", - " }\n", - " label.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " //console.log(responsesContainer);\n", - " var response = label.firstChild.innerText;\n", - " if (label.querySelector(\".QuizCode\")){\n", - " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", - " }\n", - " console.log(response);\n", - " //console.log(document.getElementById(\"quizWrap\"+id));\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " if (label.dataset.correct == \"true\") {\n", - " if (typeof(responses[qnum]) == \"object\"){\n", - " if (!responses[qnum].includes(response))\n", - " responses[qnum].push(response);\n", - " } else{\n", - " responses[qnum]= [ response ];\n", - " }\n", - " } else {\n", - " responses[qnum]= response;\n", - " }\n", - " console.log(responses);\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End save responses stuff\n", - "\n", - "\n", - "\n", - " var numcorrect = fb.dataset.numcorrect;\n", - " var answeredcorrect = fb.dataset.answeredcorrect;\n", - " if (answeredcorrect >= 0) {\n", - " fb.textContent = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", - " } else {\n", - " fb.textContent = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", - " }\n", - "\n", - "\n", - " }\n", - "\n", - " if (typeof MathJax != 'undefined') {\n", - " var version = MathJax.version;\n", - " console.log('MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([fb]);\n", - " }\n", - " } else {\n", - " console.log('MathJax not detected');\n", - " }\n", - "\n", - "}\n", - "\n", - "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", - " var shuffled;\n", - " if (shuffle_answers == \"True\") {\n", - " //console.log(shuffle_answers+\" read as true\");\n", - " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", - " } else {\n", - " //console.log(shuffle_answers+\" read as false\");\n", - " shuffled = qa.answers;\n", - " }\n", - "\n", - "\n", - " var num_correct = 0;\n", - "\n", - "\n", - "\n", - " shuffled.forEach((item, index, ans_array) => {\n", - " //console.log(answer);\n", - "\n", - " // Make input element\n", - " var inp = document.createElement(\"input\");\n", - " inp.type = \"radio\";\n", - " inp.id = \"quizo\" + id + index;\n", - " inp.style = \"display:none;\";\n", - " aDiv.append(inp);\n", - "\n", - " //Make label for input element\n", - " var lab = document.createElement(\"label\");\n", - " lab.className = \"MCButton\";\n", - " lab.id = id + '-' + index;\n", - " lab.onclick = check_mc;\n", - " var aSpan = document.createElement('span');\n", - " aSpan.classsName = \"\";\n", - " //qDiv.id=\"quizQn\"+id+index;\n", - " if (\"answer\" in item) {\n", - " aSpan.innerHTML = jaxify(item.answer);\n", - " //aSpan.innerHTML=item.answer;\n", - " }\n", - " lab.append(aSpan);\n", - "\n", - " // Create div for code inside question\n", - " var codeSpan;\n", - " if (\"code\" in item) {\n", - " codeSpan = document.createElement('span');\n", - " codeSpan.id = \"code\" + id + index;\n", - " codeSpan.className = \"QuizCode\";\n", - " var codePre = document.createElement('pre');\n", - " codeSpan.append(codePre);\n", - " var codeCode = document.createElement('code');\n", - " codePre.append(codeCode);\n", - " codeCode.innerHTML = item.code;\n", - " lab.append(codeSpan);\n", - " //console.log(codeSpan);\n", - " }\n", - "\n", - " //lab.textContent=item.answer;\n", - "\n", - " // Set the data attributes for the answer\n", - " lab.setAttribute('data-correct', item.correct);\n", - " if (item.correct) {\n", - " num_correct++;\n", - " }\n", - " if (\"feedback\" in item) {\n", - " lab.setAttribute('data-feedback', item.feedback);\n", - " }\n", - " lab.setAttribute('data-answered', 0);\n", - "\n", - " aDiv.append(lab);\n", - "\n", - " });\n", - "\n", - " if (num_correct > 1) {\n", - " outerqDiv.className = \"ManyChoiceQn\";\n", - " } else {\n", - " outerqDiv.className = \"MultipleChoiceQn\";\n", - " }\n", - "\n", - " return num_correct;\n", - "\n", - "}\n", - "function check_numeric(ths, event) {\n", - "\n", - " if (event.keyCode === 13) {\n", - " ths.blur();\n", - "\n", - " var id = ths.id.split('-')[0];\n", - "\n", - " var submission = ths.value;\n", - " if (submission.indexOf('/') != -1) {\n", - " var sub_parts = submission.split('/');\n", - " //console.log(sub_parts);\n", - " submission = sub_parts[0] / sub_parts[1];\n", - " }\n", - " //console.log(\"Reader entered\", submission);\n", - "\n", - " if (\"precision\" in ths.dataset) {\n", - " var precision = ths.dataset.precision;\n", - " // console.log(\"1:\", submission)\n", - " submission = Math.round((1 * submission + Number.EPSILON) * 10 ** precision) / 10 ** precision;\n", - " // console.log(\"Rounded to \", submission, \" precision=\", precision );\n", - " }\n", - "\n", - "\n", - " //console.log(\"In check_numeric(), id=\"+id);\n", - " //console.log(event.srcElement.id) \n", - " //console.log(event.srcElement.dataset.feedback)\n", - "\n", - " var fb = document.getElementById(\"fb\" + id);\n", - " fb.style.display = \"none\";\n", - " fb.textContent = \"Incorrect -- try again.\";\n", - "\n", - " var answers = JSON.parse(ths.dataset.answers);\n", - " //console.log(answers);\n", - "\n", - " var defaultFB = \"\";\n", - " var correct;\n", - " var done = false;\n", - " answers.every(answer => {\n", - " //console.log(answer.type);\n", - "\n", - " correct = false;\n", - " // if (answer.type==\"value\"){\n", - " if ('value' in answer) {\n", - " if (submission == answer.value) {\n", - " if (\"feedback\" in answer) {\n", - " fb.textContent = jaxify(answer.feedback);\n", - " } else {\n", - " fb.textContent = jaxify(\"Correct\");\n", - " }\n", - " correct = answer.correct;\n", - " //console.log(answer.correct);\n", - " done = true;\n", - " }\n", - " // } else if (answer.type==\"range\") {\n", - " } else if ('range' in answer) {\n", - " //console.log(answer.range);\n", - " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", - " fb.textContent = jaxify(answer.feedback);\n", - " correct = answer.correct;\n", - " //console.log(answer.correct);\n", - " done = true;\n", - " }\n", - " } else if (answer.type == \"default\") {\n", - " defaultFB = answer.feedback;\n", - " }\n", - " if (done) {\n", - " return false; // Break out of loop if this has been marked correct\n", - " } else {\n", - " return true; // Keep looking for case that includes this as a correct answer\n", - " }\n", - " });\n", - "\n", - " if ((!done) && (defaultFB != \"\")) {\n", - " fb.innerHTML = jaxify(defaultFB);\n", - " //console.log(\"Default feedback\", defaultFB);\n", - " }\n", - "\n", - " fb.style.display = \"block\";\n", - " if (correct) {\n", - " ths.className = \"Input-text\";\n", - " ths.classList.add(\"correctButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"correct\");\n", - " } else {\n", - " ths.className = \"Input-text\";\n", - " ths.classList.add(\"incorrectButton\");\n", - " fb.className = \"Feedback\";\n", - " fb.classList.add(\"incorrect\");\n", - " }\n", - "\n", - " // What follows is for the saved responses stuff\n", - " var outerContainer = fb.parentElement.parentElement;\n", - " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", - " if (responsesContainer) {\n", - " console.log(submission);\n", - " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", - " //console.log(\"Question \" + qnum);\n", - " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", - " var responses=JSON.parse(responsesContainer.dataset.responses);\n", - " console.log(responses);\n", - " if (submission == ths.value){\n", - " responses[qnum]= submission;\n", - " } else {\n", - " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", - " }\n", - " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", - " printResponses(responsesContainer);\n", - " }\n", - " // End code to preserve responses\n", - "\n", - " if (typeof MathJax != 'undefined') {\n", - " var version = MathJax.version;\n", - " console.log('MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([fb]);\n", - " }\n", - " } else {\n", - " console.log('MathJax not detected');\n", - " }\n", - " return false;\n", - " }\n", - "\n", - "}\n", - "\n", - "function isValid(el, charC) {\n", - " //console.log(\"Input char: \", charC);\n", - " if (charC == 46) {\n", - " if (el.value.indexOf('.') === -1) {\n", - " return true;\n", - " } else if (el.value.indexOf('/') != -1) {\n", - " var parts = el.value.split('/');\n", - " if (parts[1].indexOf('.') === -1) {\n", - " return true;\n", - " }\n", - " }\n", - " else {\n", - " return false;\n", - " }\n", - " } else if (charC == 47) {\n", - " if (el.value.indexOf('/') === -1) {\n", - " if ((el.value != \"\") && (el.value != \".\")) {\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " return false;\n", - " }\n", - " } else if (charC == 45) {\n", - " var edex = el.value.indexOf('e');\n", - " if (edex == -1) {\n", - " edex = el.value.indexOf('E');\n", - " }\n", - "\n", - " if (el.value == \"\") {\n", - " return true;\n", - " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else if (charC == 101) { // \"e\"\n", - " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", - " // Prev symbol must be digit or decimal point:\n", - " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", - " return true;\n", - " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", - " return true;\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " return false;\n", - " }\n", - " } else {\n", - " if (charC > 31 && (charC < 48 || charC > 57))\n", - " return false;\n", - " }\n", - " return true;\n", - "}\n", - "\n", - "function numeric_keypress(evnt) {\n", - " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", - "\n", - " if (charC == 13) {\n", - " check_numeric(this, evnt);\n", - " } else {\n", - " return isValid(this, charC);\n", - " }\n", - "}\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", - "\n", - "\n", - "\n", - " //console.log(answer);\n", - "\n", - "\n", - " outerqDiv.className = \"NumericQn\";\n", - " aDiv.style.display = 'block';\n", - "\n", - " var lab = document.createElement(\"label\");\n", - " lab.className = \"InpLabel\";\n", - " lab.textContent = \"Type numeric answer here:\";\n", - " aDiv.append(lab);\n", - "\n", - " var inp = document.createElement(\"input\");\n", - " inp.type = \"text\";\n", - " //inp.id=\"input-\"+id;\n", - " inp.id = id + \"-0\";\n", - " inp.className = \"Input-text\";\n", - " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", - " if (\"precision\" in qa) {\n", - " inp.setAttribute('data-precision', qa.precision);\n", - " }\n", - " aDiv.append(inp);\n", - " //console.log(inp);\n", - "\n", - " //inp.addEventListener(\"keypress\", check_numeric);\n", - " //inp.addEventListener(\"keypress\", numeric_keypress);\n", - " /*\n", - " inp.addEventListener(\"keypress\", function(event) {\n", - " return numeric_keypress(this, event);\n", - " }\n", - " );\n", - " */\n", - " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", - " inp.onkeypress = numeric_keypress;\n", - " inp.onpaste = event => false;\n", - "\n", - " inp.addEventListener(\"focus\", function (event) {\n", - " this.value = \"\";\n", - " return false;\n", - " }\n", - " );\n", - "\n", - "\n", - "}\n", - "function jaxify(string) {\n", - " var mystring = string;\n", - "\n", - " var count = 0;\n", - " var loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", - "\n", - " var count2 = 0;\n", - " var loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", - "\n", - " //console.log(loc);\n", - "\n", - " while ((loc >= 0) || (loc2 >= 0)) {\n", - "\n", - " /* Have to replace all the double $$ first with current implementation */\n", - " if (loc2 >= 0) {\n", - " if (count2 % 2 == 0) {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, \"$1\\\\[\");\n", - " } else {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, \"$1\\\\]\");\n", - " }\n", - " count2++;\n", - " } else {\n", - " if (count % 2 == 0) {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, \"$1\\\\(\");\n", - " } else {\n", - " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, \"$1\\\\)\");\n", - " }\n", - " count++;\n", - " }\n", - " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", - " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", - " //console.log(mystring,\", loc:\",loc,\", loc2:\",loc2);\n", - " }\n", - "\n", - " //console.log(mystring);\n", - " return mystring;\n", - "}\n", - "\n", - "\n", - "function show_questions(json, mydiv) {\n", - " console.log('show_questions');\n", - " //var mydiv=document.getElementById(myid);\n", - " var shuffle_questions = mydiv.dataset.shufflequestions;\n", - " var num_questions = mydiv.dataset.numquestions;\n", - " var shuffle_answers = mydiv.dataset.shuffleanswers;\n", - " var max_width = mydiv.dataset.maxwidth;\n", - "\n", - " if (num_questions > json.length) {\n", - " num_questions = json.length;\n", - " }\n", - "\n", - " var questions;\n", - " if ((num_questions < json.length) || (shuffle_questions == \"True\")) {\n", - " //console.log(num_questions+\",\"+json.length);\n", - " questions = getRandomSubarray(json, num_questions);\n", - " } else {\n", - " questions = json;\n", - " }\n", - "\n", - " //console.log(\"SQ: \"+shuffle_questions+\", NQ: \" + num_questions + \", SA: \", shuffle_answers);\n", - "\n", - " // Iterate over questions\n", - " questions.forEach((qa, index, array) => {\n", - " //console.log(qa.question); \n", - "\n", - " var id = makeid(8);\n", - " //console.log(id);\n", - "\n", - "\n", - " // Create Div to contain question and answers\n", - " var iDiv = document.createElement('div');\n", - " //iDiv.id = 'quizWrap' + id + index;\n", - " iDiv.id = 'quizWrap' + id;\n", - " iDiv.className = 'Quiz';\n", - " iDiv.setAttribute('data-qnum', index);\n", - " iDiv.style.maxWidth =max_width+\"px\";\n", - " mydiv.appendChild(iDiv);\n", - " // iDiv.innerHTML=qa.question;\n", - " \n", - " var outerqDiv = document.createElement('div');\n", - " outerqDiv.id = \"OuterquizQn\" + id + index;\n", - " // Create div to contain question part\n", - " var qDiv = document.createElement('div');\n", - " qDiv.id = \"quizQn\" + id + index;\n", - " \n", - " if (qa.question) {\n", - " iDiv.append(outerqDiv);\n", - "\n", - " //qDiv.textContent=qa.question;\n", - " qDiv.innerHTML = jaxify(qa.question);\n", - " outerqDiv.append(qDiv);\n", - " }\n", - "\n", - " // Create div for code inside question\n", - " var codeDiv;\n", - " if (\"code\" in qa) {\n", - " codeDiv = document.createElement('div');\n", - " codeDiv.id = \"code\" + id + index;\n", - " codeDiv.className = \"QuizCode\";\n", - " var codePre = document.createElement('pre');\n", - " codeDiv.append(codePre);\n", - " var codeCode = document.createElement('code');\n", - " codePre.append(codeCode);\n", - " codeCode.innerHTML = qa.code;\n", - " outerqDiv.append(codeDiv);\n", - " //console.log(codeDiv);\n", - " }\n", - "\n", - "\n", - " // Create div to contain answer part\n", - " var aDiv = document.createElement('div');\n", - " aDiv.id = \"quizAns\" + id + index;\n", - " aDiv.className = 'Answer';\n", - " iDiv.append(aDiv);\n", - "\n", - " //console.log(qa.type);\n", - "\n", - " var num_correct;\n", - " if ((qa.type == \"multiple_choice\") || (qa.type == \"many_choice\") ) {\n", - " num_correct = make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id);\n", - " if (\"answer_cols\" in qa) {\n", - " //aDiv.style.gridTemplateColumns = 'auto '.repeat(qa.answer_cols);\n", - " aDiv.style.gridTemplateColumns = 'repeat(' + qa.answer_cols + ', 1fr)';\n", - " }\n", - " } else if (qa.type == \"numeric\") {\n", - " //console.log(\"numeric\");\n", - " make_numeric(qa, outerqDiv, qDiv, aDiv, id);\n", - " }\n", - "\n", - "\n", - " //Make div for feedback\n", - " var fb = document.createElement(\"div\");\n", - " fb.id = \"fb\" + id;\n", - " //fb.style=\"font-size: 20px;text-align:center;\";\n", - " fb.className = \"Feedback\";\n", - " fb.setAttribute(\"data-answeredcorrect\", 0);\n", - " fb.setAttribute(\"data-numcorrect\", num_correct);\n", - " iDiv.append(fb);\n", - "\n", - "\n", - " });\n", - " var preserveResponses = mydiv.dataset.preserveresponses;\n", - " console.log(preserveResponses);\n", - " console.log(preserveResponses == \"true\");\n", - " if (preserveResponses == \"true\") {\n", - " console.log(preserveResponses);\n", - " // Create Div to contain record of answers\n", - " var iDiv = document.createElement('div');\n", - " iDiv.id = 'responses' + mydiv.id;\n", - " iDiv.className = 'JCResponses';\n", - " // Create a place to store responses as an empty array\n", - " iDiv.setAttribute('data-responses', '[]');\n", - "\n", - " // Dummy Text\n", - " iDiv.innerHTML=\"Select your answers and then follow the directions that will appear here.\"\n", - " //iDiv.className = 'Quiz';\n", - " mydiv.appendChild(iDiv);\n", - " }\n", - "//console.log(\"At end of show_questions\");\n", - " if (typeof MathJax != 'undefined') {\n", - " console.log(\"MathJax version\", MathJax.version);\n", - " var version = MathJax.version;\n", - " setTimeout(function(){\n", - " var version = MathJax.version;\n", - " console.log('After sleep, MathJax version', version);\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([mydiv]);\n", - " }\n", - " }, 500);\n", - "if (typeof version == 'undefined') {\n", - " } else\n", - " {\n", - " if (version[0] == \"2\") {\n", - " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", - " } else if (version[0] == \"3\") {\n", - " MathJax.typeset([mydiv]);\n", - " } else {\n", - " console.log(\"MathJax not found\");\n", - " }\n", - " }\n", - " }\n", - " return false;\n", - "}\n", - "/* This is to handle asynchrony issues in loading Jupyter notebooks\n", - " where the quiz has been previously run. The Javascript was generally\n", - " being run before the div was added to the DOM. I tried to do this\n", - " more elegantly using Mutation Observer, but I didn't get it to work.\n", - "\n", - " Someone more knowledgeable could make this better ;-) */\n", - "\n", - " function try_show() {\n", - " if(document.getElementById(\"qukeMLYgLRMA\")) {\n", - " show_questions(questionsqukeMLYgLRMA, qukeMLYgLRMA); \n", - " } else {\n", - " setTimeout(try_show, 200);\n", - " }\n", - " };\n", - " \n", - " {\n", - " // console.log(element);\n", - "\n", - " //console.log(\"qukeMLYgLRMA\");\n", - " // console.log(document.getElementById(\"qukeMLYgLRMA\"));\n", - "\n", - " try_show();\n", - " }\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from jupyterquiz import display_quiz\n", - "display_quiz(\"questions/summary_virtual_environments.json\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.19" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/individual_modules/python_for_data_analysis/images/type-checking.png b/individual_modules/python_for_data_analysis/images/type-checking.png deleted file mode 100644 index 3cbd2955..00000000 Binary files a/individual_modules/python_for_data_analysis/images/type-checking.png and /dev/null differ diff --git a/individual_modules/python_for_data_analysis/questions/summary_language_features.json b/individual_modules/python_for_data_analysis/questions/summary_language_features.json deleted file mode 100644 index 79dfb8d7..00000000 --- a/individual_modules/python_for_data_analysis/questions/summary_language_features.json +++ /dev/null @@ -1,80 +0,0 @@ -[ - { - "question": "What is the purpose of type hints in Python?", - "type": "many_choice", - "answers": [ - { - "answer": "To enforce type checking at runtime", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "To provide hints for IDEs and linters", - "correct": true, - "feedback": "Correct" - }, - { - "answer": "To automatically convert types", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "To improve code execution speed", - "correct": false, - "feedback": "Incorrect" - } - ] - }, - { - "question": "Which of the following best describes a decorator in Python?", - "type": "many_choice", - "answers": [ - { - "answer": "A function that returns another function", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "A function that modifies the behaviour of another function", - "correct": true, - "feedback": "Correct" - }, - { - "answer": "A function that creates new types", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "A function that adds type hints to another function", - "correct": false, - "feedback": "Incorrect" - } - ] - }, - { - "question": "What is the advantage of using generator expression in Python?", - "type": "many_choice", - "answers": [ - { - "answer": "They store all values in memory", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "They provide a concise way to create generators without using def and yield", - "correct": true, - "feedback": "Correct" - }, - { - "answer": "They automatically sort the values", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "They ensure type safety", - "correct": false, - "feedback": "Incorrect" - } - ] - } -] \ No newline at end of file diff --git a/individual_modules/python_for_data_analysis/questions/summary_testing.json b/individual_modules/python_for_data_analysis/questions/summary_testing.json deleted file mode 100644 index e947c6be..00000000 --- a/individual_modules/python_for_data_analysis/questions/summary_testing.json +++ /dev/null @@ -1,80 +0,0 @@ -[ - { - "question": "What is the primary goal of software testing?", - "type": "many_choice", - "answers": [ - { - "answer": "To prove that the code is free of defects", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "To establish that the software behaves as intended", - "correct": true, - "feedback": "Correct" - }, - { - "answer": "To increase the execution speed of the software", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "To ensure the software uses minimal memory", - "correct": false, - "feedback": "Incorrect" - } - ] - }, - { - "question": "Which statement correctly describes the Python assert statement?", - "type": "many_choice", - "answers": [ - { - "answer": "It is used to handle exceptions", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "It verifies that a condition is true and raises an error if it is not", - "correct": true, - "feedback": "Correct" - }, - { - "answer": "It is used for logging messages", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "It is a replacement for the if-else statement", - "correct": false, - "feedback": "Incorrect" - } - ] - }, - { - "question": "What is Test Driven Development (TDD)?", - "type": "many_choice", - "answers": [ - { - "answer": "A software testing technique to find bugs in the code", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "An approach to software design here tests are written before the code", - "correct": true, - "feedback": "Correct" - }, - { - "answer": "A method to enhance the performance of the software", - "correct": false, - "feedback": "Incorrect" - }, - { - "answer": "A tool used for automating software development", - "correct": false, - "feedback": "Incorrect" - } - ] - } -] \ No newline at end of file diff --git a/short_courses/python_environments.ipynb b/short_courses/python_environments.ipynb index eee9336e..b36b1fff 100644 --- a/short_courses/python_environments.ipynb +++ b/short_courses/python_environments.ipynb @@ -70,17 +70,19 @@ "\n", "\n", "```{image} figures/virtual_environments/terminal_icon.png\n", - ":alt: A simple, square icon representing a terminal. The icon is dark gray with a light gray border, and features a white \">_\" symbol in the upper left corner, commonly used to denote a command-line interface.\n", + ":alt: Terminal icon dark grey square with white greater underscore symbol\n", ":width: 300px\n", ":align: center\n", "```\n", + "\n", "This should then open a window such as: \n", "\n", "```{image} figures/virtual_environments/macos_terminal.png\n", - ":alt: A screenshot of a macOS Terminal window. The window title is \"lb788 — -zsh — 80x24.\" The terminal displays the last login time and date: \"Last login: Tue Jan 7 09:26:58 on ttys001.\" Below that, the command prompt is shown as \"(base) lb788@F6XP2KY6WJ~ %\".\n", + ":alt: MacOS terminal window showing command prompt\n", ":width: 1000px\n", ":align: center\n", "```\n", + "\n", "Commands can then be typed directly into the terminal and executed by pressing Return. \n", "\n", "#### Verifying Python Installation\n", @@ -103,17 +105,21 @@ "```bash\n", "python example_script.py\n", "```\n", + "\n", "Calling Python from command line with no other arguments will open the [interpreter](https://docs.python.org/3/tutorial/interpreter.html) (indicated by the `>>>` prompt), where python commands can be executed in an interactive manner. This is rarely used as the code written is cleared when the interpreter is exited, which can be done by executing the `exit()` command: \n", "\n", "```bash\n", ">>> exit()\n", "```\n", + "\n", "#### Checking what is currently installed\n", "Python comes with some default packages installed already, and installation of packages is typically managed through PIP (a recursive acronym for \"pip install package\"). We can see what is currently installed system-wide using:\n", + "\n", "```bash\n", "pip list \n", + "```\n", "\n", - "we _could_ install more packages system-wide using pip, but it is better to do so in a virtual environment.\n", + "we could install more packages system-wide using pip, but it is better to do so in a virtual environment.\n", "\n", "#### Creating an environment \n", "\n", @@ -132,7 +138,7 @@ "Your command line should now have a hint that highlights which environment you are currently in. This is key as it will make it easy at a glance to understand which environment you are in. Your command line should now look something like: \n", "\n", "```{image} figures/virtual_environments/venv_terminal_hint.png\n", - ":alt: A screenshot of a macOS Terminal window showing the activation of a virtual environment. The command source virtual_environment_1/bin/activate is executed. After activation, the prompt changes to (virtual_environment_1) lb788@F6XP2KY6WJ~ %, indicating that the virtual environment named \"virtual_environment_1\" is now active.\n", + ":alt: MacOS terminal with virtual environment active showing environment name in prompt\n", ":width: 1000px\n", ":align: center\n", "```\n", @@ -177,7 +183,7 @@ "\n", "### Windows \n", "\n", - "#### Openign the Command Prompt (CMD) or PowerShell \n", + "#### Opening the Command Prompt (CMD) or PowerShell \n", "\n", "On windows, you can open the **Command Prompt** by:\n", "1. Clicking on the **Start** menu (Windows icon).\n", @@ -185,23 +191,23 @@ "3. Clicking on the **Command Prompt** icon.\n", "\n", "```{image} figures/virtual_environments/Command_Prompt.png\n", - ":alt: A screenshot of a Windows Command Prompt terminal. The window displays \"Microsoft Windows [Version 10.0.19045.4651]\" and a copyright notice. The prompt C:\\Users\\lb1132> is visible, indicating the current user and directory. The background is black with white text.\n", - ":width: 500px\n", + ":alt: Windows command prompt window with black background and white text\n", + ":width: 1000px\n", ":align: center\n", "```\n", "\n", "Alternatively, you may use **PowerShell** by typing **PowerShell** in the Start menu and selecting it.\n", "\n", "```{image} figures/virtual_environments/Powershell.png\n", - ":alt: A screenshot of the Windows search results for \"Windows PowerShell\" under the \"System\" category. The result shows a blue icon with a white \">_\" symbol, and the text \"Windows PowerShell\" in white, with \"System\" in gray below it.\n", - ":width: 500px\n", + ":alt: Windows start menu search showing PowerShell icon and text\n", + ":width: 1000px\n", ":align: center\n", "```\n", "\n", "When the Command Prompt is opened, you will see a window such as:\n", "\n", "```{image} figures/virtual_environments/Command_Prompt_Terminal.png\n", - ":alt: A screenshot of the top right section of a GitHub page, showing a dropdown menu open from an icon that resembles a rocket or a spark. The dropdown menu presents two options: \"Repository\" (with a GitHub octocat icon) and \"Open issue\" (with a lightbulb icon).\n", + ":alt: GitHub page showing dropdown menu with repository and open issue options\n", ":width: 1000px\n", ":align: center\n", "```\n", @@ -210,7 +216,7 @@ "\n", "\n", "```{image} figures/virtual_environments/Powershell_terminal.png\n", - ":alt: A screenshot of a Windows PowerShell terminal. The terminal has a dark blue background with white text. It displays \"Windows PowerShell,\" followed by the copyright notice and a prompt to \"Try the new cross-platform PowerShell.\" The command prompt PS C:\\Users\\lb1132> is shown at the bottom.\n", + ":alt: Windows PowerShell terminal with dark blue background and white text\n", ":width: 1000px\n", ":align: center\n", "```\n", @@ -242,8 +248,10 @@ "```\n", "#### Checking what is currently installed\n", "Python comes with some default packages installed already, and installation of packages is typically managed through PIP (a recursive acronym for \"pip install package\"). We can see what is currently installed system-wide using:\n", + "\n", "```bash\n", "pip list \n", + "```\n", "\n", "we _could_ install more packages system-wide using pip, but it is better to do so in a virtual environment.\n", "\n", @@ -292,6 +300,7 @@ "```shell\n", "deactivate\n", ".\\virtual_environment_1\\Scripts\\activate\n", + "```\n", "\n", "#### Installing a Package \n", "\n", @@ -318,7 +327,6 @@ "There are a number of different options available for environmental and dependency management tools. Below are some of the most widely used tools for this task within Python. The following sections act as a set of signposts to the more comprehensive guides to their use on their official websites. \n", "\n", "### venv\n", - "(covered above)\n", "\n", "[venv Tutorial](https://docs.python.org/3/tutorial/venv.html)\n", "\n", @@ -326,8 +334,9 @@ "\n", "```sh\n", "python -m venv .venv\n", - "```\n", "source .venv/bin/activate\n", + "```\n", + "\n", "\n", "### Virtualenv\n", "\n", @@ -427,7 +436,7 @@ "To update the current environment from a `.yml` file (such as if you have changed the dependencies or versions listed):\n", "\n", "```sh\n", - "conva env update --file environment.yml --prune\n", + "conda env update --file environment.yml --prune\n", "```\n", "\n", "To delete the environment:\n", diff --git a/short_courses/python_language_features.ipynb b/short_courses/python_language_features.ipynb new file mode 100644 index 00000000..82b34b1e --- /dev/null +++ b/short_courses/python_language_features.ipynb @@ -0,0 +1,1065 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "584fd347-192b-4b4f-af38-264c5132e694", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Python Advanced Language Features \n", + "\n", + "```{card}\n", + "\n", + "
        \n", + "\n", + "**Author:** Michael Saunby (GitHub: [msaunby](https://github.com/msaunby))\n", + "\n", + "**License:** Creative Commons Attribution-ShareAlike 4.0 International license ([CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)).\n", + "\n", + "
        \n", + "\n", + "```\n", + "\n", + "## Course Objectives\n", + "- Understand the use and significance of docstrings and type hints in Python for better code documentation and type-checking\n", + "- Utilize introspection to inspect objects, functions, and modules\n", + "- Apply decorators to modify the behaviour of functions and methods\n", + "- Implement useful techniques such as casting and error handling with try-except blocks\n", + "- Create and use lambda functions, list comprehensions, and generator expressions for efficient Python programming\n", + "- Define and use classes in Python, including understanding inheritance and operator overloading\n", + "\n", + "\n", + "\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "5a6452ca", + "metadata": {}, + "source": [ + "## Annotating code\n", + "\n", + "Although Python code is readable by humans, software documentation is always required. This can take many forms and serve many different users.\n", + "\n", + "Good choices of names for modules, variables and functions can help to communicate the purpose, and limitations, of your code. But if you want to share your code, or even come back to it in a few months time, it's worth taking the trouble to include comments and *docstrings*.\n", + "\n", + "### Docstrings\n", + "\n", + "A *docstring* is a string literal that occurs as the first statement in a module, function, class, or method definition. Such a *docstring* becomes the ```__doc__``` special attribute of that object.\n", + "\n", + "All modules should normally have *docstrings*, and all functions and classes exported by a module should also have *docstrings*. Public methods (including the ```__init__``` constructor) should also have *docstrings*. A package may be documented in the module *docstring* of the ```__init__.py``` file in the package directory.\n", + "\n", + "Ref \n", + "\n", + "\n", + "### Type-hints\n", + "\n", + "Here's an example from the Azure Cognitive Services documentation.\n", + "\n", + "```python\n", + "can_read_data(requested_bytes: int, pos: int | None = None) -> bool\n", + "```\n", + "\n", + "Type-hints are not enforced by Python, which can seem strange if you are more familiar with compiled languages.\n", + "\n", + "Also, type-hints are evolving, although they have been available since Python 3.5 (2015) new features have been added since, with a ```type``` statement added in Python 3.12 (2023). " + ] + }, + { + "cell_type": "markdown", + "id": "8abdd292", + "metadata": {}, + "source": [ + "### Docstring Exercise\n", + "\n", + "All standard Python modules have docstrings that describe the module and exported classes and functions. They can be accessed using the built-in ```help()``` function.\n", + "\n", + "Here is an example docstring for the random module. Find the Python code for this module and compare the text with the help() output. \n", + "\n", + "Hint: ```python -m site``` " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b4da2058", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "# help(random)" + ] + }, + { + "cell_type": "markdown", + "id": "6dd38e30", + "metadata": {}, + "source": [ + "## Introspection\n", + "\n", + "The help() function uses introspection to establish the signature of functions.\n", + "\n", + "For example" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5a6ccb58", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function my_add_two in module __main__:\n", + "\n", + "my_add_two(a)\n", + " Add the value 2.0\n", + " \n", + " argument:\n", + " a -- input value\n", + " \n", + " return:\n", + " a + 2.0\n", + "\n" + ] + } + ], + "source": [ + "def my_add_two(a):\n", + " \"\"\"\n", + " Add the value 2.0\n", + "\n", + " argument:\n", + " a -- input value\n", + "\n", + " return:\n", + " a + 2.0\n", + "\n", + " \"\"\"\n", + " return a + 2.0\n", + "\n", + "help(my_add_two)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0cb8fd40", + "metadata": {}, + "outputs": [], + "source": [ + "## Other introspection functions\n", + "\n", + "# help(dir) \n", + "\n", + "# help(type)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "f39f84fa", + "metadata": {}, + "source": [ + "### Typing exercise\n", + "\n", + "Copy the following code fragment into your IDE. If type checking is enabled you should see a warning message.\n", + "\n", + "```python\n", + "word_count: \"dict[str,int]\" = {}\n", + "\n", + "word_count['the'] = 1\n", + "word_count[2] = 'error'\n", + "```\n", + "\n", + "```\n", + "Argument of type \"Literal[2]\" cannot be assigned to parameter \"key\" of type \"str\" in function \"__setitem__\"\n", + " \"Literal[2]\" is incompatible with \"str\" Pylance(reportArgumentType)\n", + "\n", + "Argument of type \"Literal['error']\" cannot be assigned to parameter \"value\" of type \"int\" in function \"__setitem__\"\n", + " \"Literal['error']\" is incompatible with \"int\" Pylance(reportArgumentType)\n", + "\n", + "(variable) word_count: dict[str, int]\n", + "```\n", + "\n", + "\n", + "Without making any changes, try running the code." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6b463bc2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'the': 1, 2: 'error'}\n" + ] + } + ], + "source": [ + "word_count: \"dict[str,int]\" = {}\n", + "\n", + "word_count['the'] = 1\n", + "word_count[2] = 'error'\n", + "\n", + "print(word_count)\n" + ] + }, + { + "cell_type": "markdown", + "id": "441d71df", + "metadata": {}, + "source": [ + "For some projects it can be important that typing, and other, errors are caught before software is deployed.\n", + "Tools such as **mypy** can be used to enforce these rules.\n", + "\n", + "```text\n", + "$ mypy type_hints.py\n", + "type_hints.py:4: error: Invalid index type \"int\" for \"dict[str, int]\"; expected type \"str\" [index]\n", + "type_hints.py:4: error: Incompatible types in assignment (expression has type \"str\", target has type \"int\") [assignment]\n", + "Found 2 errors in 1 file (checked 1 source file)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7f40b3cb", + "metadata": {}, + "source": [ + "## Decorators\n", + "\n", + "Another form of Python code annotation is decorators. Though there is an important distinction to be made. Decorators modify the behavior of code.\n", + "\n", + "Although you can create your own decorators - they are really just functions that call other functions - you will most often encounter them as part of a toolkit or framework, i.e. software that simplifies the creation of certain types of application.\n", + "\n", + "For example, the Flask web framework uses a 'route' decorator to associate functions that you supply with particular URLs.\n", + "\n", + "```python\n", + "@app.route('/')\n", + "def index():\n", + " return render_template('index.html')\n", + "\n", + "@app.route('/about')\n", + "def about():\n", + " return render_template('about.html')\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "86379c8b", + "metadata": {}, + "source": [ + "## Useful techniques\n", + "\n", + "We have already seen dir() and type() for **introspection**.\n", + "\n", + "Another useful technique is:\n", + "\n", + "### Casting\n", + "\n", + "```\n", + "float(\"6.4\")\n", + "list((1,2,3))\n", + "int(\"12\")\n", + "str() # Though print() does this by default\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b4689e96", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mon\n", + "['Mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']\n" + ] + } + ], + "source": [ + "days = ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')\n", + "print(days[0])\n", + "# days[0] = 'Mon'\n", + "days = list(days)\n", + "days[0] = 'Mon'\n", + "print(days)\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "53626169", + "metadata": {}, + "source": [ + "### try-except (and raise)\n", + "\n", + "```python\n", + "my_num = float(input('type a number '))\n", + "```\n", + "\n", + "If the user types text that cannot be converted to a number, then an error, or exception, occurs.\n", + "\n", + "\n", + "```text\n", + "---------------------------------------------------------------------------\n", + "ValueError Traceback (most recent call last)\n", + "Cell In [9], line 1\n", + "----> 1 my_num = float(input('type a number '))\n", + " 2 print(my_num)\n", + "\n", + "ValueError: could not convert string to float: 'one'\n", + "```\n", + "\n", + "These run-time errors can be caught with a 'try-except' clause.\n", + "\n", + "## Exercise\n", + "\n", + "Uncomment the following code.\n", + "\n", + "Add a try-except clause to this code sample." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4e1db602", + "metadata": {}, + "outputs": [], + "source": [ + "# my_num = float(input('type a number '))\n", + "# print(my_num)" + ] + }, + { + "cell_type": "markdown", + "id": "2d82f46e", + "metadata": {}, + "source": [ + "It can be tempting to wrap parts of a program that might fail in a try-except that simply ignores the exception. This can cause many problems, and be a nightmare for others trying to use your code.\n", + "\n", + "For example you might have a loop opening files, some of which contain invalid data. Using a 'bare' or 'catch-all' exception would also mask file permission errors, incorrect paths, and much more.\n", + "\n", + "This doesn't mean you shouldn't write code that continues after an exception, just be aware that it has downsides and take the trouble to take appropriate action, such as logging the error." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8b80a85a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"/var/folders/r7/wblx0jw96hz08nvjz9p3zsgr0000gp/T/ipykernel_2856/2841111012.py\", line 11, in \n", + " always_bad()\n", + " File \"/var/folders/r7/wblx0jw96hz08nvjz9p3zsgr0000gp/T/ipykernel_2856/2841111012.py\", line 8, in always_bad\n", + " raise Exception(\"Never happy\")\n", + "Exception: Never happy\n", + "\n", + "Carry on regardless...\n" + ] + } + ], + "source": [ + "import traceback\n", + "import sys\n", + "\n", + "def always_bad():\n", + " ''' \n", + " Throw an exception.\n", + " '''\n", + " raise Exception(\"Never happy\")\n", + "\n", + "try:\n", + " always_bad()\n", + "except Exception as e:\n", + " traceback.print_exception(type(e), value=e, tb=e.__traceback__, limit=2, file=sys.stdout)\n", + " ## It is easier to use the following in most situations -\n", + " # traceback.print_exc()\n", + "\n", + "print(\"\\nCarry on regardless...\")" + ] + }, + { + "cell_type": "markdown", + "id": "ac925ec1", + "metadata": {}, + "source": [ + "## Lambda functions\n", + "\n", + "A lambda function is a small function that is defined with the ```lambda``` keyword. It can take any number of arguments, but can only have one expression. In other programming languages these are sometimes called anonymous functions." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "10fbc05b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8\n" + ] + } + ], + "source": [ + "add = lambda x, y: x + y\n", + "print(add(5, 3))" + ] + }, + { + "cell_type": "markdown", + "id": "d882da43", + "metadata": {}, + "source": [ + "### Exercise\n", + "\n", + "Write a lambda function that returns the value of its only argument if it is an odd number,or the next odd number if it is an even number." + ] + }, + { + "cell_type": "markdown", + "id": "3290125d", + "metadata": {}, + "source": [ + "## List comprehensions\n", + "\n", + "A list comprehension is shorthand for\n", + "\n", + "```python\n", + " result = []\n", + " for x in some_list:\n", + " result.append[some_function(x)]\n", + "```\n", + "\n", + "The input to the for statement does not need to be a list, any 'iterable' will do. For example a tuple, or a file." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c5dd17b6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']\n", + "['Hello', 'World']\n" + ] + } + ], + "source": [ + "days = [d.upper() for d in ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')]\n", + "print(days)\n", + "\n", + "with open('data/rows.txt') as myFile:\n", + " lines = [x.strip().capitalize() for x in myFile]\n", + "print(lines)" + ] + }, + { + "cell_type": "markdown", + "id": "c4c6592d", + "metadata": {}, + "source": [ + "## Generators\n", + "\n", + "In the previous example we used a list comprehension to capitalise all the lines in a file.\n", + "\n", + "An alternative approach is to use a generator comprehension.\n", + "\n", + "The syntax is almost identical, but the result seems quite different.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5519293c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " at 0x105d0aeb0>\n" + ] + } + ], + "source": [ + "with open('data/rows.txt') as myFile:\n", + " lines = (x.strip().capitalize() for x in myFile)\n", + "print(lines)" + ] + }, + { + "cell_type": "markdown", + "id": "0207fdbc", + "metadata": {}, + "source": [ + "### Question\n", + "\n", + "What do we need to do to get the lines from the generator?" + ] + }, + { + "cell_type": "markdown", + "id": "615acc5d", + "metadata": {}, + "source": [ + "## Consuming generator output\n", + "Although generator output can converted to a list, the intended use is with next()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "57b85794-9225-488f-9cc8-fb2771e146a2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello\n", + "World\n" + ] + } + ], + "source": [ + "with open('data/rows.txt') as data:\n", + " lines = (x.strip().capitalize() for x in data)\n", + " while True:\n", + " line = next(lines, None)\n", + " if line is None:\n", + " break\n", + " print(line)" + ] + }, + { + "cell_type": "markdown", + "id": "2e6c886f", + "metadata": {}, + "source": [ + "## Or better - use a for loop" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "63ec21eb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello\n", + "World\n" + ] + } + ], + "source": [ + "with open('data/rows.txt') as data:\n", + " lines = (x.strip().capitalize() for x in data)\n", + " for line in lines:\n", + " print(line)" + ] + }, + { + "cell_type": "markdown", + "id": "acba2626", + "metadata": {}, + "source": [ + "### yield and next()\n", + "\n", + "Although the generator comprehension and list comprehension syntax is almost identical, it is clear that the generator mechanism is quite different.\n", + "\n", + "Generators in their long form are functions rather than a loop. However rather than a return statement there is a yield statement. Yield may be called as many times as you wish, indeed it could yield - FOREVER\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "0ed2e755", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hello\n", + "None\n", + "0\n", + "1\n", + "2\n", + "1.414\n" + ] + } + ], + "source": [ + "def my_gen():\n", + " yield \"hello\"\n", + " yield None\n", + " # Calling a generator function from within a generator function is allowed\n", + " for i in range(3):\n", + " yield i\n", + " yield 1.414\n", + "\n", + "for item in my_gen():\n", + " print(item)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "9a8d1051", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"/var/folders/r7/wblx0jw96hz08nvjz9p3zsgr0000gp/T/ipykernel_2856/679916601.py\", line 5, in \n", + " print(len(data))\n", + "TypeError: object of type '_io.TextIOWrapper' has no len()\n" + ] + } + ], + "source": [ + "# Why does this fail?\n", + "# How could we fix it?\n", + "try:\n", + " with open('data/rows.txt') as data:\n", + " print(len(data))\n", + "except Exception as e:\n", + " traceback.print_exc()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "9a084827", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['hello\\n', 'world\\n']\n" + ] + } + ], + "source": [ + "# Why does this work?\n", + "# What is happening here that is different?\n", + "with open('data/rows.txt') as data:\n", + " print(list(data))" + ] + }, + { + "cell_type": "markdown", + "id": "703c575f", + "metadata": {}, + "source": [ + "## Classes\n", + "\n", + "This course module does not cover Object Oriented Programming, or Object Oriented Design as those are both big topics. However, it is important to know that as well as the procedural style of programming the Python language also supports object orientation.\n", + "\n", + "Key concepts in OOP are - objects, classes, properties, methods, constructors and inheritance. So we will take a quick look at those.\n", + "\n", + "### classes and objects\n", + "\n", + "A class is a user defined data type.\n", + "\n", + "An object is an instance of a class.\n", + "\n", + "Rather than consider the finer details of what this means, it is easier to take a look at already familiar built-in types.\n", + "\n", + "### the string class\n", + "\n", + "Typically we create a string with\n", + "\n", + "```\n", + "my_string = \"Hello world!\"\n", + "```\n", + "\n", + "This is actually shorthand for\n", + "```\n", + "my_string = str(\"Hello world!\")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "a73b9b72", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "my_string = 'Hello'\n", + "print(type(my_string))" + ] + }, + { + "cell_type": "markdown", + "id": "ca41580a", + "metadata": {}, + "source": [ + "### The file class\n", + "\n", + "Whenever we open a file in Python, for reading or writing, we are using a file object. However, this is a little more complicated. So let's check the type of a file object. " + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "37e9a8cd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n" + ] + } + ], + "source": [ + "my_file = open('data/rows.txt')\n", + "print(type(my_file))\n", + "my_file.close()\n", + "\n", + "my_binary_file = open('data/rows.txt', 'rb')\n", + "print(type(my_binary_file))\n", + "my_binary_file.close()\n" + ] + }, + { + "cell_type": "markdown", + "id": "27d89181", + "metadata": {}, + "source": [ + "This might seem surprising. On reflection, from mistakes we have all made, and learned from, when working with files - it is to be expected that there are different types of file object for text and binary files, and also for reading and writing.\n", + "\n", + "So here we see the principle benefit of object oriented programming - information hiding. Generally coders want a simple and consistent interface to things that are conceptually similar, such as files." + ] + }, + { + "cell_type": "markdown", + "id": "5fae2074", + "metadata": {}, + "source": [ + "## Defining our own classes\n", + "\n", + "Typical reasons for defining new classes are -\n", + "\n", + "* Encapsulation (information hiding)\n", + "\n", + "* Easier code re-use\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "d16a9a88", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MICHAEL\n", + "michael\n", + "MICHAEL\n" + ] + } + ], + "source": [ + "my_name = \"michael\"\n", + "a = my_name.upper()\n", + "print(a)\n", + "print(my_name)\n", + "\n", + "# or\n", + "my_name = \"michael\"\n", + "my_name = my_name.upper()\n", + "print(my_name)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "120164a6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MICHAEL\n" + ] + } + ], + "source": [ + "# or\n", + "def upper(input:str):\n", + " return input.upper()\n", + "\n", + "my_name = upper(\"michael\")\n", + "print(my_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "d35c6ac7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MICHAEL\n", + "\n" + ] + } + ], + "source": [ + "class Upper(str):\n", + " def __new__(cls, text=\"\"):\n", + " return super().__new__(cls, text.upper())\n", + "\n", + "my_name = Upper(\"michael\")\n", + "print(my_name)\n", + "print(type(my_name))\n" + ] + }, + { + "cell_type": "markdown", + "id": "e83fbbe4", + "metadata": {}, + "source": [ + "## Features of our new class\n", + "\n", + "Our Upper() class is an example of code re-use. We have re-used the str() class to provide the implementation of Upper(). So we gain all the features of str().\n", + "\n", + "Let's see what it can do:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "60e18cd3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " HELLO\n", + " hello\n", + " HELLO\n" + ] + } + ], + "source": [ + "class Upper(str):\n", + " def __new__(cls, text=\"\"):\n", + " return super().__new__(cls, text.upper())\n", + "\n", + "## Alternative (factory) implementation \n", + "class UpperFactory(str):\n", + " def __init__(self, text=\"\"):\n", + " str.__init__(text.upper())\n", + "\n", + "\n", + "hello = Upper(\"hello\")\n", + "print(type(hello), hello)\n", + "\n", + "lh = hello.lower()\n", + "print(type(lh), lh)\n", + "\n", + "uh = hello.upper()\n", + "print(type(uh), uh)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "f5d1f988", + "metadata": {}, + "source": [ + "## Pros and cons of inheritance\n", + "\n", + "* We get all the methods of the super-class - GOOD, simplifies implementation\n", + "\n", + "* We get all the methods of the super-class - BAD, methods not adapted to features of our new class\n", + "\n", + "For now, just note that designing and implementing well behaved classes is not trivial, and filling your code with custom classes might not be what others (or future you) will appreciate. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "102b87f2", + "metadata": {}, + "source": [ + "## Operator overloading\n", + "\n", + "Although a separate concept to classes, and object orientation, a common feature of object oriented programming languages is operator overloading. This is something that you will already be familiar with from Python strings." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "9407e7f7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hello world\n", + "___________\n" + ] + } + ], + "source": [ + "print('hello' + ' ' + 'world')\n", + "print(11 * '_')\n" + ] + }, + { + "cell_type": "markdown", + "id": "843c65af", + "metadata": {}, + "source": [ + "Let's implement something similar for our Upper class -" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "63921d3b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HELLO_WORLD\n" + ] + } + ], + "source": [ + "class Upper(str):\n", + " def __new__(cls, text=\"\"):\n", + " return super().__new__(cls, text.upper())\n", + " \n", + " def __add__(self, x):\n", + " return Upper(str(self) + \"_\" + x)\n", + "\n", + "print(Upper(\"hello\") + \"world\")" + ] + }, + { + "cell_type": "markdown", + "id": "dcbf16d4", + "metadata": {}, + "source": [ + "How about this -" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "0c7eff02", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "helloWORLD\n" + ] + } + ], + "source": [ + "print(\"hello\" + Upper(\"world\"))" + ] + }, + { + "cell_type": "markdown", + "id": "69c8f60e", + "metadata": {}, + "source": [ + "Oh no!!\n", + "\n", + "### Exercise\n", + "\n", + "Fix it!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/short_courses/python_testing.ipynb b/short_courses/python_testing.ipynb new file mode 100644 index 00000000..486e40e1 --- /dev/null +++ b/short_courses/python_testing.ipynb @@ -0,0 +1,680 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Python Checking and Testing Code\n", + "\n", + "\n", + "```{card}\n", + "\n", + "
        \n", + "\n", + "**Author:** Michael Saunby (GitHub: [msaunby](https://github.com/msaunby))\n", + "\n", + "**License:** Creative Commons Attribution-ShareAlike 4.0 International license ([CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)).\n", + "\n", + "
        \n", + "\n", + "```\n", + "\n", + "\n", + "\n", + "\n", + "## Course Objectives\n", + "- Understand the importance and limitations of software testing\n", + "- Recognize different types of testing such as system-level and defect testing\n", + "- Learn how to implement and interpret assert statement in Python code\n", + "- Understand when and why to use assert statements\n", + "- Understand the concept of unit testing and Test Driven Development (TDD)\n", + "- Write and execute unit tests using the unit test module\n", + "- Learn the purpose and implementation of fixtures and mocks in testing\n", + "- Apply these concepts to test code that depends on external resources\n", + "- Understand the role of code linting and type checking\n", + "- Use tools like Flake8 and my[py] to ensure code quality and adherence to style guidelines" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Testing software\n", + "\n", + "Although we cannot prove our code is free of defects or bugs, we can, and should, establish that it behaves as intended.\n", + "\n", + "\n", + "\n", + "### System level testing\n", + "\n", + "Once we have completed our software, we should ensure that it works as intended. This is referred to as validation testing. Typically, this will involve taking a sample of input data and ensuring that the output of our software is as expected for the given input.\n", + "\n", + "This *validation* testing will tell us if the overall system runs and produces valid results. Such tests should be repeated when changes are made to the software to ensure the changes have not introduced errors.\n", + "\n", + "Changes that can impact your software are diverse and include:\n", + "\n", + " * new Python language releases\n", + " \n", + " * upgrades to imported libraries\n", + " \n", + " * operating system updates.\n", + "\n", + "### Defect testing\n", + "\n", + "In a research environment it is often the case that there is no explicit specification for the software we create.\n", + "\n", + "By specification we mean something like:\n", + "\n", + "* Written statement of user requirements - typically \"user stories\"\n", + "\n", + "* Functional requirements - e.g what file formats are to be supported\n", + "\n", + "* Non-functional requirements - e.g. subject data must be encrypted.\n", + "\n", + "### Discussion\n", + "\n", + "If you don't have a specification for your software, how might you establish suitable tests to find and resolve defects?\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Assert statement\n", + "\n", + "The built-in Python assert statement looks like this -" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Try modifying this code to deliberately fail the assert statements\n", + "\n", + "def my_add_two(a):\n", + " return a + 2.0\n", + "\n", + "assert my_add_two(1) == 3\n", + "# Better to include a message in case of failure\n", + "assert my_add_two(3) == 5, f\"my_add_two(3) failed with {my_add_two(3)}, expected 5\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## When to use assert\n", + "\n", + "```assert``` should never be used to modify control flow.\n", + "\n", + "Assertions allow you to verify that parts of your program are correct, but are only applied if the internal constant ```__debug__``` is ```True```. Although ```__debug__``` is usually set to True, it is not guaranteed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "## This is approximate what the assert statement does \n", + "\n", + "def my_assert(condition, message):\n", + " if __debug__ and not condition:\n", + " raise AssertionError(message)\n", + "\n", + "my_assert(my_add_two(1) == 3, \"my_add_two(1) failed\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "### Why might we want different behaviour from our assert statements?\n", + "\n", + "### What would you want your assert statements to do?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Unit-tests\n", + "\n", + "Unit-tests are small tests that test the behaviours of our functions and classes.\n", + "\n", + "Unit-tests are typically run within a testing framework or test-runner that automates testing, often inside our IDE.\n", + "\n", + "### Test Driven Development (TDD)\n", + "\n", + "TDD is an approach to software design, it is not software testing. TDD uses unit-tests to create a software design, especially when the design is created incrementally, as with Agile.\n", + "\n", + "### Refactoring\n", + "\n", + "Whether or not you adopt TDD, refactoring - changing the implementation of your code without changing its behaviour, is something that you are certain to do. If only to remove print statements, or change the names of variables.\n", + "\n", + "Refactoring code without appropriate tests can easily introduce new errors." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## unittest\n", + "\n", + "Python 3 distributions include the unittest module. See https://docs.python.org/3/library/unittest.html\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import unittest\n", + "\n", + "class TestMyAddTwo(unittest.TestCase):\n", + " def test_my_add_two(self):\n", + " self.assertEqual(my_add_two(1), 3)\n", + " def test_my_add_two_3(self):\n", + " self.assertEqual(my_add_two(3), 5)\n", + "\n", + "# unittest.main(argv=[''], exit=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Fixtures and mocks\n", + "\n", + "Ideally each unit of code should be tested independently.\n", + "\n", + "### Why is this?\n", + "\n", + "However, there are situations where testing code might require data read from a file or a database connection. If only one test requires this external data, then opening the file and reading the data will be part of the test. If several tests require this data, then we use a fixture. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Module level fixture setup and teardown\n", + "def setUpModule():\n", + " global sample_data\n", + " sample_data = open(\"data/rows.txt\", \"r\")\n", + "\n", + "def tearDownModule():\n", + " sample_data.close()\n", + "\n", + "# unittest.main(argv=[''], exit=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Class level fixture setup and teardown\n", + "class TestMyAddTwo(unittest.TestCase):\n", + " @classmethod\n", + " def setUpClass(cls):\n", + " cls.sample_data = open(\"data/rows.txt\", \"r\")\n", + "\n", + " @classmethod\n", + " def tearDownClass(cls):\n", + " cls.sample_data.close()\n", + "\n", + " def test_file_parsing(self):\n", + " for line in self.sample_data:\n", + " # Do something with the line\n", + " pass\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Mocks\n", + "\n", + "Mock and MagicMock objects create all attributes and methods as you access them and store details of how they have been used. You can configure them to specify return values or limit what attributes are available.\n", + "\n", + "See https://docs.python.org/3/library/unittest.mock.html\n", + "\n", + "\n", + "### How can we test a function that does not return a value?\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 2, 3]\n", + "\n" + ] + } + ], + "source": [ + "\n", + "def show_results():\n", + " arr = [1, 2, 3]\n", + " print(arr)\n", + " print()\n", + "\n", + "show_results()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Here is a possible test\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from unittest.mock import MagicMock\n", + "\n", + "class TestShowResults(unittest.TestCase):\n", + " def setUp(self):\n", + " global print\n", + " print = MagicMock()\n", + " def tearDown(self):\n", + " global print\n", + " print = __builtins__.print\n", + " def test_show_results(self):\n", + " show_results()\n", + " self.assertEqual(print.call_count, 2)\n", + "\n", + "# unittest.main(argv=[''], exit=False)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## How does this test work?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## 'Linting' code with Flake8 and my[py]\n", + "\n", + "There are various tools that can analyse Python code and suggest fixes or improvements without running the code.\n", + "\n", + "These are 'static code checkers' or 'linters' - because they help you remove fluff!\n", + "\n", + "### my[py]\n", + "We saw my[py] briefly before. It is used to find mistakes in type hints, and can even be used to enforce type hints if desired.\n", + "\n", + "https://mypy.readthedocs.io/en/stable/getting_started.html\n", + "\n", + "### Flake8\n", + "Flake8 runs a variety of checks on your Python scripts, and can be used with IDEs such as VS Code to help you write clearer, more readable, code. The, optional, but highly recommended style guide for Python is PEP 8.\n", + "\n", + "https://peps.python.org/pep-0008/\n", + "\n", + "https://flake8.pycqa.org/en/latest/index.html\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Test coverage\n", + "\n", + "### Coverage.py works in three phases:\n", + "\n", + "* Execution: Coverage.py runs your code, and monitors it to see what lines were executed.\n", + "\n", + "* Analysis: Coverage.py examines your code to determine what lines could have run.\n", + "\n", + "* Reporting: Coverage.py combines the results of execution and analysis to produce a coverage number and an indication of missing execution.\n", + "\n", + "See https://coverage.readthedocs.io/en/7.5.3/api.html" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Pytest\n", + "\n", + "### Introduction to pytest\n", + "\n", + "Pytest is another popular tool for testing in Python which makes it easier to write and run tests. \n", + "\n", + "Pytest uses file and function naming conventions to discover test. You will rarely need to run a test directly as the framework will find and run tests for you when you modify your code.\n", + "\n", + "Pytest is a package that you install into your environment from conda or PyPI. For example:\n", + "\n", + "```sh\n", + "pip install pytest\n", + "```\n", + "\n", + "You should then create a directory containing your tests called `tests`.\n", + "\n", + "```sh\n", + "mkdir tests\n", + "```\n", + "\n", + "Within tests, you can create Python scripts containing tests - e.g. `example_test.py`.\n", + "\n", + "### Example test: comparing csv files\n", + "\n", + "There are a wide variety of applications for testing. Below is an example of a test where we are running some code to generate a `.csv` file, and then confirming if the results are as expected.\n", + "\n", + "This could come in handy if you have produced code for a model, and are concerned that others running on the model on a different machine could be getting slightly different results. \n", + "\n", + "In this example, we start the `.py` file by importing:\n", + "\n", + "* `pytest` (to run the tests)\n", + "* `pandas` (to manage the csv files)\n", + "* Our model (imagining a scenario where we have a file `model_code` inside a folder `scripts/`, which is a sister folder to `tests/`)\n", + "* `tempfile` (to save our model results to a temporary directory)\n", + "\n", + "```python\n", + "import pytest\n", + "import pandas as pd\n", + "from scripts import model_code\n", + "import tempfile\n", + "```\n", + "\n", + "We then provide file paths at the start of our script:\n", + "\n", + "```python\n", + "EXP_FOLDER = 'exp_results'\n", + "TEMP_FOLDER = tempfile.mkdtemp()\n", + "```\n", + "\n", + "Assuming we might be running the model with two different parameters (so producing two `.csv` files, and comparing each of those), we can use `parametrise` to run the same test with two different inputs. First, we can define our parameters for the model:\n", + "\n", + "```python\n", + "parameters = [\n", + " {\n", + " 'arrivals': 100,\n", + " 'file': 'result100.csv'\n", + " },\n", + " {\n", + " 'arrivals': 150,\n", + " 'file': 'result150.csv'\n", + " }\n", + "]\n", + "```\n", + "\n", + "For the file paths, we should set these up as fixtures:\n", + "\n", + "```python\n", + "@pytest.fixture\n", + "def exp_folder():\n", + " return EXP_FOLDER\n", + "\n", + "\n", + "@pytest.fixture\n", + "def temp_folder():\n", + " return TEMP_FOLDER\n", + "```\n", + "\n", + "We can then write our test function, and use the file names from parameters as the ID for each test:\n", + "\n", + "```python\n", + "@pytest.mark.parametrize('param', parameters,\n", + " ids=[d['file'] for d in parameters])\n", + "def test_equal_df(param, temp_folder, exp_folder):\n", + " '''\n", + " Test that model results are consistent with the expected\n", + " results (which are saved in the EXP_FOLDER)\n", + " '''\n", + " # Run the model (assuming the function has inputs for our\n", + " # parameter dictionary and for a save location for the .csv file)\n", + " model_code.main(**param, temp_folder)\n", + "\n", + " # Import the test and expected results (we can use the filename from the\n", + " # parameter dictionary, and then the folder name)\n", + " test_result = import_xls(temp_folder, param['file'])\n", + " exp_result = import_xls(exp_folder, param['file'])\n", + "\n", + " # Check that the dataframes are equal\n", + " pd.testing.assert_frame_equal(test_result, exp_result)\n", + "```\n", + "\n", + "With our `.py` file now complete, we can run our tests from the terminal. Ensuring you are located in the parent folder to `tests/`, run the command:\n", + "\n", + "```sh\n", + "pytest\n", + "```\n", + "\n", + "If your tests take a long time to run, you may want to explore parallelising them. You can install `pytest-xdist`, which is a package that parallelises your pytests using multiple CPUs. With this package installed, you can run the command:\n", + "\n", + "```sh\n", + "pytest -n auto\n", + "```\n", + "\n", + "### Coverage\n", + "\n", + "https://pypi.org/project/pytest-cov/\n", + "\n", + "### pytest-notebook\n", + "\n", + "See https://pytest-notebook.readthedocs.io/en/latest/" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Resources\n", + "\n", + "See the testing section of https://alan-turing-institute.github.io/rse-course/html/module01_introduction_to_python/index.html" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Testing Practical Exercise\n", + "\n", + "Python 3 distributions include the unittest module. See https://docs.python.org/3/library/unittest.html\n", + "\n", + "Here is the example included in the Python documentation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "\n", + "## Exercise 1\n", + "\n", + "Using the above as a template, create a test class for the Upper class we used earlier.\n", + "\n", + "```python\n", + "class Upper(str):\n", + " def __new__(cls, text=\"\"):\n", + " return super().__new__(cls, text.upper())\n", + "```\n", + "\n", + "### Important\n", + "\n", + "What should (and can) be tested?\n", + "\n", + "See https://docs.python.org/3/library/unittest.html\n", + "\n", + "\n", + "## Exercise 2\n", + "\n", + "Design a new capability for the class using TDD.\n", + "\n", + "Here are some suggestions -\n", + "\n", + "* Do not allow strings without at least one letter\n", + "\n", + "* Only allow strings that begin with a letter\n", + "\n", + "* Limit the length of the string to 10 characters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}