diff --git a/README.md b/README.md index b154a8cd3cefde46a2a820e78077f5aa7916a191..bae14ec4305522c6b1d3617fa2b776c5040f94f3 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,3 @@ # cs220-f23-projects - - -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin https://git.doit.wisc.edu/cdis/cs/courses/cs220/cs220-f23-projects.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](https://git.doit.wisc.edu/cdis/cs/courses/cs220/cs220-f23-projects/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +UW-Madison course CS220/CS319 project repository for Fall'23 semester. diff --git a/lab-p1/README.md b/lab-p1/README.md new file mode 100644 index 0000000000000000000000000000000000000000..35249c74735273b173bb1dd541007dea168973d8 --- /dev/null +++ b/lab-p1/README.md @@ -0,0 +1,439 @@ +# Lab-P1: Running Programs + + +Welcome to your first lab! This semester, you're going to learn how to write your own Python code. But for this lab, you're just going to practice running some Python programs we give you. + +**Important:** Before going to the lab, install Anaconda with Python 3.11 on your laptop. We've created videos showing how to do this on [Windows](https://youtu.be/jUGTRDXJBtI) and [Mac](https://youtu.be/tL8reqOXwjE). Please make sure that **all** checkboxes are checked during the Windows installation. + +**If you have already installed Python or Anaconda before this semester, please uninstall and reinstall, following the instructions in these videos. Please do not upgrade your Python version anytime during the semester. We expect you to retain Python 3.11 throughout this semester.** + +As a reminder, labs are a part of your grade and are meant to be guided by a TA/peer mentor. Thus, in-person attendance is **mandatory** (unless you are a CS319 student or an AmFam student). + +The lab is divided into **Segments**. Each segment will introduce a new concept to you, and will build on concepts introduced in earlier segments. If you ever get stuck on any segment, please feel free to reach out to your TA/peer mentor. + +This lab also contains **Tasks**. Pay attention to these, as you'll be expected to be able to do similar tasks on your own moving forward. + +## Learning Objectives + +After completing this lab, you will be able to... + +* download files from GitLab, +* use basic terminal commands, +* run a Python script, +* understand and use absolute and relative paths, +* create and run a Jupyter Notebook, +* download and run a Jupyter Notebook. + +--- + +## Commonly used Terminal/PowerShell commands + +Here are a list of Terminal/PowerShell commands that you shall be learning more about, in this lab. You can refer to this section if you need to be reminded of what the different commands do. + +|Command|Function|Example| +|---|---|---| +|`pwd`|displays path of current directory|`pwd`| +|`ls`|lists files in current directory|`ls`| +|`cd`|changes the current directory|`cd "C:\Users\myname\Documents\cs220\lab-p1"`| +|`mkdir`|makes a new subdirectory inside the current directory|`mkdir lab-p2`| +|`cat`|displays the contents of a file|`cat hello.py`| +|`python`/`python3`|executes Python script|`python hello.py`| +|`jupyter notebook`|open Jupyter notebook from current directory|`jupyter notebook`| + +--- + +## Segment 1: Download Your First Program + +The first thing you're going to need to decide is where to keep your work this semester. + +### Task 1.1: Create the folders for this lab + +Create a folder named `cs220` under `Documents`. Then, inside the new `cs220` folder, create a sub-folder called `lab-p1` and use it for all your files related to this lab. + +To find `Documents`... + +#### Windows Users + +Open `File Explorer` and select `Documents`: + + <img src="images/windows-documents.png" width="400"> + +#### Mac Users + +Open `Finder` and select `Documents`: + + <img src="images/mac-documents.png" width="200"> + +--- + +Before we begin the next task, let's learn about the term **file extension**. When you have a file named `somefile.some_extension`, the part after the `.` is called the **file extension**. A `.py` extension refers to a Python program. Other examples of a file extensions include: `.txt` (text file), `.docx` (document file), and `.xlsx` (spreadsheet file). + +To show file extensions, you will need to... + +#### Mac Users + +1. Open `Finder` window and click on `Finder`(menu) > `Preferences`: + + <img src="images/finder_preferences.png" width="500"> + +2. Navigate to `Advanced` and enable `Show all filename extensions`: + + <img src="images/finder_preferences_settings.png" width="500"> + +#### Windows Users + +1. In your `File Explorer`, Click `View` > `Show` > `File name extensions`: + + <img src="images/windows_file_ext.png" width="500"> + + +### Task 1.2: Download the file `hello.py` to your `lab-p1` folder + +**We strongly recommend using Google Chrome browser, throughout this semester. Other browsers might add unpredictable extensions to your files and they might not work as intended.** + +At the top of this page, you'll see a list of files, something like this: + +<img src="images/github.png" width="800"> + +Downloading files from GitLab (the site hosting this document) is a little tricky for those new to it. Try this: + +1. Right-click on `hello.py`. +2. Choose `Save Link As...` (or similar). +3. Navigate to your `lab-p1` folder in the pop-up. +4. Ensure that you download file with the **proper extension**: + * Windows users: ensure that `Save as type` is **not** `Text Document`, but is either `Python File` or `All Files`. + + <p align="center"><img src="images/save_windows.png" width="500"></p> + + * MAC users: ensure that you have followed the above Finder settings steps before this step. Replace the `.txt` extension with `.py`. + + <p align="center"><img src="images/save.png" width="500"></p> + <p align="center"><img src="images/save_mac.png" width="500"></p> + + * You should be following this process for **all** downloads from GitLab. Here is another example: if you are downloading a `.ipynb` file, you should make sure the extension is exactly `.ipynb` and not `.txt.ipynb` or `.txt` or `.ipynb.txt`. +5. Hit Enter. + +Alright, we've downloaded our first Python file! *Or have we?* + +### Task 1.3: Open a terminal emulator aka a Terminal. + +**Windows**: + +1. Hit the Windows logo key on your keyboard. +2. Type `powershell`. +3. Open `Windows PowerShell` (be careful, do **not** choose the ones that say `ISE` or `x86`). +4. Pin this application to Start bar. If you don't know how to do this, please do a Google Search. + +**Mac**: + +1. Open Finder. +2. Click `Applications`. +3. Open `Utilities`. +4. Double-click `Terminal.app`. +5. Pin this application to Dock. If you don't know how to do this, please do a Google Search. + +We're going to talk more about terminals later - don't worry too much about the details yet. + +### Task 1.4: Copy the pathname of your `lab-p1` folder. + +1. Open your `cs220` folder in either File Explorer or Finder. +2. Copy the pathname of `lab-p1` using either these [Windows directions](https://www.pcworld.com/article/468873/windows-tips-copy-a-file-path-show-or-hide-extensions.html) or [Mac directions](https://osxdaily.com/2015/11/05/copy-file-path-name-text-mac-os-x-finder/). +3. Paste the pathname of `lab-p1` in your notes somewhere. + +### Task 1.5: Navigate to the `lab-p1` directory. + +**Directory** is just a fancy word for **folder**. You can use them interchangeably. The command to change directories is `cd`, which stands for **C**hange **D**irectory. The directions are the same for Mac and Windows. Type the following in the terminal (replace `LAB-P1-PATH` with the pathname of `lab-p1`, which you copied above; keep the quotes around the pathname, though) and hit Enter: + +``` +cd "LAB-P1-PATH" +``` + +So if my `lab-p1` pathname is "C:\Users\myname\Documents\cs220\lab-p1", I'd type: + +``` +cd "C:\Users\myname\Documents\cs220\lab-p1" +``` + +If you're using a Mac, your slashes will point the other way. This isn't a big deal... for now. + +``` +cd "/Users/myname/Documents/cs220/lab-p1" +``` + +**WARNING:** Remember to paste the pathname **inside quotes** (i.e., `cd "LAB-P1-PATH"` instead of `cd LAB-P1-PATH`). +Otherwise, you might get an error that says you have too many arguments. + +### Task 1.6: Use `cat` to inspect hello.py. + +Just type `cat hello.py` in your terminal and press Enter. As one might guess, `cat` command enables you to look into the contents of a file. + +Wooooooooah! That's a lot of stuff. Wait a minute... we've been bamboozled! This is an HTML file! **Delete this impostor `hello.py` using your file explorer!** + +So, this is the incorrect way of downloading files from GitLab, when you encounter any error while running your program which lists `\<!DOCTYPE html\>`, then you followed these wrong steps to download the program file. + +### Task 1.7: Correctly Download a Python file + +After noticing our mistake, let's download `hello.py` the **correct way**: + +1. At the top of this page, Left-click on `hello.py`. +2. Right-click on the `Open raw` (<img src="images/raw_gitlab_button.png" width="30">) button. In Chrome/Brave, right-clicking the `Open raw` button looks like this: + +<img src="images/raw_gitlab.png" width="300"> + +3. Choose `Save Link As...` (or similar). +4. Navigate to your `lab-p1` folder in the pop-up. +5. Ensure that you download file with proper extension: + * Windows users: ensure that `Save as type` is **not** `Text Document`, but is either `Python File` or `All Files`. + * MAC users: replace the `.txt` extension with `.py`. +6. Press Enter. +7. For image reference, please go back to Task 1.2. +8. Type `cat hello.py` in your terminal and press Enter. If you downloaded the file correctly, you should now see: + +```python +print("Hello, World!") +``` + +**Warning**: Verify that your file is saved as `hello.py` and not `hello.txt` or `hello.py.txt`. Reminder: we recommend you use the Chrome browser to avoid issues (other browsers might automatically change extensions of files to `.txt` when downloaded). + +------------------------------ + +## Segment 2: Run Your First Program + +A **path** is how a computer knows where a file is, and a **pathname** describes a path. Put simply, a pathname is a more complete name for a file or folder. + +`hello.py` is just a file name. `C:\Users\myname\Documents\cs220\lab-p1\hello.py` and `/Users/myname/Documents/cs220/lab-p1/hello.py` are pathnames. The former is a pathname on Windows and the latter on a MAC or Linux. Windows and MAC or Linux use a different path separator, that is `\` (Windows) and `/` on MAC or Linux. Eventually, you will learn how to handle the `\` (Windows) versus `/` difference in a generic manner. + +### Task 2.1: Use a Shell command to list the contents of the `lab-p1` folder + +The Shell command we will use is `ls` (LS, but lowercase) to **L**i**S**t and inspect the contents of the `lab-p1` folder. + +Type `ls` in the terminal and press Enter. If you've done everything correctly so far, you should see the `hello.py` file that you downloaded in Task 2 listed. + +### Task 2.2: Run `hello.py`. + +Type `python hello.py` and press Enter. Note that you may need to instead type `python3 hello.py`, depending on your setup (on most Mac setups, you'll be typing python3). If everything is working correctly, you should see the following message printed: + +``` +Hello, World! +``` + +Congrats, you just executed your first Python program! + +------------------------------ + +## Segment 3: Use Terminal to run Shell Commands + +You should have a terminal open that's working in your `lab-p1` directory. You have already tried examples for three shell commands, which are: `cat <filename>`, `cd <pathname>`, `ls`. Shell is a program that is run by the terminal application. Shell commands are commands that you type within your terminal window. + +Why learn about terminal? + +- Typing on keyboard is way faster than using a mouse (also good for your wrist) +- Mastering Terminal will enable you to work on a remote or cloud-based computer + +Let's learn more about shell commands. + +### Task 3.1: Use `pwd` (Print Working Directory) to see your working directory. + +In your Terminal, simply type `pwd` and press Enter. + +You should see that the output looks something like `C:\Users\myname\Documents\cs220\lab-p1`. +The important part is the end: `cs220\lab-p1`. + +Let's learn about two different types of paths: + +1. Absolute paths: + * full path name to your file or folder. + * there can be only one absolute path to a single file or a single folder. + * example: `C:\Users\myname\Documents\cs220\lab-p1\hello.py` and `/Users/myname/Documents/cs220/lab-p1/hello.py`. +2. Relative paths: + * specifying the part of the path relative to current working directory (CWD). + * example: assuming you are inside the `Documents` directory (that is, you have executed `cd "C:\Users\myname\Documents"` or `cd /Users/myname/Documents`), then `cs220\lab-p1` or `cs220/lab-p1` are examples of relative paths to the `lab-p1` directory. + * unlike absolute paths, there could be multiple ways of specifying relative paths. Let's say you are inside `Documents\cs220` or `Documents/cs220`. Then the relative path to `lab-p1` will just be `lab-p1`. For the same `lab-p1` directory, we were able to use two different relative paths from two different locations. + +### Task 3.2: Navigate to the parent directory of `lab-p1`, which is `cs220`. + +Remember how we used `cd` to go directly to a specific directory using its absolute pathname? We can also use `cd` to move around the file system using relative paths. To go to the parent directory, we use: + +``` +cd .. +``` + +`..` is a special reference. It always refers to the parent directory of the current working directory. + +### Task 3.3: Use `pwd` to verify that you are now in the `cs220` directory. + +### Task 3.4: Use Shell commands to make a directory for next week's lab. + +Use mkdir, that is **M**a**K**e**DIR**ectory, to *make* a `lab-p2` directory. + +Here's how: + +``` +mkdir lab-p2 +``` + +### Task 3.5: Use `ls` to verify that a `cs220` now contains `lab-p1` and `lab-p2`. + + +### Task 3.6: Navigate back to the `lab-p1` directory. + +Since `lab-p1` is so close to our current(working) directory, we don't have to use the absolute pathname of `lab-p1`. We can use a relative path. + +You can probably guess that we have to use `cd`. So let's type that first (without pressing Enter!), **followed by a space**: + +``` +cd +``` + +Now press Tab, that key right above your Caps Lock. What happens? Press it multiple times. You should see suggestions. This can be useful when navigating. This useful shortcut saves you a lot of typing time! + +Either one of the following will work: + +``` +cd lab-p1 +``` + +OR + +``` +cd .\lab-p1\ +``` + +The above options are identical. `.\\` for Windows and `./` for Mac mean **relative to the current directory**. The `\\` or `/` *after* `lab-p1` indicates that `lab-p1` is a directory. + +As an additional exercise, consider the following... + +Windows: + +``` +ls ..\ +``` + +Mac: + +``` +ls ../ +``` + +What do you think this does? + +------------------------------ + +## Segment 4: Use a Jupyter Notebook to run Python code + +You'll be doing the majority of your coding this semester in Jupyter Notebook files. Let's make one. + +### Task 4.1 Start up a Juptyer Notebook + +You should have a terminal open that's working in your `lab-p1` directory. We will be creating a new Jupyter Notebook called `lab-p1.ipynb`. + +In your terminal, type `jupyter notebook` and press Enter. This should open up Jupyter as a web page in your browser. If it doesn't, and you've already carefully followed our video install instructions (links at the top of this page), please **immediately ask for help** -- there are probably some tricky configuration issues remaining. + +**WARNING:** Your Terminal window should now look something like this: + +<img src="images/jupyter_shell.PNG" width="700"> + +Even though we'll be working in the web browser now, **NEVER** close this terminal window where you typed `jupyter notebook` until you're done -- if you do, Jupyter will crash and you will lose any unsaved work. +If you need to use other Terminal/PowerShell commands, **open a new Terminal/PowerShell window**. + +If everything works properly, you'll see something like this (notice you can see your downloaded files in Jupyter): + +<img src="images/jupyter.jpg" width="700"> + +Click `New`, then `Python 3`. A new tab like this should open: + +<img src="images/new_tab.jpg" width="700"> + +Notice how it says `Untitled` at the top? Click that word, type `lab-p1`, then click `Rename` or press Enter. + +### Task 4.2: Write and run code in your Jupyter Notebook + +Jupyter notebooks have **cells** where you write code. Cells are these rectangular boxes where you can type code... We're going to write one line in a cell (copy-paste the below code): + +```python +print("I'm done!") +``` + +To run this cell, click the `Run` button in the toolbar, which is highlighted green in the image below. You should see the output below the cell, as pictured. + +<img src="images/jupyter_lab.jpg" width="700"> + +Alternatively, you can click `Kernel & Restart & Run All`. + +### Task 4.3: Save your notebook. + +Jupyter's autosave is quite unreliable, so make sure to frequently hit the save button. It is the **save icon**. The keyboard shortcut for this would be `command + s` or `ctrl + s`. + +### Task 4.4: Download a different Notebook file. + +Download `main.ipynb` using the same steps as Task 1.7. Make sure that the extension is `.ipynb`. Now refresh the browser tab containing the `jupyter notebook` homepage. You should be able to see `main.ipynb` in the list of files. Open `main.ipynb` and run the cell containing the code. + +Most lectures will have live-coding examples, so you will be doing this step at the start of each lecture. + +------------------------------ + +## Segment 5: Reading Code + +Before we move onto the next program, let's have a look at `hello.py` again. + +Remember that you are currently running Jupyter on your previous Terminal window. +So, you cannot execute any more Shell commands from that window. Instead, you must +**open a new Terminal/PowerShell window**, and navigate back to the `lab-p1` directory +on the new Terminal. Do **not** close the old Terminal/PowerShell window unless you want +to close your Jupyter notebook. + +### Task 5.1: Use `cat` to see the contents of `hello.py`. + +You should see the following: + +```python +print("Hello, World!") +``` + +What you're looking at is the code for the `hello.py` program. Feel free to use `cat` in the following steps to view the code of other programs. + +### Task 5.2: Download `double.py` to your `lab-p1` folder. + +At this point, you should still have a terminal that is working in your `lab-p1` folder (aka directory). + +### Task 5.3: List the contents of your `lab-p1` directory. + +It should contain `hello.py`, `lab.ipynb`, `main.ipynb`, and `double.py`. + +### Task 5.4: Run `double.py`. + +Type the following and press Enter (Reminder: most of the MAC users will have to type `python3` instead of `python`): + +``` +python double.py +``` + +The program will say `please enter a number:`. This is known as a **prompt** (a fancy way to say a program is asking you a question). + +Type `5` and press Enter. Make sure that the program tells you the answer is `10.0`. + +### Task 5.5: Use Up Arrow to view previous commands + +While using the terminal, press the up arrow key to scroll through the previous commands. + +If you press the up arrow key once, the prompt should show `python double.py` again. You can use the up and down arrow keys to go through your command history. Press Enter to run `double.py`, and this time try entering a negative number. + +Let's run `double.py` one last time, but now when you're prompted for a number, **enter the word `five`**. + +Do you see something like this? + +``` +Traceback (most recent call last): + File "double.py", line 2, in <module> + print("2 times your number is " + str(2*float(x))) +ValueError: could not convert string to float: 'five' +``` + +When you see the word `Traceback`, **it means the program crashed**. The `double.py` program can only accept digits so it crashed when you typed something else. Eventually, we'll learn how to understand what gets printed when a program crashes to identify the root of the problem, but for now we won't worry about it any further. + + +---- + +Congrats on finishing your first CS220 lab! You do not need to submit anything for the labs. The labs are designed to prepare you for the project and help you engage in the lab sessions run by TAs and peer mentors. Your TA will come around to check that you finished and enter your lab attendance points on Canvas. + +It is recommended that you try to finish P1 right away during lab hours, so you can fix any installation issues with the help of your TA and peer mentor. Good luck with [P1](https://git.doit.wisc.edu/cdis/cs/courses/cs220/cs220-s23-projects/-/tree/main/p1)! diff --git a/lab-p1/double.py b/lab-p1/double.py new file mode 100644 index 0000000000000000000000000000000000000000..3cf0cc054f187f9b9379c66accd51ddc26359d86 --- /dev/null +++ b/lab-p1/double.py @@ -0,0 +1,2 @@ +x = input("please enter a number: ") +print("2 times your number is " + str(2*float(x))) diff --git a/lab-p1/hello.py b/lab-p1/hello.py new file mode 100644 index 0000000000000000000000000000000000000000..7df869a15e76c28afb609fa4dbc059144ad70161 --- /dev/null +++ b/lab-p1/hello.py @@ -0,0 +1 @@ +print("Hello, World!") diff --git a/lab-p1/images/README.md b/lab-p1/images/README.md new file mode 100644 index 0000000000000000000000000000000000000000..86d525af724dad403855ca1570ef76ba32438f6b --- /dev/null +++ b/lab-p1/images/README.md @@ -0,0 +1,3 @@ +# Images + +Images from lab-p1 are stored here. diff --git a/lab-p1/images/finder_preferences.png b/lab-p1/images/finder_preferences.png new file mode 100644 index 0000000000000000000000000000000000000000..2a14b798e74adefad8b8c5b3e316b6b59d9fe59c Binary files /dev/null and b/lab-p1/images/finder_preferences.png differ diff --git a/lab-p1/images/finder_preferences_settings.png b/lab-p1/images/finder_preferences_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..976138c8372b6552431635d8522a9826349fae56 Binary files /dev/null and b/lab-p1/images/finder_preferences_settings.png differ diff --git a/lab-p1/images/github.png b/lab-p1/images/github.png new file mode 100644 index 0000000000000000000000000000000000000000..a3e9b47e9a783ab19957d5a8ca8d67c3655ec091 Binary files /dev/null and b/lab-p1/images/github.png differ diff --git a/lab-p1/images/html_error.jpg b/lab-p1/images/html_error.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f9e6d9c6d04dabe245bc45b33bd967d600a64760 Binary files /dev/null and b/lab-p1/images/html_error.jpg differ diff --git a/lab-p1/images/jupyter.jpg b/lab-p1/images/jupyter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a21b754c2f26d9ce1bd3099051a7b021c0d4a0e2 Binary files /dev/null and b/lab-p1/images/jupyter.jpg differ diff --git a/lab-p1/images/jupyter_lab.jpg b/lab-p1/images/jupyter_lab.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d8590dc123a69b3837c3e2af3211d0dfc41a9306 Binary files /dev/null and b/lab-p1/images/jupyter_lab.jpg differ diff --git a/lab-p1/images/jupyter_shell.PNG b/lab-p1/images/jupyter_shell.PNG new file mode 100644 index 0000000000000000000000000000000000000000..87282cc26ab4fce9b3d6d111acc7dc82e09d8a08 Binary files /dev/null and b/lab-p1/images/jupyter_shell.PNG differ diff --git a/lab-p1/images/mac-documents.png b/lab-p1/images/mac-documents.png new file mode 100644 index 0000000000000000000000000000000000000000..84cdf29855ce3830135310087508f0db7ebc47bb Binary files /dev/null and b/lab-p1/images/mac-documents.png differ diff --git a/lab-p1/images/new_tab.jpg b/lab-p1/images/new_tab.jpg new file mode 100644 index 0000000000000000000000000000000000000000..264a493ea191b28ed61cfb7ed97ab27415eaed6a Binary files /dev/null and b/lab-p1/images/new_tab.jpg differ diff --git a/lab-p1/images/raw.png b/lab-p1/images/raw.png new file mode 100644 index 0000000000000000000000000000000000000000..611713da8797c02134c39f3c7be2b0c157d8d1ac Binary files /dev/null and b/lab-p1/images/raw.png differ diff --git a/lab-p1/images/raw_gitlab.png b/lab-p1/images/raw_gitlab.png new file mode 100644 index 0000000000000000000000000000000000000000..962f1d123193dc716d9394651e61c9311d91da67 Binary files /dev/null and b/lab-p1/images/raw_gitlab.png differ diff --git a/lab-p1/images/raw_gitlab_button.png b/lab-p1/images/raw_gitlab_button.png new file mode 100644 index 0000000000000000000000000000000000000000..fc2d498f43459ac78956599be9518cae71e4cc61 Binary files /dev/null and b/lab-p1/images/raw_gitlab_button.png differ diff --git a/lab-p1/images/save.png b/lab-p1/images/save.png new file mode 100644 index 0000000000000000000000000000000000000000..76dbbf92e8ac538b1c60ee1552763abefdcea7c8 Binary files /dev/null and b/lab-p1/images/save.png differ diff --git a/lab-p1/images/save_mac.png b/lab-p1/images/save_mac.png new file mode 100644 index 0000000000000000000000000000000000000000..d1cb7a66652a67a930594bc1a6695f9e315f5ed9 Binary files /dev/null and b/lab-p1/images/save_mac.png differ diff --git a/lab-p1/images/save_windows.png b/lab-p1/images/save_windows.png new file mode 100644 index 0000000000000000000000000000000000000000..0c123d872e9da10c78c9b3ac3b8e3442bd9e2cf4 Binary files /dev/null and b/lab-p1/images/save_windows.png differ diff --git a/lab-p1/images/windows-documents.png b/lab-p1/images/windows-documents.png new file mode 100644 index 0000000000000000000000000000000000000000..eefeea1e351d20ed351a954d9933dcd29cafab0e Binary files /dev/null and b/lab-p1/images/windows-documents.png differ diff --git a/lab-p1/images/windows_file_ext.png b/lab-p1/images/windows_file_ext.png new file mode 100644 index 0000000000000000000000000000000000000000..06cb0f895743aaddf5d6ee89023323a57436c587 Binary files /dev/null and b/lab-p1/images/windows_file_ext.png differ diff --git a/lab-p1/main.ipynb b/lab-p1/main.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..019303a2d3787c9461b76e2fdbd337f03baf8e25 --- /dev/null +++ b/lab-p1/main.ipynb @@ -0,0 +1,35 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "67ee3d8b", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Hello CS220 / CS319 students! Now you know how to download and run a notebook file.\")" + ] + } + ], + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/p1/README.md b/p1/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6c7d0d8f825213e1ab4c7b51f79159445e2cce42 --- /dev/null +++ b/p1/README.md @@ -0,0 +1,45 @@ +# Project 1 (P1) + +## Clarifications/Corrections + +None yet. + +**Find any issues?** Report to us: + +- Ashwin Maran <amaran@wisc.edu> + +## Learning Objectives + +In this project, you will learn to: +* run Otter public tests, +* turn in your project using Gradescope. + +## Note on Academic Misconduct: +P1 is the first project of CS220, and is the **only** project where you are **not** allowed to partner with anyone else. You **must** work on this project by yourself, and turn in your submission individually. Now may be a good time to review our [course policies](https://cs220.cs.wisc.edu/f23/syllabus.html). + +## Step 1: Setup + +You should have a folder `cs220` that you created for Lab-P1 under `Documents`. We're assuming you did the lab and know how to do all the Tasks we mentioned. If you're confused about any of the following steps, refer back to your lab. + +#### Create a sub-folder called `p1` in your `cs220` folder +Refer to [Task 1.1](https://git.doit.wisc.edu/cdis/cs/courses/cs220/cs220-f23-projects/-/tree/main/lab-p1#task-11-create-the-folders-for-this-lab) if you need help. + +This will store all your files related to P1. This way, you can keep files for different projects separate (you'll create a `p2` sub-folder for the next project and so on). Unfortunately, computers can crash and files can get accidentally deleted, so make sure you backup your work regularly (at a minimum, consider emailing yourself relevant files on occasion). + +#### Download `p1.ipynb` and `public_tests.py` to your `p1` folder. +Refer to [Task 1.7](https://git.doit.wisc.edu/cdis/cs/courses/cs220/cs220-f23-projects/-/tree/main/lab-p1#task-17-correctly-download-a-python-file) if you need help. You will be working on `p1.ipynb` this project. The file `public_tests.py` contains the code for testing the answers in your submission. + +#### Open a terminal in your `p1` folder. +Refer to [Task 1.3-1.5](https://git.doit.wisc.edu/cdis/cs/courses/cs220/cs220-f23-projects/-/tree/main/lab-p1#task-13-open-a-terminal-emulator-aka-a-terminal). + +#### Run `p1.ipynb` code using jupyter notebook +Refer to [Task 4.1](https://git.doit.wisc.edu/cdis/cs/courses/cs220/cs220-f23-projects/-/tree/main/lab-p1#task-41-start-up-a-juptyer-notebook). +Make sure to follow the directions provided in `p1.ipynb`. + +## Submission instructions +- Create a [Gradescope](https://www.gradescope.com/) login, if you haven't already created it. Invite should be available via your wisc email. **You must use your wisc email to create the Gradescope login**. +- Login to [Gradescope](https://www.gradescope.com/) and upload the zip file into the P1 assignment. +- Upload the zip file into P1 assignment. +- It is **your responsibility** to make sure that your project clears auto-grader tests on the Gradescope test system. Otter test results should be available in a few minutes after your submission. You should be able to see both PASS / FAIL results for the 2 test cases and your total score, which is accessible via Gradescope Dashboard (as in the image below): + + <img src="images/gradescope.png" width="400"> diff --git a/p1/images/README.md b/p1/images/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0d6c72054c397299b2327866a2fb95d938c23a8e --- /dev/null +++ b/p1/images/README.md @@ -0,0 +1,3 @@ +# Images + +Images from p1 are stored here. diff --git a/p1/images/gradescope.png b/p1/images/gradescope.png new file mode 100644 index 0000000000000000000000000000000000000000..a20e694efae8b4185cde369d77dd5ee138eaa30c Binary files /dev/null and b/p1/images/gradescope.png differ diff --git a/p1/p1.ipynb b/p1/p1.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..d48e0b47469ec3b6d98d221d16d8eab5878418fa --- /dev/null +++ b/p1/p1.ipynb @@ -0,0 +1,268 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "cd1ab159", + "metadata": { + "cell_type": "code", + "deletable": false, + "editable": false + }, + "outputs": [], + "source": [ + "# import and initialize otter\n", + "import otter\n", + "grader = otter.Notebook(\"p1.ipynb\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f901a05b", + "metadata": { + "editable": false, + "execution": { + "iopub.execute_input": "2023-08-25T19:05:22.388711Z", + "iopub.status.busy": "2023-08-25T19:05:22.387711Z", + "iopub.status.idle": "2023-08-25T19:05:22.608397Z", + "shell.execute_reply": "2023-08-25T19:05:22.607367Z" + } + }, + "outputs": [], + "source": [ + "import public_tests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0083a39", + "metadata": { + "execution": { + "iopub.execute_input": "2023-08-25T19:05:22.614397Z", + "iopub.status.busy": "2023-08-25T19:05:22.613397Z", + "iopub.status.idle": "2023-08-25T19:05:22.618719Z", + "shell.execute_reply": "2023-08-25T19:05:22.617689Z" + } + }, + "outputs": [], + "source": [ + "# PLEASE FILL IN THE DETAILS\n", + "\n", + "# project: p1\n", + "# submitter: NETID1\n", + "# hours: ????" + ] + }, + { + "cell_type": "markdown", + "id": "be473d6f", + "metadata": { + "deletable": false, + "editable": false + }, + "source": [ + "## Project 1: Introduction" + ] + }, + { + "cell_type": "markdown", + "id": "0e6ee915", + "metadata": { + "deletable": false, + "editable": false + }, + "source": [ + "**Question 1:** \"Hello, World!\" program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd1e26be", + "metadata": { + "execution": { + "iopub.execute_input": "2023-08-25T19:05:22.622720Z", + "iopub.status.busy": "2023-08-25T19:05:22.622720Z", + "iopub.status.idle": "2023-08-25T19:05:22.632231Z", + "shell.execute_reply": "2023-08-25T19:05:22.631201Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "name = \"World\"\n", + "greeting = \"Hello, \" + name + \"!\"\n", + "\n", + "greeting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57f63f1b", + "metadata": { + "deletable": false, + "editable": false + }, + "outputs": [], + "source": [ + "grader.check(\"q1\")" + ] + }, + { + "cell_type": "markdown", + "id": "ba62cb46", + "metadata": { + "deletable": false, + "editable": false + }, + "source": [ + "**Question 2:** Compute the current year." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "533150fa", + "metadata": { + "execution": { + "iopub.execute_input": "2023-08-25T19:05:22.648784Z", + "iopub.status.busy": "2023-08-25T19:05:22.648784Z", + "iopub.status.idle": "2023-08-25T19:05:22.655330Z", + "shell.execute_reply": "2023-08-25T19:05:22.654312Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "curr_year = 45 * 45 - 2\n", + "\n", + "curr_year" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37ca9934", + "metadata": { + "deletable": false, + "editable": false + }, + "outputs": [], + "source": [ + "grader.check(\"q2\")" + ] + }, + { + "cell_type": "markdown", + "id": "1fb3279b", + "metadata": { + "deletable": false, + "editable": false + }, + "source": [ + "## Submission\n", + "\n", + "Make sure you have run all cells in your notebook in order before running the cell below, so that all images/graphs appear in the output. The cell below will generate a zip file for you to submit. **Please save before exporting!**\n", + "\n", + "**SUBMISSION INSTRUCTIONS**:\n", + " 1. **Upload** the zipfile to Gradescope.\n", + " 2. Check **Gradescope** results as soon as the auto-grader execution gets completed.\n", + " 3. Your **final score** for this project is the score that you see on **Gradescope**.\n", + " 4. You are **allowed** to resubmit on Gradescope as many times as you want to.\n", + " 5. **Contact** a TA/PM if you lose any points on Gradescope for any **reasons**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00aaa876", + "metadata": { + "deletable": false, + "editable": false + }, + "outputs": [], + "source": [ + "# Save your notebook first, then run this cell to export your submission.\n", + "grader.export(pdf=False, run_tests=True)" + ] + }, + { + "cell_type": "markdown", + "id": "1b6a6563", + "metadata": { + "deletable": false, + "editable": false + }, + "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.3" + }, + "otter": { + "OK_FORMAT": true, + "tests": { + "q1": { + "name": "q1", + "points": 12.5, + "suites": [ + { + "cases": [ + { + "code": ">>> public_tests.check('q1', greeting)\nAll test cases passed!\n", + "hidden": false, + "locked": false + } + ], + "scored": true, + "setup": "", + "teardown": "", + "type": "doctest" + } + ] + }, + "q2": { + "name": "q2", + "points": 12.5, + "suites": [ + { + "cases": [ + { + "code": ">>> public_tests.check('q2', curr_year)\nAll test cases passed!\n", + "hidden": false, + "locked": false + } + ], + "scored": true, + "setup": "", + "teardown": "", + "type": "doctest" + } + ] + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/p1/public_tests.py b/p1/public_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..93d1ed5fb68a80c3c50163ce6d1f6cf8bf6b381d --- /dev/null +++ b/p1/public_tests.py @@ -0,0 +1,770 @@ +#!/usr/bin/python +# + +import os, json, math, copy +from collections import namedtuple +from bs4 import BeautifulSoup + +HIDDEN_FILE = os.path.join("hidden", "hidden_tests.py") +if os.path.exists(HIDDEN_FILE): + import hidden.hidden_tests as hidn +# - + +MAX_FILE_SIZE = 750 # units - KB +REL_TOL = 6e-04 # relative tolerance for floats +ABS_TOL = 15e-03 # absolute tolerance for floats +TOTAL_SCORE = 100 # total score for the project + +DF_FILE = 'expected_dfs.html' +PLOT_FILE = 'expected_plots.json' + +PASS = "All test cases passed!" + +TEXT_FORMAT = "TEXT_FORMAT" # question type when expected answer is a type, str, int, float, or bool +TEXT_FORMAT_UNORDERED_LIST = "TEXT_FORMAT_UNORDERED_LIST" # question type when the expected answer is a list or a set where the order does *not* matter +TEXT_FORMAT_ORDERED_LIST = "TEXT_FORMAT_ORDERED_LIST" # question type when the expected answer is a list or tuple where the order does matter +TEXT_FORMAT_DICT = "TEXT_FORMAT_DICT" # question type when the expected answer is a dictionary +TEXT_FORMAT_SPECIAL_ORDERED_LIST = "TEXT_FORMAT_SPECIAL_ORDERED_LIST" # question type when the expected answer is a list where order does matter, but with possible ties. Elements are ordered according to values in special_ordered_json (with ties allowed) +TEXT_FORMAT_NAMEDTUPLE = "TEXT_FORMAT_NAMEDTUPLE" # question type when expected answer is a namedtuple +PNG_FORMAT_SCATTER = "PNG_FORMAT_SCATTER" # question type when the expected answer is a scatter plot +HTML_FORMAT = "HTML_FORMAT" # question type when the expected answer is a DataFrame +FILE_JSON_FORMAT = "FILE_JSON_FORMAT" # question type when the expected answer is a JSON file +SLASHES = " SLASHES" # question SUFFIX when expected answer contains paths with slashes + +def get_expected_format(): + """get_expected_format() returns a dict mapping each question to the format + of the expected answer.""" + expected_format = {'q1': 'TEXT_FORMAT', 'q2': 'TEXT_FORMAT'} + return expected_format + + +def get_expected_json(): + """get_expected_json() returns a dict mapping each question to the expected + answer (if the format permits it).""" + expected_json = {'q1': 'Hello, World!', 'q2': 2023} + return expected_json + + +def get_special_json(): + """get_special_json() returns a dict mapping each question to the expected + answer stored in a special format as a list of tuples. Each tuple contains + the element expected in the list, and its corresponding value. Any two + elements with the same value can appear in any order in the actual list, + but if two elements have different values, then they must appear in the + same order as in the expected list of tuples.""" + special_json = {} + return special_json + + +def compare(expected, actual, q_format=TEXT_FORMAT): + """compare(expected, actual) is used to compare when the format of + the expected answer is known for certain.""" + try: + if q_format == TEXT_FORMAT: + return simple_compare(expected, actual) + elif q_format == TEXT_FORMAT_UNORDERED_LIST: + return list_compare_unordered(expected, actual) + elif q_format == TEXT_FORMAT_ORDERED_LIST: + return list_compare_ordered(expected, actual) + elif q_format == TEXT_FORMAT_DICT: + return dict_compare(expected, actual) + elif q_format == TEXT_FORMAT_SPECIAL_ORDERED_LIST: + return list_compare_special(expected, actual) + elif q_format == TEXT_FORMAT_NAMEDTUPLE: + return namedtuple_compare(expected, actual) + elif q_format == PNG_FORMAT_SCATTER: + return compare_flip_dicts(expected, actual) + elif q_format == HTML_FORMAT: + return compare_cell_html(expected, actual) + elif q_format == FILE_JSON_FORMAT: + return compare_json(expected, actual) + else: + if expected != actual: + return "expected %s but found %s " % (repr(expected), repr(actual)) + except: + if expected != actual: + return "expected %s" % (repr(expected)) + return PASS + + +def print_message(expected, actual, complete_msg=True): + """print_message(expected, actual) displays a simple error message.""" + msg = "expected %s" % (repr(expected)) + if complete_msg: + msg = msg + " but found %s" % (repr(actual)) + return msg + + +def simple_compare(expected, actual, complete_msg=True): + """simple_compare(expected, actual) is used to compare when the expected answer + is a type/Nones/str/int/float/bool. When the expected answer is a float, + the actual answer is allowed to be within the tolerance limit. Otherwise, + the values must match exactly, or a very simple error message is displayed.""" + msg = PASS + if 'numpy' in repr(type((actual))): + actual = actual.item() + if isinstance(expected, type): + if expected != actual: + if isinstance(actual, type): + msg = "expected %s but found %s" % (expected.__name__, actual.__name__) + else: + msg = "expected %s but found %s" % (expected.__name__, repr(actual)) + elif not isinstance(actual, type(expected)) and not (isinstance(expected, (float, int)) and isinstance(actual, (float, int))): + msg = "expected to find type %s but found type %s" % (type(expected).__name__, type(actual).__name__) + elif isinstance(expected, float): + if not math.isclose(actual, expected, rel_tol=REL_TOL, abs_tol=ABS_TOL): + msg = print_message(expected, actual, complete_msg) + elif isinstance(expected, (list, tuple)) or is_namedtuple(expected): + new_msg = print_message(expected, actual, complete_msg) + if len(expected) != len(actual): + return new_msg + for i in range(len(expected)): + val = simple_compare(expected[i], actual[i]) + if val != PASS: + return new_msg + elif isinstance(expected, dict): + new_msg = print_message(expected, actual, complete_msg) + if len(expected) != len(actual): + return new_msg + val = simple_compare(list(expected.keys()), list(actual.keys())) + if val != PASS: + return new_msg + for key in expected: + val = simple_compare(expected[key], actual[key]) + if val != PASS: + return new_msg + else: + if expected != actual: + msg = print_message(expected, actual, complete_msg) + return msg + + +def intelligent_compare(expected, actual, obj=None): + """intelligent_compare(expected, actual) is used to compare when the + data type of the expected answer is not known for certain, and default + assumptions need to be made.""" + if obj == None: + obj = type(expected).__name__ + if is_namedtuple(expected): + msg = namedtuple_compare(expected, actual) + elif isinstance(expected, (list, tuple)): + msg = list_compare_ordered(expected, actual, obj) + elif isinstance(expected, set): + msg = list_compare_unordered(expected, actual, obj) + elif isinstance(expected, (dict)): + msg = dict_compare(expected, actual) + else: + msg = simple_compare(expected, actual) + msg = msg.replace("CompDict", "dict").replace("CompSet", "set").replace("NewNone", "None") + return msg + + +def is_namedtuple(obj, init_check=True): + """is_namedtuple(obj) returns True if `obj` is a namedtuple object + defined in the test file.""" + bases = type(obj).__bases__ + if len(bases) != 1 or bases[0] != tuple: + return False + fields = getattr(type(obj), '_fields', None) + if not isinstance(fields, tuple): + return False + if init_check and not type(obj).__name__ in [nt.__name__ for nt in _expected_namedtuples]: + return False + return True + + +def list_compare_ordered(expected, actual, obj=None): + """list_compare_ordered(expected, actual) is used to compare when the + expected answer is a list/tuple, where the order of the elements matters.""" + msg = PASS + if not isinstance(actual, type(expected)): + msg = "expected to find type %s but found type %s" % (type(expected).__name__, type(actual).__name__) + return msg + if obj == None: + obj = type(expected).__name__ + for i in range(len(expected)): + if i >= len(actual): + msg = "at index %d of the %s, expected missing %s" % (i, obj, repr(expected[i])) + break + val = intelligent_compare(expected[i], actual[i], "sub" + obj) + if val != PASS: + msg = "at index %d of the %s, " % (i, obj) + val + break + if len(actual) > len(expected) and msg == PASS: + msg = "at index %d of the %s, found unexpected %s" % (len(expected), obj, repr(actual[len(expected)])) + if len(expected) != len(actual): + msg = msg + " (found %d entries in %s, but expected %d)" % (len(actual), obj, len(expected)) + + if len(expected) > 0: + try: + if msg != PASS and list_compare_unordered(expected, actual, obj) == PASS: + msg = msg + " (%s may not be ordered as required)" % (obj) + except: + pass + return msg + + +def list_compare_helper(larger, smaller): + """list_compare_helper(larger, smaller) is a helper function which takes in + two lists of possibly unequal sizes and finds the item that is not present + in the smaller list, if there is such an element.""" + msg = PASS + j = 0 + for i in range(len(larger)): + if i == len(smaller): + msg = "expected %s" % (repr(larger[i])) + break + found = False + while not found: + if j == len(smaller): + val = simple_compare(larger[i], smaller[j - 1], complete_msg=False) + break + val = simple_compare(larger[i], smaller[j], complete_msg=False) + j += 1 + if val == PASS: + found = True + break + if not found: + msg = val + break + return msg + +class NewNone(): + """alternate class in place of None, which allows for comparison with + all other data types.""" + def __str__(self): + return 'None' + def __repr__(self): + return 'None' + def __lt__(self, other): + return True + def __le__(self, other): + return True + def __gt__(self, other): + return False + def __ge__(self, other): + return other == None + def __eq__(self, other): + return other == None + def __ne__(self, other): + return other != None + +class CompDict(dict): + """subclass of dict, which allows for comparison with other dicts.""" + def __init__(self, vals): + super(self.__class__, self).__init__(vals) + if type(vals) == CompDict: + self.val = vals.val + elif isinstance(vals, dict): + self.val = self.get_equiv(vals) + else: + raise TypeError("'%s' object cannot be type casted to CompDict class" % type(vals).__name__) + + def get_equiv(self, vals): + val = [] + for key in sorted(list(vals.keys())): + val.append((key, vals[key])) + return val + + def __str__(self): + return str(dict(self.val)) + def __repr__(self): + return repr(dict(self.val)) + def __lt__(self, other): + return self.val < CompDict(other).val + def __le__(self, other): + return self.val <= CompDict(other).val + def __gt__(self, other): + return self.val > CompDict(other).val + def __ge__(self, other): + return self.val >= CompDict(other).val + def __eq__(self, other): + return self.val == CompDict(other).val + def __ne__(self, other): + return self.val != CompDict(other).val + +class CompSet(set): + """subclass of set, which allows for comparison with other sets.""" + def __init__(self, vals): + super(self.__class__, self).__init__(vals) + if type(vals) == CompSet: + self.val = vals.val + elif isinstance(vals, set): + self.val = self.get_equiv(vals) + else: + raise TypeError("'%s' object cannot be type casted to CompSet class" % type(vals).__name__) + + def get_equiv(self, vals): + return sorted(list(vals)) + + def __str__(self): + return str(set(self.val)) + def __repr__(self): + return repr(set(self.val)) + def __getitem__(self, index): + return self.val[index] + def __lt__(self, other): + return self.val < CompSet(other).val + def __le__(self, other): + return self.val <= CompSet(other).val + def __gt__(self, other): + return self.val > CompSet(other).val + def __ge__(self, other): + return self.val >= CompSet(other).val + def __eq__(self, other): + return self.val == CompSet(other).val + def __ne__(self, other): + return self.val != CompSet(other).val + +def make_sortable(item): + """make_sortable(item) replaces all Nones in `item` with an alternate + class that allows for comparison with str/int/float/bool/list/set/tuple/dict. + It also replaces all dicts (and sets) with a subclass that allows for + comparison with other dicts (and sets).""" + if item == None: + return NewNone() + elif isinstance(item, (type, str, int, float, bool)): + return item + elif isinstance(item, (list, set, tuple)): + new_item = [] + for subitem in item: + new_item.append(make_sortable(subitem)) + if is_namedtuple(item): + return type(item)(*new_item) + elif isinstance(item, set): + return CompSet(new_item) + else: + return type(item)(new_item) + elif isinstance(item, dict): + new_item = {} + for key in item: + new_item[key] = make_sortable(item[key]) + return CompDict(new_item) + return item + +def list_compare_unordered(expected, actual, obj=None): + """list_compare_unordered(expected, actual) is used to compare when the + expected answer is a list/set where the order of the elements does not matter.""" + msg = PASS + if not isinstance(actual, type(expected)): + msg = "expected to find type %s but found type %s" % (type(expected).__name__, type(actual).__name__) + return msg + if obj == None: + obj = type(expected).__name__ + + try: + sort_expected = sorted(make_sortable(expected)) + sort_actual = sorted(make_sortable(actual)) + except: + return "unexpected datatype found in %s; expected entries of type %s" % (obj, obj, type(expected[0]).__name__) + + if len(actual) == 0 and len(expected) > 0: + msg = "in the %s, missing" % (obj) + sort_expected[0] + elif len(actual) > 0 and len(expected) > 0: + val = intelligent_compare(sort_expected[0], sort_actual[0]) + if val.startswith("expected to find type"): + msg = "in the %s, " % (obj) + simple_compare(sort_expected[0], sort_actual[0]) + else: + if len(expected) > len(actual): + msg = "in the %s, missing " % (obj) + list_compare_helper(sort_expected, sort_actual) + elif len(expected) < len(actual): + msg = "in the %s, found un" % (obj) + list_compare_helper(sort_actual, sort_expected) + if len(expected) != len(actual): + msg = msg + " (found %d entries in %s, but expected %d)" % (len(actual), obj, len(expected)) + return msg + else: + val = list_compare_helper(sort_expected, sort_actual) + if val != PASS: + msg = "in the %s, missing " % (obj) + val + ", but found un" + list_compare_helper(sort_actual, + sort_expected) + return msg + + +def namedtuple_compare(expected, actual): + """namedtuple_compare(expected, actual) is used to compare when the + expected answer is a namedtuple defined in the test file.""" + msg = PASS + if is_namedtuple(actual, False): + msg = "expected namedtuple but found %s" % (type(actual).__name__) + return msg + if type(expected).__name__ != type(actual).__name__: + return "expected namedtuple %s but found namedtuple %s" % (type(expected).__name__, type(actual).__name__) + expected_fields = expected._fields + actual_fields = actual._fields + msg = list_compare_ordered(list(expected_fields), list(actual_fields), "namedtuple attributes") + if msg != PASS: + return msg + for field in expected_fields: + val = intelligent_compare(getattr(expected, field), getattr(actual, field)) + if val != PASS: + msg = "at attribute %s of namedtuple %s, " % (field, type(expected).__name__) + val + return msg + return msg + + +def clean_slashes(item): + """clean_slashes()""" + if isinstance(item, str): + return item.replace("\\", "/").replace("/", os.path.sep) + elif item == None or isinstance(item, (type, int, float, bool)): + return item + elif isinstance(item, (list, tuple, set)) or is_namedtuple(item): + new_item = [] + for subitem in item: + new_item.append(clean_slashes(subitem)) + if is_namedtuple(item): + return type(item)(*new_item) + else: + return type(item)(new_item) + elif isinstance(item, dict): + new_item = {} + for key in item: + new_item[clean_slashes(key)] = clean_slashes(item[key]) + return item + + +def list_compare_special_initialize(special_expected): + """list_compare_special_initialize(special_expected) takes in the special + ordering stored as a sorted list of items, and returns a list of lists + where the ordering among the inner lists does not matter.""" + latest_val = None + clean_special = [] + for row in special_expected: + if latest_val == None or row[1] != latest_val: + clean_special.append([]) + latest_val = row[1] + clean_special[-1].append(row[0]) + return clean_special + + +def list_compare_special(special_expected, actual): + """list_compare_special(special_expected, actual) is used to compare when the + expected answer is a list with special ordering defined in `special_expected`.""" + msg = PASS + expected_list = [] + special_order = list_compare_special_initialize(special_expected) + for expected_item in special_order: + expected_list.extend(expected_item) + val = list_compare_unordered(expected_list, actual) + if val != PASS: + return val + i = 0 + for expected_item in special_order: + j = len(expected_item) + actual_item = actual[i: i + j] + val = list_compare_unordered(expected_item, actual_item) + if val != PASS: + if j == 1: + msg = "at index %d " % (i) + val + else: + msg = "between indices %d and %d " % (i, i + j - 1) + val + msg = msg + " (list may not be ordered as required)" + break + i += j + return msg + + +def dict_compare(expected, actual, obj=None): + """dict_compare(expected, actual) is used to compare when the expected answer + is a dict.""" + msg = PASS + if not isinstance(actual, type(expected)): + msg = "expected to find type %s but found type %s" % (type(expected).__name__, type(actual).__name__) + return msg + if obj == None: + obj = type(expected).__name__ + + expected_keys = list(expected.keys()) + actual_keys = list(actual.keys()) + val = list_compare_unordered(expected_keys, actual_keys, obj) + + if val != PASS: + msg = "bad keys in %s: " % (obj) + val + if msg == PASS: + for key in expected: + new_obj = None + if isinstance(expected[key], (list, tuple, set)): + new_obj = 'value' + elif isinstance(expected[key], dict): + new_obj = 'sub' + obj + val = intelligent_compare(expected[key], actual[key], new_obj) + if val != PASS: + msg = "incorrect value for key %s in %s: " % (repr(key), obj) + val + return msg + + +def is_flippable(item): + """is_flippable(item) determines if the given dict of lists has lists of the + same length and is therefore flippable.""" + item_lens = set(([str(len(item[key])) for key in item])) + if len(item_lens) == 1: + return PASS + else: + return "found lists of lengths %s" % (", ".join(list(item_lens))) + +def flip_dict_of_lists(item): + """flip_dict_of_lists(item) flips a dict of lists into a list of dicts if the + lists are of same length.""" + new_item = [] + length = len(list(item.values())[0]) + for i in range(length): + new_dict = {} + for key in item: + new_dict[key] = item[key][i] + new_item.append(new_dict) + return new_item + +def compare_flip_dicts(expected, actual, obj="lists"): + """compare_flip_dicts(expected, actual) flips a dict of lists (or dicts) into + a list of dicts (or dict of dicts) and then compares the list ignoring order.""" + msg = PASS + example_item = list(expected.values())[0] + if isinstance(example_item, (list, tuple)): + val = is_flippable(actual) + if val != PASS: + msg = "expected to find lists of length %d, but " % (len(example_item)) + val + return msg + msg = list_compare_unordered(flip_dict_of_lists(expected), flip_dict_of_lists(actual), "lists") + elif isinstance(example_item, dict): + expected_keys = list(example_item.keys()) + for key in actual: + val = list_compare_unordered(expected_keys, list(actual[key].keys()), "dictionary %s" % key) + if val != PASS: + return val + for cat_key in expected_keys: + expected_category = {} + actual_category = {} + for key in expected: + expected_category[key] = expected[key][cat_key] + actual_category[key] = actual[key][cat_key] + val = list_compare_unordered(flip_dict_of_lists(expected), flip_dict_of_lists(actual), "category " + repr(cat_key)) + if val != PASS: + return val + return msg + + +def get_expected_tables(): + """get_expected_tables() reads the html file with the expected DataFrames + and returns a dict mapping each question to a html table.""" + if not os.path.exists(DF_FILE): + return None + + expected_tables = {} + f = open(DF_FILE, encoding='utf-8') + soup = BeautifulSoup(f.read(), 'html.parser') + f.close() + + tables = soup.find_all('table') + for table in tables: + expected_tables[table.get("data-question")] = table + + return expected_tables + +def parse_df_html_table(table): + """parse_df_html_table(table) takes in a table as a html string and returns + a dict mapping each row and column index to the value at that position.""" + rows = [] + for tr in table.find_all('tr'): + rows.append([]) + for cell in tr.find_all(['td', 'th']): + rows[-1].append(cell.get_text().strip("\n ")) + + cells = {} + for r in range(1, len(rows)): + for c in range(1, len(rows[0])): + rname = rows[r][0] + cname = rows[0][c] + cells[(rname,cname)] = rows[r][c] + return cells + + +def get_expected_namedtuples(): + """get_expected_namedtuples() defines the required namedtuple objects + globally. It also returns a tuple of the classes.""" + expected_namedtuples = [] + + return tuple(expected_namedtuples) + +_expected_namedtuples = get_expected_namedtuples() + + +def compare_cell_html(expected, actual): + """compare_cell_html(expected, actual) is used to compare when the + expected answer is a DataFrame stored in the `expected_dfs` html file.""" + expected_cells = parse_df_html_table(expected) + try: + actual_cells = parse_df_html_table(BeautifulSoup(actual, 'html.parser').find('table')) + except Exception as e: + return "expected to find type DataFrame but found type %s instead" % type(actual).__name__ + + expected_cols = list(set(["column %s" % (loc[1]) for loc in expected_cells])) + actual_cols = list(set(["column %s" % (loc[1]) for loc in actual_cells])) + msg = list_compare_unordered(expected_cols, actual_cols, "DataFrame") + if msg != PASS: + return msg + + expected_rows = list(set(["row index %s" % (loc[0]) for loc in expected_cells])) + actual_rows = list(set(["row index %s" % (loc[0]) for loc in actual_cells])) + msg = list_compare_unordered(expected_rows, actual_rows, "DataFrame") + if msg != PASS: + return msg + + for location, expected in expected_cells.items(): + location_name = "column {} at index {}".format(location[1], location[0]) + actual = actual_cells.get(location, None) + if actual == None: + return "in %s, expected to find %s" % (location_name, repr(expected)) + try: + actual_ans = float(actual) + expected_ans = float(expected) + if math.isnan(actual_ans) and math.isnan(expected_ans): + continue + except Exception as e: + actual_ans, expected_ans = actual, expected + msg = simple_compare(expected_ans, actual_ans) + if msg != PASS: + return "in %s, " % location_name + msg + return PASS + + +def get_expected_plots(): + """get_expected_plots() reads the json file with the expected plot data + and returns a dict mapping each question to a dictionary with the plots data.""" + if not os.path.exists(PLOT_FILE): + return None + + f = open(PLOT_FILE, encoding='utf-8') + expected_plots = json.load(f) + f.close() + return expected_plots + + +def compare_file_json(expected, actual): + """compare_file_json(expected, actual) is used to compare when the + expected answer is a JSON file.""" + msg = PASS + if not os.path.isfile(expected): + return "file %s not found; make sure it is downloaded and stored in the correct directory" % (expected) + elif not os.path.isfile(actual): + return "file %s not found; make sure that you have created the file with the correct name" % (actual) + try: + e = open(expected, encoding='utf-8') + expected_data = json.load(e) + e.close() + except json.JSONDecodeError: + return "file %s is broken and cannot be parsed; please delete and redownload the file correctly" % (expected) + try: + a = open(actual, encoding='utf-8') + actual_data = json.load(a) + a.close() + except json.JSONDecodeError: + return "file %s is broken and cannot be parsed" % (actual) + if type(expected_data) == list: + msg = list_compare_ordered(expected_data, actual_data, 'file ' + actual) + elif type(expected_data) == dict: + msg = dict_compare(expected_data, actual_data) + return msg + + +_expected_json = get_expected_json() +_special_json = get_special_json() +_expected_plots = get_expected_plots() +_expected_tables = get_expected_tables() +_expected_format = get_expected_format() + +def check(qnum, actual): + """check(qnum, actual) is used to check if the answer in the notebook is + the correct answer, and provide useful feedback if the answer is incorrect.""" + msg = PASS + error_msg = "<b style='color: red;'>ERROR:</b> " + q_format = _expected_format[qnum] + + if q_format == TEXT_FORMAT_SPECIAL_ORDERED_LIST: + expected = _special_json[qnum] + elif q_format == PNG_FORMAT_SCATTER: + if _expected_plots == None: + msg = error_msg + "file %s not parsed; make sure it is downloaded and stored in the correct directory" % (PLOT_FILE) + else: + expected = _expected_plots[qnum] + elif q_format == HTML_FORMAT: + if _expected_tables == None: + msg = error_msg + "file %s not parsed; make sure it is downloaded and stored in the correct directory" % (DF_FILE) + else: + expected = _expected_tables[qnum] + else: + expected = _expected_json[qnum] + + if SLASHES in q_format: + q_format = q_format.replace(SLASHES, "") + expected = clean_slashes(expected) + actual = clean_slashes(actual) + + if msg != PASS: + print(msg) + else: + msg = compare(expected, actual, q_format) + if msg != PASS: + msg = error_msg + msg + print(msg) + + +def check_file_size(path): + """check_file_size(path) throws an error if the file is too big to display + on Gradescope.""" + size = os.path.getsize(path) + assert size < MAX_FILE_SIZE * 10**3, "Your file is too big to be displayed by Gradescope; please delete unnecessary output cells so your file size is < %s KB" % MAX_FILE_SIZE + + +def reset_hidden_tests(): + """reset_hidden_tests() resets all hidden tests on the Gradescope autograder where the hidden test file exists""" + if not os.path.exists(HIDDEN_FILE): + return + hidn.reset_hidden_tests() + +def rubric_check(rubric_point, ignore_past_errors=True): + """rubric_check(rubric_point) uses the hidden test file on the Gradescope autograder to grade the `rubric_point`""" + if not os.path.exists(HIDDEN_FILE): + print(PASS) + return + error_msg_1 = "ERROR: " + error_msg_2 = "TEST DETAILS: " + try: + msg = hidn.rubric_check(rubric_point, ignore_past_errors) + except: + msg = "hidden tests crashed before execution" + if msg != PASS: + hidn.make_deductions(rubric_point) + if msg == "public tests failed": + comment = "The public tests have failed, so you will not receive any points for this question." + comment += "\nPlease confirm that the public tests pass locally before submitting." + elif msg == "answer is hardcoded": + comment = "In the datasets for testing hardcoding, all numbers are replaced with random values." + comment += "\nIf the answer is the same as in the original dataset for all these datasets" + comment += "\ndespite this, that implies that the answer in the notebook is hardcoded." + comment += "\nYou will not receive any points for this question." + else: + comment = hidn.get_comment(rubric_point) + msg = error_msg_1 + msg + if comment != "": + msg = msg + "\n" + error_msg_2 + comment + print(msg) + +def get_summary(): + """get_summary() returns the summary of the notebook using the hidden test file on the Gradescope autograder""" + if not os.path.exists(HIDDEN_FILE): + print("Total Score: %d/%d" % (TOTAL_SCORE, TOTAL_SCORE)) + return + score = min(TOTAL_SCORE, hidn.get_score(TOTAL_SCORE)) + display_msg = "Total Score: %d/%d" % (score, TOTAL_SCORE) + if score != TOTAL_SCORE: + display_msg += "\n" + hidn.get_deduction_string() + print(display_msg) + +def get_score_digit(digit): + """get_score_digit(digit) returns the `digit` of the score using the hidden test file on the Gradescope autograder""" + if not os.path.exists(HIDDEN_FILE): + score = TOTAL_SCORE + else: + score = hidn.get_score(TOTAL_SCORE) + digits = bin(score)[2:] + digits = "0"*(7 - len(digits)) + digits + return int(digits[6 - digit])