ICS 33 Spring 2024
Project 0: History of Modern

Due date and time: Wednesday, April 10, 11:59pm

Git repository: https://ics.uci.edu/~thornton/ics33/ProjectGuide/Project0/Project0.git


Introduction

Given your successful completion of prerequisite coursework, you will have built your programming skills to the point where you can do a lot more than you could do when you wrote your first program.

As you continue to climb the skills ladder in this course and beyond, you'll find yourself solving problems that are gradually larger, gradually more complex, require gradually larger teams of people to solve (and a gradually higher percentage of time spent coordinating with those people), and present gradually more ways to make mistakes both large and small. Scale introduces constraints, which means that we need tools that are designed to work within those constraints. This quarter, we'll begin an exploration of those tools, so you can hit the ground running when you reach the point where you need them. And, fortunately, those tools benefit us even we use them in individual, smaller-scale work like you'll be doing in this course.

But, of course, we can't use new tools without having learned about them, and there's no better way to learn about tools than to put them to use. This project will ask you to exercise your existing Python programming skills, but will also require you to use tools and organizational techniques that may be new for you. Your score on this project will largely be determined by your demonstration of how you used those tools and techniques throughout, so this isn't just a matter of doing whatever you prefer and submitting a complete solution; the focus is as much on how you get there as it is on whether you get there.

Setting up your development environment

