diff --git a/README.md b/README.md index 124083e..417bd7a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Python210CourseMaterials +# Python310CourseMaterials -Course Materials for Python 210 +Course Materials for Python 310: the first course in the UWPCE Python Certificate Program. Published here: diff --git a/source/class_schedule/index.rst b/source/class_schedule/index.rst index 27cf4f0..764b8b2 100644 --- a/source/class_schedule/index.rst +++ b/source/class_schedule/index.rst @@ -1,7 +1,7 @@ :orphan: ######################### -Python 210 Class Schedule +Python 310 Class Schedule ######################### .. toctree:: diff --git a/source/exercises/html_renderer/html_renderer_tutorial.rst b/source/exercises/html_renderer/html_renderer_tutorial.rst index 762688c..6539107 100644 --- a/source/exercises/html_renderer/html_renderer_tutorial.rst +++ b/source/exercises/html_renderer/html_renderer_tutorial.rst @@ -110,13 +110,13 @@ But this one failed: assert file_contents.startswith("") assert file_contents.endswith("") -OK -- this one really does something real -- it tries to render an html element -- which did NOT pass -- so it's time to put some real functionality in the Element class. +OK -- this one really does something real -- it tries to render an html element -- which did NOT pass -- so it's time to put some real functionality in the ``Element`` class. This is the code: .. code-block:: python - class Element(object): + class Element: def __init__(self, content=None): pass @@ -139,7 +139,7 @@ So we need to add a tiny bit of code: .. code-block:: python - class Element(object): + class Element: tag = "html" @@ -261,9 +261,9 @@ So the method looks something like this: .. code-block:: python def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) # loop through the list of contents: for content in self.contents: - out_file.write("<{}>\n".format(self.tag)) out_file.write(content) out_file.write("\n") out_file.write("\n".format(self.tag)) @@ -492,7 +492,8 @@ Part A .. rubric:: Instructions: -"Create a couple subclasses of ``Element``, for each of ````, ````, and ``