It is very common in real-world software development jobs to be given, at the outset of a new job, a list of tools that you will be required to use, along with a list of policies and procedures you'll be required to follow. Flexibility is great, and it's nice to be able to choose one's own tools, but, unfortunately, many software tools introduce constraints on how a program can be written, how its components can be arranged, what functions can be called, what documentation can be written and how, what additional software it can be combined with, how it can be "built" to be distributed to end users, and so on. So, like it or not, real-world software development usually requires at least some of the tools to be set in stone and used by all members of a team, even if not all members have the same preferences; this is simply a reality that software developers have to face, because smooth collaboration in a team environment is usually more important than satisfying every individual's whim. (While your work in this course is individual, you'll nonetheless be working with a team of course staff, who won't be able to support everyone's individual tools and bespoke preferences.)

Your first task in this project, then, is to set up the development environment that we'll all be using for our work this quarter.

Developing and testing a Python module incrementally

This project isn't just about setting up tools, though. We'll also be asking you to put those tools to use to enrich your prior skills, by developing and testing a Python module in an incremental fashion. Importantly, you'll also have to demonstrate that you followed that incremental approach by submitting every version of your module as you developed and tested it, step by step. We don't just want to see where you ended up; we want to see how you got there, even if there were missteps along the way.

Don't worry, though; you'll be using the same tools that professional software developers use to achieve these same goals, so you won't need to be making copies of files and arranging your own history as you go. As long as you can take notice that you've reached stable ground periodically, and definitively plant a stake in that stable ground each time with a brief explanation of what's changed and why, the rest of the paper trail will be built for you automatically in a form that will be easily submitted to us within a single file.

A word of warning

Before you start writing any code for this project, you're going to want to read this write-up in its entirety, because how you approach this problem is at least as important as whether you can solve it. The learning objective of this project is to familiarize yourself with the tools and processes you'll be using to begin an assignment, document and test your work incrementally as you proceed with it, and submit it to us in a way that we can accept and grade using automation to assist us. That way, we can continue to rely on your ability to adhere to a similar process throughout the quarter, so that we can focus our grading efforts on providing useful feedback, rather than the secretarial work of rearranging each student's customized submission into something we can work with.

Consequently, correct solutions that do not demonstrate the use of the necessary tools and processes will receive little or no credit. Grades are an attempt to measure learning, and it's those tools and processes that you're meant to be learning in this project.


The ICS 33 development environment

This course is built around the assumption that everyone — students and course staff alike — will be using the same development environment, which is to say that we'll all be using the same versions of the same tools to do our work. That way, everyone is on an equal footing, meaning that when there's a problem, we can rule out root causes like "My version of Python doesn't have that function in its standard library" or "My preferred editor is not compatible with your preferred version control system," so we can keep our minds focused on the course material rather than fighting uphill battles against our tools.

So, your first order of business is setting up the tools you'll need for your work this quarter. There are three things you'll need to be sure you've installed.

  1. Python 3.12
  2. Git
  3. PyCharm Professional

Before you start downloading and installing these tools on your own, though, it's worth reading to the end of this section and reading (and following) the instructions provided for your operating system.

Choosing the right versions of tools

The version of Python is important to get right. We all need to be on version 3.12 for this course, though it's not important which minor release (e.g., 3.12.1 or 3.12.0) you install. Using any version other than 3.12 exposes you to risks that will interfere with your work and your ability to obtain a high score when we grade it — and note that you bear all of this risk.

For both Git and PyCharm Professional, your best bet is to begin this quarter by installing (or upgrading to) whatever version is the most current at that point, and then stick with it throughout the quarter. Our work in this course is less sensitive to the particulars of these tools, though it would still be worth starting out as up-to-date as you can, then leaving things alone until after the quarter ends. The rest of these instructions assume that you've installed the versions of these tools that were available as of this writing.

The particulars of getting set up

If you'll be doing your work solely using the workstations in the ICS labs, you're ready to go! The software you need is already installed and configured properly, so there's nothing you'll need to do for this part of the project. Note, though, that some of our ICS labs are "laptop-only" spaces, so there may not always be a workstation available to you that has the necessary tools installed on it.

So, assuming that you will be wanting to do at least some of your work on your own machine, what you do next depends on what operating system you're running on your machine.


Getting started on your project

Each assignment will begin with a starting point, which will include a combination of provided Python code, with any other necessary tools to use for setup and submission. The starting point is provided in the form of a Git repository. (We'll discuss Git in some more detail a little later in this write-up.)

Using PyCharm on the workstations in the ICS labs

If you're using the version of PyCharm installed on the workstations in the ICS labs, you may find that you need to activate it, by connecting it to a license server that verifies that it's permitted to be used on those workstations.

If you're asked to activate PyCharm on a workstation in the ICS labs — if a dialog box named Licenses pops up — here's what you need to do.

That should be it.

One-time PyCharm setup

PyCharm is a professional-level tool, which means that it has a wealth of settings that can be used to configure it. This is great when you're an experienced software developer, because it means you can set it up in a way that best supports the kind of work you'll be doing. However, when you're relatively new — as most of you are — it can be an overwhelming set of choices, many of which you won't have the context to understand yet. Even for teams of experienced developers, synchronizing on various settings — so that everyone has things configured similarly — can be an important way to keep people on the same page.

We'll all use some of the same PyCharm settings in this course, so that we'll all see warnings and errors in the same situations, we'll all use the same source control tools in the same way, and so on. Rather than require each of you to click through hundreds of settings, you're instead being provided with a collection of settings that you can simply import into PyCharm. Let's do that now.

Additionally, you'll want to disable any generative AI plugins that PyCharm will have installed, both because we are not permitting the use of generative AI in this course — see the Course Reference for details — and because we've found that the suggestions offered have been vastly more confusing than they've been useful. (It's worth noting that PyCharm offers these plugins mainly as a way to obtain subscription revenue.) Here's how to disable the AI-related plugins.

You'll only need to do these things once, so now's a good time to do it. Then, you can proceed with starting this project.

Starting a new project

To start a new assignment, then, you'll need to follow a few procedural steps to create a new PyCharm project using the contents of the provided Git repository.

Having done this, you're ready to begin work. To do that, though, you'll need to know what you need to do, so read on.


The problem

After you've created your PyCharm project, you'll see that it contains two files of immediate interest.

Your end goal is twofold: Implementing the methods of QueensState class meeting the requirements specified in its docstrings and type annotations, and implementing unit tests of that class in the TestQueensState class.

You might now want to take a look at those two files, so you have an idea of what lies ahead. But it's important that you not barrel forward toward those goals without having read the rest of this project write-up, because there are requirements about how you do this work, in addition to requirements about whether you complete it. While I'm sure all of you have the skills to complete this task in your own chosen way already, there are learning objectives here that have more to do with tools and techniques — which, for most of you, will be a departure from what you're used to — than the code we're asking you to write, which may not be new territory for you at all.


Working and testing incrementally

In your work on this project, we're requiring you to build small-scale features one at a time, and you are required to write unit tests as you go. If you're not feeling sure what I mean by "small-scale feature" here, it might be worth having a look at the notes accompanying the Test-Driven Development topic that I cover in ICS 32 and ICS 32A, which proposes one way to approach this kind of development. In this course, we're not particularly zealous about writing tests before we write code — that's certainly reasonable, but hardly a requirement — but we do at least want to be sure that we're writing tests shortly after writing the code under test, if not before. Whether the tests are written before or after implementing a feature, we want to get a feature nailed down (including its testing) before we move on to the next one; that's mainly where the benefit of this kind of approach arises. And, once we've got it nailed down, we're on demonstrably stable ground, so we should keep a copy of our work at that point in time, for which we'll use a tool called Git.

To give you an example of a small-scale feature you might start with in this project, let's start with this one: "When a QueensState is initially created, its queen_count method will return zero." That's our initial goal and our only goal for now, so we'll focus on that and ignore everything else until later. As it turns out, there's already a unit test for this particular feature provided in test_queens.py, which you'll find in a method named test_queen_count_is_zero_initially. Let's run that test and see it fail, mainly so we know how to run unit tests in PyCharm, which we'll need to do often as we work.

Running unit tests in PyCharm

When you want to run your unit tests within PyCharm, the easiest way to do so is to right-click (or Ctrl-click on macOS) on a test module like test_queens.py, or a directory like tests that contains many of them, and select Run 'Python tests in...'. (That menu item will sometimes have slightly different wording, but will generally begin with Run 'Python tests, even if followed with something else.) This will cause PyCharm to run all of the selected tests and display their results. Because you'll be writing code and tests incrementally, you'll likely find yourself running your tests quite often. It's not uncommon for my develop-test cycle to be no more than a few minutes, or even sometimes less than a minute, depending on the kind of work I'm doing. Frequent actionable feedback about one's progress can be a huge boon to one's productivity, keeping us "in the zone" while leaving behind a lasting record — in the form of our unit tests — of exactly how our program is meant to behave.

Having run the tests, you should see the Run tab in PyCharm's user interface displayed, if it wasn't already. In that tab will be an indication that the test has failed, along with an error message describing that failure. Our next order of business is to make the test succeed, which we can do with a one-line change in queens.py. In the queen_count method, replace the pass statement with return 0 instead. Save your change to queens.py and then re-run the unit tests again, and you should now see that they've passed instead of failing.

Building our historical record in Git

Now that we've got our first feature nailed down — that's really all there is to our first feature, though they won't all be this easy — we're on stable ground, so we now need to note this in our historical record, since one of the requirements of this project is that you submit a complete record of the changes you made, one feature at a time. To do that, though, we'll need to understand a little bit about what Git is, and how we interact with it in PyCharm.

Git is what's often called a version control or source control system, which means that it's in the business of tracking many versions of our source code (i.e., the code that makes up our program) instead of just one. When you created your new PyCharm project using the provided Git repository, it triggered PyCharm to build a Git repository for it automatically, which simply means that there's an additional directory within your project directory named .git — which you may not see, because a lot of operating systems will hide it from you, unless you turn that feature off — that stores a complete history of every version of your project as it evolved. PyCharm recognizes this Git repository automatically and gives us tools for interacting with it visually.

When we want to update the history of our project, we do so by committing a change to our Git repository, which is to say that we instruct Git to remember which files changed and what changed within them, alongside a commit comment that we write explaining whatever else we want to describe about the update we're making. Notably, this doesn't replace what was in the Git repository previously; it augments it. Our Git repository contains the entire history of changes — every commit we've made from beginning to end — which means it contains enough information to allow us to reconstruct every prior version of our project. As long as we made a commit at a particular point in time, we can get back to that point in time quickly and accurately (and non-destructively!), as well as do other useful things, such as finding out the last time a particular file changed, viewing exactly what we changed in a particular commit, or comparing two commits from different points in history to see how they differ.

(When shared amongst groups of people working together on a project, Git shines even more brightly, allowing us to do things like finding out "Who was the last person to change this line of code and when was it?" or ensuring that changes made by one person don't blindly overwrite changes made by someone else. Since your work in this course will be entirely individual, you won't have occasion to experience those benefits in this course, but when it comes time to work in larger teams, you'll be able to expand your knowledge of Git so that it can support you and your teammates in those efforts, as well.)

So, how do we commit our change? Along the left-hand side of the PyCharm window, you should see a column of buttons labeled with icons. It may not be immediately apparent what each icon does, but hovering over the icon will reveal its name. One of the buttons will be named Commit — for me, this one is near the top, though I suspect this is reconfigurable — which you should click. This will reveal an area of the window that lets you stage your changes and commit them. There are two separate areas listing directories and files, one called Staged and the other labeled Unstaged.

The idea behind "staging" is simpler than it sounds. The Unstaged area lists files that are different than they were in the last commit, but for which you haven't affirmatively specified that you want to commit them. The Staged area displays the changes that you've decided you want to commit. (This allows you to work on two things at once, or to commit the things you're surer about without also committing the things still in flight.) So, committing our change is a three-step process.

If you don't see separate Staged and Unstaged areas, it's possible that you've somehow adjusted PyCharm's setting governing whether a "staging area" will be in use. (It's also possible that you didn't install this course's PyCharm settings previously.) To solve that problem, you can select Settings from the File menu. (On Windows, the menus are accessible by clicking the menu icon, which is drawn as a series of parallel horizontal lines and shown near the top-left of the window; on macOS, you'll find the application's menu in the usual place.) Along the left-hand side, expand the Version Control settings, then select Git from the list. If the setting named Enable staging area does not have a checkmark next to it, click it so that it does. Then click OK near the bottom-right of the settings window.

When you're done, you can click the icon named Project along the left-hand side of the window to return to the view you had before (i.e., with the files and directories visible again).

Meanwhile, if you click the icon named Git along the lefthand side of the PyCharm window, you should see your new commit listed in your repository's history. Selecting a commit in that history will display which files were changed in that commit and what comment you wrote accompanying the change. Double-clicking one of the files will show you how it changed in that commit (i.e., you'll be able to see what the file looked like both before and after, with a visual indication of where the differences are). This will let you explore your project's history, which can be a very useful tool when you're trying to figure out things like "How was this function written before I overhauled it a few commits ago?" or "When did I last change that method?"

Testing incrementally

While you work, you'll be required to write unit tests. Note, too, that the phrase "while you work" has meaning here; each time you've finished a small feature, it will need to be accompanied at that time by the corresponding unit tests. Ideally, a Git commit that includes a new feature or a fix to existing code would include the applicable changes to the unit tests (i.e., the new or updated tests related to your change), so that each commit is demonstrably stable ground: a finished or fixed feature, along with tests that give you confidence in your assessment that it's finished or fixed. That may be a very different way of working than you're used to, because it requires a level of discipline you might have survived without so far, but working incrementally and testing as you go is a technique that many professional software engineers — me included, as often as I'm able — follow as carefully as possible, because it's one of the many techniques that aid us in tackling substantially larger problems than those we can fit entirely in our heads at any one time.

To reiterate for clarity, following this approach is a requirement and the extent to which you follow this approach constitutes a substantial portion of your grade on this project. While there is certainly leeway in the definition of the word "feature," and we're not going to be able to spend the time negotiating with individual students about exactly how many features are required or exactly which features must be implemented in which order, it is certainly true that students who submit a complete solution with only a small handful of commits, or with tests written only after all features were implemented (or not at all) will find themselves with scores much lower than they would probably expect. Approach the problem incrementally, testing as you go, and you'll be fine from a grade perspective, but you'll also have learned a valuable set of skills that you can use moving forward, ones that I use nearly every day in my professional work.

How many unit tests are required?

You'll need to write as many unit tests as are needed to fully cover its behavior. Your goal for this project is to achieve full coverage, which is to say that at least one test should reach every line of code in queens.py, and that at least one test should exercise every branch in every direction (e.g., an if statement needs to be tested in both the truthy and falsy cases). While that's a more ambitious goal than we'll often be aiming for, the nature of this problem — a class describing a type of immutable object, whose methods have no side effects — is such that we can achieve it with techniques you'll have seen previously, which makes it a great way to learn how to achieve that standard when it's an appropriate one.

Of course, this means that how you choose your tests will have an impact on the number of tests you'll write. Many tests that, when executed, take the same route through your code don't improve the overall coverage measurement at all, which is an indication that having more of them may not be adding any more value than if there were fewer. That's why we're not specifying a number of tests that we're requiring you to write; it doesn't matter how many tests you write if they're all testing the same behavior. It's writing tests with variety that matters, and, fortunately, there's a way to measure that variety.

Measuring code coverage using PyCharm

(Note: To do what's described in this section, you'll need PyCharm Professional. If you installed PyCharm Community instead, the code coverage feature will not be available, but you'll need it from time to time in this course, so you'll want to upgrade to PyCharm Professional before proceeding.)

Code coverage measurement is the practice of running a program in an environment that keeps track of which lines of code were reached, which branches were taken in which directions (e.g., for each if statement, track whether its condition has been both True and False), and so on. Where this technique can be very useful is alongside unit testing. If we measure the level of coverage achieved by running our unit tests, we have a visible indication when there are parts of our code we've not tested at all. While achieving full coverage is not necessarily an indication that every useful test has been written — like any metric, this one can be gamed a bit, so we don't want to turn our brains off and just follow it blindly — the absence of full coverage highlights scenarios that we've certainly not tested.

PyCharm provides the built-in ability to measure code coverage while running tests. The easiest way to do it is to right-click (or Ctrl-click on macOS) on a test module like test_queens.py, or a directory like tests that contains many of them, and select More Run/Debug and then select Run ... with Coverage. (The precise wording on that last menu item will vary, but it'll generally begin with the word Run and end with the words with Coverage.) When we do that, the selected tests run and we see their results, but PyCharm will additionally display coverage statistics in a separate area of the screen. As long as those coverage statistics are visible, opening a Python module like queens.py in the PyCharm editor will also display a visual indication along the left-hand edge of the editor that shows which lines were reached:

When you're finished, executing all of your unit tests should lead to every meaningful line (i.e., every line that's not blank or only a comment) in queens.py being marked green. If not, you'll need to evaluate what tests you might need to add — the specific situations you aren't testing already — to achieve that goal.

Importantly, please note that it doesn't matter if the code in test_queens.py is marked green or not. What we're measuring is the extent to which our tests exercise the code being tested, so we only care about the code being tested, which is in queens.py.

Installing coverage.py

The first time you attempt to run you tests in PyCharm with code coverage measurement, you'll likely see an error message indicating the following.

Of these two choices, installing the coverage.py library is the better choice — we've had ongoing problems in my courses with the bundled coverage tool — so your best bet is to click on the word install in that error message, which is a link; if clicked, it will install the package and you'll be off to the races.

Another way to install the package is manually. (There's no need to do this if you used the link described in the previous paragraph, but learning how to install packages is a handy thing, so it might be worth reading through these instructions, anyway.)

Once coverage.py is installed, it will be used automatically to determine your code coverage the next time you run your tests with code coverage measurement.

It's worth noting, though, that when you start a new project, this setting may not still be in effect, so you may need to do the following to turn it back on.


Limitations

You can use the Python standard library where appropriate in this project, and you can certainly depend on the code that we've provided, but you will otherwise not be able to use code written by anyone else other than you. Notably, this includes third-party libraries (i.e., those that are not part of Python's standard library), which are strictly off-limits in this course except where specifically allowed. Colloquially, if we have to install something other than Python, Git, and PyCharm in order for your program to work, it's considered off-limits, unless specific permission is given in a particular project. This project offers no such permission.

Since we'll be automatically testing your QueensState class, rather than relying only on your tests to demonstrate its completeness and correctness, do not modify the Position namedtuple or the provided exception classes (DuplicateQueenError and MissingQueenError), since our automated tests will depend specifically on the way that they're written.


Preparing your submission

Because the grading of many of your assignments will be at least partially automated, we'll need to agree on a submission format, so that everyone's submission will be arranged identically. Fortunately, our use of Git makes this choice simple. Git allows a complete repository to be packaged up into a file called a Git bundle, in a format that Git-compatible tools (like PyCharm) can work with easily; so, you'll provide us with a Git bundle as your final submission. In other words, what you'll be submitting to us is essentially your entire Git repository, including all of the commits you made along the way, so that we have a complete history of your work and how it evolved from the starting point to the finished product.

It's important to understand that it's not our goal to judge every one of your commits, so there's no need to feel uncomfortable about submitting a repository that includes in-progress versions of your work, or to spend time curating a parallel repository of "clean" changes. We completely understand that earlier commits will have warts and rough edges, just as the earlier commits in our own repositories in our own work are of varying quality. For the most part, all we'll be grading in detail is your most recent commit on the main branch of your Git repository. But we do want to be able to see an indication of your process, which can sometimes help us to understand the details of your submission, and can allow us to verify that you had a process, which is among the techniques we want you to learn this quarter, but that can't be learned without doing it.

Demonstrating that you've worked incrementally is a substantial part of your grade, but we don't have precise numeric requirements about how you worked incrementally. There's no predetermined number of commits you must have, or a predetermined number of lines of code that can change between commits. The goal here is qualitative rather than quantitative. All you need to do is follow one simple rule: Each time you've reached stable ground, with a small-scale feature working and tested, or with a problem fixed and a test that identified the problem and now demonstrates that it was fixed, it's time to commit your change with a brief explanation of what new feature you added, or what problem you were fixing. That will benefit you as much as it does us — you'd be surprised how much it helps your thought process to stop and write a couple of sentences about what you're doing and why, and how useful it can be when you're able to refer to your own previous commits — and that's all we're asking for here.

Generating a bundle using the prepare_submission.py script

When you're ready to submit your work, you'll run a script we provided — which you'll find in your project directory — named prepare_submission.py. That script will make an attempt to create a Git bundle from the Git repository in your project directory, which would constitute your entire submission.

The prepare_submission.py script will attempt to verify a few things before it creates a Git bundle, to ensure that it's able to create a bundle, and that you're submitting a complete version of your work.

Running the prepare_submission.py is a simple matter of right-clicking it (or Ctrl-clicking it, on macOS), and selecting Run 'prepare_submission'. The Run area in the PyCharm window should be displayed, and any output from the script — including any warnings or error messages — will be displayed there. Assuming there are no warnings or errors, you'll find the file project0.bundle in your project directory, and that's the one and only file you'd submit.

Verifying your bundle before submission

Before you submit your bundle, you'll likely want to verify that it's complete and correct. Note that you bear the risk here, which is to say that we'll be grading what you submit, and you won't later be able to explain that you would have preferred to submit something else. So, now's the time to be sure that what you're submitting is what you want us to grade.

The best way to verify your bundle prior to submitting it is to create a new PyCharm project from your bundle, so you can most easily see what's in it. There are a couple of ways to do that, depending on what operating system you're using.

Afterward, you should see the files in their final form, and the Git tab in PyCharm should show your entire commit history. Be sure, too, that the main branch is where you want it to be — that's what we'll be grading. If so, you're in business; go ahead and submit your work.


Deliverables

Submit your project0.bundle file (and no others) to Canvas. There are a few rules to be aware of.

Can I submit after the deadline?

This project is not included in the late work policy for this course. The objective is not just that you do this one, but that you do it early enough to be of benefit, so it needs to be completed during the initial days of the course and submitted before the due date above. Submissions beyond that deadline will not be considered under any circumstances.

The late work policy for this course — which does not apply to this project, but applies to all of the others — is described in the section titled Late work at this link.

What do I do if Canvas adjusts my filename?

Canvas will sometimes modify your filenames when you submit them (e.g., by adding a numbering scheme like -1 or a long sequence of hexadecimal digits to its name). In general, this is fine; as long as the file you submitted has the correct name prior to submission, we'll be able to obtain it with that same name, even if Canvas adjusts it.