`` tags. All you should have to do is override the ``tag`` class attribute (you may need to add a ``tag`` class attribute to the ``Element`` class first, if you haven't already)." +"Create a couple subclasses of ``Element``, for each of ````, ````, and ``

`` tags. +All you should have to do is override the ``tag`` class attribute (you may need to add a ``tag`` class attribute to the ``Element`` class first, if you haven't already)." So this is very straightforward. We have a class that represents an element, and the only difference between basic elements is that they have a different tag. For example:: @@ -509,7 +510,9 @@ and::

-The ```` tag is for the entire contents of an html page, and the ``

`` tag is for a paragraph. But you can see that the form of the tags is identical, so we don't have to change much to make classes for these tags. In fact, all we need to change is the ``tag`` class attribute. +The ```` tag is for the entire contents of an html page, and the ``

`` tag is for a paragraph. +But you can see that the form of the tags is identical, so we don't have to change much to make classes for these tags. +In fact, all we need to change is the ``tag`` class attribute. Before we do that -- let's do some test-driven development. Uncomment the next few tests in ``test_html_render.py``: ``test_html``, ``test_body``, and ``test_p``, and run the tests:: @@ -546,7 +549,8 @@ Before we do that -- let's do some test-driven development. Uncomment the next f ====================== 3 failed, 4 passed in 0.08 seconds ====================== So we have three failures. Of course we do, because we haven't written any new code yet! -Yes, this is pedantic, and there is no real reason to run tests you know are going to fail. But there is a reason to *write* tests that you know are going to fail, and you have to run them to know that you have written them correctly. +Yes, this is pedantic, and there is no real reason to run tests you know are going to fail. +But there is a reason to *write* tests that you know are going to fail, and you have to run them to know that you have written them correctly. Now we can write the code for those three new element types. Try to do that yourself first, before you read on. @@ -664,24 +668,28 @@ Uncomment ``test_subelement`` in the test file, and run the tests:: out_file = <_io.StringIO object at 0x10325b5e8> def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) # loop through the list of contents: for content in self.contents: - out_file.write("<{}>\n".format(self.tag)) > out_file.write(content) E TypeError: string argument expected, got 'P' html_render.py:26: TypeError ====================== 1 failed, 7 passed in 0.11 seconds ====================== -Again, the new test failed; no surprise because we haven't written any new code yet. But do read the report carefully; it did not fail on an assert, but rather with a ``TypeError``. The code itself raised an exception before it could produce results to test. +Again, the new test failed; no surprise because we haven't written any new code yet. +But do read the error report carefully; it did not fail on an assert, but rather with a ``TypeError``. The code itself raised an exception before it could produce results to test. So now it's time to write the code. Look at where the exception was raised: line 26 in my code, inside the ``render()`` method. The line number will likely be different in your code, but it probably failed on the render method. Looking closer at the error:: > out_file.write(content) E TypeError: string argument expected, got 'P' -It occurred in the file ``write`` method, complaining that it expected to be writing a string to the file, but it got a ``'P'``. ``'P'`` is the name of the paragraph element class. -So we need a way to write an element to a file. How might we do that? Inside the element's render method, we need to render an element... +It occurred in the file ``write`` method, complaining that it expected to be writing a string to the file, but it got a ``'P'``. +``'P'`` is the name of the paragraph element class. +So we need a way to write an element to a file. How might we do that? + +Inside the element's render method, we need to render an element... Well, elements already know how to render themselves. This is what is meant by a recursive approach. In the ``render`` method, we want to make use of the ``render`` method itself. @@ -696,25 +704,25 @@ it becomes clear -- we render an element by passing the output file to the eleme .. code-block:: python def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) # loop through the list of contents: for content in self.contents: - out_file.write("<{}>\n".format(self.tag)) out_file.write(content) out_file.write("\n") - out_file.write("\n".format(self.tag)) + out_file.write("\n".format(self.tag)) So let's update our render by replacing that ``out_file.write()`` call with a call to the content's ``render`` method: .. code-block:: python def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) # loop through the list of contents: for content in self.contents: - out_file.write("<{}>\n".format(self.tag)) # out_file.write(content) content.render(out_file) out_file.write("\n") - out_file.write("\n".format(self.tag)) + out_file.write("\n".format(self.tag)) And let's see what happens when we run the tests:: @@ -779,20 +787,21 @@ There are a number of approaches you can take. This is a good time to read the n You may want to try one of the more complex methods, but for now, we're going to use the one that suggests itself from the error. We need to know whether we want to call a ``render()`` method, or simply write the content to the file. How would we know which to do? Again, look at the error: + We tried to call the render() method of a piece of content, but got an ``AttributeError``. So the way to know whether we can call a render method is to try to call it -- if it works, great! If not, we can catch the exception, and do something else. In this case, the something else is to try to write the content directly to the file: .. code-block:: python def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) # loop through the list of contents: for content in self.contents: - out_file.write("<{}>\n".format(self.tag)) try: content.render(out_file) except AttributeError: out_file.write(content) out_file.write("\n") - out_file.write("\n".format(self.tag)) + out_file.write("\n".format(self.tag)) And run the tests again:: @@ -815,7 +824,7 @@ So what are the downsides to this method? Well, there are two: 1. When we successfully call the ``render`` method, we have no idea if it's actually done the right thing -- it could do anything. If someone puts some completely unrelated object in the content list that happens to have a ``render`` method, this is not going to work. But what are the odds of that? -2. This is the bigger one: if the object *HAS* a render method, but that render method has something wrong with it, then it could conceivably raise an ``AttributeError`` itself, but it would not be the Attribute Error we are expecting. The trick here is that this is very hard to debug. +2. This is the bigger one: if the object *HAS* a render method, but that render method has something wrong with it, then it could conceivably raise an ``AttributeError`` itself, but it would not be the ``AttributeError`` we are expecting. The trick here is that this can be very hard to debug. However, we are saved by tests. If the render method works in all the other tests, It's not going to raise an ``AttributeError`` only in this case. Another reason to have a good test suite. @@ -845,7 +854,7 @@ OK, that should have been straightforward. Now this part: Some html elements don't tend to have a lot of content, such as the document title. So it makes sense to render them all on one line. This is going to require a new render method. Since there are multiple types of elements that should be rendered on one line, we want to create a base class for all one-line elements. It should subclass from ``Element``, and override the render method with a new one, which will be pretty much the same as the main ``Element`` method, but without the newlines. -Before we do that though -- let's write a test for that! Because the ``ONeLineTag`` class is a base class for actual elements that should be rendered on one line, we really don't need to write a test directly for it. We can write one for its first subclass: ``Title``. The title elements should be rendered something like this:: +Before we do that though -- let's write a test for that! Because the ``OneLineTag`` class is a base class for actual elements that should be rendered on one line, we really don't need to write a test directly for it. We can write one for its first subclass: ``Title``. The title elements should be rendered something like this:: PythonClass - title example @@ -965,14 +974,14 @@ So how do we get this test to pass? We need a new render method for ``OneLineTag class OneLineTag(Element): def render(self, out_file): + out_file.write("<{}>".format(self.tag)) # loop through the list of contents: for content in self.contents: - out_file.write("<{}>".format(self.tag)) try: content.render(out_file) except AttributeError: out_file.write(content) - out_file.write("\n".format(self.tag)) + out_file.write("\n".format(self.tag)) Notice that I left the newline in at the end of the closing tag -- we do want a newline there, so the next element won't get rendered on the same line. And the tests:: @@ -1109,7 +1118,7 @@ Now that we know how to initialize an element with attributes, and how it should # but it starts the same: assert file_contents.startswith("\n") out_file.write("".join(open_tag)) @@ -1205,7 +1213,7 @@ Note that I added a space after the ``p`` in the test. Now my test is failing on A paragraph of text

-However, my code for rendering the opening tag is a bit klunky -- how about yours? Perhaps you'd like to refactor it? Before you do that, you might want to make your tests a bit more robust. This is really tricky. It's very hard to test for everytihng that might go wrong, without nailing down the expected results too much. For example, in this case, we haven't tested that there is a space between the two attributes. In fact, this would pass our test:: +However, my code for rendering the opening tag is a bit klunky -- how about yours? Perhaps you'd like to refactor it? Before you do that, you might want to make your tests a bit more robust. This is really tricky. It's very hard to test for everything that might go wrong, without nailing down the expected results too much. For example, in this case, we haven't tested that there is a space between the two attributes. In fact, this would pass our test::

A paragraph of text @@ -1315,7 +1323,7 @@ You'll need to override the ``render()`` method: What needs to be there? Well, self closing tags can have attributes, same as other elements. So we need a lot of the same code here as with the other ``render()`` methods. You could copy and paste the ``Element.render()`` method, and edit it a bit. But that's a "Bad Idea" -- remember DRY (Don't Repeat Yourself)? You really don't want two copies of that attribute rendering code you worked so hard on. -How do we avoid that? We take advantage of the power of subclassing. If you put the code to render the opening (and closing) tags in it's own method, then we can call that method from multiple render methods, something like: +How do we avoid that? We take advantage of the power of subclassing. If you put the code to render the opening (and closing) tags in its own method, then we can call that method from multiple render methods, something like: .. code-block:: python diff --git a/source/exercises/list_lab.rst b/source/exercises/list_lab.rst index f4c7860..b366e56 100644 --- a/source/exercises/list_lab.rst +++ b/source/exercises/list_lab.rst @@ -29,9 +29,9 @@ to query the user for info at the command line, you use: Procedure --------- -In your student dir in the class repo, create a ``lesson03`` dir and put in a new ``list_lab.py`` file. +In the github classroom repo for this exercise, you will find a``list_lab.py`` file (if it not there, you can create it and add it to git yourself). -The file should be an executable Python script. That is to say that one +The file should be made an executable Python script. That is to say that one should be able to run the script directly like so: .. code-block:: bash @@ -50,11 +50,11 @@ should be able to run the script directly like so: The file will also need this on the first line:: - #!/usr/bin/env python3 + #!/usr/bin/env python -This is known as the "she-bang" line -- it tells the shell how to execute that file -- in this case, with ``python3`` +This is known as the "she-bang" line -- it tells the shell how to execute that file -- in this case, with ``python`` -NOTE: on Windows, there is a python launcher which, if everything is configured correctly will look at that line to know you want python3 if there is more than one python on your system. +NOTE: on Windows, there is a python launcher which, if everything is configured correctly will look at that line to know you want python if there is more than one python on your system. If this doesn't work on Windows, just run the file some other way: @@ -63,14 +63,11 @@ If this doesn't work on Windows, just run the file some other way: - from your IDE or editor is you are using one -Add the file to your clone of the repository and commit changes frequently +Make sure the file is added to your clone of the repository and commit changes frequently while working on the following tasks. When you are done, push your changes to -GitHub and issue a pull request. +GitHub and issue a pull request to let the instructors know it is ready for review. -(if you are still struggling with git -- just write the code for now). - -When the script is run, it should accomplish the following four series of -actions: +When the script is run, it should accomplish the following four series of actions: Series 1 -------- @@ -79,8 +76,8 @@ Series 1 - Display the list (plain old ``print()`` is fine...). - Ask the user for another fruit and add it to the end of the list. - Display the list. -- Ask the user for a number and display the number back to the user and the - fruit corresponding to that number (on a 1-is-first basis). Remember that Python uses zero-based indexing, so you will need to correct. +- Ask the user for a number and display the number back to the user + and the fruit corresponding to that number (on a 1-is-first basis). Remember that Python uses zero-based indexing, so you will need to correct for that. - Add another fruit to the beginning of the list using "+" and display the list. - Add another fruit to the beginning of the list using ``insert()`` and display the list. @@ -105,8 +102,8 @@ Again, using the list from series 1: - Ask the user for input displaying a line like "Do you like apples?" for each fruit in the list (making the fruit all lowercase). - For each "no", delete that fruit from the list. -- For any answer that is not "yes" or "no", prompt the user to answer with one - of those two values (a while loop is good here) +- For any answer that is not "yes" or "no", prompt the user to answer + with one of those two values (a while loop is good here) - Display the list. Series 4 diff --git a/source/exercises/mailroom/mailroom-oo.rst b/source/exercises/mailroom/mailroom-oo.rst index 0a84e9b..5484002 100644 --- a/source/exercises/mailroom/mailroom-oo.rst +++ b/source/exercises/mailroom/mailroom-oo.rst @@ -8,9 +8,7 @@ Making Mailroom Object Oriented. **Goal:** Refactor the mailroom program using classes to help organize the code. -The functionality is the same as the earlier mailroom: - -:ref:`exercise_mailroom_part1` +The functionality is the same as the earlier mailroom(s). But this time, we want to use an OO approach to better structure the code to make it more extensible. @@ -32,6 +30,9 @@ As you design appropriate classes, keep in mind these three guidelines for good There should be no use of the ``input()`` function in the classes that hold the data! Nor any use of ``print()`` -- these are for user interaction, and you want the data handling code to be potentially usable with totally different user interaction -- such as a desktop GUI or Web interface. +In the spirit of separation of concerns, you also want keep your Command Line Interface code in a separate file(s), as they already should be in your mailroom-as-a-package version. In fact, you should be able to keep the structure of your package pretty much the same -- simply replacing the model code with classes. + + 3) As always: **DRY** (Don't Repeat Yourself): Anywhere you see repeated code; refactor it! @@ -52,7 +53,14 @@ For this simple problem, simple tuples could work fine. However, in order for th So now you have to think about using a dict or class. Again for flexibility, a dict is a bit easier; you can add fields to it very easily. However, with a class, you can build some functionality in there, too. This is where encapsulation comes in. For instance, one thing you might want to do is get the total of all donations a donor has made in the past. If you add a method to compute that (or a property!), then the rest of the code doesn't need to know how the donations are stored. -Consider ``data[0]`` vs ``data["name"]`` vs ``data.name``. Which one is more readable? Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDEs to provide auto-completion, which reduces the number of bugs and helps to produce code faster. +Consider ``data[0]`` (stored in a tuple) vs ``data["name"]`` (stored in a dict) vs ``data.name`` (a class). +Which one is more readable? +Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDEs to provide auto-completion, which reduces the number of bugs and helps to produce code faster. + +Another way to think about it is "data" vs "code" -- dicts are for data, classes are for code. +Sometimes it's not totally clear which is which, but if you think about how static is, that can be be a guide. +In this example, every donor is going to have a name, and a donation history, and maybe in the future a bunch of other information (address, email, phone number ....). +Whatever it is, every donor will have the same set -- so it's good to use code to manage it. Below are more detailed suggestions on breaking down your existing code into multiple modules that will be part of a single mailroom program. @@ -60,26 +68,32 @@ Below are more detailed suggestions on breaking down your existing code into mul Modules and Classes ................... -You may organize your code to your preference and keep it simple by having all of the code in a single file. +You may organize your code to your preference and keep it simple by having all of the model code in a single file. But you should at least separate the code that manipulates data (the "model" code) from the command line interface code, as you did for mailroom-as-a-package. -Optionally, you could organize your code into modules, which helps to keep code organized and re-usable. +Optionally, you could further organize your model code into separate modules: maybe one for the Donor object, one for the DonorCollection object. What is a module? A module is a python file with a collection of code that can be imported into other python files. Modules can contain functions, classes, and even variables (constants). -Here is an example file structure for ``mailroom_oo`` package that contains 3 modules: +Here is an example file structure for ``mailroom_oo`` package -- it should be similar to what you already have: -.. code-block:: bash +:: - └── mailroom_oo - ├── cli_main.py - ├── donor_models.py - └── test_mailroom_oo.py + └── mailroom + ├── __init__.py + ├── cli.py + ├── model.py + └── tests + ├── __init__.py + ├── test_cli.py + └── test_model.py -The module ``donor_models.py`` can contain the ``Donor`` and ``DonorCollection`` classes. -The module ``cli_main.py`` would include all of your user interaction functions and main program flow. +The module ``model.py`` can contain the ``Donor`` and ``DonorCollection`` classes. + +The module ``cli.py`` would include all of your user interaction functions and main program flow (and a ``main()`` function that can be called to start up the program (and used as an entry_point). + ``Donor`` Class ............... @@ -117,7 +131,9 @@ This design allows you to quickly look up donor by their name and get a donor ob Another option is to simply use a list of donor objects. You get to choose which you think is more appropriate. -Remember that you should use `self.donors` attribute any time you want to work with data about a single donor, most of your methods in this class will utilize it in some way. This is really what classes are desined for. +Remember that you should use `self.donors` attribute in any methods that need access the individual donors. Most of your methods in this class will utilize it in some way. This is really what classes are designed for. + +Note that external code probably shouldn't access the ``.donors`` list (or dict, or ...) directly but rather ask the DonorCollection class for the information it needs: e.g. if you need to add a new donation to a particular donor, calla method like ``find_donor()`` to get the donor you want, and then work with that Donor object directly. **Examples:** @@ -125,17 +141,15 @@ Generating a thank you letter to a donor only requires knowledge of that one don Generating a report about all the donors requires knowledge of all the donors, so that code belongs in the ``DonorCollection`` class. -Hint: -You've previously sorted simple data structures like list and dictionaries, but here we're dealing with objects - not to worry that is a really simple thing to do with python! -You can use `operator.attrgetter` with a sorted function (review python docs for usage documentation). +.. note:: You've previously sorted simple data structures like list and dictionaries, but here we're dealing with objects -- not to worry, that is a really simple thing to do with python! You can use ``operator.attrgetter`` with the ``sorted`` function (review the python docs for usage documentation) to make it easy to sort based on various attributes of a Donor. + Command Line Interface ....................... **Module responsible for main program flow (CLI - Command Line Interface)** -Let's call this module ``cli_main.py`` to represent the entry point for the mailroom program. -This module will be using the classes we defined: ``Donor`` and ``DonorCollection``. +Let's call this module ``cli.py``. This module will be using the classes we defined: ``Donor`` and ``DonorCollection``. It will also handle interaction with the user via the ``input`` function calls that gather user input and to provide the output to the console. What should go into this module? @@ -152,6 +166,10 @@ The idea here is that we should be able to fairly easy replace this CLI program such as a GUI (Graphical User Interface), without having to make any changes to our data classes. If that was the case, then you would implement the GUI elements and use your data classes the same way as they are used in CLI. +Note that this arrangement is a one-way street: the CLI code will need to know about the Donor and DonorCollection classes, but the model code shouldn't need to know anything about the CLI code: it manages the information, and returns what's asked for (like a donor letter or report), but it doesn't know what is done with that information. + +In short: the cli.py module will import the model module, but the model module shouldn't import the cli module. + Test-Driven Development ----------------------- @@ -170,15 +188,15 @@ That is, rather than take a non-OO function and try to make it a method of a cla You should expect to re-use a lot of the command line interface code, while refactoring most of the logic code. -If you are not sure at the start what functionality you data classes will need, you can start with the CLI code, and as you find the need for a function, add it to your data classes (after writing a test first, of course). +If you are not sure at the start what functionality you data classes will need, you can start with the CLI code, and as you find the need for a function, add it to your model classes (after writing a test first, of course). Exercise Guidelines =================== -OO mailroom is the final project for the class. +OO mailroom is the final step in the mailroom project. And you are near the end of the class. -So this is your chance to really do things "right". Strive to make this code as good, by every definition, as you can. +So this is your chance to really do things "right". Strive to make this code as good, by every definition, as you can. If you have gotten feedback from your instructors, now is the chance to incorporate recommended changes. With that in mind: @@ -209,9 +227,9 @@ Functionality - You should be able to re-use all the logic code with a different UI -- Web App, GUI, etc. - - There should be no ``input()`` or ``print`` functions in the logic code. + - There should be no ``input()`` or ``print()`` functions in the logic code. - - The logic code should be 100% testable (without mocking input() or any fancy stuff like that) + - The logic code should be 100% testable (without mocking ``input()`` or any fancy stuff like that) .. rubric:: Testing @@ -231,9 +249,8 @@ Functionality .. rubric:: The "soft" stuff: Style: - - conform to PEP8! (or another consistent style) - - - You can use 95 or some other reasonable number for line length + - Conform to PEP8! (or another consistent style) + (You can use 95 char or some other reasonable number for line length) Docstrings: Functions and classes should all have good docstrings. They can be very short if the function does something simple. @@ -241,13 +258,16 @@ Docstrings: Naming: All classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed. + Extra Ideas: ------------ In case you are bored -- what features can you add? -* How about an html report using your html_render code? - * Fancier reporting -* The sky's the limit +* More error checking -- does the user really want to create a new donor? or was it a typo in the donor name? + +* The next project is an html renderer -- maybe use it to make a nice html report? + +* The sky's the limit! diff --git a/source/exercises/mailroom/mailroom-pkg.rst b/source/exercises/mailroom/mailroom-pkg.rst index 28fe14d..faa8a62 100644 --- a/source/exercises/mailroom/mailroom-pkg.rst +++ b/source/exercises/mailroom/mailroom-pkg.rst @@ -10,29 +10,25 @@ Code Structure Start with your existing version of mailroom. -It should already be structured with the "logic" code distinct from the user interface (yes, a command line *is* a user interface). But you may have it all in one file. This isn't *too* bad for such a small program, but as a program grows, you really want to keep things separate, in a well organized package. +It may already be structured with the "logic" code distinct from the user interface (yes, a command line *is* a user interface). But you may have it all in one file. This isn't *too* bad for such a small program, but as a program grows, you really want to keep things separate, in a well organized package. The first step is to re-structure your code into separate files: - - one (or more) for the logic code: the code than manipulates the data - - one for the user-interface code: the code with the interactive loops and all the "input" and "print" statements - - one (or more) for tests. + - One (or more) for the logic code: the code that manipulates the data + - One for the user-interface code: the code with the interactive loops and all the "input" and "print" statements + - One (or more) for the unit tests. You should have all this pretty distinct after having refactored for the unit testing. If not, this is a good time to do it! -In addition to those three, you will want to write a top-level script file (perhaps called ``mailman.py``) that does little but import the ui code and run a ``main()`` function. It should look something like this: +In addition to those three, you will need a single function to call that will start the program. +That can be defined in a new file, as a "script", but for something as simple as this, it can be in with your interface code. +That file can then have an ``if __name__ == "__main__"`` block +which should be as simple as: .. code-block:: python - #!/usr/bin/env python - from mailman import cli - if __name__ == "__main__": - cli.main() - -Yes, that's it! This has the advantage of keeping the top-level script really simple, as it has to get put somewhere else and it can keep the "real" code in the package where it belongs. - -.. note:: Be careful here -- it is important not to call your top-level script the same thing as your package, in this case ``mailroom.py``. If you do, than when installed, python will find the script, rather than the package, when you do ``import mailroom``. You can call it ``mailroom`` without the Python, but that may confuse Windows. + main() Making the Package @@ -51,9 +47,10 @@ Put all these in a python package structure, something like this:: test_model.py test_cli.py -You will need to import the logic code from model.py in the cli code in order to use it. You can wait until you learn about mocking to write the code in test_cli.py (so you can leave that out) +You will need to import the logic code from model.py in the cli code in order to use it. +You can wait until you learn about mocking to write the code in test_cli.py (so you can leave that out) -Now write your ``setup.py`` to support your package. +Now write a ``setup.py`` file to support the installation of your package. Making the top-level script runnable @@ -61,17 +58,42 @@ Making the top-level script runnable To get the script installed you have two options. I used to prefer the more straightforward one, `the scripts keyword argument `_ -But it turns out that while the simple ``scripts`` keyword argument method works well and is simple, it may not work as well on Windows -- it relies in your script being named ``something.py`` and that Windows is configured to run all files with ``.py`` extensions. Not all windows systems are set up this way. But the "entry points" method builds a little exe file to call your script, so it's more reliable. +But it turns out that while the simple ``scripts`` keyword argument method works well and is simple, it may not work as well on Windows -- it relies in your script being named ``something.py`` and that Windows is configured to run all files with ``.py`` extensions. Not all windows systems are set up this way. But the "entry points" method builds a little ``.exe`` file to call your script, so it's more reliable. -And the Python community has moved very much towards using setuptools entry points, so That's really the way to go now: +And the Python community has moved very much towards using setuptools entry points, so that's really the way to go now: http://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point +In this case, that will look a lot like this: + +.. code-block:: python + + entry_points={'console_scripts': ['mailroom=mailroom.cli:main']}, + +That's a bit complicated, so I'll break it down for you. In this case, we only want a single script, but setuptools allows multiple types of entry points, and multiple instances of each type (e.g. you can have more than one script) to the code, so the argument is a potentially large dictionary. +The keys of the dict are the various types of entry points. +In this case, we want a single script that can be run in a terminal (or "console"), so we have a dict with one key: ``console_scripts``. + +The value of that entry is a list of strings -- each one describing the console script. This string is of the form:: + + SCRIPTNAME=MODULE:FUNCTION_NAME + +setuptools will create a wrapper script with the name given, and that wrapper will call the function in the module that is specified. +So: ``'mailroom=mailroom.cli:main'`` means: create a start up script called "mailroom" that will then call the ``main`` function in the ``cli`` module in the ``mailroom`` package. + +Once this is all set up, and you install the package (either in editable mode or not):: + + pip install -e ./ + +you should then be able to type "mailroom" at the command line and have your program run. + -Including data files +Including Data Files -------------------- -NOTE: If you have a database of donors in a file that you load, then that should go in the package as well. Probably inside the mailroom dir, in a ``data`` dir or similar. Then you need to add it to your setup.py to make sure it gets copied into the installed package. +If you have a database of donors in a file that you load, then that should go in the package as well. Probably inside the mailroom dir, in a ``data`` dir or similar. Then you need to add it to your setup.py to make sure it gets copied into the installed package. + +(If you are not saving the donor data to a file -- that's fine. You can ignore this section for now) There are a few ways to do this: @@ -83,7 +105,10 @@ I personally like the simplest one with the least magic: Then you'll get the data file included in the package in the same place. -Now you'll need to write your code to find that data file. You can do that by using the __file__ module attribute -- then the location of the data file will be relative to the __file__ that your code is in. A little massaging with a ``pathlib.Path`` should do it. +Now you'll need to write your code to find that data file. +You can do that by using the ``__file__`` module attribute, which is the path to a python module at run time -- then the location of the data file will be relative to the path that your code is in. +A little massaging with a ``pathlib.Path`` should do it. + Testing your Package -------------------- @@ -104,7 +129,7 @@ When that is done, you should be able to run the top-level script from anywhere: .. code-block:: bash - $ mailroom.py + $ mailroom and run the test from within the package: @@ -112,15 +137,7 @@ and run the test from within the package: $ pytest --pyargs mailroom -If you installed in editable mode, then you can update the code and re-run the tests or the script, and it will use the new code right away. - - - - - - - - - +(or run the tests from the test dir as well) +If you installed in editable mode, then you can update the code and re-run the tests or the script, and it will use the new code right away. diff --git a/source/exercises/mailroom/mailroom_tutorial.rst b/source/exercises/mailroom/mailroom_tutorial.rst index 4d9cc8e..9c2f9ff 100644 --- a/source/exercises/mailroom/mailroom_tutorial.rst +++ b/source/exercises/mailroom/mailroom_tutorial.rst @@ -136,12 +136,14 @@ Here is a potential data structure to consider: .. code-block:: python - donor_db = [("William Gates, III", [653772.32, 12.17]), + donor_db = [("William Gates, III", [100.0, 120.10]), ("Jeff Bezos", [877.33]), - ("Paul Allen", [663.23, 43.87, 1.32]), - ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]), + ("Paul Allen", [663.23, 343.87, 411.32]), + ("Mark Zuckerberg", [1660.23, 4320.87, 10432.0]), ] +Here we have the first item in a tuple as a donor name, which we will use to determine if we need to add to existing donor or add a new one and the second item is a list of donation values. + Why choose tuples for the inner donor record? Well, another part of using the right data structure is to reduce bugs; you are setting clear expectations that a single donor entry only contains two items. diff --git a/source/exercises/mailroom/mailroom_with_dicts.rst b/source/exercises/mailroom/mailroom_with_dicts.rst index b627c18..d0a46e9 100644 --- a/source/exercises/mailroom/mailroom_with_dicts.rst +++ b/source/exercises/mailroom/mailroom_with_dicts.rst @@ -11,19 +11,18 @@ Use dicts where appropriate. The first version of this assignment used these basic data types: numbers, strings, lists and tuples. -However, using dictionaries, an import built in data structure in Python, will let you re-write your program a bit more simply and efficiently. +However, using dictionaries, an important built in data structure in Python, will let you re-write your program a bit more simply and efficiently. Update your mailroom program to: - - Use dicts where appropriate. + - Convert your main donor data structure to be a dict. Think about an appropriate key for easy donor look up. - - See if you can use a dict to switch between the users selections. - see :ref:`dict_as_switch` for what this means. + - Use dicts anywhere else, as appropriate. - - Convert your main donor data structure to be a dict. + - Use a dict to switch between the users selections. + see :ref:`dict_as_switch` for what this means. - - Try to use a dict and the ``.format()`` method to produce the letter as one - big template, rather than building up a big string that produces the letter in parts. + - Use a dict and the ``.format()`` method to produce the letter as one big template, rather than building up a big string that produces the letter in parts. Example: diff --git a/source/exercises/python_pushups.rst b/source/exercises/python_pushups.rst index 39b4e5b..61f13bc 100644 --- a/source/exercises/python_pushups.rst +++ b/source/exercises/python_pushups.rst @@ -4,7 +4,8 @@ Python Pushups ############## -These are a couple exercises to kick you off with Python +These are a quick exercise to kick you off with Python: + Explore Errors ============== diff --git a/source/modules/Decorators.rst b/source/modules/Decorators.rst index 4234f11..105cc64 100644 --- a/source/modules/Decorators.rst +++ b/source/modules/Decorators.rst @@ -43,8 +43,7 @@ So many, that we give it a special name: An Example ---------- -Imagine you are trying to debug a module with a number of functions like this -one: +Imagine you are trying to debug a module with a number of functions like this one: .. code-block:: python diff --git a/source/modules/Modules.rst b/source/modules/Modules.rst index b71efd3..bfc0dd3 100644 --- a/source/modules/Modules.rst +++ b/source/modules/Modules.rst @@ -84,7 +84,7 @@ But you should strive for proper style. Isn't this easier to read? x = (3 * 4) + (12 / func(x, y, z)) -.. centered:: **Read** `**PEP 8** `_ **and install a linter in your editor.** +.. centered:: **Read** `PEP 8 `_ **and install a linter in your editor.** Modules and Packages @@ -388,11 +388,13 @@ In that case, you can simply add more "dots" and follow the same rules as above. from packagename import my_funcs.this_func -Here's a nice reference for more detail: +.. Here's a nice reference for more detail: -http://effbot.org/zone/import-confusion.htm +.. http://effbot.org/zone/import-confusion.htm -And :ref:`packaging` goes into more detail about creating (and distributing!) your own package. +.. And + +:ref:`packaging` goes into more detail about creating (and distributing!) your own package. What does ``import`` actually do? @@ -483,6 +485,6 @@ Running ``mod2.py`` results in:: Still in mod2: mod1.x = 555 mod3 changed the value in mod1, and that change shows up in mod2 -You can see that when ``mod2`` changed the value of ``mod1.x``, that changed the value everywhere that ``mod1`` is imported. You want to be very careful about this. +You can see that when ``mod3`` changed the value of ``mod1.x``, that changed the value everywhere that ``mod1`` is imported. You want to be very careful about this. If you are writing ``mod2.py``, and did not write ``mod3`` (or wrote it long enough ago that you don't remember its details), you might be very surprised that a value in ``mod1`` changes simply because you imported ``mod3``. This is known as a "side effect", and you generally want to avoid them! diff --git a/source/modules/Packaging.rst b/source/modules/Packaging.rst index 3025046..9b5524a 100644 --- a/source/modules/Packaging.rst +++ b/source/modules/Packaging.rst @@ -405,7 +405,7 @@ Current State of Packaging To build packages: setuptools ............................. - * https://pythonhosted.org/setuptools/ + * https://setuptools.readthedocs.io/en/latest/ setuptools provides extensions to the build-in distutils: diff --git a/source/topics/01-setting_up/git_workflow.rst b/source/topics/01-setting_up/git_workflow.rst index 47dc3f4..763dead 100644 --- a/source/topics/01-setting_up/git_workflow.rst +++ b/source/topics/01-setting_up/git_workflow.rst @@ -1,6 +1,7 @@ :orphan: .. NOTE: This is the "old" version, before we adopted gitHub Classroom +.. It is not currently published. .. _git_workflow: @@ -9,7 +10,7 @@ git Workflow Git is a very flexible system that can be used in a lot of different ways to manage code development. This page describes the workflow we are using for this class -- about as simple a workflow as you can have with git. -We start with an overview of the usual process. This overview may be all you need for future work, once you have created your home directory within the students directory. +We start with an overview of the usual process. This overview may be all you need for future work, once you have set up git on your workstation. The instructions following the overview are very explicit for those new to git and the command line. @@ -18,6 +19,7 @@ The usual process This is the usual series of steps you will want to go through when you do a new project / assignment. + First make sure you are on the command line "in" your copy of the class repo. Remember that ``git status`` is your friend -- when in doubt, run that command to see what's going on in your repo. diff --git a/source/topics/01-setting_up/github_classroom.rst b/source/topics/01-setting_up/github_classroom.rst index 8f54f6c..36274ee 100644 --- a/source/topics/01-setting_up/github_classroom.rst +++ b/source/topics/01-setting_up/github_classroom.rst @@ -33,7 +33,9 @@ Initial Setup ============= You will need an account on gitHub to participate in this course. -If you don't already have a gitHub account, or if you would prefer to create a new one for this course, make sure you setup a new account on `gitHub `_. Always keep in mind that your account name will be part of the private repositories that will be created for each of your assignments and it will be visible to both your instructors and your classmates. Make sure you let your instructors know what your gitHub handle is -- it's not always obvious! +If you don't already have a gitHub account, or if you would prefer to create a new one for this course, make sure you setup a new account on `gitHub `_. +Always keep in mind that your account name will be part of the private repositories that will be created for each of your assignments and it will be visible to both your instructors and your classmates. +Make sure you let your instructors know what your gitHub handle is -- it's not always obvious! You will need to have git setup on the computer you will use for developing your code for this course. You can find instructions for setting up git (and the rest of your development environment) here: @@ -142,19 +144,19 @@ b) Make a new branch: After that command, git will be "in" the develop branch -- anything you change will only be reflected in that branch. -.. note:: A git "branch" is an independent "version" of your code where you can write and change code, create and delete files, etc, and it will be kept separate from the main code. When you are happy with this version, it can be merged into the main branch. For the purposed of this course, it will not be merged into the main branch until it has been reviewed, and both you and your instructors think its done. +.. note:: A git "branch" is an independent "version" of your code where you can write and change code, create and delete files, etc, and it will be kept separate from the main code. When you are happy with this version, it can be merged into the main branch. For the purposed of this course, it will not be merged into the main branch until it has been reviewed, and both you and your instructors think it's done. If you get an error from this command that says:: fatal: A branch named 'develop' already exists -That means two things: +That means one of two things: - 1) You have already created a develop branch. IN which case you chould already be using it, or you can "check it out" again: `git checkout develop` + 1) You have already created a develop branch. In which case you should already be using it, or you can "check it out" again: `git checkout develop` or - 2) That branch was created already by gitHub classroom. Which you'd think would be nice, but it turns out that the way it's created doesn't allow the next steps: the Pull Request. THe solution in this case is to use a different name for your working branch, e.g. + 2) That branch was created already by gitHub classroom. Which you'd think would be nice, but it turns out that the way it's created doesn't allow the next steps: the Pull Request. The solution in this case is to use a different name for your working branch, e.g. :: @@ -204,7 +206,7 @@ b) Commit your work. When you have gotten to a good "pause point" in your work: (use "git restore ..." to discard changes in working directory) modified: install_test.py -note that in this case, I edited the ``install_test.py`` file after adding it. When you edit a file, git will not track those changes unless you tell it to, which you can do by running ``git add`` again. So ``git add`` tells git that you want it to keep track of that file -- called "staging for commit":: +Note that in this case, I edited the ``install_test.py`` file after adding it. When you edit a file, git will not track those changes unless you tell it to, which you can do by running ``git add`` again. So ``git add`` tells git that you want it to keep track of that file -- called "staging for commit":: $ git add install_test.py @@ -234,12 +236,12 @@ There is a trick to save a step -- you can ask git to commit all changes you've create mode 100644 another_file.py create mode 100644 install_test.py -The ``-a`` means "all". Note that you still need to use ``git add`` to ask git to track a new file that it is not already managing. +The ``-a`` means "all". Note that you still need to use ``git add`` to ask git to track a new file that it is not already managing. And be sure to run ``git status`` first to make sure you haven't accidentally added things you didn't want to. 5) Push your work to gitHub --------------------------- -All this adding and committing has only affected the repository on your own machine -- gitHub has not been changed. +All this adding and committing has only affected the repository on your own machine -- the one on gitHub has not been changed. In order to get your changes up to gitHub you need to "push" them. It's always a good idea to check the status before you push -- to make sure you're ready. :: @@ -258,7 +260,8 @@ Note that I am on the "develop" branch, which is what's wanted, and nothing new git push --set-upstream origin develop -Hmm -- "fatal" -- I don't like the look of that! But it's pretty simple, really. git is telling you that it doesn't know where to push the code to -- your gitHub version of the repo doesn't have a develop branch. But it tells you want to do to create that branch on gitHub (origin), so do that: +Hmm -- **fatal** -- I don't like the look of that! +But it's pretty simple, really. git is telling you that it doesn't know where to push the code to -- your gitHub version of the repo doesn't have a develop branch. But it tells you want to do to create that branch on gitHub (origin), so do that: :: @@ -327,7 +330,7 @@ Put a link to the PR in the LMS, to let us know that you have "turned in" the as 8) Wait for review ------------------ -Once you make your PR, your instructors will be notified by gitHub (and the LMS), and will review your code. They can make general comments, or comment line by line. When a review is done, you should get an email form gitHub. But you can always go and check the PR yourself and see if anything new is there. +Once you make your PR, your instructors will be notified by gitHub (and the LMS), and will review your code. They can make general comments, or comment line by line. When a review is done, you should get an email from gitHub. But you can always go and check the PR yourself and see if anything new is there. At this point, two things might happen. @@ -413,7 +416,7 @@ After adding the file(s), you can commit your code by typing the following:: Note that the commit message should be replaced with something descriptive of what that commit includes ("added new functionality", "fixed floating point error", "ready for review", etc.) that will later help you remember what that particular commit was about. -.. note:: If you omit the message, git will bring up a text editor to let you write one. If you have not configured git to use another editor, it will be "vi", an venerable old Unix editor that is a real challenge for some. To get out of vi, hit the >escape> key, the a colon and an x: ``:x``. You can configure git to use an editor you are familiar with. See: :ref:`install_nano_win` for how to do that on Windows. +.. note:: If you omit the message, git will bring up a text editor to let you write one. If you have not configured git to use another editor, it will be "vi", a venerable old Unix editor that is a real challenge for some. To get out of vi, hit the key, the a colon and an x: ``:x``. You can configure git to use an editor you are familiar with. See: :ref:`install_nano_win` for how to do that on Windows. After every change to the file, you will need to "commit" the changes. Keep in mind that git will not commit all the changes you have made, only the ones that are "staged for commit". You can stage them with the ``git add`` command again. So ``add`` means either "add this file" or "stage this file for committing", depending on whether it's already been added or not. @@ -460,7 +463,7 @@ When you submit a comment with a tag, the instructor will be notified by gitHub Submitting your assignment -------------------------- -Once your assignment is ready for review, copy the link of your Feedback pull request and submit it in the submission form. Here is an example of a submission link (yours will look a little different but will end with `/pull/1`):: +Once your assignment is ready for review, copy the link of your Feedback Pull Request and submit it in the submission form. Here is an example of a submission link (yours will look a little different but will end with `/pull/1`):: https://github.com/UWPCE-Py210-SelfPaced-2021/lesson-02-fizzbuzz-exercise-uw-test-student-natasha/pull/1