From aedc418afb97730e27311cd11500dcf4a87dccbe Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 12 Aug 2018 21:28:25 -0700 Subject: [PATCH 01/87] updated README --- README.rst | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 671a94f3..11c933d6 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,72 @@ This repository contains the source materials for the University of Washington P `Certificate in Python Programming `_ - These materials are available in html rendered version here: https://uwpce-pythoncert.github.io/PythonCertDevel/ + +If you find any errors, typos, etc, or have suggestions, please feel free to raise an issue or submit a pull request to this repo. + +Building These Documents +------------------------ + +These documents are written in `Restructured Text `_, and rendered with the `Sphinx Documentation system `_. + +To build the docs, you need Python, Sphinx, and sphinx-rtd-theme:: + + python3 -m pip install sphinx, sphinx-rtd-theme + +Should do it. + +Building the docs locally: +.......................... + +in the main dir, run:: + + make html + +The html docs will be generated in the ``build/html`` dir -- +``build/html/index.html`` is the main page. + +Building the docs for uploading to gh-pages +........................................... + +The html docs are published by gitHub's gh-pages. This is accomplished by putting all the html in the ``gh-pages`` branch of the repo. + +YOu can do that by hand, by copying any new html to the branch, but if you want to do it more often, it's easier to automate it. + +There is a bash script that will do it for you: ``build_gh_pages.sh``. + +It requires a bit of setup: + +* make another clone of this repo "next to" the main clone (i.e. in the same dir), but name that clone: "PythonCertDevel.gh-pages". + +* checkout the gh-pages branch in that clone:: + + git checkout gh-pages + +* it's a good idea to do a pull once to make sure you are up to date: + + git pull + +* Once this is setup, you can run the build_gh_pages script from the name repo, and it should: + + - build the html docs + - copy that build docs over to the other clone + - add any new files to the gh-pages branch + - commit the changes + - push the changes to gitHub + +Once run, the new version of the docs should be published on gitHub at: + +https://uwpce-pythoncert.github.io/PythonCertDevel/ + +**NOTE:** ``build_gh_pages`` is a pretty hacky script with limited functionality -- in particular, it does not clear out any old files that have been removed. If you remove a page, it must be removed by hand from the gh-pages branch. PRs welcome. + + + + + + + + From 181e59f442449382d380adff3f0252087f845eee Mon Sep 17 00:00:00 2001 From: UWEditor <42452826+UWEditor@users.noreply.github.com> Date: Fri, 17 Aug 2018 11:10:10 -0700 Subject: [PATCH 02/87] Update EnvironmentOverview.rst --- source/modules/EnvironmentOverview.rst | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/source/modules/EnvironmentOverview.rst b/source/modules/EnvironmentOverview.rst index 44e1d295..1f075932 100644 --- a/source/modules/EnvironmentOverview.rst +++ b/source/modules/EnvironmentOverview.rst @@ -12,7 +12,7 @@ There are three basic elements to your environment when working with Python: The Command Line (cli) ---------------------- -Having some facility on the command line is important +Having some facility on the command line is important. We won't cover this much in class, so if you are not comfortable, please bone up on your own. @@ -25,13 +25,13 @@ http://cli.learncodethehardway.org/book/ **Windows:** -Most of the demos in class, etc, will be done using the "bash" command line shell on OS-X. This is identical to the bash shell on Linux. +Most of the demos in class, etc., will be done using the "bash" command line shell on OS-X. This is identical to the bash shell on Linux. -Windows provides the "DOS" command line, which is OK, but pretty old and limited, or "Power Shell" -- a more modern, powerful, flexible command shell. +Windows provides the "DOS" command line, which is OK, but pretty old and limited, or "Power Shell," a more modern, powerful, flexible command shell. -If you are comfortable with either of these -- go for it. +If you are comfortable with either of these, go for it. -If not, you can use the "git Bash" shell -- which is much like the bash shell on OS-X and Linux. Or, on Windows 10, look into the "bash shell for Windows" otherwise known as the "Windows Subsystem for Linux" - - more info here: `PythonResources--Windows Bash `_ +If not, you can use the "git Bash" shell, which is much like the bash shell on OS-X and Linux. Or, on Windows 10, look into the "bash shell for Windows," otherwise known as the "Windows Subsystem for Linux." More info is available here `PythonResources--Windows Bash `_ The Interpreter @@ -49,7 +49,7 @@ You see it when you type ``python`` at the command line: Type "help", "copyright", "credits" or "license" for more information. >>> -That last thing you see, ``>>>`` is the "Python prompt". +That last thing you see, ``>>>`` is the "Python prompt." This is where you type code. @@ -114,7 +114,7 @@ This allows you quite a bit of latitude in exploring what Python is. In addition to the built-in interpreter, there are several more advanced interpreters available to you. -We'll be using one in this course called ``iPython`` +We'll be using one in this course called ``iPython``. More on this soon. @@ -124,10 +124,10 @@ The Editor Typing code in an interpreter is great for exploring. -But for anything "real", you'll want to save the work you are doing in a more permanent +But for anything "real," you'll want to save the work you are doing in a more permanent fashion. -This is where an Editor fits in. +This is where an editor fits in. Any good text editor will do. @@ -136,7 +136,7 @@ MS Word is **not** a text editor. Nor is *TextEdit* on a Mac. -``Notepad`` on Windows is a text editor -- but a crappy one. +``Notepad`` on Windows is a text editor, but a crappy one. You need a real "programmers text editor" @@ -190,8 +190,7 @@ Why Not an IDE? That said ... -You may want to go get the educational edition of PyCharm: +You may want to go get the educational edition of PyCharm, which is awesome: https://www.jetbrains.com/pycharm-edu/ -Which is awesome. From f505745ff3f3ff40d20ba9bf7d1c604d31fc65f2 Mon Sep 17 00:00:00 2001 From: Natasha Date: Sun, 7 Oct 2018 18:27:41 -0700 Subject: [PATCH 03/87] Update python_for_windows.rst (#167) --- source/supplemental/installing/python_for_windows.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/supplemental/installing/python_for_windows.rst b/source/supplemental/installing/python_for_windows.rst index 11c42411..b35f77c5 100644 --- a/source/supplemental/installing/python_for_windows.rst +++ b/source/supplemental/installing/python_for_windows.rst @@ -99,6 +99,8 @@ Type ``ctrl+Z`` to get out (or ``exit()``) Note: if you have trouble running ``python`` command in your gitbash (it hangs), try running this instead: ``winpty python``. To avoid having to type ``winpty python`` all the time, it's strongly recommended that you create an alias like below: +:: + $ echo "alias python='winpty python'" >> ~/.bashrc You will need to close the current bash window and restart a new one to get this alias. Then from now on, you can just type ``python`` and it should work on git bash (no more hanging) as well. From 5511bdff6f99fb4e17d594789a40e41309556dc2 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 7 Oct 2018 19:29:17 -0700 Subject: [PATCH 04/87] fixed a cross-reference --- source/exercises/mailroom-oo.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/exercises/mailroom-oo.rst b/source/exercises/mailroom-oo.rst index fc377b80..74d61e02 100644 --- a/source/exercises/mailroom-oo.rst +++ b/source/exercises/mailroom-oo.rst @@ -9,7 +9,7 @@ Goal: Refactor the mailroom program using classes to help organize the code. The functionality is the same as the earlier mailroom: -:ref:`exercise_mailroom` +:ref:`exercise_mailroom_part1` But this time, we want to use an OO approach to better structure the code to make it more extensible. @@ -37,7 +37,7 @@ As you design appropriate classes, keep in mind these three guidelines for good The Program ----------- -See: :ref:`exercise_mailroom` to remind yourself what the program needs to do. +See: :ref:`exercise_mailroom_part1` to remind yourself what the program needs to do. Suggestions From 85eafa833235356ca54e8d209dcdb2cef8add84f Mon Sep 17 00:00:00 2001 From: "Christopher H.Barker, PhD" Date: Sun, 7 Oct 2018 19:32:39 -0700 Subject: [PATCH 05/87] a bit more htmlrender tutorial (#168) --- source/exercises/html_renderer_tutorial.rst | 3 ++- source/modules/Class_introduction.rst | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/source/exercises/html_renderer_tutorial.rst b/source/exercises/html_renderer_tutorial.rst index d2459ffa..c75f0c7e 100644 --- a/source/exercises/html_renderer_tutorial.rst +++ b/source/exercises/html_renderer_tutorial.rst @@ -1057,7 +1057,6 @@ OK, I've got 11 tests passing now. How about you? Time for the next step. .. _render_tutorial_4: - Step 4. ------- @@ -1591,7 +1590,9 @@ You've gotten to the end of the project. By going through this whole procedure, * overriding: - class attributes + - class attributes with instance attributes + - methods * calling a superclass' method from an overridden method. diff --git a/source/modules/Class_introduction.rst b/source/modules/Class_introduction.rst index 80b788b9..16d6dd9d 100644 --- a/source/modules/Class_introduction.rst +++ b/source/modules/Class_introduction.rst @@ -36,9 +36,9 @@ Who are you? Introduction to This Class ========================== -The overall class is managed by a learning managment system -- Canvs or EdX. +The overall class is managed by a learning managment system -- Canvas or EdX. -You have gotten a link to the instance for the class sent to you. IN fact, you probably found this page with a link from there. +You have gotten a link to the instance for the class sent to you. Class Structure --------------- From 5cfe293b1290390c877bc56d84b143c19a916b16 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 8 Oct 2018 14:02:07 -0700 Subject: [PATCH 06/87] moved lightning talk discussion to class intro --- source/class_schedule/session_1_01.rst | 2 - source/modules/Class_introduction.rst | 66 ++++++++++++++++++++------ source/modules/lightning_talks.rst | 37 --------------- 3 files changed, 52 insertions(+), 53 deletions(-) delete mode 100644 source/modules/lightning_talks.rst diff --git a/source/class_schedule/session_1_01.rst b/source/class_schedule/session_1_01.rst index 7d924862..85598b2e 100644 --- a/source/class_schedule/session_1_01.rst +++ b/source/class_schedule/session_1_01.rst @@ -40,8 +40,6 @@ In-class Activities ../modules/Git - ../modules/lightning_talks - ../modules/BasicPython ../modules/Py2vsPy3 diff --git a/source/modules/Class_introduction.rst b/source/modules/Class_introduction.rst index 16d6dd9d..cbf1abeb 100644 --- a/source/modules/Class_introduction.rst +++ b/source/modules/Class_introduction.rst @@ -18,14 +18,21 @@ In which you are introduced to this class, your instructors, your environment an Who are we? ------------ +=========== +Introduction to your instructors. Who are you? ------------ +Despite the common myth of the lone programmer, most software development is a collaborative activity. As such, we encourage students in this program to work together whenever possible. - Tell us a tiny bit about yourself: +As you will be working with your fellow students for the rest of the program +Take a couple minutes now to get to know your neighbors -- what is their favorite coffee shop or bar? + +Then we'll go around the room and introduce ourselves: + +Tell us a tiny bit about yourself: * name * programming background: what languages have you used? @@ -36,7 +43,7 @@ Who are you? Introduction to This Class ========================== -The overall class is managed by a learning managment system -- Canvas or EdX. +The overall class is managed by a learning management system -- Canvas or EdX. You have gotten a link to the instance for the class sent to you. @@ -49,7 +56,7 @@ for this program. This means that the "homework" will be reading, watching videos, coding, etc. -And class time will be spent primarly coding: +And class time will be spent primarily coding: * Still some lecture -- as little as possible * Lots of demos @@ -58,9 +65,9 @@ And class time will be spent primarly coding: - In small groups - Instructor led. -This means that you are expected to complete the reading BEFORE each class. That way, we don't have to take class time introducing the basic material and can focus on questions and applying what you've read about. +This means that you are expected to complete the reading (and video watching) BEFORE each class. That way, we don't have to take class time introducing the basic material and can focus on questions and applying what you've read about. -Interrupt with questions -- please! +Interrupt us with questions -- please! (Some of the best learning prompted by questions) @@ -71,7 +78,7 @@ Homework: * Exercises will be started in class -- but you can finish them at home. -* You are adults -- it's up to you to do the homework. But if you dont code, you won't learn to code... +* You are adults -- it's up to you to do the homework. But if you don't code, you won't learn to code... * To submit your work, you can do a gitHub "pull request" to the class repo. @@ -80,17 +87,14 @@ There is a video about that, and we will show you in the first class as well. Communication ------------- -**Mailing list:** - -You should have been subscribed to the mailing list, if not talk to your instructors. +**MS Teams:** -We will be using this list to communicate with you. If you have not gotten notes from the list -- make sure to let your instructors know, and we'll make sure to add you. +There has been an MS Team channel set up for this class. You should have been invited to join -- if not, let your instructors know. -Slack may set up a slack channel for discussions. Anything python related is fair game. We'll poll the class in the first session to decide if that would be helpful. +Anything Python related is fair game. Keep general discussion about the class or Python in the main channel, and maybe set up separate channels for particular assignments. -We highly encourage you to work together. You will learn at a much deeper level if you work together, and it gets you ready to collaborate with colleagues. If you have never used slack before, you may want to look at their intro: +We highly encourage you to work together. You will learn at a much deeper level if you work together, and it gets you ready to collaborate with colleagues. -https://get.slack.help/hc/en-us/articles/115004071768 Office Hours ------------ @@ -102,3 +106,37 @@ Please feel free to attend even if you do not have a specific question. It is an What are good times for you? And what locations? + +.. _lightning_talks: + +Lightning Talks +=============== + +"Lightning Talks" are a tradition in open-source technical conferences (and maybe others?). The idea is that people can do a quick talk about a topic of their choice -- much lower pressure than a "real" talk -- but gives folks a chance to show off something they have worked on. + +For this class, it's a chance to us to learn a bit about each-other and maybe something new about Python. + +Each of you will be required to give one lightning talk at some point during the course. + +**Lightning Talks Requirements** + + * 5 minutes each (including setup) - no kidding! + * Every student will give one + * Purposes: introduce yourself, share interests, show Python applications + * Any topic you like that is related to Python -- according to you! + +Schedule the lightning talks: +----------------------------- + +We need to schedule your lightning talks. + +**Let's use Python for that !** + +There is a class list in the class repo here: + +``examples/session01/students.txt`` + +Let's write a script to generate a random talk schedule... + + + diff --git a/source/modules/lightning_talks.rst b/source/modules/lightning_talks.rst deleted file mode 100644 index 4c43d469..00000000 --- a/source/modules/lightning_talks.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. _lightning_talks: - -######################## -Schedule Lightning talks -######################## - -Lightning Talks ----------------- - -"Lightning Talks" are a tradition in open-source technical conferences (and maybe others?). The idea is that people can do a nice, quick talk about a topic of their choice -- much lower pressure than a "real" talk -- but gives folks a chance to show off something they have worked on. - -For this class, it's a chance to us to learn a bit about each-other and maybe something new about Python. - -**Lightning Talks Requirments** - - * 5 minutes each (including setup) - no kidding! - * Every student will give one - * Purposes: introduce yourself, share interests, show Python applications - * Any topic you like that is related to Python -- according to you! - -Schedule the lightning talks: ------------------------------ - -* We need to schedule your lightning talks. - -* **Let's use Python for that !** - -There is a class list in the class repo here: - -``examples/session01/students.txt`` - -Let's write a script to generate a random talk schedule... - - - - - From 2d624918368ec0c2673f8bc3398887e2ac0aad6e Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 17 Oct 2018 23:08:30 -0700 Subject: [PATCH 07/87] added note about detached HEAD in git_hints --- .../dev_environment/git_hints.rst | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/source/supplemental/dev_environment/git_hints.rst b/source/supplemental/dev_environment/git_hints.rst index 8c19196a..7221e9c2 100644 --- a/source/supplemental/dev_environment/git_hints.rst +++ b/source/supplemental/dev_environment/git_hints.rst @@ -248,8 +248,43 @@ There is a saying in the git world: It's a good way to work -- branching and merging is easy enough it git that it pays off to do it often. +"detached HEAD" +--------------- +Above, we talked about using ``git checkout`` to restore a file to the state it was in in a previous commit, like so:: + git checkout 8e5908a37d7d examples/Session05/maillroom_test.py + +But what happens if you do a checkout with a commit, and no specific file? + +It does what you might expect -- puts ALL the files back the way they were at that commit. But there is a hitch ... let's see what happens when I do that:: + + $ git checkout c03bb5b2c401c + Note: checking out 'c03bb5b2c401c'. + + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by performing another checkout. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -b with the checkout command again. Example: + + git checkout -b + + HEAD is now at c03bb5b adding print_grid from class + +So the files are set to the old state -- but now there is that note about "detached HEAD" -- this means that changes you make, even commits, will not effect the git repo. IF you want to start from here and make changes that will stick, you need to do what it says, and make a new branch. But what it DOESN'T tell you is how to simpel "re-attach" the HEAD. Turns out there is an easy way:: + + $ git checkout - + Previous HEAD position was c03bb5b adding print_grid from class + Switched to branch 'master' + Your branch is up to date with 'origin/master'. + +the dash means "the branch or commit you were on before your last checkout command". + +For more info about "detached HEAD", see: + +https://howtogit.net/recipes/getting-out-of-detached-head-state.html From 8a59ce9b1f5836590b02d6ba54ea452d4736a324 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 17 Oct 2018 23:09:38 -0700 Subject: [PATCH 08/87] assorted updates -- mostly copy editing --- source/modules/Class_introduction.rst | 2 +- source/modules/Git.rst | 12 ++- source/modules/ObjectOrientationOverview.rst | 37 +++---- source/modules/Py2vsPy3.rst | 17 ++- source/modules/PythonClasses.rst | 104 ++++++++++++++++++- source/modules/SubclassingAndInheritance.rst | 10 +- 6 files changed, 141 insertions(+), 41 deletions(-) diff --git a/source/modules/Class_introduction.rst b/source/modules/Class_introduction.rst index cbf1abeb..308326bd 100644 --- a/source/modules/Class_introduction.rst +++ b/source/modules/Class_introduction.rst @@ -78,7 +78,7 @@ Homework: * Exercises will be started in class -- but you can finish them at home. -* You are adults -- it's up to you to do the homework. But if you don't code, you won't learn to code... +* You are adults -- it's up to you to do the homework. But if you don't code, you won't learn to code. And we can't give you a certificate if you haven't demonstrated that you've done the work. * To submit your work, you can do a gitHub "pull request" to the class repo. diff --git a/source/modules/Git.rst b/source/modules/Git.rst index 069f85da..a7a6bb26 100644 --- a/source/modules/Git.rst +++ b/source/modules/Git.rst @@ -26,7 +26,7 @@ That last one is a bit tricky, and is not necessary to understand right out of t There are other versioning systems, such as Mercurial and Subversion (and commercial offerings), but Git is the most popular. -It is incredibly important to learn and understand versioning control to work as a developer today, so we have incorporated Git into our work flow for submitting students' work in this class. +It is incredibly important to learn and understand version control to work as a developer today, so we have incorporated Git into our work flow for submitting students' work in this class. Setting up Git @@ -86,7 +86,7 @@ Repositories A repository is just a collection of files that 'belong together'. Since ``git`` is a *distributed* versioning system, there is no **primary** -repository that serves as the one to rule them all. This simply means that all repositories should look the same. +repository that serves as the one to rule them all. This simply means that all repositories on each users machine should look the same. However, to keep things sane, there is generally one "central" repository chosen that users check with for changes. For us this is the one hosted on GitHub in the UWPCE-PythonCert-ClassRepos organization. @@ -124,9 +124,11 @@ The class repositories are on *GitHub* in the *UWPCE-PythonCert-ClassRepos* orga :width: 50% :class: center +https://github.com/UWPCE-PythonCert-ClassRepos + Each class will have a repository created specifically for it, called something like: "Wi2018-Online". -In examples below it is called YourClassRepoNameHere, so replace that in your head with the name of your class's repository. +In examples below it is called YourClassRepoNameHere, so replace that in your head with the name of your class' repository. This class repository will include examples and relevant materials (and some exercise solutions) will be added throughout the class. @@ -172,7 +174,7 @@ From that directory, run $ git clone https://github.com/your_github_id/YourClassRepoNameHere -Be sure to replace "YourClassRepoNameHere" with the name of your class repository (you can get the url by going to the class repo on gitHub and clicking “clone or download”). +Be sure to replace "YourClassRepoNameHere" with the name of your class repository (you can get the url by going to the class repo on gitHub and clicking "clone or download"). This will download the repository from your GitHub account into the local directory that you ran the git *clone* command from. @@ -191,6 +193,8 @@ For this class, you will add an *upstream* remote to our local copy that points ``UWPCE-PythonCert-ClassRepos`` account, and we will call it, appropriately, "upstream". Change directories into your local version of the class repository and run (remembering to use the name of your class): +(you can get that full url by going to GitHub, finding the repo, and copying the "clone" url) + .. code-block:: bash $ git remote add upstream https://github.com/UWPCE-PythonCert-ClassRepos/YourClassRepoNameHere diff --git a/source/modules/ObjectOrientationOverview.rst b/source/modules/ObjectOrientationOverview.rst index 792a2da3..4abb9dc6 100644 --- a/source/modules/ObjectOrientationOverview.rst +++ b/source/modules/ObjectOrientationOverview.rst @@ -31,10 +31,9 @@ What is Object Oriented Programming? within a set of functions designed to ensure that the data are used appropriately, and to assist in that use" - | -http://en.wikipedia.org/wiki/Object-oriented_programming +- http://en.wikipedia.org/wiki/Object-oriented_programming **Even simpler:** @@ -47,7 +46,8 @@ This is the core of "encapsulation" The Dominant Model ------------------ -OO is the dominant model for the past couple decades, but it is not the only model, and languages such as Python increasingly mix and blend models. +OO is the dominant model for the past couple decades, but it is not the only model, and languages such as Python increasingly mix and blend models (such as Procedural, Object Oriented, Functional). In Python, it is best to choose the approach that best solves your problem. + Object Oriented Concepts ------------------------ @@ -56,7 +56,7 @@ These are all terms you will hear when talking about Object Oriented Programming Class - A category of objects: particular data and behavior: for example, a "circle" (same as a type in Python). + A category of objects: particular data and behavior: for example, a "circle" (same as a "type" in Python). Instance A particular object of a class: a specific circle. @@ -66,18 +66,18 @@ Object Attribute Something that belongs to an object (or class): generally thought of - as a variable, or single object, as opposed to a ... + as a simple value, variable, or single object, as opposed to a ... Method - A function that belongs to a class. In Python, functions *are* semantically the same as any other type -- so all methods are "attributes", but not all attributes are methods --methods are the functions -- or more strictly speaking the 'callable' attributes. + A function that belongs to a class. In Python, functions *are* semantically the same as any other type -- so all methods are "attributes", but not all attributes are methods. Methods are the functions, or more strictly speaking: the 'callable' attributes. Encapsulation The approach where the details of the structure are "hidden" in a class -- the user of the class does not need to know how the data is stored (and may not be able to know...) Data Protection - This is the cocept that classes can hide data from outside access (sometimes called "private" attributes. Python does not strictly support data protection. + This is the concept that classes can hide data from outside access (sometimes called "private" attributes. Python does not strictly support data protection. -Class vs Instance attributes +Class vs. Instance Attributes Attributes can be attached to a class -- that is, shared by all instances of that class, or they can be attached to only that instance. Subclassing @@ -99,20 +99,16 @@ Is Python a "True" Object-Oriented Language? What are its strengths and weaknesses vis-a-vis OO? -Python does support all of the above concepts (except data protection). - But it does not support *full* encapsulation, i.e., it does not require classes, and classes don't have "private" attributes. **but ...** Folks can't even agree on what OO "really" means. -See: The Quarks of Object-Oriented Development +See: `The Quarks of Object-Oriented Development `_ - Deborah J. Armstrong -http://ontheturingtest.blogspot.com/2013/11/the-quarks-of-objected-orientation-la.html - Object Oriented Design ---------------------- @@ -137,13 +133,12 @@ Python's roots You can do OO in C ------------------ -Which today is not considered an OO Language. - -See the GTK+ project. +Which today is not considered an OO Language. See the GTK+ project. So OO is really a design approach -- putting the data together with the functions that manipulate that data. It isn't defined by language features. -That being said: OO languages give you some handy tools to make it easier (and safer): +That being said: OO languages give you some handy tools to make it easier +(and safer): * Polymorphism (duck typing gives you this) * Inheritance @@ -178,10 +173,4 @@ As you learn what is possible, this will all start to make more sense. So time to move on to how to actually **do** OO in Python! - - - - - - - +Here's how to do it in Python: :ref:`python_classes` diff --git a/source/modules/Py2vsPy3.rst b/source/modules/Py2vsPy3.rst index 02d537be..555b38d1 100644 --- a/source/modules/Py2vsPy3.rst +++ b/source/modules/Py2vsPy3.rst @@ -38,7 +38,7 @@ So -- if you get this error, simply add the parentheses: .. code-block:: ipython - In [16]: print ("this") + In [16]: print("this") this Division @@ -76,6 +76,19 @@ And in Python2, you can get the behavior of Python3 with "true division": For the most part, you just need to be a bit careful with the rare cases where Python2 code counts on integer division. +Iterators vs Lists +------------------ + +In Python2, a number of functions returned a full list of the contents. But most of the time, you didn't need a list -- you only needed a way to loop through all the items returned. Such an object is called an "iterable" -- more about that later in the class. But for now, if you get an error like:: + + TypeError: 'dict_keys' object does not support indexing + +Then you likely got an iterator, rather than a "proper" list. You can fix this by making a list out of it:: + + list(an_iterator) + +the list constructor will make a list out of any iterable. So you can now index it, etc. + Other Python2 / Python3 differences ----------------------------------- @@ -85,4 +98,4 @@ Most of the other differences are essentially implementation details, like getti There are also a few syntax differences with more advanced topics: Exceptions, ``super()``, etc. -We'll talk about all that when we cover those topics. +We'll talk about all that when we cover those topics as well. diff --git a/source/modules/PythonClasses.rst b/source/modules/PythonClasses.rst index 803eaa5f..eebd5856 100644 --- a/source/modules/PythonClasses.rst +++ b/source/modules/PythonClasses.rst @@ -238,12 +238,57 @@ Example: In [10]: c1.y is c2.y # does each instance see the same y? Out[10]: False +But what are the consequences of this? It's a **very** important distinction. watch what happens if we change something in these objects, adding a new item to both the lists in ``c1``: + +.. code-block:: ipython + + # add an item to c1's x list + In [5]: c1.x.append(100) + + In [6]: c1.x + Out[6]: [1, 2, 3, 100] + + In [7]: c2.x + Out[7]: [1, 2, 3, 100] + +Note that adding something to ``c1.x`` also changed ``c2.x`` that is because they are the *same* list -- ``.x`` is a *class attribute* -- c1 and c2 share the same class, so they share the same class attributes. + +But if we change ``y``, an instance attribute: + +.. code-block:: ipython + + In [8]: c1.y.append(200) + + In [9]: c1.y + Out[9]: [4, 5, 6, 200] + + In [10]: c2.y + Out[10]: [4, 5, 6] + +appending to ``c1.y`` did not change ``c2.y`` -- ``y`` in this case is a an *instance* attribute -- each instance has its own version -- changing one will not affect the others. + +So when you are deciding where to "put" something, you need to think about whether all instances are the same, or if they each need their own version of the attribute. As a class attribute, you can access it from the class namespace as well, and it will affect all instances of that class: + +.. code-block:: python + + In [11]: C.x.append(2222) + + In [12]: c1.x + Out[12]: [1, 2, 3, 100, 2222] + + In [13]: c2.x + Out[13]: [1, 2, 3, 100, 2222] + +So here we changed ``x`` on the *class* object, ``C``, and the change showed up in all the instances, ``c1`` and ``c2``. + Typical methods --------------- .. code-block:: python + import math + class Circle: color = "red" @@ -252,12 +297,25 @@ Typical methods def expand(self, factor=2): self.diameter = self.diameter * factor + return None # note that if you leave that off, it will still return None + + def area(self): + area = (self.diameter / 2)**2 * math.pi + return area -Methods take some parameters, manipulate the attributes in ``self``. +Methods take some parameters, and possibly manipulate the attributes in ``self``. + +Remember that classes are about encapsulating the data and the functions that act on that data -- the methods are the functions that act on the data. They may or may not return something useful. +.. note:: + + It is convent in Python that methods that change the internal state of an object return None, whereas methods that return a new object, or some calculated result without changing the state return that value. + + You can see examples of this in the python built ins -- methods of lists like ``append`` or ``sort`` return None. + Gotcha ! -------- @@ -278,13 +336,49 @@ Huh???? I only gave 2: ``self`` is implicitly passed in for you by Python. so it actually *did* get three! -Functions (methods) are First Class ------------------------------------ +Functions (methods) are First Class Objects +------------------------------------------- Note that in Python, functions are first class objects, so a method *is* an attribute. -All the same rules apply about attribute access: note that the methods are defined in the class -- so they are class attributes. All the instances share the same methods. +All the same rules apply about attribute access: note that the methods are defined in the class -- so they are class attributes. +All the instances share the same methods. + +But each method gets its own namespace when it is actually called, so there is no confusion -- just like when you call a regular function multiple times. + +Manipulating Attributes +----------------------- + +Python makes it very easy to manipulate object's attributes -- you can access them with the "dot" notation, and simply set them like any other variable. With the Circle class above: + +.. code-block:: python + + In [15]: c = Circle(2) + + In [16]: c.area() + Out[16]: 3.141592653589793 + + In [17]: c.diameter = 4 + + In [18]: c.area() + Out[18]: 12.566370614359172 + +Note that after I changed the diameter attribute, when I called the ``area()`` method --it used the new diameter. Simple attribute access changed the state of the object. + +So you now know how to: + + * Define a class + * Give the class shared (class) attributes + * Add an initializer to set up it's initial state + * Add methods to manipulate that state. + * Add methods that return the results of calculations of the current state + +You can do a lot with this simple functionality -- but all it's done is put everything together in a neat package -- useful, but the real power of OO comes when you can subclass. So time to move on: + +:ref:`subclassing_inheritance` + + + -But each method gets its own namespace when it is actually called, so there is no confusion-- just like when you call a regular function multiple times. diff --git a/source/modules/SubclassingAndInheritance.rst b/source/modules/SubclassingAndInheritance.rst index 1bc3247b..edc7d154 100644 --- a/source/modules/SubclassingAndInheritance.rst +++ b/source/modules/SubclassingAndInheritance.rst @@ -26,7 +26,7 @@ A subclass "inherits" all the attributes (methods, etc) of the parent class. You can then change ("override") some or all of the attributes to change the behavior. -You can also add new attributes to extend the behavior. +You can also add new attributes to extend the behavior. You create a subclass by passing the superclass to the class statement. The simplest subclass in Python: @@ -82,13 +82,13 @@ Same thing, but with methods (remember, a method *is* an attribute in Python) all the instances will have the new method. -Here's a program design suggestion: +**Here's a program design suggestion:** -Whenever you override a method, the interface of the new method should be the same as the old. It should take the same parameters, return the same type, and obey the same preconditions and postconditions. + Whenever you override a method, the interface of the new method should be the same as the old. It should take the same parameters, return the same type, and obey the same preconditions and postconditions. -If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand. If you violate this rule, your code will collapse like (sorry) a house of cards. + If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand. If you violate this rule, your code will collapse like (sorry) a house of cards. -Overriding \_\_init\_\_ +Overriding ``__init__`` ----------------------- ``__init__`` is a common method to override. From 38439c0d5e59ebc68c45227f9c6bc31bb3ac188f Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Thu, 18 Oct 2018 08:45:40 -0700 Subject: [PATCH 09/87] Remove invalid link in exercises/string_formatting.rst (#170) The Canvas link doesn't work except for that specific class, so I think it should be removed. Or, please feel free to add the correct link. Thanks. --- source/exercises/string_formatting.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/source/exercises/string_formatting.rst b/source/exercises/string_formatting.rst index 5af98291..6e0c45b1 100644 --- a/source/exercises/string_formatting.rst +++ b/source/exercises/string_formatting.rst @@ -10,11 +10,7 @@ In this exercise we will reinforce the important concepts of string formatting, Procedure ========= -Be sure to follow all the steps described in the Procedure section at: - -https://canvas.uw.edu/courses/1200526/assignments/3970003?module_item_id=7963708 - -but this time, creating a new file called strformat_lab.py in your student dir in the class repo. +Create a new file called ``strformat_lab.py`` in your student dir in the class repo. When the empty script is available and runnable, complete the following four tasks. From 3684d94d6a0f283ff92b3a5a618226678769c335 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Thu, 18 Oct 2018 22:21:32 -0700 Subject: [PATCH 10/87] Update sum_series() comment to avoid unexpected confusion (#171) The original comment caused confusion to a lot of students in my class, so proposing this rewrite. --- source/exercises/series_template.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/source/exercises/series_template.py b/source/exercises/series_template.py index 8af982d0..0a87f509 100644 --- a/source/exercises/series_template.py +++ b/source/exercises/series_template.py @@ -21,10 +21,11 @@ def sum_series(n, n0=0, n1=1): :param n0=0: value of zeroth element in the series :param n1=1: value of first element in the series - - if n0 == 0 and n1 == 1, the result is the Fibbonacci series - - if n0 == 2 and n1 == 1, the result is the Lucas series + + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). """ pass From ca1b284dcfb5b15f250e1a4c3aca2e769ef82a1d Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 18 Oct 2018 22:28:46 -0700 Subject: [PATCH 11/87] updated to be more accurate in shortcutting examples. --- source/modules/Booleans.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/source/modules/Booleans.rst b/source/modules/Booleans.rst index 0667806a..26208e77 100644 --- a/source/modules/Booleans.rst +++ b/source/modules/Booleans.rst @@ -45,8 +45,10 @@ This is similar to making an integer out of another value, such a float or a str In [2]: int("345") Out[2]: 345 +``bool(something)`` will always evaluate to either ``True`` or ``False``. -What is False? + +What is Falsy? -------------- * ``None`` @@ -59,7 +61,7 @@ What is False? - Any empty sequence, for example, ``"", (), []``. - - Any empty mapping, for example, ``{}`` . + - Any empty mapping, for example, ``dict()``. - Instances of user-defined classes: @@ -71,8 +73,8 @@ What is False? (Don't worry about that last one -- what that means is that user-defined types can control their truthiness behavior). -What is True? -------------- +What is Truthy? +--------------- Everything else. @@ -141,7 +143,7 @@ of this operand: Shortcutting ------------ -If you think about it, what ``and`` and ``or`` are doing is as little work as possible. They will only evaluate as much as they need to get the answer. +``and`` and ``or`` returning teh first value that determines the result is known as "shortcutting". If you think about it, what ``and`` and ``or`` are doing is as little work as possible. They will only evaluate as much as they need to get the answer. Think about ``and``: it is testing if *both* the operands are True. If the first one is False, there is no need to bother checking the second. @@ -171,21 +173,20 @@ In this case, the second expression needs to be evaluated -- so it DID raise an This can be exploited to provide compact logic -- but it can also hide bugs! - Because of the return value of the boolean operators, you can write concise -statements: +statements, rather than a full ``if -- else`` block like so: :: - if x is False: + if bool(x) is False: x or y return y else: return x - if x is False: + if bool(x) is False: x and y return x else: return y - if x is False: + if bool(x) is False: not x return True else: return False From a70033d95d9317952e66bee5e8366f00c40c2ece Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Fri, 19 Oct 2018 18:54:09 -0700 Subject: [PATCH 12/87] Fixes some typos in Lesson 4 pages (#172) --- source/conf.py | 2 +- source/exercises/kata_fourteen.rst | 14 +++++++------- source/exercises/mailroom-part2.rst | 2 +- source/modules/DictsAndSets.rst | 4 ++-- source/modules/Files.rst | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/source/conf.py b/source/conf.py index b27ab256..013802c2 100644 --- a/source/conf.py +++ b/source/conf.py @@ -66,7 +66,7 @@ "Andy Miles" "Rick Riehle", "Joseph Schilz", - "Joseph Sheedy" + "Joseph Sheedy", "Hosung Song" ] diff --git a/source/exercises/kata_fourteen.rst b/source/exercises/kata_fourteen.rst index a2ad4f5f..999084a7 100644 --- a/source/exercises/kata_fourteen.rst +++ b/source/exercises/kata_fourteen.rst @@ -212,7 +212,7 @@ Building the Trigrams dict So you've got a list of words, and you need to build up a dict like one of the above. -It time to create a python file and start writting some code! +It's time to create a python file and start writting some code! .. code-block:: python @@ -244,7 +244,7 @@ So how do you actually build up that dict? That's kind of the point of the exerc **Looping through the words** -Obviously you need to loop through all the words, so a ``for loop`` makes sense. However, this is a bit tricky. Usually in Python you loop through all the items in a list, and don't worry about the indices: +Obviously you need to loop through all the words, so a ``for`` loop makes sense. However, this is a bit tricky. Usually in Python you loop through all the items in a list, and don't worry about the indices: .. code-block:: python @@ -256,7 +256,7 @@ So contrary to the usual practice, an index can be helpful here: .. code-block:: python - for i in len(words)-2: # why -2 ? + for i in range(len(words)-2): # why -2 ? pair = words[i:i + 2] follower = words[i + 2] @@ -264,7 +264,7 @@ So contrary to the usual practice, an index can be helpful here: For each pair in the text, you need to add it to the dict. But: -- words[i:i + 2] is a list with two words in it. Can that be used as a key in a dict? (Try it.) If not, how can you make a valid key out of it? +- ``words[i:i + 2]`` is a list with two words in it. Can that be used as a key in a dict? (Try it.) If not, how can you make a valid key out of it? - As you loop through the text, you will collect pairs of words. Each time, a given pair may already be in the dict. @@ -272,7 +272,7 @@ For each pair in the text, you need to add it to the dict. But: ("may", "I"): ["wish"] - - If the pair already is in the dict, then you want to add the follower (the second word in the pair) to the list that's already there + - If the pair already is in the dict, then you want to add the follower (the second word in the pair) to the list that's already there:: ("wish", "I"): ["may", "might"] @@ -294,7 +294,7 @@ Using the Trigrams dict This is the fun part. Once you have a mapping of word pairs to following words, you can build up some new "fake" text. Re-read the previous sections again to remind yourself of the procedure. Here are a couple of additional hints and questions to consider: -- The ```random`` module `_ is your friend here: +- The ``random`` module is your friend here: .. code-block:: python @@ -357,7 +357,7 @@ Do get the full trigrams code working first, then play with some of the fancier Code Structure -------------- -Break your code down into a handful of separate functions. This way you can test each on its own, and it's easier to refactor one part without messing with the others. For instance, your __main__ block might look something like: +Break your code down into a handful of separate functions. This way you can test each on its own, and it's easier to refactor one part without messing with the others. For instance, your ``__main__`` block might look something like: .. code-block:: python diff --git a/source/exercises/mailroom-part2.rst b/source/exercises/mailroom-part2.rst index 7e94ccdb..36a1a600 100644 --- a/source/exercises/mailroom-part2.rst +++ b/source/exercises/mailroom-part2.rst @@ -50,7 +50,7 @@ In the first version of mailroom, you generated a letter to a donor who had just In this version of your program, add a function (and a menu item to invoke it), that goes through all the donors in your donor data structure, generates a thank you letter for each donor, and writes each letter to disk as a text file. -Your main menu may look something like: +Your main menu may look something like:: Choose an action: diff --git a/source/modules/DictsAndSets.rst b/source/modules/DictsAndSets.rst index 70c2e373..7e47f73e 100644 --- a/source/modules/DictsAndSets.rst +++ b/source/modules/DictsAndSets.rst @@ -57,7 +57,7 @@ Calling the dict type object constructor: >>> dict(key1=3, key2= 5) {'key1': 3, 'key2': 5} - # creating and empty dict, and then populating it: + # creating an empty dict, and then populating it: >>> d = {} >>> d['key1'] = 3 >>> d['key2'] = 5 @@ -181,7 +181,7 @@ Traditionally, dictionaries have had no defined order. See this example from Pyt Note how I defined the dict in a natural order, but when it gets printed, or you display the keys, they are in a different order. However, In cPython 3.6, the internal implementation was changed, and it *does* happen to preserve order. In cPython 3.6, that is considered an implementation detail -- and you should not count on it! However, as of cPython 3.7, dictionaries preserving order will be part of the language specification. This was declared by Guido on the python-dev mailing list on -`Dec 15, 2017 ` +Dec 15, 2017 . .. code-block:: ipython diff --git a/source/modules/Files.rst b/source/modules/Files.rst index 55c189b6..1d0fc4b3 100644 --- a/source/modules/Files.rst +++ b/source/modules/Files.rst @@ -107,7 +107,7 @@ Common Idioms do_something_with_line() -We will learn more about the keyword with later, but for now, just understand +We will learn more about the keyword ``with`` later, but for now, just understand the syntax and the advantage over the try-finally block: .. code-block:: python @@ -192,11 +192,11 @@ Absolute paths: '/home/chris/secret.txt' -Either work with ``open()`` , etc. +Either works with ``open()`` , etc. (A working directory only makes sense with command-line programs.) -os module +``os`` module ---------- .. code-block:: python From 454b8c8e43f96f006bd6f291770b40507767a03d Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Fri, 19 Oct 2018 21:45:27 -0700 Subject: [PATCH 13/87] updated Unicode page -- still needs cleaning up for py3 --- source/class_schedule/session_1_03.rst | 2 +- source/class_schedule/session_1_04.rst | 2 +- source/class_schedule/session_1_05.rst | 2 +- source/class_schedule/session_1_06.rst | 2 +- .../examples/unicode/ICanEatGlass.utf16.txt | Bin 0 -> 1428 bytes source/examples/unicode/ICanEatGlass.utf8.txt | 23 + source/examples/unicode/exception_test.py | 16 + source/examples/unicode/hello_unicode.py | 13 + source/examples/unicode/latin1_test.py | 66 +++ source/examples/unicode/text.utf16 | Bin 0 -> 1358 bytes source/examples/unicode/text.utf32 | Bin 0 -> 2716 bytes source/examples/unicode/text.utf8 | 17 + source/examples/unicode/unicodify.py | 54 ++ source/modules/Files.rst | 84 +-- source/modules/Unicode.rst | 542 ++++++++++++++++++ source/modules/index.rst | 1 + 16 files changed, 783 insertions(+), 41 deletions(-) create mode 100644 source/examples/unicode/ICanEatGlass.utf16.txt create mode 100644 source/examples/unicode/ICanEatGlass.utf8.txt create mode 100755 source/examples/unicode/exception_test.py create mode 100644 source/examples/unicode/hello_unicode.py create mode 100644 source/examples/unicode/latin1_test.py create mode 100644 source/examples/unicode/text.utf16 create mode 100644 source/examples/unicode/text.utf32 create mode 100644 source/examples/unicode/text.utf8 create mode 100644 source/examples/unicode/unicodify.py create mode 100644 source/modules/Unicode.rst diff --git a/source/class_schedule/session_1_03.rst b/source/class_schedule/session_1_03.rst index 8e635f7a..3b6d9bce 100644 --- a/source/class_schedule/session_1_03.rst +++ b/source/class_schedule/session_1_03.rst @@ -68,7 +68,7 @@ Mailroom Exercises You've now got the basics of the language down -- enough to write the first full "program": -:ref:`exercise_mailroom` +:ref:`exercise_mailroom_part1` Post-class Activites diff --git a/source/class_schedule/session_1_04.rst b/source/class_schedule/session_1_04.rst index bbf2d141..0f07b0e7 100644 --- a/source/class_schedule/session_1_04.rst +++ b/source/class_schedule/session_1_04.rst @@ -49,7 +49,7 @@ Exercises: * :ref:`exercise_file_lab` - * Update mailroom with dicts and files: :ref:`exercise_mailroom_plus` + * Update mailroom with dicts and files: :ref:`exercise_mailroom_part2_dict_files` * :ref:`exercise_trigrams` diff --git a/source/class_schedule/session_1_05.rst b/source/class_schedule/session_1_05.rst index 7f364b83..c4171b8a 100644 --- a/source/class_schedule/session_1_05.rst +++ b/source/class_schedule/session_1_05.rst @@ -36,7 +36,7 @@ Exercises: 3. Optional: :ref:`exercise_exceptions_lab` -4. Update mailroom with Exceptions: :ref:`exercise_mailroom_exceptions` +4. Update mailroom with Exceptions: :ref:`exercise_mailroom_part3_exceptions` Post-class Activities diff --git a/source/class_schedule/session_1_06.rst b/source/class_schedule/session_1_06.rst index 7fefaa75..5d6a037a 100644 --- a/source/class_schedule/session_1_06.rst +++ b/source/class_schedule/session_1_06.rst @@ -53,7 +53,7 @@ Exercises: Testing mailroom: ................. -:ref:`exercise_mailroom_testing` +:ref:`exercise_mailroom_part4_testing` Write a complete set of unit tests for your mailroom program. diff --git a/source/examples/unicode/ICanEatGlass.utf16.txt b/source/examples/unicode/ICanEatGlass.utf16.txt new file mode 100644 index 0000000000000000000000000000000000000000..24a0858d5f8f2322c8715c2add9bfc46466054f6 GIT binary patch literal 1428 zcma)+O;1xn6oyZo)pgItg`EWnYE0P~#Y94g8u0^+9~)P-6uHvg)^bZE{s8}kKLKMZ znn-{EQZ;Ve>E4ZotqYgn^UhEp#wO5Ih9;bO02`lAm71cEbP2ZgOKh_av>Q7bv|fEk(Tfv3vbm#mHIml zc0^THs$iMus$(896Q4@p(?pU|pUh4NW^-03R6viP^!A`ohIJO|zKAU6X;bZs_>r`U zY&O(Kb&(&Bwos^BDfDjqpqJ`HnVtHp>R0irI@~LE+^XAf>(1L(*Ky14t@~Eg?u~oK z+Hq^VEABaxF1Q~>eZ6hCyG3tnpuN4u!;|8s>#|#MG4f#)RiPg8Yrs%>ic8G(h1Z{n+@>mm8s%nrrpoJ{8easI$xxQ_?#jVycta z>v!V^>B;vyK1pVpvMZs>8M@gJV*-q4UfNje)YW02vyz5bOgXEjV*!6D zwwjR=`&+>-p6@JJ_agO+PzCk$NCf2~|Lx#iJNed4@DmQoK+Ix<86>KOm};UU&)R-& zQ&=`&s8cUn@g3%!B+E*$Q%H)NJPndP1|3-_mOLoO$fB29Ioxovo8v`si--Heo}c`sF(WKjpUoUfn&Ka~(_N+KY0S=O0Cs9m@a! literal 0 HcmV?d00001 diff --git a/source/examples/unicode/ICanEatGlass.utf8.txt b/source/examples/unicode/ICanEatGlass.utf8.txt new file mode 100644 index 00000000..9ecba2b9 --- /dev/null +++ b/source/examples/unicode/ICanEatGlass.utf8.txt @@ -0,0 +1,23 @@ +I Can Eat Glass: + +And from the sublime to the ridiculous, here is a certain phrase in an assortment of languages: + +Sanskrit: काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥ + +Sanskrit (standard transcription): kācaṃ śaknomyattum; nopahinasti mām. + +Classical Greek: ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει. + +Greek (monotonic): Μπορώ να φάω σπασμένα γυαλιά χωρίς να πάθω τίποτα. + +Greek (polytonic): Μπορῶ νὰ φάω σπασμένα γυαλιὰ χωρὶς νὰ πάθω τίποτα. + +Latin: Vitrum edere possum; mihi non nocet. + +Old French: Je puis mangier del voirre. Ne me nuit. + +French: Je peux manger du verre, ça ne me fait pas mal. + +Provençal / Occitan: Pòdi manjar de veire, me nafrariá pas. + +Québécois: J'peux manger d'la vitre, ça m'fa pas mal. \ No newline at end of file diff --git a/source/examples/unicode/exception_test.py b/source/examples/unicode/exception_test.py new file mode 100755 index 00000000..975f1df8 --- /dev/null +++ b/source/examples/unicode/exception_test.py @@ -0,0 +1,16 @@ +#!/usr/bin/python + +""" +example for what happens when you pass non-ascii unicode to a Exception +""" + +msg = u'This is an ASCII-compatible unicode message' + +#msg = u'This is an non ASCII\N{EM DASH}compatible unicode message' + +print "\nDo you see this message in the Exception report?\n" +print msg +print + +raise ValueError(msg) + diff --git a/source/examples/unicode/hello_unicode.py b/source/examples/unicode/hello_unicode.py new file mode 100644 index 00000000..6bbad1de --- /dev/null +++ b/source/examples/unicode/hello_unicode.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +hello = 'Hello ' +world = u'世界' + +print hello + world + +print u"It was nice weather today: it reached 80\u00B0" + +print u"Maybe it will reach 90\N{degree sign}" + +print u"It is extremely rare for it ever to reach 100° in Seattle" diff --git a/source/examples/unicode/latin1_test.py b/source/examples/unicode/latin1_test.py new file mode 100644 index 00000000..3990078f --- /dev/null +++ b/source/examples/unicode/latin1_test.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +""" +An example of using latin-1 as a universal encoding + +latin-1 is a superset of ASCII that is suitable for western european languages. + +Is the most common, and a good default, if you need a one-byte per char encoding +for European text. + +It also has a nice property: + : every byte value from 0 to 255 is avalid charactor + +Thus you will never get an UnicodeDecodeError if +you try to decode arbitrary bytes with latin-1. + +And it can "round-trip" trhough a unicode object. + +This can be useful is you don't know the encoding -- at least it won't break. +It's also useful if you need to work with cobined text+binary data. + + + +""" + +# all the byte values in a bytes (str) object: +all_bytes = ''.join( [chr(i) for i in range(255)] ) + +print type(all_bytes) +print len(all_bytes) + +print "Example value: 20" +print ord(all_bytes[20]) == 20 +print "Example high value: 245" +print ord(all_bytes[245]) == 245 + +# now decode it to a unicode object: +try: + uni = all_bytes.decode() +except UnicodeDecodeError: + print "OOPS: can't decode with default encoding" + +# latin-1 works: +try: + all_uni = all_bytes.decode('latin-1') + print "Yup -- that worked" + print all_uni + print "note that the ASCII subset is the same..." +except UnicodeDecodeError: + print "OOPS: This should have worked!!" + raise + +## now show that it round-trips: +all_bytes2 = all_uni.encode('latin-1') + +if all_bytes2 == all_bytes: + print "yup -- that worked...the values are preserved on the round trip." +else: + print "Hey, that should have worked" + + + + + + + diff --git a/source/examples/unicode/text.utf16 b/source/examples/unicode/text.utf16 new file mode 100644 index 0000000000000000000000000000000000000000..b80b2efca2c24110bedfabf5e9b5dfbddfa5d6aa GIT binary patch literal 1358 zcmb7@Pe@cz6vl6I-{Q>_H{!}&_S)4hOGzk09T!n#kYT#(spBY&PkhY`j1)>DE`mfX zD>vbyU4*!hN(uzQZMf*q%5}5~T35gOoZ-!=BNLbN&OPUR=eyti^Zx$1B|eu)|wy=xlXh)v-hO$=T7I~pUpJnW1m2C7r#B@7zuI8TJ!i4O`znLzM!T4eGcRk6wQT8aeYbH#zt)Z()^Yye?3eznKZ8i<75!Du M=upo#9^~`HKl&`ur2qf` literal 0 HcmV?d00001 diff --git a/source/examples/unicode/text.utf32 b/source/examples/unicode/text.utf32 new file mode 100644 index 0000000000000000000000000000000000000000..c529531076d3d1be03e02f47418ac309e26472a8 GIT binary patch literal 2716 zcmchZOGuSL7>1|FMOqY5v~1edF1t`rhMJ2gD#$R~b-a~ONv48QB#E>LlCb+GT(pY_ z7tw_RL2w%_x~yDFtDtrDJU9a($ICfci--4{f4t_fjLI~Z^3FnetLL+RO zv-WubdeZYgv>hT`0QdAUPj6al>$4pq90&a_LXKVTI+f(Et)LF{CcTDUPj(*~@hO#R z;s#TWo|K~|t^1Q&L!VsNTJ}?{Rvpis(-m;0od!HcpXC8YL?Of+Pb}WG-_}ZIsxd;1*&sN)a7rsI*RKpHvhS$li zK_l@?WkZVFh3#>g7%RR@#X+_xNme2=Czrj3>L?-g4Y4 z@=UrGtT+Wt_z_F#-Ru5p$ZCV-HNV-?7tLk#@{?wp%Kh?Dy_9! zU~JC%0qX&<*4)1DZVuriJcc_k0Z*WcW-^1UAA)}O;Vs;Rmr$%{eR9qzZ1dzeYq)m{ zhF}t`XRhI-_OgZ;{T_kw)_Vt|X?+_tW&|n&-&QDq_f&058qsnw=M3ck1?J9ov;n&l zy1?i55Eo%`V+d1ldJ{85pF#z6z+$oU?SCis2Yg65D$z0c3`5Wj!_-?!f7bX7{uI%r literal 0 HcmV?d00001 diff --git a/source/examples/unicode/text.utf8 b/source/examples/unicode/text.utf8 new file mode 100644 index 00000000..9de18890 --- /dev/null +++ b/source/examples/unicode/text.utf8 @@ -0,0 +1,17 @@ +Origin (in native language) Name (in native language) +Հայաստան Արամ Խաչատրյան + Australia Nicole Kidman + Österreich Johann Strauß + Azərbaycan Vaqif Səmədoğlu + Азәрбајҹан Вагиф Сәмәдоғлу + Azərbaycan Heydər Əliyev + Азәрбајҹан Һејдәр Әлијев + België René Magritte + Belgique René Magritte + Belgien René Magritte + বাংলা সুকুমার রায় + འབྲུག་ཡུལ། མགོན་པོ་རྡོ་རྗེ། + ប្រទេស​​​កម្ពុជា ព្រះ​ពុទ្ឋឃោសាចារ‌្យ​ជួន​ណាត +Canada Céline Dion + ᓄᓇᕗᒻᒥᐅᑦ ᓱᓴᓐ ᐊᒡᓗᒃᑲᖅ + \ No newline at end of file diff --git a/source/examples/unicode/unicodify.py b/source/examples/unicode/unicodify.py new file mode 100644 index 00000000..15683ee6 --- /dev/null +++ b/source/examples/unicode/unicodify.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +''' +Decorators to convert all arguments passed to a function or method to +unicode or str, including default arguments + +From: http://axialcorps.com/2014/03/20/unicode-str/ + +''' + + +import sys +import functools +import inspect + +def _convert_arg(arg, from_, conv, enc): + '''Safely convert unicode to string or string to unicode''' + return getattr(arg, conv)(encoding=enc) if isinstance(arg, from_) else arg + +def _wrap_convert(from_type, fn, encoding=None): + '''Decorate a function converting all str arguments to unicode or + vice-versa''' + conv = 'decode' if from_type is str else 'encode' + encoding = encoding or sys.getdefaultencoding() + + # override string defaults using partial + aspec, dflts = inspect.getargspec(fn), {} + if aspec.defaults: + for k,v in zip(aspec.args[-len(aspec.defaults):],aspec.defaults): + dflts[k] = _convert_arg(v, from_type, conv, encoding) + fn = functools.partial(fn, **dflts) + + @functools.wraps(fn.func if isinstance(fn, functools.partial) else fn) + def converted(*args, **kwargs): + args = [_convert_arg(a, from_type, conv, encoding) for a in args] + for k,v in kwargs.iteritems(): + kwargs[k] = _convert_arg(v, from_type, conv, encoding) + return fn(*args, **kwargs) + + return converted + +def unicodify(fn=None, encoding=None): + '''Convert all str arguments to unicode''' + if fn is None: + return functools.partial(unicodify, encoding=encoding) + return _wrap_convert(str, fn, encoding=encoding) + +def stringify(fn=None, encoding=None): + '''Convert all unicode arguments to str''' + if fn is None: + return functools.partial(stringify, encoding=encoding) + return _wrap_convert(unicode, fn, encoding=encoding) + +__all__ = ['unicodify', 'stringify'] \ No newline at end of file diff --git a/source/modules/Files.rst b/source/modules/Files.rst index 1d0fc4b3..0f372cb6 100644 --- a/source/modules/Files.rst +++ b/source/modules/Files.rst @@ -21,7 +21,7 @@ Text Files ``secret_data`` is a string -NOTE: these days, you probably need to use Unicode for text -- we'll get to that later... +.. note:: In Python 3, files are opened by default in text mode, and the default encoding is UTF-8. This means that in the usual case, you get a proper Unicode string to work with, as UTF-8 is the most common encoding for text. Also, it is ASCII compatible, so ASCII Files with "just work". IF "Unicode" and "ASCII" mean nothing to you -- don't worry about it, just know that things will usually work for text, even non-English text. And if you get odd characters or an ``EncodingError``, then your file is not UTF-8, and it's time to Google "Python Unicode". (more info here: :ref:`unicode`) Binary Files @@ -55,7 +55,7 @@ in the Python docs. But these BSD docs make it pretty clear: http://www.manpagez.com/man/3/fopen/ -**Gotcha** -- 'w' modes always clear the file. +**Gotcha** -- 'w' modes always clear the file if it already exists! Text File Notes --------------- @@ -95,7 +95,7 @@ Common Idioms for line in open('secrets.txt'): print(line) -(the file object is an iterator!) +(The file object is an iterable that iterates through the lines in a text file.) .. code-block:: python @@ -107,8 +107,7 @@ Common Idioms do_something_with_line() -We will learn more about the keyword ``with`` later, but for now, just understand -the syntax and the advantage over the try-finally block: +We will learn more about the keyword ``with`` later (it creates a "context manager"), but for now, just understand the syntax and the advantage over simply opening the file: .. code-block:: python @@ -117,6 +116,9 @@ the syntax and the advantage over the try-finally block: f.closed True +You use ``with`` to open the file, and assign it a name (``f`` in this case). +The file remains open while in the ``with`` block. +At the end of the ``with`` block, the file is unconditionally closed, even if an Exception is raised. You code will (mostly) work without it, but it's a good habit to get into to always use ``with`` to open a file. File Writing ------------ @@ -148,27 +150,34 @@ Commonly Used Methods: f.close() -StringIO --------- +``StringIO`` +------------ + +A ``StringIO`` method is a "file like" object that stores the content in memory. +That is, it has all the methods of a file, and behaves the same way, but never writes anything to disk. .. code-block:: python - In [417]: import io - In [420]: f = io.StringIO() - In [421]: f.write("somestuff") - In [422]: f.seek(0) - In [423]: f.read() - Out[423]: 'somestuff' - Out[424]: stuff = f.getvalue() - Out[425]: f.close() + In [6]: import io -(This can be handy for testing file handling code...) + In [7]: f = io.StringIO() -There is also cStringIO -- a bit faster. + In [8]: f.write("some stuff") + Out[8]: 10 -.. code-block:: python + In [9]: f.seek(0) + Out[9]: 0 + + In [10]: f.read() + Out[10]: 'some stuff' + + In [11]: f.getvalue() + Out[11]: 'some stuff' + + In [12]: f.close() + +(This can be handy for testing file handling code...) - from cStringIO import StringIO Paths and Directories ===================== @@ -176,7 +185,7 @@ Paths and Directories Paths ----- -Paths are generally handled with simple strings (or Unicode strings). +Paths are generally handled with simple strings. Relative paths: @@ -194,10 +203,10 @@ Absolute paths: Either works with ``open()`` , etc. -(A working directory only makes sense with command-line programs.) +Relative paths are relative to the current working directory, which is only relevant to command-line programs. ``os`` module ----------- +------------- .. code-block:: python @@ -243,17 +252,20 @@ All the stuff in os.path and more: .. code-block:: ipython - In [64]: import pathlib - In [65]: pth = pathlib.Path('./') - In [66]: pth.is_dir() - Out[66]: True - In [67]: pth.absolute() - Out[67]: PosixPath('/Users/Chris/PythonStuff/UWPCE/IntroPython2015') - In [68]: for f in pth.iterdir(): - print(f) - junk2.txt - junkfile.txt - ... + In [14]: import pathlib + + In [15]: pth = pathlib.Path('./') + + In [16]: pth.is_dir() + Out[16]: True + + In [17]: pth.absolute() + Out[17]: PosixPath('/Users/Chris/PythonStuff/UWPCE/Fall2018-PY210A/examples/Session02') + + In [18]: for f in pth.iterdir(): + ...: print(f) + ...: + ...: And it has a really nifty way to join paths, by overloading the "division" operator: @@ -303,7 +315,7 @@ What this means to you Unless you are writing a path manipulation library, or a library that deals with paths other than with the stdlib packages (like ``open()``), all you need to know is that you can use ``Path`` objects most places you need a path. -I expect we will see expanded use of pathlib as python 3.6 becomes widely used. +I expect we will see expanded use of pathlib as python 3.6 and 3.7 becomes widely used. Some added notes: ================= @@ -323,8 +335,7 @@ When working with files, unless you have a good reason not to, use ``with``: # now done with out file -- it will be closed, regardless of errors, etc. do_other_stuff -``with`` invokes a context manager -- which can be confusing, but for now, -just follow this pattern -- it really is more robust. +``with`` invokes a context manager -- which can be confusing, but for now, just follow this pattern -- it really is more robust. And you can even do two at once: @@ -348,7 +359,6 @@ All data in all files is binary -- that's how computers work. So in Python3, "te But this too is complicated -- there are multiple ways that binary data can be mapped to Unicode text, known as "encodings". In Python, text files are by default opened with the "utf-8" encoding. These days, that mostly "just works". - But if you read a binary file as text, then Python will try to interpret the bytes as utf-8 encoded text -- and this will likely fail: .. code-block:: ipython diff --git a/source/modules/Unicode.rst b/source/modules/Unicode.rst new file mode 100644 index 00000000..e2c0f49f --- /dev/null +++ b/source/modules/Unicode.rst @@ -0,0 +1,542 @@ +:orphan: + +.. _unicode: + +================= +Unicode in Python +================= + + +A quick run-down of Unicode, + +Its use in Python 2 and 3, + +and some of the gotchas that arise. + + +History +======= + +A bit about where all this mess came from... + + +What the heck is Unicode anyway? +--------------------------------- + +* First there was chaos... + + * Different machines used different encodings -- different ways of mapping + binary data that the computer stores to letters. + +* Then there was ASCII -- and all was good (7 bit), 127 characters + + * (for English speakers, anyway) + +* But each vendor used the top half of 8bit bytes (127-255) for different things. + + * MacRoman, Windows 1252, etc... + + * There is now "latin-1", but still a lot of old files around. + +* Non-Western European languages required totally incompatible 1-byte encodings + +* No way to mix languages with different alphabets. + + +Enter Unicode +-------------- + +The Unicode idea is pretty simple: + * One "code point" for all characters in all languages + +But how do you express that in bytes? + * Early days: we can fit all the code points in a two byte integer (65536 characters) + + * Turns out that didn't work -- now need 32 bit integer to hold all of unicode "raw" (UTC-4) + +Enter "encodings": + * An encoding is a way to map specific bytes to a code point. + + * Each code point can be represented by one or more bytes. + + * Each encoding is different -- if you don't know the encoding, you don't know how to interpret the bytes! (though maybe you can guess...) + + +Unicode +------- + +A good start: + +The Absolute Minimum Every Software Developer Absolutely, +Positively Must Know About Unicode and Character Sets (No Excuses!) + +http://www.joelonsoftware.com/articles/Unicode.html + + +**Everything is Bytes** + +* If it's on disk or on a network, it's bytes + +* Python provides some abstractions to make it easier to deal with bytes + +**Unicode is a biggie** + +Actually, dealing with numbers rather than bytes is big + +-- but we take that for granted + + +Mechanics +========= + +What are strings? +----------------- + +Py2 strings were simply sequences of bytes. When text was one per charater that worked fine. + +Py3 strings (or Unicode strings in py2) are sequences of platonic characters. + +It's almost one code point per character -- there are complications +with combined characters: accents, etc -- but we can mostly ignore those. + +Platonic characters cannot be written to disk or network! + +(ANSI: one character == one byte -- so easy!) + + +Strings vs Unicode +------------------ + +Python 2 had two types that let you work with text: + +* ``str`` + +* ``unicode`` + +And two ways to work with binary data: + +* ``str`` + +* ``bytes()`` (and ``bytearray``) + +**but:** + +.. code-block:: ipython + + In [86]: str is bytes + Out[86]: True + +``bytes`` is there in py2 for py3 compatibility -- but it's good for making your intentions clear, too. + +py3 is more clear: + + ``str`` for text + ``byte`` for binary data + +Unicode +-------- + +The py3 string (py2 ``Unicode``) object lets you work with characters, instead of bytes. + +It has all the same methods you'd expect a string object to have. + +Encoding / Decoding +------------------- + +If you need to deal with the actual bytes for some reason, you may need to convert between a string object and a particular set of bytes. + +**"encoding"** is converting from a string object to bytes + +**"decoding"** is converting from bytes to a string object + +(sometimes this feels backwards...) + +And can get even more confusing with py2 strings being *both* text and bytes! + +This is actually one of teh biggest differences between Python 2 and Python 3. As an ordinary user (particulary one that used English...), you may not notice -- text is text, and things generally "just work", but under the hood it is very different, and folks writting libraries for things like internet protocols struggle with the differences. + +Using unicode in Py2 +--------------------- + +IF you do need to write Python2 code, you really should use Unicode. + +Here are the basics: + +Built in functions +.................. + +.. code-block:: python + + ord() + chr() + unichr() + str() + unicode() + +The codecs module +................. + +.. code-block:: python + + import codecs + codecs.encode() + codecs.decode() + codecs.open() # better to use ``io.open`` + + +Encoding and Decoding +---------------------- + +(Python 2!) + +**Encoding:** text to bytes -- you get a bytes (str) object + +.. code-block:: ipython + + In [17]: u"this".encode('utf-8') + Out[17]: 'this' + + In [18]: u"this".encode('utf-16') + Out[18]: '\xff\xfet\x00h\x00i\x00s\x00' + +**Decoding** bytes to text -- you get a unicode object + +.. code-block:: ipython + + In [2]: text = '\xff\xfe."+"x\x00\xb2\x00'.decode('utf-16') + + In [3]: type(text) + Out[3]: unicode + + In [4]: print text + ∮∫x² + + +Unicode Literals +------------------ + +1) Use unicode in your source files: + +.. code-block:: python + + # -*- coding: utf-8 -*- + +2) Escape the unicode characters: + +.. code-block:: python + + print u"The integral sign: \u222B" + print u"The integral sign: \N{integral}" + +Lots of tables of code points online: + +One example: + http://inamidst.com/stuff/unidata/ + +:download:`hello_unicode.py <../examples/unicode/hello_unicode.py>`. + + +Using Unicode +-------------- + +Use ``unicode`` objects in all your code + +**Decode on input** + +**Encode on output** + +Many packages do this for you: *XML processing, databases, ...* + +**Gotcha:** + +Python has a default encoding (usually ascii) + +.. code-block:: ipython + + In [2]: sys.getdefaultencoding() + Out[2]: 'ascii' + +The default encoding will get used in unexpected places! + +Using Unicode everywhere +------------------------- + +Python 2.6 and above have a nice feature to make it easier to use unicode everywhere + +.. code-block:: python + + from __future__ import unicode_literals + +After running that line, the ``u''`` is assumed + +.. code-block:: ipython + + In [1]: s = "this is a regular py2 string" + In [2]: print type(s) + + + In [3]: from __future__ import unicode_literals + In [4]: s = "this is now a unicode string" + In [5]: type(s) + Out[5]: unicode + +NOTE: You can still get py2 strings from other sources! + +This is a really good idea if you want to write code compatible with Python2 and 3 + +Encodings +---------- + +What encoding should I use??? + +There are a lot: + +http://en.wikipedia.org/wiki/Comparison_of_Unicode_encodings + +But only a couple you are likely to need: + +* utf-8 (``*nix``) +* utf-16 (Windows) + +and of course, still the one-bytes ones. + +* ASCII +* Latin-1 + +UTF-8 +----- + +Probably the one you'll use most -- most common in Internet protocols (xml, JSON, etc.) + +Nice properties: + +* ASCII compatible: First 127 characters are the same + +* Any ascii string is a utf-8 string + +* Compact for mostly-english text. + +Gotchas: + +* "higher" code points may use more than one byte: up to 4 for one character + +* ASCII compatible means in may work with default encoding in tests -- but then blow up with real data... + +UTF-16 +------ + +Kind of like UTF-8, except it uses at least 16bits (2 bytes) for each character: NOT ASCII compatible. + +But is still needs more than two bytes for some code points, so you still can't process it as one per character. + +In C/C++ held in a "wide char" or "wide string". + +MS Windows uses UTF-16, as does (I think) Java. + +UTF-16 criticism +----------------- + +There is a lot of criticism on the net about UTF-16 -- it's kind of the worst of both worlds: + +* You can't assume every character is the same number of bytes +* It takes up more memory than UTF-8 + +`UTF Considered Harmful `_ + +But to be fair: + +Early versions of Unicode: everything fit into two bytes (65536 code points). MS and Java were fairly early adopters, and it seemed simple enough to just use 2 bytes per character. + +When it turned out that 4 bytes were really needed, they were kind of stuck in the middle. + +Latin-1 +-------- + +**NOT Unicode**: + +A 1-byte per char encoding. + +* Superset of ASCII suitable for Western European languages. + +* The most common one-byte per char encoding for European text. + +* Nice property -- every byte value from 0 to 255 is a valid character ( at least in Python ) + +* You will never get an UnicodeDecodeError if you try to decode arbitrary bytes with latin-1. + +* And it can "round-trip" through a unicode object. + +* Useful if you don't know the encoding -- at least it won't raise an Exception + +* Useful if you need to work with combined text+binary data. + +:download:`latin1_test.py <../examples/unicode/latin1_test.py>`. + + +Unicode Docs +------------ + +Python Docs Unicode HowTo: + +http://docs.python.org/howto/unicode.html + +"Reading Unicode from a file is therefore simple" + +use io.open: + +.. code-block:: python + + from io import open + io.open('unicode.rst', encoding='utf-8') + for line in f: + print repr(line) + +(https://docs.python.org/2/library/io.html#module-interface) + +Encodings Built-in to Python: + http://docs.python.org/2/library/codecs.html#standard-encodings + + +Gotchas in Python 2 +-------------------- + +file names, etc: + +If you pass in unicode, you get unicode + +.. code-block:: ipython + + In [9]: os.listdir('./') + Out[9]: ['hello_unicode.py', 'text.utf16', 'text.utf32'] + + In [10]: os.listdir(u'./') + Out[10]: [u'hello_unicode.py', u'text.utf16', u'text.utf32'] + +Python deals with the file system encoding for you... + +But: some more obscure calls don't support unicode filenames: + +``os.statvfs()`` (http://bugs.python.org/issue18695) + + +Exception messages: + + * Py2 Exceptions use str when they print messages. + + * But what if you pass in a unicode object? + + * It is encoded with the default encoding. + + * ``UnicodeDecodeError`` Inside an Exception???? + + NOPE: it swallows it instead. + +:download:`exception_test.py <../examples/unicode/exception_test.py>`. + +Unicode in Python 3 +---------------------- + +The "string" object is unicode. + +Py3 has two distinct concepts: + +* "text" -- uses the str object (which is always unicode!) +* "binary data" -- uses bytes or bytearray + +Everything that's about text is unicode. + +Everything that requires binary data uses bytes. + +It's all much cleaner. + +(by the way, the recent implementations are very efficient...) + + +Exercises +========= + +Basic Unicode LAB +------------------- + +* Find some nifty non-ascii characters you might use. + + - Create a unicode object with them in two different ways. + - :download:`here <../examples/unicode/hello_unicode.py>` is one example + +* Read the contents into unicode objects: + + - :download:`ICanEatGlass.utf8.txt <../examples/unicode/ICanEatGlass.utf8.txt>` + - :download:`ICanEatGlass.utf16.txt <../examples/unicode/ICanEatGlass.utf16.txt>` + +and / or + + - :download:`text.utf8 <../examples/unicode/text.utf8>` + - :download:`text.utf16 <../examples/unicode/text.utf16>` + - :download:`text.utf32 <../examples/unicode/text.utf32>` + +* write some of the text from the first exercise to file -- read that file back in. + +Some Help +--------- + +reference: http://inamidst.com/stuff/unidata/ + +NOTE: if your terminal does not support unicode -- you'll get an error trying to print. +Try a different terminal or IDE, or google for a solution. + +Challenge Unicode LAB +---------------------- + +Here is an error in Python2: + +.. code-block:: ipython + + In [38]: u'to \N{INFINITY} and beyond!'.decode('utf-8') + --------------------------------------------------------------------------- + UnicodeEncodeError Traceback (most recent call last) + in () + ----> 1 u'to \N{INFINITY} and beyond!'.decode('utf-8') + + /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/encodings/utf_8.pyc in decode(input, errors) + 14 + 15 def decode(input, errors='strict'): + ---> 16 return codecs.utf_8_decode(input, errors, True) + 17 + 18 class IncrementalEncoder(codecs.IncrementalEncoder): + + UnicodeEncodeError: 'ascii' codec can't encode character u'\u221e' in position 3: ordinal not in range(128) + + +But why would you **decode** a unicode object? + +And it should be a no-op -- why the exception? + +And why 'ascii'? I specified 'utf-8'! + +It's there for backward compatibility + +What's happening under the hood + +.. code-block:: python + + u'to \N{INFINITY} and beyond!'.encode().decode('utf-8') + +It encodes with the default encoding (ascii), then decodes + +In this case, it barfs on attempting to encode to 'ascii' + +So never call decode on a unicode object! + +But what if someone passes one into a function of yours that's expecting a py2 string? + +Type checking and converting -- yeach! + +Read: + +http://axialcorps.com/2014/03/20/unicode-str/ + +See if you can figure out the decorators: + +:download:`unicodify.py <../examples/unicode/unicodify.py>`. + +(This is advanced Python JuJu: Aren't you glad I didn't ask you to write that yourself?) diff --git a/source/modules/index.rst b/source/modules/index.rst index ac591c31..a37ee714 100644 --- a/source/modules/index.rst +++ b/source/modules/index.rst @@ -223,4 +223,5 @@ Assorted Additional Topics CodeReviews Packaging PersistanceAndSerialization + Unicode From 7eaff9b67f63bfef63376a85068079c121051ad6 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Fri, 19 Oct 2018 23:00:35 -0700 Subject: [PATCH 14/87] more on Recursion --- source/modules/Recursion.rst | 136 ++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/source/modules/Recursion.rst b/source/modules/Recursion.rst index 9d7e0bc9..b3e91ec0 100644 --- a/source/modules/Recursion.rst +++ b/source/modules/Recursion.rst @@ -13,7 +13,6 @@ With recursion, if you are not careful, this stack can get *very* deep. Python has a maximum limit to how much it can recurse. This is intended to save your machine from running out of RAM. - Recursion is especially useful for a particular set of problems. For example, take the case of the *factorial* function. @@ -25,4 +24,137 @@ integer by every integer smaller than it down to 1. 5! == 5 * 4 * 3 * 2 * 1 -We can use a recursive function nicely to model this mathematical function +We can use a recursive function nicely to model this mathematical function: + +:: + + 1! = 1 + 2! = 2 * 1 = 2 * 1! + 3! = 3 * 2 * 1 = 3 * 2! + +So we have a pattern here -- each value can be defined in terms of the previous value. + +So generically:: + + 1! = 1 + n! = n * (n-1)! + +How would we put that in code? Pretty straightforward translation: + +.. code-block:: python + + def factorial(n): + return n * factorial(n-1) + +That was pretty easy -- what happens when we run it? + +.. code-block:: ipython + + In [2]: factorial(3) + --------------------------------------------------------------------------- + RecursionError Traceback (most recent call last) + in () + ----> 1 factorial(3) + + in factorial(n) + 1 def factorial(n): + ----> 2 return n * factorial(n-1) + + ... last 1 frames repeated, from the frame below ... + + in factorial(n) + 1 def factorial(n): + ----> 2 return n * factorial(n-1) + + RecursionError: maximum recursion depth exceeded + +OOPS! that didn't work -- why not? Let's add a print... + +.. code-block:: python + + def factorial(n): + print("factorial called with", n) + return n * factorial(n-1) + +And call it: + +.. code-block:: ipython + + In [5]: factorial(3) + factorial called with 3 + factorial called with 2 + factorial called with 1 + factorial called with 0 + factorial called with -1 + factorial called with -2 + factorial called with -3 + factorial called with -4 + factorial called with -5 + ... + in factorial(n) + 1 def factorial(n): + 2 print("factorial called with", n) + ----> 3 return n * factorial(n-1) + + RecursionError: maximum recursion depth exceeded while calling a Python object + +Now it's clear what's going on -- each time you call the function, it calls itself with a value one less -- but then it just keeps going into the deep negative numbers, and only stops because Python reaches its recursion limit. + +This makes clear a core requirement of recursive functions: + + **Recursive functions must have a termination criteria!** + +That is, there must be a case (or more than one) for which they return a direct value. What should that be for factorial? Well, it's part of the definition that 1! == 1 -- so let's put that in our function: + +.. code-block:: python + + def factorial(n): + print("factorial called with", n) + if n == 1: + return 1 + return n * factorial(n-1) + +and try that: + +.. code-block:: ipython + + In [7]: factorial(3) + factorial called with 3 + factorial called with 2 + factorial called with 1 + Out[7]: 6 + +Much better! Try it out now with various values, and maybe without the print: + +.. code-block:: ipython + + In [14]: factorial(1) + Out[14]: 1 + + In [15]: factorial(2) + Out[15]: 2 + + In [16]: factorial(3) + Out[16]: 6 + + In [17]: factorial(4) + Out[17]: 24 + +Looking good! + +Exercise for the reader: What happens if you pass in a negative number? +Think about it first, before you try it. Hint -- it won't work! +How would you change your code to make it more robust? + +Summary +------- + +* Whenever you have a function that can be defined in terms of itself, you have a use case for recursion. It can make for nice compact, clear code. + +* Python will create a new "stack frame" for each call to the function -- so each call is kept separate, with separate local variables. + +But: + +* Python has a limited recursion depth -- so it can't be used for "big" problems. + +* You do need to make sure the calls will terminate. From 156470a69c9689aba31fe3aac56257772c22c037 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Tue, 23 Oct 2018 16:48:11 -0700 Subject: [PATCH 15/87] cleaned out extra fizzbuzz soltuions. updated learning for Dive Into Python no longer online :-( --- source/references/learning.rst | 6 +- source/solutions/fizz_buzz/fizz_buzz.py | 83 ------------------- .../fizz_buzz/fizz_buzz_one_liner.py | 59 ------------- 3 files changed, 3 insertions(+), 145 deletions(-) delete mode 100755 source/solutions/fizz_buzz/fizz_buzz.py delete mode 100644 source/solutions/fizz_buzz/fizz_buzz_one_liner.py diff --git a/source/references/learning.rst b/source/references/learning.rst index 8d531b3c..c50f4196 100644 --- a/source/references/learning.rst +++ b/source/references/learning.rst @@ -48,9 +48,9 @@ Complete Books / Series ....................... * **Dive Into Python 3** - (http://www.diveintopython3.net/): The updated version + (https://www.apress.com/us/book/9781430224150): The updated version of a classic. This book offers an introduction to Python aimed at the student - who has experience programming in another language. + who has experience programming in another language. It used to be available free online -- you can still find PDF copies on the web. * **Python for You and Me** (http://pymbook.readthedocs.org/en/latest/): Simple @@ -72,7 +72,7 @@ Complete Books / Series print, with updated appendixes available for new language features. * **Python 101** - (http://www.blog.pythonlibrary.org/2014/06/03/python-101-book-published-today/) + (https://leanpub.com/python_101) Available as a reasonably priced ebook. This is a new one from a popular blogger about Python. Lots of practical examples. Also available as a Kindle book: http://www.amazon.com/Python-101-Michael-Driscoll-ebook/dp/B00KQTFHNK diff --git a/source/solutions/fizz_buzz/fizz_buzz.py b/source/solutions/fizz_buzz/fizz_buzz.py deleted file mode 100755 index 36189d59..00000000 --- a/source/solutions/fizz_buzz/fizz_buzz.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python - -""" -Fizz Buzz examples -- from most straightforward, to most compact. -""" - - -# basic approach: -def fizzbuzz1(n): - for i in range(1, n + 1): - if i % 3 == 0 and i % 5 == 0: - print("FizzBuzz") - elif i % 3 == 0: - print("Fizz") - elif i % 5 == 0: - print("Buzz") - else: - print(i) - - -def fizzbuzz2(n): - """ - Why evaluate i%3 and i%5 twice? - """ - for i in range(1, n + 1): - msg = '' - if i % 3 == 0: - msg += "Fizz" - if i % 5 == 0: - msg += "Buzz" - if msg: - print(msg) - else: - print(i) - - -def fizzbuzz3(n): - """ - Or print on one line... - """ - for i in range(1, n + 1): - num = True - if i % 3 == 0: - print("\nFizz", end='') - num = False - if i % 5 == 0: - if num: - print() - print("Buzz", end='') - num = False - else: - if num: - print("\n", i, end='') - - -def fizzbuzz4(n): - """ - use conditional expressions: - """ - for i in range(1, n + 1): - msg = "Fizz" if i % 3 == 0 else '' - msg += "Buzz" if i % 5 == 0 else '' - print(msg or i) - - -def fizzbuzz5(n): - """ - a one liner - """ - for i in range(1, n + 1): print (("Fizz" * (not (i % 3)) + "Buzz" * (not (i % 5))) or i) - - -if __name__ == "__main__": - fizzbuzz1(16) - print() - fizzbuzz2(16) - print() - fizzbuzz3(16) - print() - fizzbuzz4(16) - print() - fizzbuzz4(16) - print() diff --git a/source/solutions/fizz_buzz/fizz_buzz_one_liner.py b/source/solutions/fizz_buzz/fizz_buzz_one_liner.py deleted file mode 100644 index 43bb9d2e..00000000 --- a/source/solutions/fizz_buzz/fizz_buzz_one_liner.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 - -# This One Liner solution to the Fizz Buzz problem -# was found by a student on the internet - -for i in range(1,101): print([i,'Fizz','Buzz','FizzBuzz'][(i%3==0)+2*(i%5==0)]) - -# this is a good example of why the most compact code is not always the -# best -- readability counts! -# And this is pretty impenetrable. -# but it's also pretty nifty logic, so below, -# It's unpacked to make it easeir to understand. - -# first, add some white space to make it pep8 compatible, and more readable. - -for i in range(1, 101): print([i, 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)]) - -# second, take the for loop off one line -- that really makes no difference: - -for i in range(1, 101): - print([i, 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)]) - -# so we are looping through the numbers, and the contents of the print() -# is deciding what to print for i - -# unpack that line: - -for i in range(1, 101): - options_to_print = [i, 'Fizz', 'Buzz', 'FizzBuzz'] - index = 0 # default index is zero - index += (i % 3 == 0) # add one to index if it's a multiple of 3 - index += (2 * (i % 5 == 0)) # add two to the index if a multiple of 5 - print(options_to_print[index]) # print the selection. - -# there are 4 possible options that might get printed on each line: -# 1) the number, i -# 2) Fizz -# 3) Buzz -# 4) FizzBuzz - -# we now need an index to pick which of these to print -# -# remember that True and False are also the integers 1 and 0: -# so (i % 3 == 0) will return 1 (True) if i is a multiple of 3 -# and 0 (False) if not -# and (i % 5 == 0) will do the same for 5 -# so (2 * (i % 5 == 0)) will return either 0 or 2 -# -# so if the number is a multiple of neither, index will be zero, -# and we'll get the zeroth element of the list: i -# -# if i is a multiple of three (i % 3 == 0), then the index will be 1 -# -# and if i is a multiple of 5 the index will then be 3 (1+2) -# if it wasn't a multiple of three, then it will be 2 (0+2) -# -# so using the index will get the right element from the options list. -# -# pretty slick! From 25047319f902e653f61ad5f3ed21d93ca37911ea Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Wed, 24 Oct 2018 16:20:04 -0700 Subject: [PATCH 16/87] Fix minor typos --- README.rst | 4 ++-- source/exercises/dict_lab.rst | 2 ++ source/modules/DictsAndSets.rst | 6 +++--- source/modules/Files.rst | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index eea28014..9f08477b 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Building the docs for uploading to gh-pages The html docs are published by gitHub's gh-pages. This is accomplished by putting all the html in the ``gh-pages`` branch of the repo. -YOu can do that by hand, by copying any new html to the branch, but if you want to do it more often, it's easier to automate it. +You can do that by hand, by copying any new html to the branch, but if you want to do it more often, it's easier to automate it. There is a bash script that will do it for you: ``build_gh_pages.sh``. @@ -54,7 +54,7 @@ It requires a bit of setup: git pull -* Once this is setup, you can run the build_gh_pages script from the name repo, and it should: +* Once this is setup, you can run the build_gh_pages script from the main repo, and it should: - build the html docs - copy that build docs over to the other clone diff --git a/source/exercises/dict_lab.rst b/source/exercises/dict_lab.rst index 12ae4ab4..c887fa33 100644 --- a/source/exercises/dict_lab.rst +++ b/source/exercises/dict_lab.rst @@ -28,6 +28,8 @@ should be able to run the script directly like so: To make this work you, make sure you include the 'shebang' on the first line of your file. +.. code-block:: bash + #!/usr/bin/env python3 diff --git a/source/modules/DictsAndSets.rst b/source/modules/DictsAndSets.rst index 7e47f73e..4191fdcc 100644 --- a/source/modules/DictsAndSets.rst +++ b/source/modules/DictsAndSets.rst @@ -213,7 +213,7 @@ When new items are added to a dict, they go on the "end": and ``dict.popitem()`` will remove the "last" item in the dict. -**CAUTION** This is new behavior in cPython 3.6 -- older versions of Python (notably including Python 2) do not preserve order. In older versions, there is a special version of a dict in the collections module: ``Collections.OrderedDict`` which preserves order in all versions of Python, and has a couple extra features. +**CAUTION** This is new behavior in cPython 3.6 -- older versions of Python (notably including Python 2) do not preserve order. In older versions, there is a special version of a dict in the collections module: ``collections.OrderedDict`` which preserves order in all versions of Python, and has a couple extra features. Dictionary Iterating @@ -442,8 +442,8 @@ If you want a copy, use the explicit copy method to get a copy: 'something': 'a value', 'something else': 'another value'} - In [54]: item_copy - Out[54]: {'something': 'a value', 'something else': 'another value'} + In [54]: item_copy + Out[54]: {'something': 'a value', 'something else': 'another value'} Sets diff --git a/source/modules/Files.rst b/source/modules/Files.rst index 0f372cb6..622e1abb 100644 --- a/source/modules/Files.rst +++ b/source/modules/Files.rst @@ -62,7 +62,7 @@ Text File Notes Text is default: - * Newlines are translated: ``\r\n -> \n`` + * Newlines are translated: ``\r\n`` -> ``\n`` * -- reading and writing! * Use \*nix-style in your code: ``\n`` @@ -212,8 +212,6 @@ Relative paths are relative to the current working directory, which is only rele os.getcwd() os.chdir(path) - os.path.abspath() - os.path.relpath() ``os.path`` module @@ -226,6 +224,8 @@ Relative paths are relative to the current working directory, which is only rele os.path.basename() os.path.dirname() os.path.join() + os.path.abspath() + os.path.relpath() (all platform independent) From ad825d6d195aa08a9496c76973a7d433fcba753e Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Wed, 24 Oct 2018 16:32:04 -0700 Subject: [PATCH 17/87] Update #!/bin/sh to #!/bin/bash (for Linux) --- build_gh_pages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_gh_pages.sh b/build_gh_pages.sh index d5d3047c..6a8a84c8 100755 --- a/build_gh_pages.sh +++ b/build_gh_pages.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # simple script to build and push to gh-pages # designed to be run from master From acc0422cdbb31c41771845986cf51c3bf4704cd1 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Thu, 25 Oct 2018 18:10:08 -0700 Subject: [PATCH 18/87] Fix Lesson 5 typos --- source/modules/CollectionsModule.rst | 2 +- source/modules/Comprehensions.rst | 10 ++++++---- source/modules/Documentation.rst | 6 +++--- source/modules/Exceptions.rst | 24 ++++++++++++------------ source/modules/Modules.rst | 6 +++--- source/modules/NamingThings.rst | 2 +- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/source/modules/CollectionsModule.rst b/source/modules/CollectionsModule.rst index c23897cc..842e2fa6 100644 --- a/source/modules/CollectionsModule.rst +++ b/source/modules/CollectionsModule.rst @@ -4,7 +4,7 @@ The Collections Module ###################### -Python has a very complete set of built in standard types that support most programming tasks. These include strings and numbers, and also types that can be used to hold other objects -- or "collection" types" +Python has a very complete set of built in standard types that support most programming tasks. These include strings and numbers, and also types that can be used to hold other objects -- or "collection" types. * tuples * lists diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst index f48a569a..93d56cd5 100644 --- a/source/modules/Comprehensions.rst +++ b/source/modules/Comprehensions.rst @@ -164,7 +164,9 @@ This results in the same set as this for loop: or, indeed, the same as passing a list comp to ``set()``. -new_set = set([expression_with_variable for variable in a_sequence]) +.. code-block:: python + + new_set = set([expression_with_variable for variable in a_sequence]) **Example:** Finding all the vowels in a string... @@ -180,7 +182,7 @@ new_set = set([expression_with_variable for variable in a_sequence]) .. note:: - Why did I use ``set('aeiou')`` rather than just `aeiou` ? ... ``in`` works with strings as well, but is it efficient? + Why did I use ``set('aeiou')`` rather than just ``'aeiou'`` ? ... ``in`` works with strings as well, but is it efficient? Dict Comprehensions @@ -297,7 +299,7 @@ dict comps are still nice if you need to filter the results, though: Generator Expressions --------------------- -There is yet another type of comprehension: generator comprehensions, technically known as "generator expressions". They are very much like a list comprehension, except that they evaluate to an lazy-evaluated "iterable", rather than a list. That is, they *generate* the items on the fly. +There is yet another type of comprehension: generator comprehensions, technically known as "generator expressions". They are very much like a list comprehension, except that they evaluate to a lazy-evaluated "iterable", rather than a list. That is, they *generate* the items on the fly. This is useful, because we often create a comprehension simply to loop over it right away: @@ -372,7 +374,7 @@ If we use it in a list comp: test called with: 2 Out[10]: [0, 1, 4] -We see that test gets called for all the values, and then a list is returned with all the results. +We see that ``test()`` gets called for all the values, and then a list is returned with all the results. But if we use it in a generator comprehension: .. code-block:: ipython diff --git a/source/modules/Documentation.rst b/source/modules/Documentation.rst index b1e370cf..9b471f91 100644 --- a/source/modules/Documentation.rst +++ b/source/modules/Documentation.rst @@ -60,7 +60,7 @@ This is not useful: # apply soap to each sponge worker.apply_soap(sponge) -Note: Nothing special about Python here -- basic good programing practice. Note that you will need a lto fewer comments if you choose your names well! +Note: Nothing special about Python here -- basic good programing practice. Note that you will need a lot fewer comments if you choose your names well! Docstrings ---------- @@ -89,9 +89,9 @@ A Function Docstring Should: * Be a complete sentence in the form of a command describing what the function does. - * """Return a list of values based on blah blah""" is a good docstring + * ``"""Return a list of values based on blah blah"""`` is a good docstring - * """Returns a list of values based on blah blah""" is *not* as good.. + * ``"""Returns a list of values based on blah blah"""`` is *not* as good.. * Have a useful single line. diff --git a/source/modules/Exceptions.rst b/source/modules/Exceptions.rst index 3d5b3fd1..cb314857 100644 --- a/source/modules/Exceptions.rst +++ b/source/modules/Exceptions.rst @@ -166,7 +166,7 @@ This is really important if your code does anything before the exception occurre In this case, the file will be properly closed regardless. And many other systems, like database managers, etc. can also be used with ``with``. -This is known as a "context manager", and was added to Python specifically to handle the common cases that required finally clauses. But if your use case does not already have a context manager that handles the cleanup you may need. +This is known as a "context manager", and was added to Python specifically to handle the common cases that required ``finally`` clauses. But if your use case does not already have a context manager that handles the cleanup you may need. Exceptions -- ``else`` ---------------------- @@ -185,11 +185,11 @@ Yet another flow control option: So the ``else`` block only runs if there was no exception. That was also the case in the previous code, so what's the difference? -**Advantage of ``else``:** +**Advantage of** ``else`` **:** Using the ``else`` block lets you catch the exception as close to where it occurred as possible -- always a good thing. -Why? -- because maybe the "process(f)" could raise an exception, too? Then you don't know if the exception came from the ``open()`` call or in some code after that. +Why? -- because maybe the ``process(f)`` could raise an exception, too? Then you don't know if the exception came from the ``open()`` call or in some code after that. This bears repeating: @@ -250,16 +250,16 @@ For an example -- try running this code: .. code-block:: ipython -In [34]: try: - ...: f = open("blah") - ...: except IOError as err: - ...: print(err) - ...: print(dir(err)) - ...: the_err = err + In [34]: try: + ...: f = open("blah") + ...: except IOError as err: + ...: print(err) + ...: print(dir(err)) + ...: the_err = err The ``print(dir(err))`` will print all the names (attributes) in the error object. A number of those are ordinary names that all objects have, but a few are specific to this error. -the ``the_err`` = err line is there so that we can keep a name bound to the err after the code is run. ``err`` as bound by the except line only exists inside the following block. +the ``the_err = err`` line is there so that we can keep a name bound to the ``err`` after the code is run. ``err`` as bound by the except line only exists inside the following block. Now that we have a name to access it, we can look at some of its attributes. The name of the file that was attempted to be opened: @@ -268,14 +268,14 @@ Now that we have a name to access it, we can look at some of its attributes. The In [35]: the_err.filename Out[35]: 'blah' -The message that will be pronted is usually in the "args" attribute: +The message that will be printed is usually in the ``.args`` attribute: .. code-block:: ipython In [37]: the_err.args Out[37]: (2, 'No such file or directory') -the ``.__traceback__`` attribute hold the actual traceback object -- all the information about the context the exception was raised in. That can inpsected to get all sorts of info. tHat is very advanced stuff, but you can investigate the ``inspect`` module if you want to know how. +the ``.__traceback__`` attribute hold the actual traceback object -- all the information about the context the exception was raised in. That can be inspected to get all sorts of info. That is very advanced stuff, but you can investigate the ``inspect`` module if you want to know how. Multiple Exceptions ------------------- diff --git a/source/modules/Modules.rst b/source/modules/Modules.rst index 314d9d82..865a781e 100644 --- a/source/modules/Modules.rst +++ b/source/modules/Modules.rst @@ -126,7 +126,7 @@ Packages A package is a module with other modules in it. -On a filesystem, this is represented as a directory that contains one or more``.py`` files, one of which **must** be called ``__init__.py``. +On a filesystem, this is represented as a directory that contains one or more ``.py`` files, one of which **must** be called ``__init__.py``. When you have a package, you can import only the package, or any of the modules inside it. When a package is imported, the code in the ``__init__.py`` file is run, and any names defined in that file are available in the *package namespace*. @@ -155,7 +155,7 @@ Save another file in your my_package dir called ``a_module.py``, and put this in def a_function(): print("a_function has been called") -You now have about the simplest package you can have. If make sure your current working dir is the dir that ``my_package`` is in, and start python or iPython. Then try this code: +You now have about the simplest package you can have. Make sure your current working dir is the dir that ``my_package`` is in, and start python or iPython. Then try this code: .. code-block:: ipython @@ -204,7 +204,7 @@ Note that you can also put a package inside a package. So you can create arbitra "Flat is better than nested." -So don't overdue it -- only go as deep as you really need to to keep the your code organized. +So don't overdo it -- only go as deep as you really need to to keep your code organized. Importing modules ----------------- diff --git a/source/modules/NamingThings.rst b/source/modules/NamingThings.rst index b343b353..bddc0c7a 100644 --- a/source/modules/NamingThings.rst +++ b/source/modules/NamingThings.rst @@ -54,7 +54,7 @@ Naming Guidelines ----------------- Whenever possible, use strong, unambiguous names that relate to a concept in the business area applicable for your program. -For example, cargo_weight is probably better than item_weight, current_fund_price is better than value. +For example, ``cargo_weight`` is probably better than ``item_weight``, ``current_fund_price`` is better than ``value``. Only use single-letter names for things with limited scope: indexes and the like: From dfaeaa97317baf5c8880a466d2b71a6f38da59a6 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Fri, 26 Oct 2018 19:33:35 -0700 Subject: [PATCH 19/87] added a nice link to the functions page. and a typi fix --- source/exercises/string_formatting.rst | 2 +- source/modules/Functions.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/source/exercises/string_formatting.rst b/source/exercises/string_formatting.rst index 6e0c45b1..d480f3dd 100644 --- a/source/exercises/string_formatting.rst +++ b/source/exercises/string_formatting.rst @@ -120,7 +120,7 @@ It will look like: def formatter(in_tuple): do_something_here_to_make_a_format_string - return form_string.format(in_tuple) + return form_string.format(*in_tuple) Task Four diff --git a/source/modules/Functions.rst b/source/modules/Functions.rst index 27b73c0c..c2503909 100644 --- a/source/modules/Functions.rst +++ b/source/modules/Functions.rst @@ -233,6 +233,12 @@ B) If not, then it does a computation using the same function with another value It is critical that the first check is there, or the function will never terminate. +Further Reading +--------------- + +Here's a nice blog post about writting better functions: + +https://jeffknupp.com/blog/2018/10/11/write-better-python-functions/ From 4cef2fde391929faabc16a2baff0d7867beb6cad Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Sat, 27 Oct 2018 20:12:27 -0700 Subject: [PATCH 20/87] Add initial .travis.yml --- .travis.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b858b1ac --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +dist: trusty + +language: python + +python: "3.6" + +cache: pip + +install: "pip install -r requirements.txt" + +script: + - make html + +deploy: + provider: pages + skip-cleanup: true + github-token: $GITHUB_TOKEN + keep-history: true + local-dir: build/html + on: + branch: master + target-branch: gh-pages From 82a3e12f45717625aba60cf4d8b48a27517fbb72 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Sun, 28 Oct 2018 10:22:19 -0700 Subject: [PATCH 21/87] Try to fix build warnings --- source/lesson_templates/course1_lesson03.rst | 2 +- source/lesson_templates/course1_lesson04.rst | 2 +- source/modules/PersistanceAndSerialization.rst | 2 +- source/modules/Properties.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/lesson_templates/course1_lesson03.rst b/source/lesson_templates/course1_lesson03.rst index ac830266..fc61c077 100644 --- a/source/lesson_templates/course1_lesson03.rst +++ b/source/lesson_templates/course1_lesson03.rst @@ -57,7 +57,7 @@ Mailroom Exercise You've now got the basics of the language down -- enough to write your first full "program": -:ref:`exercise_mailroom` (10 points) +:ref:`exercise_mailroom_part1` (10 points) Ungraded exercises ------------------ diff --git a/source/lesson_templates/course1_lesson04.rst b/source/lesson_templates/course1_lesson04.rst index 8e5b1c75..a1a14ecf 100644 --- a/source/lesson_templates/course1_lesson04.rst +++ b/source/lesson_templates/course1_lesson04.rst @@ -43,7 +43,7 @@ Graded Assignments * :ref:`exercise_file_lab` - * Update mailroom with dicts :ref:`exercise_mailroom_plus` + * Update mailroom with dicts :ref:`exercise_mailroom_part2_dict_files` * :ref:`exercise_trigrams` diff --git a/source/modules/PersistanceAndSerialization.rst b/source/modules/PersistanceAndSerialization.rst index 19ba8d93..f564b512 100644 --- a/source/modules/PersistanceAndSerialization.rst +++ b/source/modules/PersistanceAndSerialization.rst @@ -315,7 +315,7 @@ The CSV module Reading ``CSV`` files: -(uses: :download:`eggs.csv <../Examples/persistence/eggs.csv>`) +(uses: :download:`eggs.csv <../examples/persistence/eggs.csv>`) .. code-block:: ipython diff --git a/source/modules/Properties.rst b/source/modules/Properties.rst index 7668cf30..86f7e247 100644 --- a/source/modules/Properties.rst +++ b/source/modules/Properties.rst @@ -150,4 +150,4 @@ what you want. Play around with some properties code: -:download:`properties_example.py <../examples/Properties/properties_example.py>` +:download:`properties_example.py <../examples/properties/properties_example.py>` From c3b6d3afcc7f4f5b1dcab5f9df0e2eb113aa2379 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Thu, 1 Nov 2018 15:02:44 -0700 Subject: [PATCH 22/87] Try fixing sphinx version (for _downloads hash dir issue) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 29a37631..1b6df6a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -sphinx +sphinx==1.7.7 sphinx-rtd-theme ipython From 0125f55f38b7c197400a0a5c1ce3ec8040b26109 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Thu, 1 Nov 2018 17:12:50 -0700 Subject: [PATCH 23/87] Fix improper directory reference in Testing.rst --- source/modules/Testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/modules/Testing.rst b/source/modules/Testing.rst index a6ce481d..7a37ae3b 100644 --- a/source/modules/Testing.rst +++ b/source/modules/Testing.rst @@ -182,7 +182,7 @@ Pre-existing Tests Let's take a look at some examples. -in ``IntroPython-2017\Examples\Session06`` +in ``/examples/testing`` .. code-block:: bash From 38a28e10d60e0ae77eb4fa1f62d8e609df0bc18c Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 4 Nov 2018 21:08:39 -0800 Subject: [PATCH 24/87] fixed some typos and a few clarifications --- source/exercises/args_kwargs_lab.rst | 39 ++++++++++++++-------------- source/exercises/unit_testing.rst | 20 +++++++++++--- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/source/exercises/args_kwargs_lab.rst b/source/exercises/args_kwargs_lab.rst index 155dc188..0273c287 100644 --- a/source/exercises/args_kwargs_lab.rst +++ b/source/exercises/args_kwargs_lab.rst @@ -1,7 +1,7 @@ .. _exercise_args_kwargs_lab: -Args and Kwargs LAB -=================== +``args`` and ``kwargs`` LAB +=========================== Goal: ----- @@ -12,33 +12,31 @@ If this is all confusing -- you may want to review this: http://stupidpythonideas.blogspot.com/2013/08/arguments-and-parameters.html -Note: +.. note:: -This is not all that clearly specified -- the goal is for you to -experiment with various ways to define and call functions, so you -can understand what's possible, and what happens with each call. -It is also entirely silly, since the function does not do anything -at all, but it will teach you about using parameters effectively. + This is not all that clearly specified -- the goal is for you to + experiment with various ways to define and call functions, so you + can understand what's possible, and what happens with each call. + It is also entirely silly, since the function does not do anything + at all, but it will teach you about using parameters effectively. Test Driven Development? ------------------------ -Since this code isn't really going to do anything, it doesn't make a lot of sense to test it. However, you need to run the code somehow anyway. So this is a good chance to practice test-driven development anyway. +Since this code isn't really going to do anything, it doesn't make a lot of sense to test it. However, you need to run the code somehow. So this is a good chance to practice test-driven development -- even if only as a way to run your code as you write it. -So for each step of the exercise, write a test that calls your function in a particular way, and test that it returns what you expect. In this case, what you will be testing is not really the code -- but rather your own expectations of what the results should be. +For each step of the exercise, write a test that calls your function in a particular way, and test that it returns what you expect. In this case, what you will be testing is not really the code -- but rather your own expectations of what the results should be. You will also be testing Python's argument handling, which you can be pretty sure DOES work correctly. -So while these won't be useful tests in the usual sense, this is a chance to practice test driven development. +So while these won't be useful tests in the usual sense, this is a chance to get used to test driven development. Procedure --------- -We are going to do this as test driven development. So your first task for -each assignment below is to write a test that will ensure your code does what -we are telling you it should do. +We are going to do this as test driven development: Your first task for each step below is to write a test that will ensure your code does whatvwe are telling you it should do. **Keyword arguments:** @@ -49,15 +47,15 @@ we are telling you it should do. - `link_color` - `visited_color` -* Have it return the colors (use strings for the colors) +* Have it return the colors (use strings for the colors, e.g. "blue", "red", etc.) -* Call it with a couple different parameters set. IOW, write tests that verify that all of the following work as advertised: +* Call it with a couple different parameters set. That is, write tests that verify that all of the following work as advertised: - - using just positional arguments: + - Using just positional arguments: - ``func('red', 'blue', 'yellow', 'chartreuse')`` - - using just keyword arguments: + - Using just keyword arguments: - ``func(link_color='red', back_color='blue')`` @@ -80,10 +78,11 @@ we are telling you it should do. ``*args`` and ``**kwargs`` -* Have it return the colors (use strings for the colors) +* Have it return the colors (use strings for the colors again) * Call it with the same various combinations of arguments used above. -* Also have it print `args` and `kwargs` directly, so you can be sure you understand what's going on. +* Also have it print ``args`` and ``kwargs`` directly, so you can be sure you understand what's going on. * Note that in general, you can't know what will get passed into ``**kwargs`` So maybe adapt your function to be able to do something reasonable with any keywords. + diff --git a/source/exercises/unit_testing.rst b/source/exercises/unit_testing.rst index c1a7834b..72ba4057 100644 --- a/source/exercises/unit_testing.rst +++ b/source/exercises/unit_testing.rst @@ -4,6 +4,16 @@ Introduction To Unit Testing ############################ +Preparation +----------- + +In order to do unit testing, you need a framework in which to write and run your tests. +Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact a kind of unit test. +But as you build larger systems, you'll want a more structured way to write and run your tests. + + + + Test Driven Development ----------------------- @@ -19,7 +29,7 @@ and this test file: Put them in the same directory, and make that directory your working directory. -Then then try running the test file with pytest: +Then try running the test file with pytest: .. code-block:: bash @@ -48,7 +58,7 @@ and: In your editor. -Now edit ``cigar_party.py``, and each time you make a change, run teh tests again. Continue until all the tests pass. +Now edit ``cigar_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. Doing your own: --------------- @@ -62,11 +72,13 @@ Do a bit of test-driven development on it: * run something on the web site. * write a few tests using the examples from the site. -These tests should be in a file names ``test_something.py`` -- I usually name the test file the same as the module it tests, with ``test_`` prepended. +These tests should be in a file names ``test_something.py`` -- I usually name the test file the same as the module it tests, +with ``test_`` prepended. * then write the function, and fix it 'till it passes the tests. Do at least two of these to get the hang of the process. -Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner? Give it a shot -- you'll know if it still works if the tests still pass! +Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner? +Give it a shot -- you'll know if it still works if the tests still pass! From 8c8dbfa337d47a385bcc2d3a042ab4fdb61cc590 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 4 Nov 2018 22:01:06 -0800 Subject: [PATCH 25/87] clarification in the git setup page. --- source/modules/Git.rst | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/source/modules/Git.rst b/source/modules/Git.rst index a7a6bb26..0ee378ac 100644 --- a/source/modules/Git.rst +++ b/source/modules/Git.rst @@ -151,13 +151,15 @@ Once you are logged in to your gitHub account, go to the appropriate class repos https://github.com/UWPCE-PythonCert-ClassRepos +Make sure you find the right repo for YOUR class! + Once in the repo for your class, click on the "fork" button in the upper right of the page to create a fork in your gitHub account. You will now have a copy of the class repo, and can then set up your personal machine to connect to that copy. .. figure:: /_static/remotes_fork.png :width: 50% :class: center -Everyone should now have a copy of the class repository in their account on the GitHub website. +Yoy should now have a copy of the class repository in your account on the GitHub website. The next step is to make a *clone* of your fork on your own computer, which means that **your fork** in GitHub is the *origin*: @@ -183,22 +185,28 @@ Adding a remote Since you are working on a repository that you do not own, you will need to make a git shortcut to the original repository, so that you can get changes made by other contributors (i.e. the instructors and other students) before you start working. -You can add *remotes* at will, to connect your *local* repository to other -copies of it in different remote locations. +You can add *remotes* at will, to connect your *local* repository to other copies of it in different remote locations. This allows you to grab changes made to the repository in these other locations. For this class, you will add an *upstream* remote to our local copy that points to the original copy of the material in the -``UWPCE-PythonCert-ClassRepos`` account, and we will call it, appropriately, "upstream". Change directories into your local version of the class -repository and run (remembering to use the name of your class): +``UWPCE-PythonCert-ClassRepos`` account, and we will call it, appropriately, "upstream". +Change directories into your local version of the class repository: + +.. code-block:: bash + + $ cd YourClassRepoNameHere + +and run (remembering to use the name of your class): -(you can get that full url by going to GitHub, finding the repo, and copying the "clone" url) .. code-block:: bash $ git remote add upstream https://github.com/UWPCE-PythonCert-ClassRepos/YourClassRepoNameHere +You can get that full url by going to GitHub, finding the repo, and copying the "clone" url. then you can type the command, and simply paste the url. + Your local setup should now look something like this: .. code-block:: bash From 103db10c121ade6e040bf3632f182e3be6a05f45 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 10 Nov 2018 08:55:01 -0800 Subject: [PATCH 26/87] clean up packaging module a bit --- source/modules/Packaging.rst | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/source/modules/Packaging.rst b/source/modules/Packaging.rst index 02753d94..0afd2e33 100644 --- a/source/modules/Packaging.rst +++ b/source/modules/Packaging.rst @@ -39,7 +39,7 @@ of python files or other package directories:: The ``__init__.py`` can be totally empty -- or it can have arbitrary python code in it. The code will be run when the package is imported -- just like a module, -modules inside packages are *not* automatically imported. So, with the above sgructure:: +modules inside packages are *not* automatically imported. So, with the above structure:: import a_package @@ -95,12 +95,10 @@ means: "import all the names in the module" You really don't want to do that! It is an old pattern that is now an anti-pattern -But if you do encounter it, it doesn't actually import all the names -- -it imports the ones defined in the module's ``__all__`` variable. +But if you do encounter it, it doesn't actually import all the names -- it imports the ones defined in the module's ``__all__`` variable. -``__all__`` is a list of names that you want import * to import -- so -the module author can control it, and not expect all sorts of build ins -and other modules. +``__all__`` is a list of names that you want ``import *`` to import. +So the module author can control it, and not expect all sorts of builtins and other modules. But really -- don't use it! @@ -120,7 +118,7 @@ This gets confusing! There is a good discussion on Stack Overflow here: http://stackoverflow.com/questions/14132789/relative-imports-for-the-billionth-time -Relative imports allow you to refer to other modules relative to where the existing module is in the package hierarchy, rather than in the whole thing. For instance, with the following package structure:: +Relative imports allow you to refer to other modules relative to where the existing module is in the package hierarchy, rather than in the entire python module namespace. For instance, with the following package structure:: package/ __init__.py @@ -207,7 +205,7 @@ sys.modules '__file__', '__builtins__'] -you can access the module through the modules dict: +you can access the module through the ``sys.modules`` dict: .. code-block:: ipython @@ -251,15 +249,14 @@ Implications of module import process: * Every place your code imports a module it gets the *same* object - You can use this to share "global" state where you want to. -* If you change the code in a module while the program is running -- the - change will **not** show up, even if re-imported. +* If you change the code in a module while the program is running -- the change will **not** show up, even if re-imported. - That's what ``imp.reload()`` is for. The module search path ---------------------- -The interpreter keeps a list of all the places that it looks for modules or packages when you do an import: +The interpreter keeps a list (``sys.path``) of all the places that it looks for modules or packages when you do an import: .. code-block:: python @@ -267,11 +264,11 @@ The interpreter keeps a list of all the places that it looks for modules or pack for p in sys.path: print p -you can manipulate that list to add or remove paths to let python find modules on a new place. +you can manipulate that list to add or remove paths to let python find modules in a new place. -And every module has a ``__file__`` name that points to the path it lives in. This lets you add paths relative to where you are, etc. +Every module has a ``__file__`` name that points to the path it lives in. This lets you add paths relative to where you are, etc. -*NOTE* it's usually better to use setuptools' "develop" mode instead -- see below. + .. note:: It's usually better to use setuptools' "develop" mode (or ``pip install -e``) instead -- see below. Reloading --------- @@ -311,7 +308,8 @@ So far in this class, we've used the Python from python.org. It works great, and But there are also a few "curated" distributions. These provide python and a package management system for hard-to-build packages. -Widely used by the scipy community +Widely used by the scipy community: + (lots of hard to build stuff that needs to work together...) * Anaconda (https://store.continuum.io/cshop/anaconda/) @@ -328,9 +326,9 @@ If you are doing data science or scientific development -- I recommend you take Installing Packages =================== -Every Python installation has its own stdlib and ``site-packages`` folder +Every Python installation has its own stdlib and ``site-packages`` folder. -``site-packages`` is the default place for third-party packages +``site-packages`` is the default place for third-party packages. From source @@ -453,7 +451,7 @@ Sometimes simpler: 1) A lot of packages have Windows wheels now. - - often installable with pip (pip will install a wheel for you if it exists) + - Often installable with pip (pip will install a wheel for you if it exists) - Usually for python.org builds - Excellent source: http://www.lfd.uci.edu/~gohlke/pythonlibs/ - Make sure you get 32 or 64 bit consistent From 151ee5b4b4ae6a813753490d2faaec24830b754a Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 12 Nov 2018 09:54:43 -0800 Subject: [PATCH 27/87] a bit of clean up / clarification --- source/exercises/list_lab.rst | 8 +++---- source/exercises/mailroom-part3.rst | 9 +++++++- source/modules/Comprehensions.rst | 35 +++++++++++++++++------------ source/modules/Packaging.rst | 11 ++++----- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/source/exercises/list_lab.rst b/source/exercises/list_lab.rst index 3e787fb9..205acf9d 100644 --- a/source/exercises/list_lab.rst +++ b/source/exercises/list_lab.rst @@ -14,7 +14,7 @@ Goal: Learn the basic ins and outs of Python lists. -hint +Hint ---- to query the user for info at the command line, you use: @@ -114,6 +114,6 @@ Series 4 Once more, using the list from series 1: -- Make a copy of the list and reverse the letters in each fruit in the copy. -- Delete the last item of the original list. Display the original list and the - copy. +- Make a new list with the contents of the original, but with all the letters in each item reversed. + +- Delete the last item of the original list. Display the original list and the copy. diff --git a/source/exercises/mailroom-part3.rst b/source/exercises/mailroom-part3.rst index b2be6384..71701d2a 100644 --- a/source/exercises/mailroom-part3.rst +++ b/source/exercises/mailroom-part3.rst @@ -31,10 +31,17 @@ with [print(donor) for donor in donors] -That's not the intended use of comprehensions. Because ``print`` function does not have a value, this code will allocate a space for an "empty" result list filled with None values: +That's not the intended use of comprehensions. Because ``print`` function does not return a value, this code will allocate a space for an "empty" result list filled with None values: >>> [print(donor) for donor in donors] jane wendy [None, None] >>> + +List comprehensions are designed for a very specific use case: + +*Processing a sequence of items to create another sequence.* + +They are not designed to replace all for loops. + diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst index 93d56cd5..a594c669 100644 --- a/source/modules/Comprehensions.rst +++ b/source/modules/Comprehensions.rst @@ -14,7 +14,7 @@ The concept of "functional programming" is clearly defined in some contexts, but In general, code is considered "Pythonic" that uses functional paradigms where they are natural, but not when they have to be forced in. -We will cover functional programming concepts more clearly later in the program, but for now, we'll talk about the syntax for a common functional paradigm: applying an expression to all the members of a sequence to produce another sequence: +We will cover functional programming concepts more clearly later in the program, but for now, we'll talk about the syntax for a common functional paradigm: applying an expression to all the members of a sequence to produce another sequence. Consider this common ``for`` loop structure: @@ -26,7 +26,7 @@ Consider this common ``for`` loop structure: This is such a common pattern that python added syntax to directly support it. This syntax is known as "comprehensions". The most common of which is a list comprehension, used to build up a new list. There are a couple others, which we will get too later, but they all share a similar structure. -The above structure can be expressed with a single line using a "list comprehension" as so: +The above structure can be expressed with a single line using a "list comprehension" like so: .. code-block:: python @@ -34,7 +34,7 @@ The above structure can be expressed with a single line using a "list comprehens Nice and clear and compact, and the use of the "list" brackets (``[...]``) makes it clear you are making a list. -Recall what an expression is in Python: a bit of code (names and operators) that evaluates to a value. So in the beginning of a comprehension, you can put anything that evaluates to a value -- and that value is what gets added to the list. +Recall what an expression is in Python: a bit of code (names and operators) that evaluates to a value. So in the beginning of a comprehension, you can put anything that evaluates to a value -- and that value is what gets added to the new list. This can be a simple (or complex) math operation: ``x * 3``, or a function or method call: ``a_string.upper()``, ``int(x)``, etc. But it can not contain any statements: code that does not return a value, such as assignment (``x = 5``), or ``for`` loops, or ``if`` blocks. @@ -188,14 +188,14 @@ or, indeed, the same as passing a list comp to ``set()``. Dict Comprehensions ------------------- -Also with dictionaries +You can also build up a dictionary with a comprehension: .. code-block:: python new_dict = {key: value for variable in a_sequence} -Same as this for loop: +Which is the same as this for loop: .. code-block:: python @@ -203,7 +203,7 @@ Same as this for loop: for key in a_list: new_dict[key] = value -A dict comprehension also uses curly brackets -- Python knows it's a dict comprehension due to the ``key: value`` construct. +A dict comprehension also uses curly brackets like the set comprehension -- Python knows it's a dict comprehension due to the ``key: value`` construct. **Example:** @@ -269,11 +269,13 @@ But there is also a ``dict()`` constructor (actually the type object for dict): in the keyword argument list. For example: dict(one=1, two=2) Type: type -So the first one is an empty dict -- simple enough +``dict()`` can take different types of arguments, and will do something different with each one. -The second makes a dict from the contents of another dict (or similar object) +The first option (no argument) is an empty dict -- simple enough. -The third one is of interest here -- it makes a dict from an iterable of key, value pairs -- exactly what ``zip()`` gives you. +The option makes a dict from the contents of another dict or similar object (called a "mapping"). + +The options is of interest here -- it makes a dict from an iterable of key, value pairs -- exactly what ``zip()`` gives you. So we can create a dict from data like so: @@ -296,8 +298,8 @@ dict comps are still nice if you need to filter the results, though: Out[17]: {1: 'fred', 2: 'john'} -Generator Expressions ---------------------- +Generator Comprehensions +------------------------ There is yet another type of comprehension: generator comprehensions, technically known as "generator expressions". They are very much like a list comprehension, except that they evaluate to a lazy-evaluated "iterable", rather than a list. That is, they *generate* the items on the fly. @@ -308,7 +310,7 @@ This is useful, because we often create a comprehension simply to loop over it r for x in [y**2 for y in a_sequence]: outfile.write(f"The number is: {x}") -In this case, the comprehension: ``[y**2 for y in a_sequence]`` iterates over ``a_sequence``, computes the square of each item, and creates a whole new list with the new values. +In this case, the list comprehension: ``[y**2 for y in a_sequence]`` iterates over ``a_sequence``, computes the square of each item, and creates a whole new list with the new values. All this, just so it can be iterated over again right away. If the original sequence is large (or is itself a lazy-evaluated iterable), then the step of creating the extra list can be expensive and unnecessary. Generator comprehensions, on the other hand, create an iterable that evaluates the items as they are iterated over, rather than all at once ahead of time -- so the entire collection is never stored. @@ -353,7 +355,7 @@ A generator is an object that can be iterated over with a for loop, and it will 4 9 -You will learn more about generators, and other ways to make them, in future lessons. +You will learn more about generators and other ways to make them in future lessons. Let's use a little function to make this clear: @@ -364,6 +366,11 @@ Let's use a little function to make this clear: ...: return x ** 2 It simply returns the square of the passed-in value, but prints it as it does so, so we can see when it is called. + +.. note:: + Having a "print" in a function is a example of a "side effect" -- something that is an effect of the function being called that is not reflected in the return value of that function. + As a rule, it's not a good idea to use functions with side effects in comprehensions. We're only doing it here as a debugging aid -- so we can clearly see when the function is being called. + If we use it in a list comp: .. code-block:: ipython @@ -411,7 +418,7 @@ You usually don't assign a generator expression to a variable, but rather, loop test called with: 2 4 -When to use What +When to Use What ................ It's pretty simple: diff --git a/source/modules/Packaging.rst b/source/modules/Packaging.rst index 0afd2e33..522ddbf5 100644 --- a/source/modules/Packaging.rst +++ b/source/modules/Packaging.rst @@ -1047,7 +1047,7 @@ I personally like the simplest one with the least magic: setup( ... package_data={'pkg_name': ['data/datatfile1', - 'data/datafile2']}, + 'data/datafile2']}, ... ) @@ -1133,15 +1133,12 @@ LAB: A Small Example Package - ``at least one working test`` -* If you are ready -- it can be the start of your project package. +* If you have some code of your own ready to go -- use that. -Start with the silly code in: +* If you don't have any code of your own to package, start with the silly code in: :download:`capitalize.zip <../examples/packaging/capitalize.zip>` -Or go straight to making a package our of mailroom project. - - - +Or go straight to making a package our of your mailroom project. From af274515513c07163deb083f44ca93d9e190fa62 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 12 Nov 2018 10:08:37 -0800 Subject: [PATCH 28/87] removed duplicate solutions --- .../exceptions/except_exercise_solution.py | 54 ------------------- source/solutions/exceptions/except_test.py | 41 -------------- source/solutions/exceptions/safe_input.py | 41 -------------- 3 files changed, 136 deletions(-) delete mode 100644 source/solutions/exceptions/except_exercise_solution.py delete mode 100644 source/solutions/exceptions/except_test.py delete mode 100644 source/solutions/exceptions/safe_input.py diff --git a/source/solutions/exceptions/except_exercise_solution.py b/source/solutions/exceptions/except_exercise_solution.py deleted file mode 100644 index 14572ca8..00000000 --- a/source/solutions/exceptions/except_exercise_solution.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/python - -""" -An exercise in playing with Exceptions. -Make lots of try/except blocks for fun and profit. -Please remember to catch specifically the error you find, -and not all errors. -""" - -from except_test import fun, more_fun, last_fun - - -# Figure out what the exception is, catch it and while still -# in that catch block, try again with the second item in the list -first_try = ['spam', 'cheese', 'mr death'] - -try: - joke = fun(first_try[0]) -except NameError: - joke = fun(first_try[1]) - -# Here is a try/except block. Add an else that prints not_joke -try: - not_joke = fun(first_try[2]) -except SyntaxError: - print('Run Away!') -else: - print(not_joke) - -# What did that do? You can think of else in this context, as well -# as in loops as meaning: else if nothing went wrong. -# (no breaks in loops, no exceptions in try blocks) - -# Figure out what the exception is, catch it and in that same block -# try calling the more_fun function with the 2nd language -# in the list, again assigning it to next_joke. - -# If there are no exceptions, call the more_fun -# function with the last language in the list -# Regardless of whether there was an exception - -# Finally, while still in the try/except block -# and regardless of whether there were any exceptions, -# call the function last_fun with no parameters. (pun intended) - -langs = ['java', 'c', 'python'] -try: - more_joke = more_fun(langs[0]) -except IndexError: - more_joke = more_fun(langs[1]) -else: - more_joke = more_fun(langs[-1]) -finally: - last_fun() diff --git a/source/solutions/exceptions/except_test.py b/source/solutions/exceptions/except_test.py deleted file mode 100644 index 905dd675..00000000 --- a/source/solutions/exceptions/except_test.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 - -""" -silly little test module that is designed to trigger Exceptions when -run from the except_exercise.py file -""" - -import time - -conclude = "And what leads you to that conclusion?" -district = "Finest in the district, sir." -cheese = "It's certainly uncontaminated by cheese." -clean = "Well, it's so clean." -shop = "Not much of a cheese shop really, is it?" -cust = "Customer: " -clerk = "Shopkeeper: " - - -def fun(reaper): - if reaper == 'spam': - print(s) - elif reaper == 'cheese': - print() - print('Spam, Spam, Spam, Spam, Beautiful Spam') - elif reaper == 'mr death': - print() - return('{}{}\n{}{}'.format(cust, shop, clerk, district)) - - -def more_fun(language): - if language == 'java': - test = [1, 2, 3] - test[5] = language - elif language == 'c': - print('{}{}\n{}{}'.format(cust, conclude, clerk, clean)) - - -def last_fun(): - print(cust, cheese) - time.sleep(1) - import antigravity diff --git a/source/solutions/exceptions/safe_input.py b/source/solutions/exceptions/safe_input.py deleted file mode 100644 index e01d346f..00000000 --- a/source/solutions/exceptions/safe_input.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python - -""" -Exceptions Lab solution: - -The raw_input() function can generate two exceptions: -EOFError or KeyboardInterrupt on end-of-file(EOF) or canceled input. - -Create a wrapper function, perhaps safe_input() that returns None rather -rather than raising these exceptions, when the user enters ^C for Keyboard -Interrupt, or ^D (^Z on Windows) for End Of File. - -NOTE: if the user types a few charactors, and the hits ctrl+C, the -KeyboardInterrupt gets caught somewhere deeper in the process, and -this function doesn't work. - -Don't worry about that -- I don't really understnd what's going on in -the REPL (Read, Evaluate, Print Loop) either -- and the point of this -assigment is simple Exception handling. -""" - - -def safe_input(prompt_string=""): - """ - print user for input -- returning a string. - - This is just like the built-in input(), but it will return None - if the user hits ctrl+c, (or ctrl+D) rather than raise an Exception. - """ - try: - response = input(prompt_string) - return response - except (EOFError, KeyboardInterrupt): - return None - -if __name__ == "__main__": - response = safe_input("Say Something: ") - if response is None: - print("Hey! you cancled out!!!") - else: - print("You said:", response) From d1cdfa8b87a33c8e026f7937ead3fe82590bea4d Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 12 Nov 2018 10:09:32 -0800 Subject: [PATCH 29/87] removed duplicate solutions --- .../dict_set_with_comps_solution.py | 74 ------------------- .../comprehensions/fizz_buzz_comprehension.py | 6 -- 2 files changed, 80 deletions(-) delete mode 100644 source/solutions/comprehensions/dict_set_with_comps_solution.py delete mode 100755 source/solutions/comprehensions/fizz_buzz_comprehension.py diff --git a/source/solutions/comprehensions/dict_set_with_comps_solution.py b/source/solutions/comprehensions/dict_set_with_comps_solution.py deleted file mode 100644 index a60375eb..00000000 --- a/source/solutions/comprehensions/dict_set_with_comps_solution.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python - -""" -dict/set lab solutions: Chris' version. - -This time with comprehensions: -""" - -food_prefs = {"name": "Chris", - "city": "Seattle", - "cake": "chocolate", - "fruit": "mango", - "salad": "greek", - "pasta": "lasagna"} - -# 1. Print the dict by passing it to a string format method, so that you -# get something like: - -print("{name} is from Seattle, and he likes {cake} cake, {fruit} fruit," - "{salad} salad, and {pasta} pasta".format(**food_prefs)) - -# 2. Using a list comprehension, build a dictionary of numbers from zero -# to fifteen and the hexadecimal equivalent (string is fine). - -print(dict([(i, hex(i)) for i in range(16)])) - -# 3. Do the previous entirely with a dict comprehension -- should be a one-liner - -print({i: hex(i) for i in range(16)}) - -# 4. Using the dictionary from item 1: Make a dictionary using the same -# keys but with the number of 'a's in each value. You can do this either -# by editing the dict in place, or making a new one. If you edit in place, -# make a copy first! - -print({key: val.count('a') for key, val in food_prefs.items()}) - - -# 5. Create sets s2, s3 and s4 that contain numbers from zero through twenty, -# divisible 2, 3 and 4. - -# a. Do this with one set comprehension for each set. -s2 = {i for i in range(21) if not i % 2} -s3 = {i for i in range(21) if not i % 3} -s4 = {i for i in range(21) if not i % 4} - - -print("\nHere are the three sets:") -print(s2) -print(s3) -print(s4) - -# b. What if you had a lot more than 3? -- Don't Repeat Yourself (DRY) -# - create a sequence that holds all three sets -# - loop through that sequence to build the sets up -- so no repeated code. - -n = 5 -divisors = range(2, n + 1) -# create a list of empty sets -sets = [set() for i in divisors] - -# fill up the sets -for i, st in zip(divisors, sets): - [st.add(j) for j in range(21) if not j % i] - -print("\nHere are all the sets:\n", sets) - - -# c. Extra credit: do it all as a one-liner by nesting a set comprehension -# inside a list comprehension. (OK, that may be getting carried away!) - -sets = [{i for i in range(21) if not i % j} for j in range(2, n + 1)] - -print("\nHere are all the sets from the one liner:\n", sets) diff --git a/source/solutions/comprehensions/fizz_buzz_comprehension.py b/source/solutions/comprehensions/fizz_buzz_comprehension.py deleted file mode 100755 index 44588fde..00000000 --- a/source/solutions/comprehensions/fizz_buzz_comprehension.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 - - -fb = [[str(i), 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)] for i in range(1, 101)] - -print('\n'.join(fb)) From f1d2471f2bb5c1292e160b6f6cdb1f491a858b85 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Tue, 13 Nov 2018 20:30:49 -0800 Subject: [PATCH 30/87] Fix typos in source/exercises/html_renderer_tutorial.rst --- source/exercises/html_renderer_tutorial.rst | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/source/exercises/html_renderer_tutorial.rst b/source/exercises/html_renderer_tutorial.rst index c75f0c7e..137e01b6 100644 --- a/source/exercises/html_renderer_tutorial.rst +++ b/source/exercises/html_renderer_tutorial.rst @@ -441,7 +441,7 @@ Darn! Something is wrong here. And this time it errored out before it even got r It failed when we tried to write to the file. We're trying to write a piece of content, and we got a ``NoneType``. How in the world did a ``NoneType`` (which is the type of None) get in there? -Where does the ``self.contents`` list get created? In the ``__init__``. Let's do a little print debugging here. Add a print to the __init__: +Where does the ``self.contents`` list get created? In the ``__init__``. Let's do a little print debugging here. Add a print to the ``__init__``: .. code-block:: python @@ -460,9 +460,9 @@ And run the tests again:: ====================== 1 failed, 3 passed in 0.06 seconds ====================== -Same failure -- but pytest does a nice job of showing you what was printed (stdout) when a test fails. So in this case, at the end of the ``__init__`` method, the contents list looks like ``[None]`` -- a list with a single None object in it. No wonder it failed later when we tried to write that None to a file! +Same failure -- but pytest does a nice job of showing you what was printed (stdout) when a test fails. So in this case, at the end of the ``__init__`` method, the contents list looks like ``[None]`` -- a list with a single None object in it. No wonder it failed later when we tried to write that ``None`` to a file! -But why? Well, looking at the ``__init__``, it looks like content gets set to None by default:: +But why? Well, looking at the ``__init__``, it looks like content gets set to None by default: .. code-block:: python @@ -561,13 +561,17 @@ OK, did you do something as simple as this? That's it! But what does that mean? This line: -``class Body(Element):`` +.. code-block:: python + + class Body(Element): -means: make a new subclass of the ``Element`` tag called "Body". +means: make a new subclass of the ``Element`` tag called ``Body``. and this line: -`` tag = 'body'`` +.. code-block:: python + + tag = 'body' means: set the ``tag`` class attribute to ``'body'``. Since this class attribute was set on the ``Element`` class already, this is called "overriding" the tag attribute. @@ -585,7 +589,7 @@ Let's run the tests and see if this worked:: =========================== 7 passed in 0.02 seconds =========================== -Success!. We now have three different tags. +Success! We now have three different tags. .. note:: Why the ``Html`` element? Doesn't the ``Element`` class already use the "html" tag? @@ -681,7 +685,7 @@ So we need a way to write an element to a file. How might we do that? Inside the 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. -Looking at the signature of the render method:: +Looking at the signature of the render method: .. code-block:: python @@ -1169,7 +1173,7 @@ So we need to render the ``<``, then the ``p``, then a bunch of attribute name=v out_file.write("".join(open_tag)) ... -OK, the rest of the tests are still passing for me; I haven't broken anything else. Now to add code to render the attributes. You'll need to write some sort of loop to loop through each attribute, probably looping through the keys and the values:: +OK, the rest of the tests are still passing for me; I haven't broken anything else. Now to add code to render the attributes. You'll need to write some sort of loop to loop through each attribute, probably looping through the keys and the values: .. code-block:: python From d52d3c9647568e27776a87830f20a129c65ddb7d Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Tue, 13 Nov 2018 20:57:10 -0800 Subject: [PATCH 31/87] More typo fixes in html_renderer_tutorial.rst --- source/exercises/html_renderer_tutorial.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/source/exercises/html_renderer_tutorial.rst b/source/exercises/html_renderer_tutorial.rst index 137e01b6..e86de07a 100644 --- a/source/exercises/html_renderer_tutorial.rst +++ b/source/exercises/html_renderer_tutorial.rst @@ -252,7 +252,7 @@ In this case, the "html" part is stored in a class attribute. So how would you m "<{}>".format(self.tag) -and +and:: "".format(self.tag) @@ -1258,7 +1258,13 @@ Step 5: Create a ``SelfClosingTag`` subclass of Element, to render tags like:: -
and
(horizontal rule and line break)." +
+ +and:: + +
+ +(horizontal rule and line break) Including with attributes:: From c6e67be326bc08cb912a8e1b10683a6a1ec2b2fc Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 17 Nov 2018 13:40:26 -0800 Subject: [PATCH 32/87] a bit of formatting and a new video link in the metaclasses page. --- source/conf.py | 2 +- source/modules/MetaProgramming.rst | 9 +++++++-- source/modules/StaticAndClassMethods.rst | 11 ++++++++--- source/scripts/Collections.rst | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/source/conf.py b/source/conf.py index 013802c2..ee5d2fbe 100644 --- a/source/conf.py +++ b/source/conf.py @@ -63,7 +63,7 @@ "Christy Heaton", "Jon Jacky", "Maria McKinley", - "Andy Miles" + "Andy Miles", "Rick Riehle", "Joseph Schilz", "Joseph Sheedy", diff --git a/source/modules/MetaProgramming.rst b/source/modules/MetaProgramming.rst index d23d7a3c..efe62c0e 100644 --- a/source/modules/MetaProgramming.rst +++ b/source/modules/MetaProgramming.rst @@ -497,7 +497,7 @@ In Python 2, instead of the keyword argument, a special class attribute: Otherwise it's the same. -The __metaclass__ attribute is part of determining that function. If __metaclass__ is a key in the body dictionary then the value of that key is used. This value could be anything, although if not callable an exception will be raised. +The __metaclass__ attribute is part of determining that function. If __metaclass__ is a key in the body dictionary then the value of that key is used. This value could be anything, although if not callable an exception will be raised. from http://jfine-python-classes.readthedocs.io/en/latest/decorators-versus-metaclass.html Why use metaclasses? @@ -764,7 +764,7 @@ This also happens at compile time, rather than run time, just like metaclasses. class decorators were actually introduced AFTER metaclasses -- maybe they are a clearer solution to some problems? -As an example, in Python 3.7 (which is in beta release as of this writing), there is a new feature in the standard library: ``Data Classes``, introduced in +As an example, in Python 3.7, there is a new feature in the standard library: ``Data Classes``, introduced in `PEP 557 `_ They are a quick way to make a simple class whose prime purpose is to store a set of fields -- kind of like a database record. What the new tool provides is auto-generation of all the boilerplate code for the ``__init__``, etc. They could have been implemented with a metaclass, but it was decided to use a class decorator instead. From the PEP: @@ -785,6 +785,11 @@ And another one: `A Study of Python's More Advanced Features Part III: Classes and Metaclasses `_ +And this is a argument for class decorators by the author or the patch that enabled them (in Python 2.6): + +`Jack Diederich: Class Decorators: Radically Simple `_ + + NameMangler Decorator Edition ----------------------------- diff --git a/source/modules/StaticAndClassMethods.rst b/source/modules/StaticAndClassMethods.rst index ff8b7a76..206ee2c9 100644 --- a/source/modules/StaticAndClassMethods.rst +++ b/source/modules/StaticAndClassMethods.rst @@ -139,9 +139,9 @@ implements an alternate constructor that *can*. self[key] = value return self -(This is actually from the OrderedDict implementation in ``collections.py``). +(This is actually from the ``OrderedDict`` implementation in ``collections.py``). -See also datetime.datetime.now(), etc.... +See also ``datetime.datetime.now()``, etc.... Properties, Static Methods and Class Methods are powerful features of Python's @@ -157,4 +157,9 @@ well. .. _Here is a low level look: https://docs.python.org/2/howto/descriptor.html -For the Circle Lab: use a class method to make an alternate constructor that takes the diameter instead. +For the Circle Excercise: use a class method to make an alternate constructor that takes the diameter instead. + +Ultimately, make a subclass of ``Circle``, called ``Sphere``. Check and see if the ``.from_diameter`` alternate consructor sill works! + + + diff --git a/source/scripts/Collections.rst b/source/scripts/Collections.rst index 4644c829..dcc4c8d0 100644 --- a/source/scripts/Collections.rst +++ b/source/scripts/Collections.rst @@ -2,7 +2,7 @@ .. _script_collections: -Collecitons Video +Collections Video ================= With Rick Riehle From 37aacb4fe253f8260ba14b5aace5c12e304aea0d Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 17 Nov 2018 17:30:47 -0800 Subject: [PATCH 33/87] added a video link to naming things --- source/modules/NamingThings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/modules/NamingThings.rst b/source/modules/NamingThings.rst index bddc0c7a..dbbfddd6 100644 --- a/source/modules/NamingThings.rst +++ b/source/modules/NamingThings.rst @@ -54,7 +54,7 @@ Naming Guidelines ----------------- Whenever possible, use strong, unambiguous names that relate to a concept in the business area applicable for your program. -For example, ``cargo_weight`` is probably better than ``item_weight``, ``current_fund_price`` is better than ``value``. +For example, ``cargo_weight`` is probably better than ``item_weight``, ``current_fund_price`` is better than ``value``. But all of those are better than ``item``, or ``x``, or ... Only use single-letter names for things with limited scope: indexes and the like: From 8dab64a8a0850fcda6e0a8ad98fc71673bc25f43 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 17 Nov 2018 23:50:17 -0800 Subject: [PATCH 34/87] a bit of formatting cleanup --- source/modules/NamingThings.rst | 3 +-- source/modules/ObjectOrientationOverview.rst | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/modules/NamingThings.rst b/source/modules/NamingThings.rst index dbbfddd6..95593868 100644 --- a/source/modules/NamingThings.rst +++ b/source/modules/NamingThings.rst @@ -93,5 +93,4 @@ And then singular for a single item in that collection: Here's a nice talk about naming: -http://pyvideo.org/video/3792/name-things-once-0 - +`Jack Diederich: Name things Once `_ diff --git a/source/modules/ObjectOrientationOverview.rst b/source/modules/ObjectOrientationOverview.rst index 4abb9dc6..1e695674 100644 --- a/source/modules/ObjectOrientationOverview.rst +++ b/source/modules/ObjectOrientationOverview.rst @@ -118,7 +118,8 @@ This may be a good approach for a "pure" OO language, but with Python it tends t So my recommendation is to think in terms of what makes sense for your project: - -- there is no single paradigm for software design. +.. centered:: **There is no single best paradigm for software design** + Python's roots From 7dff4e9e77ca67278f22f27b1a605908d2e4f229 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 18 Nov 2018 17:29:14 -0800 Subject: [PATCH 35/87] removing redundant solutions --- source/modules/NamingThings.rst | 2 +- .../mailroom/mailroom_test/mailroom2.py | 220 ------------------ .../mailroom/mailroom_test/test_mailroom2.py | 104 --------- 3 files changed, 1 insertion(+), 325 deletions(-) delete mode 100755 source/solutions/mailroom/mailroom_test/mailroom2.py delete mode 100644 source/solutions/mailroom/mailroom_test/test_mailroom2.py diff --git a/source/modules/NamingThings.rst b/source/modules/NamingThings.rst index 95593868..362ada94 100644 --- a/source/modules/NamingThings.rst +++ b/source/modules/NamingThings.rst @@ -93,4 +93,4 @@ And then singular for a single item in that collection: Here's a nice talk about naming: -`Jack Diederich: Name things Once `_ +`Jack Diederich: Name things Once `_ diff --git a/source/solutions/mailroom/mailroom_test/mailroom2.py b/source/solutions/mailroom/mailroom_test/mailroom2.py deleted file mode 100755 index 49dfd81d..00000000 --- a/source/solutions/mailroom/mailroom_test/mailroom2.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python -""" -mailroom assignment - -This version uses a dict for the main db, and exception handling to -check input, and has been factored to be amenable to testing. -""" - -import sys -import math - -# handy utility to make pretty printing easier -from textwrap import dedent - - -# In memory representation of the donor database -# using a tuple for each donor -# -- kind of like a record in a database table -# using a dict with a lower case version of the donor's name as the key -# This makes it easier to have a 'normalized' key. -# you could get a bit fancier by having each "record" be a dict, with -# "name" and "donations" as keys. -def get_donor_db(): - return {'william gates iii': ("William Gates III", [653772.32, 12.17]), - 'jeff bezos': ("Jeff Bezos", [877.33]), - 'paul allen': ("Paul Allen", [663.23, 43.87, 1.32]), - 'mark zuckerberg': ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]), - } - - -def list_donors(): - """ - creates a list of the donors as a string, so they can be printed - - Not calling print from here makes it more flexible and easier to - test - """ - listing = ["Donor list:"] - for donor in donor_db.values(): - listing.append(donor[0]) - return "\n".join(listing) - - -def find_donor(name): - """ - find a donor in the donor db - - :param: the name of the donor - - :returns: The donor data structure -- None if not in the donor_db - """ - key = name.strip().lower() - return donor_db.get(key) - - -def add_donor(name): - """ - Add a new donor to the donor db - - :param: the name of the donor - - :returns: the new Donor data structure - """ - name = name.strip() - donor = (name, []) - donor_db[name.lower()] = donor - return donor - - -def main_menu_selection(): - """ - Print out the main application menu and then read the user input. - """ - action = input(dedent(''' - Choose an action: - - 1 - Send a Thank You - 2 - Create a Report - 3 - Send letters to everyone - 4 - Quit - - > ''')) - return action.strip() - - -def gen_letter(donor): - """ - Generate a thank you letter for the donor - - :param: donor tuple - - :returns: string with letter - - note: This doesn't actually write to a file -- that's a separate - function. This makes it more flexible and easier to test. - """ - return dedent('''Dear {0:s}, - - Thank you for your very kind donation of ${1:.2f}. - It will be put to very good use. - - Sincerely, - -The Team - '''.format(donor[0], donor[1][-1])) - - -def send_thank_you(): - """ - Execute the logic to record a donation and generate a thank you message. - """ - # Read a valid donor to send a thank you from, handling special commands to - # let the user navigate as defined. - while True: - name = input("Enter a donor's name (or list to see all donors or 'menu' to exit)> ").strip() - if name == "list": - print(list_donors()) - elif name == "menu": - return - else: - break - - # Now prompt the user for a donation amount to apply. Since this is - # also an exit point to the main menu, we want to make sure this is - # done before mutating the db. - while True: - amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip() - if amount_str == "menu": - return - # Make sure amount is a valid amount before leaving the input loop - try: - amount = float(amount_str) - # extra check here -- unlikely that someone will type "NaN", but - # it IS possible, and it is a valid floating point number: - # http://en.wikipedia.org/wiki/NaN - if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: - raise ValueError - # in this case, the ValueError could be raised by the float() call, or by the NaN-check - except ValueError: - print("error: donation amount is invalid\n") - else: - break - - # If this is a new user, ensure that the database has the necessary - # data structure. - donor = find_donor(name) - if donor is None: - donor = add_donor(name) - - # Record the donation - donor[1].append(amount) - print(gen_letter(donor)) - - -def sort_key(item): - # used to sort on name in donor_db - return item[1] - - -def generate_donor_report(): - """ - Generate the report of the donors and amounts donated. - - :returns: the donor report as a string. - """ - # First, reduce the raw data into a summary list view - report_rows = [] - for (name, gifts) in donor_db.values(): - total_gifts = sum(gifts) - num_gifts = len(gifts) - avg_gift = total_gifts / num_gifts - report_rows.append((name, total_gifts, num_gifts, avg_gift)) - - # sort the report data - report_rows.sort(key=sort_key) - report = [] - report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name", - "Total Given", - "Num Gifts", - "Average Gift")) - report.append("-" * 66) - for row in report_rows: - report.append("{:25s} ${:10.2f} {:9d} ${:11.2f}".format(*row)) - return "\n".join(report) - - -def save_letters_to_disk(): - """ - make a letter for each donor, and save it to disk. - """ - for donor in donor_db.values(): - letter = gen_letter(donor) - # I don't like spaces in filenames... - filename = donor[0].replace(" ", "_") + ".txt" - open(filename, 'w').write(letter) - - -def print_donor_report(): - print(generate_donor_report()) - - -def quit(): - sys.exit(0) - -if __name__ == "__main__": - - donor_db = get_donor_db() - - running = True - - selection_dict = {"1": send_thank_you, - "2": print_donor_report, - "3": save_letters_to_disk, - "4": quit} - - while True: - selection = main_menu_selection() - try: - selection_dict[selection]() - except KeyError: - print("error: menu selection is invalid!") diff --git a/source/solutions/mailroom/mailroom_test/test_mailroom2.py b/source/solutions/mailroom/mailroom_test/test_mailroom2.py deleted file mode 100644 index e633e575..00000000 --- a/source/solutions/mailroom/mailroom_test/test_mailroom2.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python - -""" -unit tests for the mailroom program -""" -import os - -import mailroom2 as mailroom - -# so that it's there for the tests -mailroom.donor_db = mailroom.get_donor_db() - - -def test_list_donors(): - listing = mailroom.list_donors() - - # hard to test this throughly -- better not to hard code the entire - # thing. But check for a few aspects -- this will catch the likely - # errors - assert listing.startswith("Donor list:\n") - assert "Jeff Bezos" in listing - assert "William Gates III" in listing - assert len(listing.split('\n')) == 5 - - -def test_find_donor(): - """ checks a donor that is there, but with odd case and spaces""" - donor = mailroom.find_donor("jefF beZos ") - - assert donor[0] == "Jeff Bezos" - - -def test_find_donor_not(): - "test one that's not there" - donor = mailroom.find_donor("Jeff Bzos") - - assert donor is None - - -def test_gen_letter(): - """ test the donor letter """ - - # create a sample donor - donor = ("Fred Flintstone", [432.45, 65.45, 230.0]) - letter = mailroom.gen_letter(donor) - # what to test? tricky! - assert letter.startswith("Dear Fred Flintstone") - assert letter.endswith("-The Team\n") - assert "donation of $230.00" in letter - - -def test_add_donor(): - name = "Fred Flintstone " - - donor = mailroom.add_donor(name) - donor[1].append(300) - assert donor[0] == "Fred Flintstone" - assert donor[1] == [300] - assert mailroom.find_donor(name) == donor - - -def test_generate_donor_report(): - - report = mailroom.generate_donor_report() - - print(report) # printing so you can see it if it fails - # this is pretty tough to test - # these are not great, because they will fail if unimportant parts of the - # report are changed. - # but at least you know that codes working now. - assert report.startswith("Donor Name | Total Given | Num Gifts | Average Gift") - - assert "Jeff Bezos $ 877.33 1 $ 877.33" in report - - -def test_save_letters_to_disk(): - """ - This only tests that the files get created, but that's a start - - Note that the contents of the letter was already - tested with test_gen_letter - """ - - mailroom.save_letters_to_disk() - - assert os.path.isfile('Jeff_Bezos.txt') - assert os.path.isfile('William_Gates_III.txt') - # check that it'snot empty: - with open('William_Gates_III.txt') as f: - size = len(f.read()) - assert size > 0 - - -if __name__ == "__main__": - # this is best run with a test runner, like pytest - # But if not, at least this will run them all. - test_list_donors() - test_find_donor() - test_find_donor_not() - test_gen_letter() - test_add_donor() - test_generate_donor_report() - test_save_letters_to_disk() - print("All tests Passed") From 8bdd96cac87eb4051a1fd97c1df6bb0b5a33d499 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Tue, 27 Nov 2018 20:31:27 -0800 Subject: [PATCH 36/87] Fix minor typos --- source/exercises/circle_class.rst | 11 ++++------- source/exercises/sparse_array.rst | 2 +- source/modules/Properties.rst | 17 ++++++++--------- source/modules/StaticAndClassMethods.rst | 2 +- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/source/exercises/circle_class.rst b/source/exercises/circle_class.rst index a94310d5..26377e95 100644 --- a/source/exercises/circle_class.rst +++ b/source/exercises/circle_class.rst @@ -129,9 +129,9 @@ with the diameter: .. code-block:: python >> c = Circle.from_diameter(8) - >> print c.diameter + >> print(c.diameter) 8 - >> print c.radius + >> print(c.radius) 4 Step 6: @@ -139,7 +139,7 @@ Step 6: Every class should have a nice way to print it out... -Add __str__ and __repr__ methods to your Circle class. +Add ``__str__`` and ``__repr__`` methods to your Circle class. Now you can print it: @@ -147,7 +147,7 @@ Now you can print it: In [2]: c = Circle(4) - In [3]: print c + In [3]: print(c) Circle with radius: 4.000000 In [4]: repr(c) @@ -214,9 +214,6 @@ Once the comparing is done, you should be able to sort a list of circles: In [18]: print circles [Circle(6), Circle(7), Circle(8), Circle(4), Circle(0), Circle(2), Circle(3), Circle(5), Circle(9), Circle(1)] - In [19]: circl - circle circle.py circle.pyc circles - In [19]: circles.sort() In [20]: print circles diff --git a/source/exercises/sparse_array.rst b/source/exercises/sparse_array.rst index 23b1a742..939ebcb9 100644 --- a/source/exercises/sparse_array.rst +++ b/source/exercises/sparse_array.rst @@ -48,7 +48,7 @@ Some ideas of how to do that: len(my_array) - This will give its "virtual" length -- with the zeros +This will give its "virtual" length -- with the zeros * It should support getting and setting particular elements via indexing: diff --git a/source/modules/Properties.rst b/source/modules/Properties.rst index 86f7e247..dbe72873 100644 --- a/source/modules/Properties.rst +++ b/source/modules/Properties.rst @@ -64,15 +64,14 @@ Properties .. code-block:: ipython - class C: - _x = None - @property - def x(self): - return self._x - @x.setter - def x(self, value): - self._x = value - + In [27]: class C: + ....: _x = None + ....: @property + ....: def x(self): + ....: return self._x + ....: @x.setter + ....: def x(self, value): + ....: self._x = value In [28]: c = C() In [30]: c.x = 5 In [31]: print(c.x) diff --git a/source/modules/StaticAndClassMethods.rst b/source/modules/StaticAndClassMethods.rst index 206ee2c9..5ef64ca4 100644 --- a/source/modules/StaticAndClassMethods.rst +++ b/source/modules/StaticAndClassMethods.rst @@ -159,7 +159,7 @@ well. For the Circle Excercise: use a class method to make an alternate constructor that takes the diameter instead. -Ultimately, make a subclass of ``Circle``, called ``Sphere``. Check and see if the ``.from_diameter`` alternate consructor sill works! +Ultimately, make a subclass of ``Circle``, called ``Sphere``. Check and see if the ``.from_diameter`` alternate consructor still works! From 21e41eb75ecf0a51af7a955afb00c720b1475bc1 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 3 Dec 2018 19:25:42 -0800 Subject: [PATCH 37/87] a bit of cleanup of the final solution --- .../solutions/Lesson07/step_8/html_render.py | 6 +- .../Lesson07/step_8/run_html_render.py | 231 ++++++++++++++++++ 2 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 source/solutions/Lesson07/step_8/run_html_render.py diff --git a/source/solutions/Lesson07/step_8/html_render.py b/source/solutions/Lesson07/step_8/html_render.py index 0fec4873..e2e0f5f6 100644 --- a/source/solutions/Lesson07/step_8/html_render.py +++ b/source/solutions/Lesson07/step_8/html_render.py @@ -42,17 +42,15 @@ def append(self, content): # it no longer holds strings -- so a test will fail # but that test was testing internal API -- # it's probably better remove it - # if isinstance(content, Element): if hasattr(content, 'render'): self.content.append(content) else: self.content.append(TextWrapper(str(content))) - # self.content.append(content) def make_tags(self): """ create the tags - -- in a separate method so different subclass's render methods can use it + -- in a separate method so different subclass' render methods can use it """ attrs = " ".join(['{}="{}"'.format(key, val) for key, val in self.attributes.items()]) if attrs.strip(): @@ -64,7 +62,6 @@ def make_tags(self): return open_tag, close_tag def render(self, out_file, cur_ind=""): - print("in render, type of self", type(self)) open_tag, close_tag = self.make_tags() out_file.write(cur_ind + open_tag + "\n") for stuff in self.content: @@ -97,6 +94,7 @@ class Body(Element): class P(Element): tag = "p" + class Head(Element): tag = "head" diff --git a/source/solutions/Lesson07/step_8/run_html_render.py b/source/solutions/Lesson07/step_8/run_html_render.py new file mode 100644 index 00000000..30adfb50 --- /dev/null +++ b/source/solutions/Lesson07/step_8/run_html_render.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +""" +a simple script can run and test your html rendering classes. + +Uncomment the steps as you add to your rendering. + +""" + +from io import StringIO + +# importing the html_rendering code with a short name for easy typing. +import html_render as hr + + +# writing the file out: +def render_page(page, filename, indent=None): + """ + render the tree of elements + + This uses StringIO to render to memory, then dump to console and + write to file -- very handy! + """ + + f = StringIO() + if indent is None: + page.render(f) + else: + page.render(f, indent) + + print(f.getvalue()) + with open(filename, 'w') as outfile: + outfile.write(f.getvalue()) + + +# # Step 1 +# ######### + +# page = hr.Element() + +# page.append("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text") + +# page.append("And here is another piece of text -- you should be able to add any number") + +# render_page(page, "test_html_output1.html") + +# The rest of the steps have been commented out. +# Uncomment them as you move along with the assignment. + +# ## Step 2 +# ########## + +# page = hr.Html() + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text")) + +# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) + +# page.append(body) + +# render_page(page, "test_html_output2.html") + +# # Step 3 +# ########## + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text")) +# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) + +# page.append(body) + +# render_page(page, "test_html_output3.html") + +# # Step 4 +# ########## + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# page.append(body) + +# render_page(page, "test_html_output4.html") + +# # Step 5 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# page.append(body) + +# render_page(page, "test_html_output5.html") + +# Step 6 +######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# body.append("And this is a ") +# body.append(hr.A("http://google.com/", "link")) +# body.append("to google") + +# page.append(body) + +# render_page(page, "test_html_output6.html") + +# # Step 7 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append( hr.H(2, "PythonClass - Class 6 example") ) + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# list = hr.Ul(id="TheList", style="line-height:200%") + +# list.append( hr.Li("The first item in a list") ) +# list.append( hr.Li("This is the second item", style="color: red") ) + +# item = hr.Li() +# item.append("And this is a ") +# item.append( hr.A("http://google.com/", "link") ) +# item.append("to google") + +# list.append(item) + +# body.append(list) + +# page.append(body) + +# render_page(page, "test_html_output7.html") + +# Step 8 and 9 +############## + +page = hr.Html() + + +head = hr.Head() +head.append(hr.Meta(charset="UTF-8")) +head.append(hr.Title("PythonClass = Revision 1087:")) + +page.append(head) + +body = hr.Body() + +body.append(hr.H(2, "PythonClass - Example")) + +body.append(hr.P("Here is a paragraph of text -- there could be more of them, " + "but this is enough to show that we can do some text", + style="text-align: center; font-style: oblique;")) + +body.append(hr.Hr()) + +list = hr.Ul(id="TheList", style="line-height:200%") + +list.append(hr.Li("The first item in a list")) +list.append(hr.Li("This is the second item", style="color: red")) + +item = hr.Li() +item.append("And this is a ") +item.append(hr.A("http://google.com/", "link")) +item.append("to google") + +list.append(item) + +body.append(list) + +page.append(body) + +render_page(page, "test_html_output8.html") From 4235bc2804a72feaaf0f343cf9a2f0574e1cc72c Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 3 Dec 2018 19:30:23 -0800 Subject: [PATCH 38/87] removed redundant solution --- source/solutions/html_render/.gitignore | 1 - .../class_html_render/html_render.py | 20 - .../class_html_render/html_render_wrap.py | 40 -- .../class_html_render/run_html_render.py | 235 --------- .../class_html_render/sample_html.html | 27 -- .../class_html_render/test_html_render.py | 425 ----------------- .../html_render/step_1/html_render.py | 24 - .../html_render/step_1/test_html_render.py | 67 --- .../step_2_complete/html_render.py | 67 --- .../step_2_complete/test_html_render.py | 201 -------- .../step_2_noindent/html_render.py | 66 --- .../step_2_noindent/test_html_render.py | 159 ------- .../html_render/step_3/html_render.py | 85 ---- .../html_render/step_3/test_html_render.py | 293 ------------ .../html_render/step_4/html_render.py | 103 ---- .../html_render/step_4/test_html_render.py | 304 ------------ .../html_render/step_5/html_render.py | 136 ------ .../html_render/step_5/test_html_render.py | 334 ------------- .../html_render/step_6/html_render.py | 148 ------ .../html_render/step_6/test_html_render.py | 335 ------------- .../html_render/step_7/html_render.py | 171 ------- .../html_render/step_7/test_html_render.py | 372 --------------- .../html_render/step_8/html_render.py | 183 -------- .../html_render/step_8/test_html_render.py | 444 ------------------ .../html_render/step_8/test_html_render2.py | 425 ----------------- 25 files changed, 4665 deletions(-) delete mode 100644 source/solutions/html_render/.gitignore delete mode 100644 source/solutions/html_render/class_html_render/html_render.py delete mode 100644 source/solutions/html_render/class_html_render/html_render_wrap.py delete mode 100644 source/solutions/html_render/class_html_render/run_html_render.py delete mode 100644 source/solutions/html_render/class_html_render/sample_html.html delete mode 100644 source/solutions/html_render/class_html_render/test_html_render.py delete mode 100644 source/solutions/html_render/step_1/html_render.py delete mode 100644 source/solutions/html_render/step_1/test_html_render.py delete mode 100644 source/solutions/html_render/step_2_complete/html_render.py delete mode 100644 source/solutions/html_render/step_2_complete/test_html_render.py delete mode 100644 source/solutions/html_render/step_2_noindent/html_render.py delete mode 100644 source/solutions/html_render/step_2_noindent/test_html_render.py delete mode 100644 source/solutions/html_render/step_3/html_render.py delete mode 100644 source/solutions/html_render/step_3/test_html_render.py delete mode 100644 source/solutions/html_render/step_4/html_render.py delete mode 100644 source/solutions/html_render/step_4/test_html_render.py delete mode 100644 source/solutions/html_render/step_5/html_render.py delete mode 100644 source/solutions/html_render/step_5/test_html_render.py delete mode 100644 source/solutions/html_render/step_6/html_render.py delete mode 100644 source/solutions/html_render/step_6/test_html_render.py delete mode 100644 source/solutions/html_render/step_7/html_render.py delete mode 100644 source/solutions/html_render/step_7/test_html_render.py delete mode 100644 source/solutions/html_render/step_8/html_render.py delete mode 100644 source/solutions/html_render/step_8/test_html_render.py delete mode 100644 source/solutions/html_render/step_8/test_html_render2.py diff --git a/source/solutions/html_render/.gitignore b/source/solutions/html_render/.gitignore deleted file mode 100644 index 82268575..00000000 --- a/source/solutions/html_render/.gitignore +++ /dev/null @@ -1 +0,0 @@ -step_8/sample_output.html diff --git a/source/solutions/html_render/class_html_render/html_render.py b/source/solutions/html_render/class_html_render/html_render.py deleted file mode 100644 index 89f121c9..00000000 --- a/source/solutions/html_render/class_html_render/html_render.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 - - -class Element: - - tag = 'html' - - def __init__(self, content=None): - self.content = [] - if content: - self.content.append(content) - - def append(self, content): - self.content.append(content) - - def render(self, out_file, ind=""): - out_file.write("<{}>\n".format(self.tag)) - for stuff in self.content: - out_file.write(stuff + "\n") - out_file.write("\n".format(self.tag)) diff --git a/source/solutions/html_render/class_html_render/html_render_wrap.py b/source/solutions/html_render/class_html_render/html_render_wrap.py deleted file mode 100644 index 7fb1ca9f..00000000 --- a/source/solutions/html_render/class_html_render/html_render_wrap.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python - -""" -html render code -- this shows how to wrap plain text in a simple class -for rendering. -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for simple text - """ - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -# here is how you might use it: -class Element: - - tag = 'html' # shouldn't really be usable without properly subclassing - indent = ' ' - - def __init__(self, content=None, **attributes): - - self.content = [] - self.attributes = attributes - - if content is not None: - # call this class's append -- so any magic done in there is used. - self.append(content) - - def append(self, content): - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) diff --git a/source/solutions/html_render/class_html_render/run_html_render.py b/source/solutions/html_render/class_html_render/run_html_render.py deleted file mode 100644 index d282fdc9..00000000 --- a/source/solutions/html_render/class_html_render/run_html_render.py +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env python - -""" -a simple script can run and test your html rendering classes. - -Uncomment the steps as you add to your rendering. - -""" - -from io import StringIO - -# importing the html_rendering code with a short name for easy typing. -import html_render as hr -# reloading in case you are running this in iPython -# -- we want to make sure the latest version is used -import importlib -importlib.reload(hr) - - -# writing the file out: -def render_page(page, filename): - """ - render the tree of elements - - This uses StringIO to render to memory, then dump to console and - write to file -- very handy! - """ - - f = StringIO() - page.render(f, " ") - - f.seek(0) - - print(f.read()) - - f.seek(0) - open(filename, 'w').write(f.read()) - - -# Step 1 -######### - -page = hr.Element() - -page.append("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text") - -page.append("And here is another piece of text -- you should be able to add any number") - -render_page(page, "test_html_output1.html") - -# The rest of the steps have been commented out. -# Uncomment them a you move along with the assignment. - -# ## Step 2 -# ########## - -# page = hr.Html() - -# body = hr.Body() - -# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " -# "but this is enough to show that we can do some text")) - -# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) - -# page.append(body) - -# render_page(page, "test_html_output2.html") - -# # Step 3 -# ########## - -# page = hr.Html() - -# head = hr.Head() -# head.append(hr.Title("PythonClass = Revision 1087:")) - -# page.append(head) - -# body = hr.Body() - -# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " -# "but this is enough to show that we can do some text")) -# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) - -# page.append(body) - -# render_page(page, "test_html_output3.html") - -# # Step 4 -# ########## - -# page = hr.Html() - -# head = hr.Head() -# head.append(hr.Title("PythonClass = Revision 1087:")) - -# page.append(head) - -# body = hr.Body() - -# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " -# "but this is enough to show that we can do some text", -# style="text-align: center; font-style: oblique;")) - -# page.append(body) - -# render_page(page, "test_html_output4.html") - -# # Step 5 -# ######### - -# page = hr.Html() - -# head = hr.Head() -# head.append(hr.Title("PythonClass = Revision 1087:")) - -# page.append(head) - -# body = hr.Body() - -# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " -# "but this is enough to show that we can do some text", -# style="text-align: center; font-style: oblique;")) - -# body.append(hr.Hr()) - -# page.append(body) - -# render_page(page, "test_html_output5.html") - -# # Step 6 -# ######### - -# page = hr.Html() - -# head = hr.Head() -# head.append(hr.Title("PythonClass = Revision 1087:")) - -# page.append(head) - -# body = hr.Body() - -# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " -# "but this is enough to show that we can do some text", -# style="text-align: center; font-style: oblique;")) - -# body.append(hr.Hr()) - -# body.append("And this is a ") -# body.append( hr.A("http://google.com/", "link") ) -# body.append("to google") - -# page.append(body) - -# render_page(page, "test_html_output6.html") - -# # Step 7 -# ######### - -# page = hr.Html() - -# head = hr.Head() -# head.append(hr.Title("PythonClass = Revision 1087:")) - -# page.append(head) - -# body = hr.Body() - -# body.append( hr.H(2, "PythonClass - Class 6 example") ) - -# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " -# "but this is enough to show that we can do some text", -# style="text-align: center; font-style: oblique;")) - -# body.append(hr.Hr()) - -# list = hr.Ul(id="TheList", style="line-height:200%") - -# list.append( hr.Li("The first item in a list") ) -# list.append( hr.Li("This is the second item", style="color: red") ) - -# item = hr.Li() -# item.append("And this is a ") -# item.append( hr.A("http://google.com/", "link") ) -# item.append("to google") - -# list.append(item) - -# body.append(list) - -# page.append(body) - -# render_page(page, "test_html_output7.html") - -# # Step 8 -# ######## - -# page = hr.Html() - - -# head = hr.Head() -# head.append( hr.Meta(charset="UTF-8") ) -# head.append(hr.Title("PythonClass = Revision 1087:")) - -# page.append(head) - -# body = hr.Body() - -# body.append( hr.H(2, "PythonClass - Class 6 example") ) - -# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " -# "but this is enough to show that we can do some text", -# style="text-align: center; font-style: oblique;")) - -# body.append(hr.Hr()) - -# list = hr.Ul(id="TheList", style="line-height:200%") - -# list.append( hr.Li("The first item in a list") ) -# list.append( hr.Li("This is the second item", style="color: red") ) - -# item = hr.Li() -# item.append("And this is a ") -# item.append( hr.A("http://google.com/", "link") ) -# item.append("to google") - -# list.append(item) - -# body.append(list) - -# page.append(body) - -# render_page(page, "test_html_output8.html") diff --git a/source/solutions/html_render/class_html_render/sample_html.html b/source/solutions/html_render/class_html_render/sample_html.html deleted file mode 100644 index f2687e95..00000000 --- a/source/solutions/html_render/class_html_render/sample_html.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - PythonClass = Revision 1087: - - -

PythonClass - Class 6 example

-

- Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text -

-
-
    -
  • - The first item in a list -
  • -
  • - This is the second item -
  • -
  • - And this is a - link - to google -
  • -
- - \ No newline at end of file diff --git a/source/solutions/html_render/class_html_render/test_html_render.py b/source/solutions/html_render/class_html_render/test_html_render.py deleted file mode 100644 index 21eee4ad..00000000 --- a/source/solutions/html_render/class_html_render/test_html_render.py +++ /dev/null @@ -1,425 +0,0 @@ -#!/usr/bin/env python - - -import os -from io import StringIO - -from html_render import Element, Body, Para, HTML, Head, Title - - -# test utilities - -def render_element_file(element, filename='temp_render_file.html', remove=False): - """ - renders an element, and returns what got rendered - - This version uses an actual file on disk -- yu may want to use it so you - can see the file afterward. - - :param element: the element to be rendered (its render() method will be called) - - :param filename='temp_render_file.html': the name of the temporary file to be used. - - :param remove=True: Whether to remove the file after using it to render. - Set this to True if you want to be able to look at after - the tests run. - - NOTE: - this could be refactored, and still used everywhere. - """ - with open(filename, 'w') as out_file: - element.render(out_file) - with open(filename, 'r') as in_file: - contents = in_file.read() - if remove: - os.remove(filename) - return contents - - -def render_element(element, cur_ind=""): - # this version uses a StringIO object, to keep it all in memory - """ - renders an element, and returns what got rendered - - This can be used by multiple tests. - - :param element: the element to be rendered (its render() method will be called) - - :param filename='temp_render_file.html': the name of the temporary file to be used. - - :param remove=True: Whether to remove the file after using it to render. - Set this to True if you want to be able to look at after - the tests run. - - NOTE: - this could be refactored, and still used everywhere. - """ - sio = StringIO() - element.render(sio, cur_ind=cur_ind) - # if remove: - # os.remove(filename) - return sio.getvalue() - - -def test_new_element(): - """ - not much here, but it shows Elements can be initialized - """ - el_object = Element() - el_object2 = Element('content') - - -# careful here -- this is testing internal implimentations -# sometimes helpful as you are developing, but you may want to remove -# these tests once you have more working. -def test_add_content(): - el_object = Element('content') - assert el_object.content == ['content'] - - el_object = Element() - assert el_object.content == [] - - -def test_adding_empty_string(): - el_object = Element('') - assert el_object.content == [''] - - -def test_append_string(): - el_object = Element('spam, spam, spam') - el_object.append(', wonderful spam') - assert el_object.content == ['spam, spam, spam', ', wonderful spam'] - - -def test_tag_exists(): - assert Element.tag == 'html' - el_object = Element('spam, spam, spam') - assert el_object.tag == 'html' - - -def test_indent_exists(): - assert Element.indent == ' ' - - -# Now we get tot he real "meat" of the tests --does the code do what -# it is supposed to do? -def test_render(): - my_stuff = 'spam, spam, spam' - el_object = Element(my_stuff) - more_stuff = 'eggs, eggs, eggs' - el_object.append(more_stuff) - contents = render_element(el_object).strip() - assert contents.startswith('') - assert contents.endswith('') - assert my_stuff in contents - assert more_stuff in contents - - - -# you want to be careful with these: -# It is testing an implementation detail, which is less than ideal. -# sometimes in TDD, it's helpful to have quickies tests of -# implementation details so you can see that partially written code -# is working -- but if/when you can test actual functionality, that's -# better. In this case, once we have a render() method, we can test -# that the tag gets rendered properly, so don't need to test if the -# tag attribute is correct. - -# def test_body_tag(): -# assert Body.tag == 'body' - -# def test_p_tag(): -# assert Para.tag == 'p' - - -# def test_html_tag(): -# assert HTML.tag == 'html' - -# finally! a really good test. -# This is an actual element that we want to render -# so whatever the implimentation deatails, it's working. -def test_render_body(): - my_stuff = 'spam, spam, spam' - el_object = Body(my_stuff) - more_stuff = 'eggs, eggs, eggs' - el_object.append(more_stuff) - contents = render_element(el_object).strip() - assert contents.startswith('') - assert contents.endswith('') - assert my_stuff in contents - assert more_stuff in contents - - -def test_render_para(): - my_stuff = 'spam, spam, spam' - p = Para(my_stuff) - more_stuff = 'eggs, eggs, eggs' - p.append(more_stuff) - contents = render_element(p).strip() - assert contents.startswith('

') - assert contents.endswith('

') - assert my_stuff in contents - assert more_stuff in contents - - -def test_render_html(): - my_stuff = 'spam, spam, spam' - el_object = HTML(my_stuff) - more_stuff = 'eggs, eggs, eggs' - el_object.append(more_stuff) - contents = render_element(el_object) - assert contents.startswith('') - assert contents.endswith('') - assert my_stuff in contents - assert more_stuff in contents - - -def test_render_non_strings(): - # this is creating a html page with a single body() element in it - # and a string inside that. - el_object = HTML(Body('any string I like')) - - contents = render_element(el_object) - # make sure extra whitespace at beginning or end doesn't mess things up. - contents = contents.strip() - - print(contents) # so we can see what's going on if a test fails - - # so what should the results be? - # the html tag is the outer tag, so the contents should start and end with that. - assert contents.startswith('') - assert contents.endswith('') - - # the body tags had better be there too - assert '' in contents - assert '') < contents.index('') - # the opening tag should come before the content - assert contents.index('') < contents.index('any string') - - -def test_render_non_strings2(): - """ - Testing nested elements and text, in a more complex way - """ - html = HTML() - body = Body() - html.append(body) - p = Para('any string I like') - p.append('even more random text') - body.append(p) - body.append(Para('and this is a different string')) - contents = render_element(html).strip() - - print(contents) # so we can see what's going on if a test fails - - # so what should the results be? - # the html tag is the outer tag, so the contents should start and end with that. - assert contents.startswith('') - assert contents.endswith('') - - # the body tags had better be there too - assert '' in contents - assert ' tags - assert contents.count('

') - - # we want the text, too: - assert 'any string I like' in contents - assert 'even more random text' in contents - assert 'and this is a different string' in contents - - # you could, of course, test much more..but hopefully other things are tested, too. - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = HTML("some content") - cur_ind = 6 * " " - file_contents = render_element(html, cur_ind=cur_ind) - - print(file_contents) - lines = file_contents.split("\n") - - assert lines[0].startswith(cur_ind + "<") - assert lines[1].startswith(cur_ind + Element.indent + "som") - assert lines[-1].startswith(cur_ind + "<") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = HTML("some content") - file_contents = render_element(html, cur_ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented properly - """ - body = Body() - body.append(Para("some text")) - body.append(Para("even more text")) - html = HTML(body) - - file_contents = render_element(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): - assert lines[i].startswith(i * Element.indent + "<") - assert lines[3].startswith(3 * Element.indent + "some") - assert lines[4].startswith(2 * Element.indent + "

") - assert lines[5].startswith(2 * Element.indent + "

") - assert lines[6].startswith(3 * Element.indent + "even ") - for i in range(3): - assert lines[-(i + 1)].startswith(i * Element.indent + "<") - - -def test_title(): - """ - This will implicitly test the OneLineTag element - """ - t = Title("Isn't this a nice title?") - - # making sure indentation still works - file_contents = render_element(t, cur_ind=" ") - - print(file_contents) - # no "strip()" -- making sure there are no extra newlines - assert file_contents.startswith(" I") - assert file_contents.endswith("? ") - # the only newline should be at the end - - -def test_head(): - """ - testing Head with a title in it -- it should never be blank - """ - h = Head() - h.append(Title("A nifty title for the page")) - file_contents = render_element(h, cur_ind=' ') - - print(file_contents) - assert file_contents.startswith(" ") - assert file_contents.endswith(" ") - - assert "" in file_contents - assert "" in file_contents - assert "A nifty title for the page" in file_contents - - -def test_full_page_with_title(): - """ - not much to actually test here, but good to see it put together. - - everything should have already been tested. - """ - page = HTML() - - head = Head() - head.append(Title("PythonClass Example")) - - page.append(head) - - body = Body() - - body.append(Para("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text")) - body.append(Para("And here is another piece of text -- you should be able to add any number")) - - page.append(body) - - file_contents = render_element(page) - - print(file_contents) - - # uncomment this to see results - # assert False - - -def test_single_attribute(): - #

- # Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text - #

- p = Para("Here is a paragraph of text", style="text-align: center; font-style: oblique;") - - results = render_element(p) - - assert results.startswith('

') - - print(results) - -def test_multiple_attributes(): - #

- # Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text - #

- p = Para("Here is a paragraph of text", - id="fred", - color="red", - size="12px", - ) - - results = render_element(p) - print(results) - - lines = results.split('\n') - assert lines[0].startswith('

') - assert 'id="fred"' in lines[0] - assert 'color="red"' in lines[0] - assert 'size="12px"' in lines[0] - -def test_multiple_attributes_title(): - t = Title("Here is a paragraph of text", - id="fred", - color="red", - size="12px", - ) - - results = render_element(t) - print(results) - - lines = results.split('\n') - assert lines[0].startswith('') - assert 'id="fred"' in lines[0] - assert 'color="red"' in lines[0] - assert 'size="12px"' in lines[0] - - -# test class attribute -def test_class_attribute(): - atts = {"id": "fred", - "class": "special", - "size": "12px", - } - p = Para("Here is a paragraph of text", - **atts) - - results = render_element(p) - print(results) - - lines = results.split('\n') - assert lines[0].startswith('<p ') - assert lines[0].strip().endswith('">') - assert 'id="fred"' in lines[0] - assert 'class="special"' in lines[0] - assert 'size="12px"' in lines[0] - - - - diff --git a/source/solutions/html_render/step_1/html_render.py b/source/solutions/html_render/step_1/html_render.py deleted file mode 100644 index d5e6819a..00000000 --- a/source/solutions/html_render/step_1/html_render.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 - -""" -step 1 of Chris's solution -""" - - -class Element: - - tag = 'html' - - def __init__(self, content=None): - self.content = [] - if content: - self.content.append(content) - - def append(self, content): - self.content.append(content) - - def render(self, out_file, ind=""): - out_file.write("<{}>\n".format(self.tag)) - for stuff in self.content: - out_file.write(stuff + "\n") - out_file.write("</{}>\n".format(self.tag)) diff --git a/source/solutions/html_render/step_1/test_html_render.py b/source/solutions/html_render/step_1/test_html_render.py deleted file mode 100644 index 45e51019..00000000 --- a/source/solutions/html_render/step_1/test_html_render.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -test code for html_render.py - -only step 1 -""" - -import io - -from html_render import Element - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -def test_content(): - # fixme: this tests internals!!!! - e = Element("this is some text") - - assert "this is some text" in e.content - - -def test_append(): - e = Element("this is some text") - - e.append("some more text") - - assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<html>") - assert file_contents.strip().endswith("</html>") diff --git a/source/solutions/html_render/step_2_complete/html_render.py b/source/solutions/html_render/step_2_complete/html_render.py deleted file mode 100644 index 75d8d842..00000000 --- a/source/solutions/html_render/step_2_complete/html_render.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 - -""" -Chris's solution through step 2 with full indentation -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for just text - - This allows the Element classes to render either Element objects or - plain text - - """ - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -class Element: - - tag = "html" - indent = " " - - def __init__(self, content=None): - self.content = [] - if content: - # call the classes append method - # so that it can do anything special it needs to do - self.append(content) - - def append(self, content): - """ - add a new piece of content or another element to this element - """ - # note: this changed the internal represntation of content - # it no longer holds strings -- so a test will fail - # but that test was testing internal API -- - # it's probably better remove it - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) - - def render(self, out_file, ind=""): - out_file.write("{}<{}>\n".format(ind, self.tag)) - for stuff in self.content: - stuff.render(out_file, ind + self.indent) - out_file.write("\n") - out_file.write("{}</{}>".format(ind, self.tag)) - - -class Html(Element): - tag = 'html' - - -class Body(Element): - tag = "body" - - -class P(Element): - tag = "p" - diff --git a/source/solutions/html_render/step_2_complete/test_html_render.py b/source/solutions/html_render/step_2_complete/test_html_render.py deleted file mode 100644 index c9346389..00000000 --- a/source/solutions/html_render/step_2_complete/test_html_render.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -test code for html_render.py - -includes through step 2 -""" -import io - -from html_render import Element, Html, Body, P, TextWrapper - - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -# These two tests were testing internals -# so they failed when I added the TextWrapper -# but I"m removing them because tests really should be testing -# the external API. -# def test_content(): -# # fixme: this tests internals!!!! -# e = Element("this is some text") - -# assert "this is some text" in e.content - -# def test_append(): -# e = Element("this is some text") - -# e.append("some more text") - -# assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<html>") - assert file_contents.strip().endswith("</html>") - - -def test_html(): - e = Html("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<html>") - assert file_contents.strip().endswith("</html>") - - -def test_body(): - e = Body("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<body>") - assert file_contents.strip().endswith("</body>") - - -def test_p(): - e = P("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<p>") - assert file_contents.strip().endswith("</p>") - - -def test_text_wrapper(): - tw = TextWrapper("A basic piece of text") - - file_contents = render_result(tw) - assert file_contents == "A basic piece of text" - - -def test_sub_element(): - """ - tests that you can add another element and still render properly - """ - page = Html() - page.append("some plain text.") - page.append(P("A simple paragraph of text")) - page.append("Some more plain text.") - - file_contents = render_result(page) - - # note: the above tests should make sure that the tags are getting rendered. - assert "some plain text" in file_contents - assert "A simple paragraph of text" in file_contents - assert "Some more plain text." in file_contents - assert "some plain text" in file_contents - - -def test_step_2_noindent(): - """ - This is more if an integration test -- a number of things together - - this test does not yet include indentation - """ - page = Html() - body = Body() - page.append(body) - body.append(P("a small paragraph of text")) - body.append(P("another small paragraph of text")) - body.append(P("and here is a bit more")) - - file_contents = render_result(page).strip() - - print(file_contents) - assert file_contents.startswith("<html>") - assert file_contents.endswith("</html>") - assert "a small paragraph of text" in file_contents - assert "<body>" in file_contents - # you could do more here, but it should all be covered above. - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = Html("some content") - file_contents = render_result(html, ind=" ") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[0].startswith(" <") - assert lines[-1].startswith(" <") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = Html("some content") - file_contents = render_result(html, ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented fully - """ - body = Body() - body.append(P("some text")) - html = Html(body) - - file_contents = render_result(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): - assert lines[i].startswith(i * Element.indent + "<") - - assert lines[3].startswith(3 * Element.indent + "some") diff --git a/source/solutions/html_render/step_2_noindent/html_render.py b/source/solutions/html_render/step_2_noindent/html_render.py deleted file mode 100644 index 9dbd2110..00000000 --- a/source/solutions/html_render/step_2_noindent/html_render.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -""" -Chris's solution through step 2 without indentation -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for just text - - This allows the Element classes to render either Element objects or - plain text - - """ - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -class Element: - - tag = 'html' - - def __init__(self, content=None): - self.content = [] - if content: - # call the classes append method - # so that it can do anything special it needs to do - self.append(content) - - def append(self, content): - """ - add a new piece of content or another element to this element - """ - # note: this changed the internal represntation of content - # it no longer holds strings -- so a test will fail - # but that test was testing internal API -- - # it's probably better remove it - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) - - def render(self, out_file, ind=""): - out_file.write("<{}>\n".format(self.tag)) - for stuff in self.content: - stuff.render(out_file) - out_file.write("\n") - out_file.write("</{}>".format(self.tag)) - - -class Html(Element): - tag = 'html' - - -class Body(Element): - tag = "body" - - -class P(Element): - tag = "p" - diff --git a/source/solutions/html_render/step_2_noindent/test_html_render.py b/source/solutions/html_render/step_2_noindent/test_html_render.py deleted file mode 100644 index 7fed7dc3..00000000 --- a/source/solutions/html_render/step_2_noindent/test_html_render.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -test code for html_render.py - -includes through step 2 without indentation -""" -import io - -from html_render import Element, Html, Body, P, TextWrapper - - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -# These two tests were testing internals -# so they failed when I added the TextWrapper -# but I"m removing them because tests really should be testing -# the external API. -# def test_content(): -# # fixme: this tests internals!!!! -# e = Element("this is some text") - -# assert "this is some text" in e.content - -# def test_append(): -# e = Element("this is some text") - -# e.append("some more text") - -# assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<html>") - assert file_contents.strip().endswith("</html>") - - -def test_html(): - e = Html("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<html>") - assert file_contents.strip().endswith("</html>") - - -def test_body(): - e = Body("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<body>") - assert file_contents.strip().endswith("</body>") - - -def test_p(): - e = P("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<p>") - assert file_contents.strip().endswith("</p>") - - -def test_text_wrapper(): - tw = TextWrapper("A basic piece of text") - - file_contents = render_result(tw) - assert file_contents == "A basic piece of text" - - -def test_sub_element(): - """ - tests that you can add another element and still render properly - """ - page = Html() - page.append("some plain text.") - page.append(P("A simple paragraph of text")) - page.append("Some more plain text.") - - file_contents = render_result(page) - - # note: the above tests should make sure that the tags are getting rendered. - assert "some plain text" in file_contents - assert "A simple paragraph of text" in file_contents - assert "Some more plain text." in file_contents - assert "some plain text" in file_contents - - -def test_step_2_noindent(): - """ - This is more if an integration test -- a number of things together - - this test does not yet include indentation - """ - page = Html() - body = Body() - page.append(body) - body.append(P("a small paragraph of text")) - body.append(P("another small paragraph of text")) - body.append(P("and here is a bit more")) - - file_contents = render_result(page).strip() - - print(file_contents) - assert file_contents.startswith("<html>") - assert file_contents.endswith("</html>") - assert "a small paragraph of text" in file_contents - assert "<body>" in file_contents - # you could do more here, but it should all be covered above. - assert False - diff --git a/source/solutions/html_render/step_3/html_render.py b/source/solutions/html_render/step_3/html_render.py deleted file mode 100644 index 8cfdce28..00000000 --- a/source/solutions/html_render/step_3/html_render.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 - -""" -Chris's solution through step 3 -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for just text - - This allows the Element classes to render either Element objects or - plain text - - """ - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -class Element: - - tag = "html" - indent = " " - - def __init__(self, content=None): - self.content = [] - if content: - # call the classes append method - # so that it can do anything special it needs to do - self.append(content) - - def append(self, content): - """ - add a new piece of content or another element to this element - """ - # note: this changed the internal represntation of content - # it no longer holds strings -- so a test will fail - # but that test was testing internal API -- - # it's probably better remove it - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) - - def render(self, out_file, ind=""): - out_file.write("{}<{}>\n".format(ind, self.tag)) - for stuff in self.content: - stuff.render(out_file, ind + self.indent) - out_file.write("\n") - out_file.write("{}</{}>".format(ind, self.tag)) - - -class OneLineTag(Element): - def render(self, out_file, ind=""): - # there is some repition here -- maybe factor that out? - out_file.write("{}<{}>".format(ind, self.tag)) - for stuff in self.content: - stuff.render(out_file) - out_file.write("</{}>".format(self.tag)) - - -class Html(Element): - tag = 'html' - - -class Body(Element): - tag = "body" - - -class P(Element): - tag = "p" - - -class Head(Element): - tag = "head" - - -class Title(OneLineTag): - tag = "title" - - diff --git a/source/solutions/html_render/step_3/test_html_render.py b/source/solutions/html_render/step_3/test_html_render.py deleted file mode 100644 index 6f183363..00000000 --- a/source/solutions/html_render/step_3/test_html_render.py +++ /dev/null @@ -1,293 +0,0 @@ -""" -test code for html_render.py - -includes through step 3 -""" - -import io - -from html_render import (Element, - Html, - Body, - P, - TextWrapper, - Head, - Title, - ) - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -# These two tests were testing internals -# so they failed when I added the TextWrapper -# but I"m removing them because tests really should be testing -# the external API. -# def test_content(): -# # fixme: this tests internals!!!! -# e = Element("this is some text") - -# assert "this is some text" in e.content - -# def test_append(): -# e = Element("this is some text") - -# e.append("some more text") - -# assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<html>") - assert file_contents.strip().endswith("</html>") - - -def test_html(): - e = Html("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<html>") - assert file_contents.strip().endswith("</html>") - - -def test_body(): - e = Body("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<body>") - assert file_contents.strip().endswith("</body>") - - -def test_p(): - e = P("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("<p>") - assert file_contents.strip().endswith("</p>") - - -def test_text_wrapper(): - tw = TextWrapper("A basic piece of text") - - file_contents = render_result(tw) - assert file_contents == "A basic piece of text" - - -def test_non_str(): - """ you should be able to pass anything in, and it will get - "stringified" - """ - e = P(34) # a number - e.append((3, 4, 5)) # even a tuple - - file_contents = render_result(e) - - print(file_contents) - assert("34") in file_contents - assert("(3, 4, 5)") in file_contents - - -def test_sub_element(): - """ - tests that you can add another element and still render properly - """ - page = Html() - page.append("some plain text.") - page.append(P("A simple paragraph of text")) - page.append("Some more plain text.") - - file_contents = render_result(page) - - # note: the above tests should make sure that the tags are getting rendered. - assert "some plain text" in file_contents - assert "A simple paragraph of text" in file_contents - assert "Some more plain text." in file_contents - assert "some plain text" in file_contents - - -def test_step_2_noindent(): - """ - This is more if an integration test -- a number of things together - - this test does not yet include indentation - """ - page = Html() - body = Body() - page.append(body) - body.append(P("a small paragraph of text")) - body.append(P("another small paragraph of text")) - body.append(P("and here is a bit more")) - - file_contents = render_result(page).strip() - - print(file_contents) - assert file_contents.startswith("<html>") - assert file_contents.endswith("</html>") - assert "a small paragraph of text" in file_contents - assert "<body>" in file_contents - # you could do more here, but it should all be covered above. - # assert False - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = Html("some content") - file_contents = render_result(html, ind=" ") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[0].startswith(" <") - assert lines[-1].startswith(" <") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = Html("some content") - file_contents = render_result(html, ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented fully - """ - body = Body() - body.append(P("some text")) - body.append(P("even more text")) - - html = Html(body) - - file_contents = render_result(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): - assert lines[i].startswith(i * Element.indent + "<") - assert lines[3].startswith(3 * Element.indent + "some") - assert lines[4].startswith(2 * Element.indent + "</p>") - assert lines[5].startswith(2 * Element.indent + "<p>") - assert lines[6].startswith(3 * Element.indent + "even ") - for i in range(3): - assert lines[-(i + 1)].startswith(i * Element.indent + "<") - - -def test_title(): - """ - This will implicitly test the OneLineTag element - """ - t = Title("Isn't this a nice title?") - - # making sure indentation still works - file_contents = render_result(t, ind=" ") - - print(file_contents) - # no "strip()" -- making sure there are no extra newlines - assert "\n" not in file_contents - assert file_contents.startswith(" <title>") - assert file_contents.endswith("") - # the only newline should be at the end - assert "\n" not in file_contents - - -def test_head(): - """ - testing Head with a title in it -- it should never be blank - """ - h = Head() - h.append(Title("A nifty title for the page")) - file_contents = render_result(h, ind=" ") - - print(file_contents) - assert file_contents.startswith(" ") - assert file_contents.endswith("") - - assert "" in file_contents - assert "" in file_contents - assert "A nifty title for the page" in file_contents - - -def test_full_page_with_title(): - """ - not much to actually test here, but good to see it put together. - - everything should have already been tested. - """ - page = Html() - - head = Head() - head.append(Title("PythonClass Example")) - - page.append(head) - - body = Body() - - body.append(P("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text")) - body.append(P("And here is another piece of text -- you should be able to add any number")) - - page.append(body) - - file_contents = render_result(page) - - print(file_contents) - - # uncomment this to see results - # assert False diff --git a/source/solutions/html_render/step_4/html_render.py b/source/solutions/html_render/step_4/html_render.py deleted file mode 100644 index 510bbafc..00000000 --- a/source/solutions/html_render/step_4/html_render.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 - -""" -Chris's solution through step 4 -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for just text - - This allows the Element classes to render either Element objects or - plain text - - """ - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -class Element: - - tag = "html" - indent = " " - - def __init__(self, content=None, **kwargs): - self.attributes = kwargs - self.content = [] - if content: - # call the classes append method - # so that it can do anything special it needs to do - self.append(content) - - def append(self, content): - """ - add a new piece of content or another element to this element - """ - # note: this changed the internal representation of content - # it no longer holds strings -- so a test will fail - # but that test was testing internal API -- - # it's probably better remove it - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) - - def make_tags(self): - """ - create the tags - -- in a separate method so different subclass's render methods can use it - """ - attrs = " ".join(['{}="{}"'.format(key, val) for key, val in self.attributes.items()]) - if attrs.strip(): - open_tag = "<{} {}>".format(self.tag, attrs.strip()) - else: - open_tag = "<{}>".format(self.tag) - close_tag = "".format(self.tag) - - return open_tag, close_tag - - def render(self, out_file, ind=""): - open_tag, close_tag = self.make_tags() - out_file.write(ind + open_tag + "\n") - for stuff in self.content: - stuff.render(out_file, ind + self.indent) - out_file.write("\n") - out_file.write(ind + close_tag) - - -class OneLineTag(Element): - # note: by re-writting the render - def render(self, out_file, ind=""): - # there is some repition here -- maybe factor that out? - open_tag, close_tag = self.make_tags() - out_file.write(ind + open_tag) - for stuff in self.content: - stuff.render(out_file) - out_file.write(close_tag) - - -class Html(Element): - tag = 'html' - - -class Body(Element): - tag = "body" - - -class P(Element): - tag = "p" - - -class Head(Element): - tag = "head" - - -class Title(OneLineTag): - tag = "title" - - diff --git a/source/solutions/html_render/step_4/test_html_render.py b/source/solutions/html_render/step_4/test_html_render.py deleted file mode 100644 index b1308def..00000000 --- a/source/solutions/html_render/step_4/test_html_render.py +++ /dev/null @@ -1,304 +0,0 @@ -""" -test code for html_render.py - -includes step 4 -""" -import io - -from html_render import (Element, - Html, - Body, - P, - TextWrapper, - Head, - Title, - ) - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -# These two tests were testing internals -# so they failed when I added the TextWrapper -# but I"m removing them because tests really should be testing -# the external API. -# def test_content(): -# # fixme: this tests internals!!!! -# e = Element("this is some text") - -# assert "this is some text" in e.content - -# def test_append(): -# e = Element("this is some text") - -# e.append("some more text") - -# assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_html(): - e = Html("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_body(): - e = Body("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_p(): - e = P("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("

") - assert file_contents.strip().endswith("

") - - -def test_text_wrapper(): - tw = TextWrapper("A basic piece of text") - - file_contents = render_result(tw) - assert file_contents == "A basic piece of text" - - -def test_non_str(): - """ you should be able to pass anything in, and it will get - "stringified" - """ - e = P(34) # a number - e.append((3, 4, 5)) # even a tuple - - file_contents = render_result(e) - - print(file_contents) - assert("34") in file_contents - assert("(3, 4, 5)") in file_contents - - -def test_sub_element(): - """ - tests that you can add another element and still render properly - """ - page = Html() - page.append("some plain text.") - page.append(P("A simple paragraph of text")) - page.append("Some more plain text.") - - file_contents = render_result(page) - - # note: the above tests should make sure that the tags are getting rendered. - assert "some plain text" in file_contents - assert "A simple paragraph of text" in file_contents - assert "Some more plain text." in file_contents - assert "some plain text" in file_contents - - -def test_step_2_noindent(): - """ - This is more if an integration test -- a number of things together - - this test does not yet include indentation - """ - page = Html() - body = Body() - page.append(body) - body.append(P("a small paragraph of text")) - body.append(P("another small paragraph of text")) - body.append(P("and here is a bit more")) - - file_contents = render_result(page).strip() - - print(file_contents) - assert file_contents.startswith("") - assert file_contents.endswith("") - assert "a small paragraph of text" in file_contents - assert "" in file_contents - # you could do more here, but it should all be covered above. - # assert False - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = Html("some content") - file_contents = render_result(html, ind=" ") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[0].startswith(" <") - assert lines[-1].startswith(" <") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = Html("some content") - file_contents = render_result(html, ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented fully - """ - body = Body() - body.append(P("some text")) - html = Html(body) - - file_contents = render_result(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): - assert lines[i].startswith(i * Element.indent + "<") - - assert lines[3].startswith(3 * Element.indent + "some") - - -def test_title(): - """ - This will implicitly test the OneLineTag element - """ - t = Title("Isn't this a nice title?") - - # making sure indentation still works - file_contents = render_result(t, ind=" ") - - print(file_contents) - # no "strip()" -- making sure there are no extra newlines - assert "\n" not in file_contents - assert "> " not in file_contents - assert file_contents.startswith(" ") - assert file_contents.endswith("") - # the only newline should be at the end - assert "\n" not in file_contents - - -def test_head(): - """ - testing Head with a title in it -- it should never be blank - """ - h = Head() - h.append(Title("A nifty title for the page")) - - -def test_full_page_with_title(): - """ - not much to actually test here, but good to see it put together. - - everything should have already been tested. - """ - page = Html() - - head = Head() - head.append(Title("PythonClass Example")) - - page.append(head) - - body = Body() - - body.append(P("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text")) - body.append(P("And here is another piece of text -- you should be able to add any number")) - - page.append(body) - - file_contents = render_result(page) - - print(file_contents) - - # uncomment this to see results - # assert False - - -def test_attributes(): - """ - tests that you can pass attributes in to the tag - """ - e = Element("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - # note -- dicts aren't ordered, so you can't enforce order! - # assert '' in file_contents - - -def test_attributes_one_line_tag(): - """ - tests that you can pass attributes in to the tag - """ - e = Title("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - - diff --git a/source/solutions/html_render/step_5/html_render.py b/source/solutions/html_render/step_5/html_render.py deleted file mode 100644 index fe3fd7d0..00000000 --- a/source/solutions/html_render/step_5/html_render.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 - -""" -Chris's solution through step 5 -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for just text - - This allows the Element classes to render either Element objects or - plain text - """ - - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -class Element: - - tag = "html" - indent = " " - - def __init__(self, content=None, **kwargs): - self.attributes = kwargs - self.content = [] - if content: - # call the classes append method - # so that it can do anything special it needs to do - self.append(content) - - def append(self, content): - """ - add a new piece of content or another element to this element - """ - # note: this changed the internal representation of content - # it no longer holds strings -- so a test will fail - # but that test was testing internal API -- - # it's probably better remove it - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) - - def make_tags(self): - """ - create the tags - -- in a separate method so different subclass's render methods can use it - """ - attrs = " ".join(['{}="{}"'.format(key, val) for key, val in self.attributes.items()]) - if attrs.strip(): - open_tag = "<{} {}>".format(self.tag, attrs.strip()) - else: - open_tag = "<{}>".format(self.tag) - close_tag = "".format(self.tag) - - return open_tag, close_tag - - def render(self, out_file, ind=""): - open_tag, close_tag = self.make_tags() - out_file.write(ind + open_tag + "\n") - for stuff in self.content: - stuff.render(out_file, ind + self.indent) - out_file.write("\n") - out_file.write(ind + close_tag) - - -class OneLineTag(Element): - # note: by re-writting the render - def render(self, out_file, ind=""): - # there is some repition here -- maybe factor that out? - open_tag, close_tag = self.make_tags() - out_file.write(ind + open_tag) - for stuff in self.content: - stuff.render(out_file) - out_file.write(close_tag) - - -class Html(Element): - tag = 'html' - - -class Body(Element): - tag = "body" - - -class P(Element): - tag = "p" - - -class Head(Element): - tag = "head" - - -class Title(OneLineTag): - tag = "title" - - -class SelfClosingTag(Element): - """ - base class for tags that have no content - """ - - def append(self, *args, **kwargs): - """ - self closing tags can't have content, so we raise an error if someone - tries to add some. - """ - raise TypeError("You can not add content to a self closing tag") - - def render(self, out_file, ind=""): - # there is some repition here -- maybe factor that out? - open_tag, _ = self.make_tags() - # make it a self cloding tag by adding the / - out_file.write(ind + open_tag.replace(">", " />")) - - -class Hr(SelfClosingTag): - """ - Horizontal Rule - """ - tag = "hr" - - -class Br(SelfClosingTag): - """ - Line break - """ - tag = "br" - - diff --git a/source/solutions/html_render/step_5/test_html_render.py b/source/solutions/html_render/step_5/test_html_render.py deleted file mode 100644 index 8098870d..00000000 --- a/source/solutions/html_render/step_5/test_html_render.py +++ /dev/null @@ -1,334 +0,0 @@ -""" -test code for html_render.py - -includes up to step 5 -""" - -import io -import pytest # for utilities like pytest.raises - -from html_render import (Element, - Html, - Body, - P, - TextWrapper, - Head, - Title, - Hr, - Br - ) - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -# These two tests were testing internals -# so they failed when I added the TextWrapper -# but I"m removing them because tests really should be testing -# the external API. -# def test_content(): -# # fixme: this tests internals!!!! -# e = Element("this is some text") - -# assert "this is some text" in e.content - -# def test_append(): -# e = Element("this is some text") - -# e.append("some more text") - -# assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_html(): - e = Html("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_body(): - e = Body("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_p(): - e = P("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("

") - assert file_contents.strip().endswith("

") - - -def test_text_wrapper(): - tw = TextWrapper("A basic piece of text") - - file_contents = render_result(tw) - assert file_contents == "A basic piece of text" - - -def test_non_str(): - """ you should be able to pass anything in, and it will get - "stringified" - """ - e = P(34) # a number - e.append((3, 4, 5)) # even a tuple - - file_contents = render_result(e) - - print(file_contents) - assert("34") in file_contents - assert("(3, 4, 5)") in file_contents - - -def test_sub_element(): - """ - tests that you can add another element and still render properly - """ - page = Html() - page.append("some plain text.") - page.append(P("A simple paragraph of text")) - page.append("Some more plain text.") - - file_contents = render_result(page) - - # note: the above tests should make sure that the tags are getting rendered. - assert "some plain text" in file_contents - assert "A simple paragraph of text" in file_contents - assert "Some more plain text." in file_contents - assert "some plain text" in file_contents - - -def test_step_2_noindent(): - """ - This is more if an integration test -- a number of things together - - this test does not yet include indentation - """ - page = Html() - body = Body() - page.append(body) - body.append(P("a small paragraph of text")) - body.append(P("another small paragraph of text")) - body.append(P("and here is a bit more")) - - file_contents = render_result(page).strip() - - print(file_contents) - assert file_contents.startswith("") - assert file_contents.endswith("") - assert "a small paragraph of text" in file_contents - assert "" in file_contents - # you could do more here, but it should all be covered above. - # assert False - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = Html("some content") - file_contents = render_result(html, ind=" ") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[0].startswith(" <") - assert lines[-1].startswith(" <") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = Html("some content") - file_contents = render_result(html, ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented fully - """ - body = Body() - body.append(P("some text")) - html = Html(body) - - file_contents = render_result(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): - assert lines[i].startswith(i * Element.indent + "<") - - assert lines[3].startswith(3 * Element.indent + "some") - - -def test_title(): - """ - This will implicitly test the OneLineTag element - """ - t = Title("Isn't this a nice title?") - - # making sure indentation still works - file_contents = render_result(t, ind=" ") - - print(file_contents) - # no "strip()" -- making sure there are no extra newlines - assert "\n" not in file_contents - assert "> " not in file_contents - assert file_contents.startswith(" ") - assert file_contents.endswith("") - # the only newline should be at the end - assert "\n" not in file_contents - - -def test_head(): - """ - testing Head with a title in it -- it should never be blank - """ - h = Head() - h.append(Title("A nifty title for the page")) - - -def test_full_page_with_title(): - """ - not much to actually test here, but good to see it put together. - - everything should have already been tested. - """ - page = Html() - - head = Head() - head.append(Title("PythonClass Example")) - - page.append(head) - - body = Body() - - body.append(P("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text")) - body.append(P("And here is another piece of text -- you should be able to add any number")) - - page.append(body) - - file_contents = render_result(page) - - print(file_contents) - - # uncomment this to see results - # assert False - - -def test_attributes(): - """ - tests that you can pass attributes in to the tag - """ - e = Element("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - # note -- dicts aren't ordered, so you can't enforce order! - # assert '' in file_contents - - -def test_attributes_one_line_tag(): - """ - tests that you can pass attributes in to the tag - """ - e = Title("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - - -def test_br(): - br = Br("") - file_contents = render_result(br) - print(file_contents) - assert file_contents == "
" - - -def test_content_in_br(): - with pytest.raises(TypeError): - br = Br("some content") - - -def test_br_in_p(): - p = P("here is a small paragraph of text") - p.append(Br()) - p.append("And here is some more text after a line break") - - file_contents = render_result(p).split('\n') - print(file_contents) - assert file_contents[2].strip() == "
" - -def test_hr(): - hr = Hr(width=400) - file_contents = render_result(hr) - print(file_contents) - assert file_contents == '
' diff --git a/source/solutions/html_render/step_6/html_render.py b/source/solutions/html_render/step_6/html_render.py deleted file mode 100644 index e6104e8a..00000000 --- a/source/solutions/html_render/step_6/html_render.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 - -""" -Chris's solution through step 6 -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for just text - - This allows the Element classes to render either Element objects or - plain text - - """ - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -class Element: - - tag = "html" - indent = " " - - def __init__(self, content=None, **kwargs): - self.attributes = kwargs - self.content = [] - if content: - # call the classes append method - # so that it can do anything special it needs to do - self.append(content) - - def append(self, content): - """ - add a new piece of content or another element to this element - """ - # note: this changed the internal representation of content - # it no longer holds strings -- so a test will fail - # but that test was testing internal API -- - # it's probably better remove it - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) - - def make_tags(self): - """ - create the tags - -- in a separate method so different subclass's render methods can use it - """ - attrs = " ".join(['{}="{}"'.format(key, val) for key, val in self.attributes.items()]) - if attrs.strip(): - open_tag = "<{} {}>".format(self.tag, attrs.strip()) - else: - open_tag = "<{}>".format(self.tag) - close_tag = "".format(self.tag) - - return open_tag, close_tag - - def render(self, out_file, ind=""): - open_tag, close_tag = self.make_tags() - out_file.write(ind + open_tag + "\n") - for stuff in self.content: - stuff.render(out_file, ind + self.indent) - out_file.write("\n") - out_file.write(ind + close_tag) - - -class OneLineTag(Element): - # note: by re-writting the render - def render(self, out_file, ind=""): - # there is some repition here -- maybe factor that out? - open_tag, close_tag = self.make_tags() - out_file.write(ind + open_tag) - for stuff in self.content: - stuff.render(out_file) - out_file.write(close_tag) - - -class Html(Element): - tag = 'html' - - -class Body(Element): - tag = "body" - - -class P(Element): - tag = "p" - - -class Head(Element): - tag = "head" - - -class Title(OneLineTag): - tag = "title" - - -class SelfClosingTag(Element): - """ - Base class for tags that have no content - """ - - def append(self, *args, **kwargs): - """ - self closing tags can't have content, so we raise an error if someone - tries to add some. - """ - raise TypeError("You can not add content to a self closing tag") - - def render(self, out_file, ind=""): - # there is some repition here -- maybe factor that out? - open_tag, _ = self.make_tags() - # make it a self cloding tag by adding the / - out_file.write(ind + open_tag.replace(">", " />")) - - -class Hr(SelfClosingTag): - """ - Horizontal Rule - """ - tag = "hr" - - -class Br(SelfClosingTag): - """ - Line break - """ - tag = "br" - - -class A(OneLineTag): - """ - anchor element - """ - tag = "a" - - def __init__(self, link, *args, **kwargs): - kwargs['href'] = link - super().__init__(*args, **kwargs) - # this could also be direct: - # Element.__init__(self, *args, **kwargs) - diff --git a/source/solutions/html_render/step_6/test_html_render.py b/source/solutions/html_render/step_6/test_html_render.py deleted file mode 100644 index 6d27407c..00000000 --- a/source/solutions/html_render/step_6/test_html_render.py +++ /dev/null @@ -1,335 +0,0 @@ -""" -test code for html_render.py - -includes up to step 6 -""" -import io -import pytest - -from html_render import (Element, - Html, - Body, - P, - TextWrapper, - Head, - Title, - Hr, - Br, - A, - ) - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -# These two tests were testing internals -# so they failed when I added the TextWrapper -# but I"m removing them because tests really should be testing -# the external API. -# def test_content(): -# # fixme: this tests internals!!!! -# e = Element("this is some text") - -# assert "this is some text" in e.content - -# def test_append(): -# e = Element("this is some text") - -# e.append("some more text") - -# assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_html(): - e = Html("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_body(): - e = Body("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_p(): - e = P("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("

") - assert file_contents.strip().endswith("

") - - -def test_text_wrapper(): - tw = TextWrapper("A basic piece of text") - - file_contents = render_result(tw) - assert file_contents == "A basic piece of text" - - -def test_non_str(): - """ you should be able to pass anything in, and it will get - "stringified" - """ - e = P(34) # a number - e.append((3, 4, 5)) # even a tuple - - file_contents = render_result(e) - - print(file_contents) - assert("34") in file_contents - assert("(3, 4, 5)") in file_contents - - -def test_sub_element(): - """ - tests that you can add another element and still render properly - """ - page = Html() - page.append("some plain text.") - page.append(P("A simple paragraph of text")) - page.append("Some more plain text.") - - file_contents = render_result(page) - - # note: the above tests should make sure that the tags are getting rendered. - assert "some plain text" in file_contents - assert "A simple paragraph of text" in file_contents - assert "Some more plain text." in file_contents - assert "some plain text" in file_contents - - -def test_step_2_noindent(): - """ - This is more if an integration test -- a number of things together - - this test does not yet include indentation - """ - page = Html() - body = Body() - page.append(body) - body.append(P("a small paragraph of text")) - body.append(P("another small paragraph of text")) - body.append(P("and here is a bit more")) - - file_contents = render_result(page).strip() - - print(file_contents) - assert file_contents.startswith("") - assert file_contents.endswith("") - assert "a small paragraph of text" in file_contents - assert "" in file_contents - # you could do more here, but it should all be covered above. - # assert False - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = Html("some content") - file_contents = render_result(html, ind=" ") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[0].startswith(" <") - assert lines[-1].startswith(" <") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = Html("some content") - file_contents = render_result(html, ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented fully - """ - body = Body() - body.append(P("some text")) - html = Html(body) - - file_contents = render_result(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): - assert lines[i].startswith(i * Element.indent + "<") - - assert lines[3].startswith(3 * Element.indent + "some") - - -def test_title(): - """ - This will implicitly test the OneLineTag element - """ - t = Title("Isn't this a nice title?") - - # making sure indentation still works - file_contents = render_result(t, ind=" ") - - print(file_contents) - # no "strip()" -- making sure there are no extra newlines - assert "\n" not in file_contents - assert "> " not in file_contents - assert file_contents.startswith(" ") - assert file_contents.endswith("") - # the only newline should be at the end - assert "\n" not in file_contents - - -def test_head(): - """ - testing Head with a title in it -- it should never be blank - """ - h = Head() - h.append(Title("A nifty title for the page")) - - -def test_full_page_with_title(): - """ - not much to actually test here, but good to see it put together. - - everything should have already been tested. - """ - page = Html() - - head = Head() - head.append(Title("PythonClass Example")) - - page.append(head) - - body = Body() - - body.append(P("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text")) - body.append(P("And here is another piece of text -- you should be able to add any number")) - - page.append(body) - - file_contents = render_result(page) - - print(file_contents) - - # uncomment this to see results - # assert False - - -def test_attributes(): - """ - tests that you can pass attributes in to the tag - """ - e = Element("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - # note -- dicts aren't ordered, so you can't enforce order! - # assert '' in file_contents - - -def test_attributes_one_line_tag(): - """ - tests that you can pass attributes in to the tag - """ - e = Title("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - - -def test_br(): - br = Br("") - file_contents = render_result(br) - print(file_contents) - assert file_contents == "
" - - -def test_content_in_br(): - with pytest.raises(TypeError): - br = Br("some content") - - -def test_hr(): - hr = Hr(width=400) - file_contents = render_result(hr) - print(file_contents) - assert file_contents == '
' - - -def test_anchor(): - a = A("http://google.com/", "link to google") - file_contents = render_result(a) - print(file_contents) - assert file_contents.startswith('') - assert 'href="http://google.com/"' in file_contents - assert 'link to google' in file_contents diff --git a/source/solutions/html_render/step_7/html_render.py b/source/solutions/html_render/step_7/html_render.py deleted file mode 100644 index 4b1c17a4..00000000 --- a/source/solutions/html_render/step_7/html_render.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 - -""" -Chris's solution through step 7 -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for just text - - This allows the Element classes to render either Element objects or - plain text - - """ - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -class Element: - - tag = "html" - indent = " " - - def __init__(self, content=None, **kwargs): - self.attributes = kwargs - self.content = [] - if content: - # call the classes append method - # so that it can do anything special it needs to do - self.append(content) - - def append(self, content): - """ - add a new piece of content or another element to this element - """ - # note: this changed the internal representation of content - # it no longer holds strings -- so a test will fail - # but that test was testing internal API -- - # it's probably better remove it - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) - - def make_tags(self): - """ - create the tags - -- in a separate method so different subclass's render methods can use it - """ - attrs = " ".join(['{}="{}"'.format(key, val) for key, val in self.attributes.items()]) - if attrs.strip(): - open_tag = "<{} {}>".format(self.tag, attrs.strip()) - else: - open_tag = "<{}>".format(self.tag) - close_tag = "".format(self.tag) - - return open_tag, close_tag - - def render(self, out_file, ind=""): - open_tag, close_tag = self.make_tags() - out_file.write(ind + open_tag + "\n") - for stuff in self.content: - stuff.render(out_file, ind + self.indent) - out_file.write("\n") - out_file.write(ind + close_tag) - - -class OneLineTag(Element): - def render(self, out_file, ind=""): - # there is some repition here -- maybe factor that out? - open_tag, close_tag = self.make_tags() - out_file.write(ind + open_tag) - for stuff in self.content: - stuff.render(out_file) - out_file.write(close_tag) - - -class Html(Element): - tag = 'html' - - -class Body(Element): - tag = "body" - - -class P(Element): - tag = "p" - - -class Head(Element): - tag = "head" - - -class Title(OneLineTag): - tag = "title" - - -class SelfClosingTag(Element): - """ - Base class for tags that have no content - """ - - def append(self, *args, **kwargs): - """ - self closing tags can't have content, so we raise an error if someone - tries to add some. - """ - raise TypeError("You can not add content to a self closing tag") - - def render(self, out_file, ind=""): - # there is some repition here -- maybe factor that out? - open_tag, _ = self.make_tags() - # make it a self cloding tag by adding the / - out_file.write(ind + open_tag.replace(">", " />")) - - -class Hr(SelfClosingTag): - """ - Horizontal Rule - """ - tag = "hr" - - -class Br(SelfClosingTag): - """ - Line break - """ - tag = "br" - - -class A(OneLineTag): - """ - anchor element - """ - tag = "a" - - def __init__(self, link, *args, **kwargs): - kwargs['href'] = link - super().__init__(*args, **kwargs) - # this could also be direct: - # Element.__init__(self, *args, **kwargs) - - -class Ul(Element): - """ - unordered list - """ - tag = "ul" - - -class Li(Element): - """ - list element - """ - tag = "li" - - -class H(OneLineTag): - """ - section head - """ - tag = "H" - - def __init__(self, level, *args, **kwargs): - self.tag = "h" + str(int(level)) - super().__init__(*args, **kwargs) diff --git a/source/solutions/html_render/step_7/test_html_render.py b/source/solutions/html_render/step_7/test_html_render.py deleted file mode 100644 index 99291435..00000000 --- a/source/solutions/html_render/step_7/test_html_render.py +++ /dev/null @@ -1,372 +0,0 @@ -""" -test code for html_render.py - -includes step 7 -""" -import io -import pytest - -from html_render import (Element, - Html, - Body, - P, - TextWrapper, - Head, - Title, - Hr, - Br, - A, - Ul, - Li, - H, - ) - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -# These two tests were testing internals -# so they failed when I added the TextWrapper -# but I"m removing them because tests really should be testing -# the external API. -# def test_content(): -# # fixme: this tests internals!!!! -# e = Element("this is some text") - -# assert "this is some text" in e.content - -# def test_append(): -# e = Element("this is some text") - -# e.append("some more text") - -# assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_html(): - e = Html("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_body(): - e = Body("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_p(): - e = P("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("

") - assert file_contents.strip().endswith("

") - - -def test_text_wrapper(): - tw = TextWrapper("A basic piece of text") - - file_contents = render_result(tw) - assert file_contents == "A basic piece of text" - - -def test_non_str(): - """ you should be able to pass anything in, and it will get - "stringified" - """ - e = P(34) # a number - e.append((3, 4, 5)) # even a tuple - - file_contents = render_result(e) - - print(file_contents) - assert("34") in file_contents - assert("(3, 4, 5)") in file_contents - - -def test_sub_element(): - """ - tests that you can add another element and still render properly - """ - page = Html() - page.append("some plain text.") - page.append(P("A simple paragraph of text")) - page.append("Some more plain text.") - - file_contents = render_result(page) - - # note: the above tests should make sure that the tags are getting rendered. - assert "some plain text" in file_contents - assert "A simple paragraph of text" in file_contents - assert "Some more plain text." in file_contents - assert "some plain text" in file_contents - - -def test_step_2_noindent(): - """ - This is more if an integration test -- a number of things together - - this test does not yet include indentation - """ - page = Html() - body = Body() - page.append(body) - body.append(P("a small paragraph of text")) - body.append(P("another small paragraph of text")) - body.append(P("and here is a bit more")) - - file_contents = render_result(page).strip() - - print(file_contents) - assert file_contents.startswith("") - assert file_contents.endswith("") - assert "a small paragraph of text" in file_contents - assert "" in file_contents - # you could do more here, but it should all be covered above. - # assert False - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = Html("some content") - file_contents = render_result(html, ind=" ") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[0].startswith(" <") - assert lines[-1].startswith(" <") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = Html("some content") - file_contents = render_result(html, ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented fully - """ - body = Body() - body.append(P("some text")) - html = Html(body) - - file_contents = render_result(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): - assert lines[i].startswith(i * Element.indent + "<") - - assert lines[3].startswith(3 * Element.indent + "some") - - -def test_title(): - """ - This will implicitly test the OneLineTag element - """ - t = Title("Isn't this a nice title?") - - # making sure indentation still works - file_contents = render_result(t, ind=" ") - - print(file_contents) - # no "strip()" -- making sure there are no extra newlines - assert "\n" not in file_contents - assert "> " not in file_contents - assert file_contents.startswith(" ") - assert file_contents.endswith("") - # the only newline should be at the end - assert "\n" not in file_contents - - -def test_head(): - """ - testing Head with a title in it -- it should never be blank - """ - h = Head() - h.append(Title("A nifty title for the page")) - - -def test_full_page_with_title(): - """ - not much to actually test here, but good to see it put together. - - everything should have already been tested. - """ - page = Html() - - head = Head() - head.append(Title("PythonClass Example")) - - page.append(head) - - body = Body() - - body.append(P("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text")) - body.append(P("And here is another piece of text -- you should be able to add any number")) - - page.append(body) - - file_contents = render_result(page) - - print(file_contents) - - # uncomment this to see results - # assert False - - -def test_attributes(): - """ - tests that you can pass attributes in to the tag - """ - e = Element("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - # note -- dicts aren't ordered, so you can't enforce order! - # assert '' in file_contents - - -def test_attributes_one_line_tag(): - """ - tests that you can pass attributes in to the tag - """ - e = Title("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - - -def test_br(): - br = Br("") - file_contents = render_result(br) - print(file_contents) - assert file_contents == "
" - - -def test_content_in_br(): - with pytest.raises(TypeError): - br = Br("some content") - - -def test_hr(): - hr = Hr(width=400) - file_contents = render_result(hr) - print(file_contents) - assert file_contents == '
' - - -def test_anchor(): - a = A("http://google.com/", "link to google") - file_contents = render_result(a) - print(file_contents) - assert file_contents.startswith('
') - assert 'href="http://google.com/"' in file_contents - assert 'link to google' in file_contents - - -def test_ul(): - ul = Ul() - ul.append(Li("item one in a list")) - ul.append(Li("item two in a list")) - file_contents = render_result(ul) - print(file_contents) - assert file_contents.startswith('
    ') - assert file_contents.endswith('
') - assert "item one in a list" in file_contents - assert "item two in a list" in file_contents - assert file_contents.count("
  • ") == 2 - assert file_contents.count("
  • ") == 2 - - -def test_header(): - h = H(3, "A nice header line") - file_contents = render_result(h) - print(file_contents) - assert file_contents.startswith('

    ') - assert file_contents.endswith('

    ') - assert "A nice header line" in file_contents - - -def test_header(): - h = H(3, "A nice header line", align="center") - file_contents = render_result(h) - print(file_contents) - assert file_contents.startswith('') - assert "A nice header line" in file_contents - assert ' align="center"' in file_contents - diff --git a/source/solutions/html_render/step_8/html_render.py b/source/solutions/html_render/step_8/html_render.py deleted file mode 100644 index 1a4bddb9..00000000 --- a/source/solutions/html_render/step_8/html_render.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python3 - -""" -Chris's solution through step 8 -""" - - -class TextWrapper: - """ - A simple wrapper that creates a class with a render method - for just text - - This allows the Element classes to render either Element objects or - plain text - - """ - def __init__(self, text): - self.text = text - - def render(self, file_out, current_ind=""): - file_out.write(current_ind + self.text) - - -class Element: - - tag = "html" - indent = " " - - def __init__(self, content=None, **kwargs): - self.attributes = kwargs - self.content = [] - if content: - # call the class' append method - # so that it can do anything special it needs to do - self.append(content) - - def append(self, content): - """ - add a new piece of content or another element to this element - """ - # note: this changed the internal representation of content - # it no longer holds strings -- so a test will fail - # but that test was testing internal API -- - # it's probably better remove it - # if isinstance(content, Element): - if hasattr(content, 'render'): - self.content.append(content) - else: - self.content.append(TextWrapper(str(content))) - # self.content.append(content) - - def make_tags(self): - """ - create the tags - -- in a separate method so different subclass's render methods can use it - """ - attrs = " ".join(['{}="{}"'.format(key, val) for key, val in self.attributes.items()]) - if attrs.strip(): - open_tag = "<{} {}>".format(self.tag, attrs.strip()) - else: - open_tag = "<{}>".format(self.tag) - close_tag = "".format(self.tag) - - return open_tag, close_tag - - def render(self, out_file, cur_ind=""): - print("in render, type of self", type(self)) - open_tag, close_tag = self.make_tags() - out_file.write(cur_ind + open_tag + "\n") - for stuff in self.content: - stuff.render(out_file, cur_ind + self.indent) - out_file.write("\n") - out_file.write(cur_ind + close_tag) - - -class OneLineTag(Element): - def render(self, out_file, cur_ind=""): - open_tag, close_tag = self.make_tags() - out_file.write(cur_ind + open_tag) - for stuff in self.content: - stuff.render(out_file) - out_file.write(close_tag) - - -class Html(Element): - tag = 'html' - - def render(self, file_out, cur_ind=""): - file_out.write(cur_ind + "\n") - super().render(file_out, cur_ind=cur_ind) - - -class Body(Element): - tag = "body" - - -class P(Element): - tag = "p" - -class Head(Element): - tag = "head" - - -class Title(OneLineTag): - tag = "title" - - -class SelfClosingTag(Element): - """ - base class for tags that have no content - """ - - def append(self, *args, **kwargs): - """ - self closing tags can't have content, so we raise an error if someone - tries to add some. - """ - raise TypeError("You can not add content to a self closing tag") - - def render(self, out_file, ind=""): - # there is some repetition here -- maybe factor that out? - open_tag, _ = self.make_tags() - # make it a self closing tag by adding the / - out_file.write(ind + open_tag.replace(">", " />")) - - -class Hr(SelfClosingTag): - """ - Horizontal Rule - """ - tag = "hr" - - -class Br(SelfClosingTag): - """ - Line break - """ - tag = "br" - - -class A(OneLineTag): - """ - anchor element - """ - tag = "a" - - def __init__(self, link, *args, **kwargs): - kwargs['href'] = link - super().__init__(*args, **kwargs) - # this could also be direct: - # Element.__init__(self, *args, **kwargs) - - -class Ul(Element): - """ - unordered list - """ - tag = "ul" - - -class Li(Element): - """ - list element - """ - tag = "li" - - -class H(OneLineTag): - """ - section head - """ - tag = "H" - - def __init__(self, level, *args, **kwargs): - self.tag = "h" + str(int(level)) - super().__init__(*args, **kwargs) - - -class Meta(SelfClosingTag): - """ - metadata tag - """ - tag = "meta" diff --git a/source/solutions/html_render/step_8/test_html_render.py b/source/solutions/html_render/step_8/test_html_render.py deleted file mode 100644 index bca98560..00000000 --- a/source/solutions/html_render/step_8/test_html_render.py +++ /dev/null @@ -1,444 +0,0 @@ -""" -test code for html_render.py - -Includes step 8 -""" -import io -import pytest - -from html_render import (Element, - Html, - Body, - P, - TextWrapper, - Head, - Title, - Hr, - Br, - A, - Ul, - Li, - H, - Meta, - ) - -# utility function for testing render methods -# needs to be used in multiple tests, so write it once here. - - -def render_result(element, ind=""): - """ - calls element's render method, and returns what got rendered as a string - """ - outfile = io.StringIO() - element.render(outfile, ind) - return outfile.getvalue() - - -def test_init(): - """ - this only tests that it can be initialized -- but it's a start - """ - e = Element() - - e = Element("this is some text") - - -# These two tests were testing internals -# so they failed when I added the TextWrapper -# but I"m removing them because tests really should be testing -# the external API. -# def test_content(): -# # fixme: this tests internals!!!! -# e = Element("this is some text") - -# assert "this is some text" in e.content - -# def test_append(): -# e = Element("this is some text") - -# e.append("some more text") - -# assert "some more text" in e.content - - -def test_two_instances(): - e = Element("this is some text") - e2 = Element("this is some text") - - e.append("some more text") - - assert "some more text" not in e2.content - - -def test_render(): - e = Element("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -# # this test obsoleted by the one below with the DOCTYPE tag -# def test_html(): -# e = Html("this is some text") -# e.append("and this is some more text") - -# file_contents = render_result(e) - -# assert("this is some text") in file_contents -# assert("and this is some more text") in file_contents - -# assert file_contents.startswith("") -# assert file_contents.strip().endswith("") - - -def test_body(): - e = Body("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("") - assert file_contents.strip().endswith("") - - -def test_p(): - e = P("this is some text") - e.append("and this is some more text") - - file_contents = render_result(e) - - assert("this is some text") in file_contents - assert("and this is some more text") in file_contents - - assert file_contents.startswith("

    ") - assert file_contents.strip().endswith("

    ") - - -def test_text_wrapper(): - tw = TextWrapper("A basic piece of text") - - file_contents = render_result(tw) - assert file_contents == "A basic piece of text" - - -def test_non_str(): - """ you should be able to pass anything in, and it will get - "stringified" - """ - e = P(34) # a number - e.append((3, 4, 5)) # even a tuple - - file_contents = render_result(e) - - print(file_contents) - assert("34") in file_contents - assert("(3, 4, 5)") in file_contents - - -def test_sub_element(): - """ - tests that you can add another element and still render properly - """ - page = Html() - page.append("some plain text.") - page.append(P("A simple paragraph of text")) - page.append("Some more plain text.") - - file_contents = render_result(page) - - # note: the above tests should make sure that the tags are getting rendered. - assert "some plain text" in file_contents - assert "A simple paragraph of text" in file_contents - assert "Some more plain text." in file_contents - assert "some plain text" in file_contents - - -def test_step_2_noindent(): - """ - This is more if an integration test -- a number of things together - - """ - page = Html() - body = Body() - page.append(body) - body.append(P("a small paragraph of text")) - body.append(P("another small paragraph of text")) - body.append(P("and here is a bit more")) - - file_contents = render_result(page).strip() - - print(file_contents) - # teh DOCTYPE tag messed this up :-( - # assert file_contents.startswith("") - assert file_contents.endswith("") - assert "a small paragraph of text" in file_contents - assert "" in file_contents - # you could do more here, but it should all be covered above. - # assert False - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = Html("some content") - file_contents = render_result(html, ind=" ") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[0].startswith(" <") - assert lines[-1].startswith(" <") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = Element("some content") - file_contents = render_result(html, ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented fully - """ - body = Body() - body.append(P("some text")) - html = Html(body) - - file_contents = render_result(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): # this needed to be adapted to the tag - assert lines[i + 1].startswith(i * Element.indent + "<") - - assert lines[4].startswith(3 * Element.indent + "some") - - -def test_title(): - """ - This will implicitly test the OneLineTag element - """ - t = Title("Isn't this a nice title?") - - # making sure indentation still works - file_contents = render_result(t, ind=" ") - - print(file_contents) - # no "strip()" -- making sure there are no extra newlines - assert "\n" not in file_contents - assert "> " not in file_contents - assert file_contents.startswith(" ") - assert file_contents.endswith("") - # the only newline should be at the end - assert "\n" not in file_contents - - -def test_head(): - """ - testing Head with a title in it -- it should never be blank - """ - h = Head() - h.append(Title("A nifty title for the page")) - - -def test_full_page_with_title(): - """ - not much to actually test here, but good to see it put together. - - everything should have already been tested. - """ - page = Html() - - head = Head() - head.append(Title("PythonClass Example")) - - page.append(head) - - body = Body() - - body.append(P("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text")) - body.append(P("And here is another piece of text -- you should be able to add any number")) - - page.append(body) - - file_contents = render_result(page) - - print(file_contents) - - # uncomment this to see results - # assert False - - -def test_attributes(): - """ - tests that you can pass attributes in to the tag - """ - e = Element("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - # note -- dicts aren't ordered, so you can't enforce order! - # assert '' in file_contents - - -def test_attributes_one_line_tag(): - """ - tests that you can pass attributes in to the tag - """ - e = Title("some text", id="this", color="red") # could be any attributes - file_contents = render_result(e) - print(file_contents) - assert 'id="this"' in file_contents - assert 'color="red"' in file_contents - - -def test_br(): - br = Br() - file_contents = render_result(br) - print(file_contents) - assert file_contents == "
    " - - -def test_content_in_br(): - with pytest.raises(TypeError): - br = Br("some content") - - -def test_hr(): - hr = Hr(width=400) - file_contents = render_result(hr) - print(file_contents) - assert file_contents == '
    ' - - -def test_anchor(): - a = A("http://google.com/", "link to google") - file_contents = render_result(a) - print(file_contents) - assert file_contents.startswith('
    ') - assert 'href="http://google.com/"' in file_contents - assert 'link to google' in file_contents - - -def test_ul(): - ul = Ul() - ul.append(Li("item one in a list")) - ul.append(Li("item two in a list")) - file_contents = render_result(ul) - print(file_contents) - assert file_contents.startswith('
      ') - assert file_contents.endswith('
    ') - assert "item one in a list" in file_contents - assert "item two in a list" in file_contents - assert file_contents.count("
  • ") == 2 - assert file_contents.count("
  • ") == 2 - - -def test_header(): - h = H(3, "A nice header line") - file_contents = render_result(h) - print(file_contents) - assert file_contents.startswith('

    ') - assert file_contents.endswith('

    ') - assert "A nice header line" in file_contents - - -def test_header2(): - """ - adding an attribute to a header - """ - h = H(2, "A nice header line", align="center") - file_contents = render_result(h) - print(file_contents) - assert file_contents.startswith('') - assert "A nice header line" in file_contents - assert ' align="center"' in file_contents - - -def test_html_doctype(): - html = Html() - file_contents = render_result(html) - print(file_contents) - - assert file_contents.startswith("\n") - - -def test_meta(): - """ - test the meta tag - """ - m = Meta(charset="UTF-8") - file_contents = render_result(m) - print(file_contents) - - assert file_contents == '' - - -def test_whole_thing(): - """ - Render a complete page - - This is not really a unit test, and does not test that the results - are correct, but does ensure that it all runs, and provides output - to look at - """ - page = Html() - - head = Head() - head.append(Meta(charset="UTF-8")) - head.append(Title("Python Class Sample page")) - page.append(head) - - body = Body() - - body.append(H(2, "Python Class - Html rendering example")) - - body.append(P("Here is a paragraph of text -- there could be more of them, " - "but this is enough to show that we can do some text", - style="text-align: center; font-style: oblique;")) - - body.append(Hr()) - - list = Ul(id="TheList", style="line-height:200%") - - list.append(Li("The first item in a list")) - list.append(Li("This is the second item", style="color: red")) - - item = Li() - item.append("And this is a ") - item.append(A("http://google.com/", "link")) - item.append("to google") - - list.append(item) - - body.append(list) - - page.append(body) - - # Element.indent = " " - # now render it: - with open("sample_output.html", 'w') as f: - page.render(f) - - # assert False diff --git a/source/solutions/html_render/step_8/test_html_render2.py b/source/solutions/html_render/step_8/test_html_render2.py deleted file mode 100644 index cdf8c48d..00000000 --- a/source/solutions/html_render/step_8/test_html_render2.py +++ /dev/null @@ -1,425 +0,0 @@ -#!/usr/bin/env python - - -import os -from io import StringIO - -from html_render import Element, Body, P, Html, Head, Title - - -# test utilities - -def render_element_file(element, filename='temp_render_file.html', remove=False): - """ - renders an element, and returns what got rendered - - This version uses an actual file on disk -- yu may want to use it so you - can see the file afterward. - - :param element: the element to be rendered (its render() method will be called) - - :param filename='temp_render_file.html': the name of the temporary file to be used. - - :param remove=True: Whether to remove the file after using it to render. - Set this to True if you want to be able to look at after - the tests run. - - NOTE: - this could be refactored, and still used everywhere. - """ - with open(filename, 'w') as out_file: - element.render(out_file) - with open(filename, 'r') as in_file: - contents = in_file.read() - if remove: - os.remove(filename) - return contents - - -def render_element(element, cur_ind=""): - # this version uses a StringIO object, to keep it all in memory - """ - renders an element, and returns what got rendered - - This can be used by multiple tests. - - :param element: the element to be rendered (its render() method will be called) - - :param filename='temp_render_file.html': the name of the temporary file to be used. - - :param remove=True: Whether to remove the file after using it to render. - Set this to True if you want to be able to look at after - the tests run. - - NOTE: - this could be refactored, and still used everywhere. - """ - sio = StringIO() - element.render(sio, cur_ind=cur_ind) - # if remove: - # os.remove(filename) - return sio.getvalue() - - -def test_new_element(): - """ - not much here, but it shows Elements can be initialized - """ - el_object = Element() - el_object2 = Element('content') - - -# careful here -- this is testing internal implimentations -# sometimes helpful as you are developing, but you may want to remove -# these tests once you have more working. -def test_add_content(): - el_object = Element('content') - assert el_object.content == ['content'] - - el_object = Element() - assert el_object.content == [] - - -def test_adding_empty_string(): - el_object = Element('') - assert el_object.content == [''] - - -def test_append_string(): - el_object = Element('spam, spam, spam') - el_object.append(', wonderful spam') - assert el_object.content == ['spam, spam, spam', ', wonderful spam'] - - -def test_tag_exists(): - assert Element.tag == 'html' - el_object = Element('spam, spam, spam') - assert el_object.tag == 'html' - - -def test_indent_exists(): - assert Element.indent == ' ' - - -# Now we get tot he real "meat" of the tests --does the code do what -# it is supposed to do? -def test_render(): - my_stuff = 'spam, spam, spam' - el_object = Element(my_stuff) - more_stuff = 'eggs, eggs, eggs' - el_object.append(more_stuff) - contents = render_element(el_object).strip() - assert contents.startswith('') - assert contents.endswith('') - assert my_stuff in contents - assert more_stuff in contents - - - -# you want to be careful with these: -# It is testing an implementation detail, which is less than ideal. -# sometimes in TDD, it's helpful to have quickies tests of -# implementation details so you can see that partially written code -# is working -- but if/when you can test actual functionality, that's -# better. In this case, once we have a render() method, we can test -# that the tag gets rendered properly, so don't need to test if the -# tag attribute is correct. - -# def test_body_tag(): -# assert Body.tag == 'body' - -# def test_p_tag(): -# assert P.tag == 'p' - - -# def test_html_tag(): -# assert HTML.tag == 'html' - -# finally! a really good test. -# This is an actual element that we want to render -# so whatever the implimentation deatails, it's working. -def test_render_body(): - my_stuff = 'spam, spam, spam' - el_object = Body(my_stuff) - more_stuff = 'eggs, eggs, eggs' - el_object.append(more_stuff) - contents = render_element(el_object).strip() - assert contents.startswith('') - assert contents.endswith('') - assert my_stuff in contents - assert more_stuff in contents - - -def test_render_P(): - my_stuff = 'spam, spam, spam' - p = P(my_stuff) - more_stuff = 'eggs, eggs, eggs' - p.append(more_stuff) - contents = render_element(p).strip() - assert contents.startswith('

    ') - assert contents.endswith('

    ') - assert my_stuff in contents - assert more_stuff in contents - - -def test_render_html(): - my_stuff = 'spam, spam, spam' - el_object = Html(my_stuff) - more_stuff = 'eggs, eggs, eggs' - el_object.append(more_stuff) - contents = render_element(el_object) - assert contents.startswith('') - assert contents.endswith('') - assert my_stuff in contents - assert more_stuff in contents - - -def test_render_non_strings(): - # this is creating a html page with a single body() element in it - # and a string inside that. - el_object = Html(Body('any string I like')) - - contents = render_element(el_object) - # make sure extra whitespace at beginning or end doesn't mess things up. - contents = contents.strip() - - print(contents) # so we can see what's going on if a test fails - - # so what should the results be? - # the html tag is the outer tag, so the contents should start and end with that. - assert contents.startswith('') - assert contents.endswith('') - - # the body tags had better be there too - assert '' in contents - assert '') < contents.index('') - # the opening tag should come before the content - assert contents.index('') < contents.index('any string') - - -def test_render_non_strings2(): - """ - Testing nested elements and text, in a more complex way - """ - html = Html() - body = Body() - html.append(body) - p = P('any string I like') - p.append('even more random text') - body.append(p) - body.append(P('and this is a different string')) - contents = render_element(html).strip() - - print(contents) # so we can see what's going on if a test fails - - # so what should the results be? - # the html tag is the outer tag, so the contents should start and end with that. - assert contents.startswith('') - assert contents.endswith('') - - # the body tags had better be there too - assert '' in contents - assert ' tags - assert contents.count('

    ') - - # we want the text, too: - assert 'any string I like' in contents - assert 'even more random text' in contents - assert 'and this is a different string' in contents - - # you could, of course, test much more..but hopefully other things are tested, too. - - -def test_indent(): - """ - Tests that the indentation gets passed through to the renderer - """ - html = Html("some content") - cur_ind = 6 * " " - file_contents = render_element(html, cur_ind=cur_ind) - - print(file_contents) - lines = file_contents.split("\n") - - assert lines[0].startswith(cur_ind + "<") - assert lines[1].startswith(cur_ind + Element.indent + "som") - assert lines[-1].startswith(cur_ind + "<") - - -def test_indent_contents(): - """ - The contents in a element should be indented more than the tag - by the amount in the indent class attribute - """ - html = Html("some content") - file_contents = render_element(html, cur_ind="") - - print(file_contents) - lines = file_contents.split("\n") - assert lines[1].startswith(Element.indent) - - -def test_multiple_indent(): - """ - make sure multiple levels get indented properly - """ - body = Body() - body.append(P("some text")) - body.append(P("even more text")) - html = Html(body) - - file_contents = render_element(html) - - print(file_contents) - lines = file_contents.split("\n") - for i in range(3): - assert lines[i].startswith(i * Element.indent + "<") - assert lines[3].startswith(3 * Element.indent + "some") - assert lines[4].startswith(2 * Element.indent + "

    ") - assert lines[5].startswith(2 * Element.indent + "

    ") - assert lines[6].startswith(3 * Element.indent + "even ") - for i in range(3): - assert lines[-(i + 1)].startswith(i * Element.indent + "<") - - -def test_title(): - """ - This will implicitly test the OneLineTag element - """ - t = Title("Isn't this a nice title?") - - # making sure indentation still works - file_contents = render_element(t, cur_ind=" ") - - print(file_contents) - # no "strip()" -- making sure there are no extra newlines - assert file_contents.startswith(" I") - assert file_contents.endswith("? ") - # the only newline should be at the end - - -def test_head(): - """ - testing Head with a title in it -- it should never be blank - """ - h = Head() - h.append(Title("A nifty title for the page")) - file_contents = render_element(h, cur_ind=' ') - - print(file_contents) - assert file_contents.startswith(" ") - assert file_contents.endswith(" ") - - assert "" in file_contents - assert "" in file_contents - assert "A nifty title for the page" in file_contents - - -def test_full_page_with_title(): - """ - not much to actually test here, but good to see it put together. - - everything should have already been tested. - """ - page = Html() - - head = Head() - head.append(Title("PythonClass Example")) - - page.append(head) - - body = Body() - - body.append(P("Here is a Pgraph of text -- there could be more of them, " - "but this is enough to show that we can do some text")) - body.append(P("And here is another piece of text -- you should be able to add any number")) - - page.append(body) - - file_contents = render_element(page) - - print(file_contents) - - # uncomment this to see results - # assert False - - -def test_single_attribute(): - #

    - # Here is a Pgraph of text -- there could be more of them, but this is enough to show that we can do some text - #

    - p = P("Here is a paragraph of text", style="text-align: center; font-style: oblique;") - - results = render_element(p) - - assert results.startswith('

    ') - - print(results) - -def test_multiple_attributes(): - #

    - # Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text - #

    - p = P("Here is a paragraph of text", - id="fred", - color="red", - size="12px", - ) - - results = render_element(p) - print(results) - - lines = results.split('\n') - assert lines[0].startswith('

    ') - assert 'id="fred"' in lines[0] - assert 'color="red"' in lines[0] - assert 'size="12px"' in lines[0] - -def test_multiple_attributes_title(): - t = Title("Here is a paragraph of text", - id="fred", - color="red", - size="12px", - ) - - results = render_element(t) - print(results) - - lines = results.split('\n') - assert lines[0].startswith('') - assert 'id="fred"' in lines[0] - assert 'color="red"' in lines[0] - assert 'size="12px"' in lines[0] - - -# test class attribute -def test_class_attribute(): - atts = {"id": "fred", - "class": "special", - "size": "12px", - } - p = P("Here is a paragraph of text", - **atts) - - results = render_element(p) - print(results) - - lines = results.split('\n') - assert lines[0].startswith('<p ') - assert lines[0].strip().endswith('">') - assert 'id="fred"' in lines[0] - assert 'class="special"' in lines[0] - assert 'size="12px"' in lines[0] - - - - From 50a64313ec2214cece70f4227c24e21620f51332 Mon Sep 17 00:00:00 2001 From: Chris Barker <PythonCHB@gmail.com> Date: Mon, 3 Dec 2018 19:32:28 -0800 Subject: [PATCH 39/87] removed redundant sparse array solution --- source/solutions/sparse_array/slice_sparse.py | 139 ------------------ source/solutions/sparse_array/sparse_array.py | 75 ---------- .../sparse_array/test_slice_sparse.py | 129 ---------------- .../sparse_array/test_sparse_array.py | 113 -------------- 4 files changed, 456 deletions(-) delete mode 100644 source/solutions/sparse_array/slice_sparse.py delete mode 100644 source/solutions/sparse_array/sparse_array.py delete mode 100644 source/solutions/sparse_array/test_slice_sparse.py delete mode 100644 source/solutions/sparse_array/test_sparse_array.py diff --git a/source/solutions/sparse_array/slice_sparse.py b/source/solutions/sparse_array/slice_sparse.py deleted file mode 100644 index c40fb76a..00000000 --- a/source/solutions/sparse_array/slice_sparse.py +++ /dev/null @@ -1,139 +0,0 @@ - -""" -example of emulating a sequence using slices -""" - - -class SparseArray(object): - - def __init__(self, my_array=()): - self.length = len(my_array) - self.sparse_array = self._convert_to_sparse(my_array) - - def _convert_to_sparse(self, my_array): - sparse_array = {} - for index, number in enumerate(my_array): - if number: - sparse_array[index] = number - return sparse_array - - def __len__(self): - return self.length - - def __str__(self): - msg = ['SparseArray: ['] - for i in range(self.length): - msg.append("{} ".format(self[i])) - msg.append(']') - return "".join(msg) - - def __getitem__(self, index): - # this version supports slicing -- far more complicated - mini_array = [] - if isinstance(index, slice): - start, stop, step = index.indices(len(self)) - if step is None: - step = 1 - key = start - mini_array = [] - while key < stop: - mini_array.append(self[key]) - key += step - return mini_array - else: - # makes it an int, even if it's some other - # type that supports being used as an index - index = index.__index__() - return self._get_single_value(index) - - def _get_single_value(self, key): - if key >= self.length: - raise IndexError('array index out of range') - else: - return self.sparse_array.get(key, 0) - - def __setitem__(self, index, value): - if isinstance(index, slice): - start, stop, step = index.indices(len(self)) - if step is None: - step = 1 - key = start - new_values = [] - new_keys = [] - for each in value: - if key < stop: - self[key] = each - else: - # now instead of replacing values, we need to add (a) value(s) in the center, - # and move stuff over, probably want to collect all of the changes, - # and then make a new dictionary - new_values.append(each) - new_keys.append(key) - key += step - if new_keys: - self._add_in_slice(new_keys, new_values) - else: - index = index.__index__() - self._set_single_value(index, value) - - def _set_single_value(self, key, value): - if key > self.length: - raise IndexError('array assignment index out of range') - if value != 0: - self.sparse_array[key] = value - else: - # if the value is being set to zero, we probably need to - # remove a key from our dictionary. - self.sparse_array.pop(key, None) - - def _add_in_slice(self, new_keys, new_values): - # sometimes we need to add in extra values - # any existing values - # greater than the last key of the new data - # will be increased by how many - new_dict = {} - slice_length = len(new_keys) - for k, v in self.sparse_array.items(): - if k >= new_keys[-1]: - # print('change keys') - # if greater than slice, change key - new_dict[k + slice_length] = v - elif k in new_keys: - # if this is a key we are changing, change it, - # unless we are changing to a zero... - new_value = values[new_keys.index(k)] - if new_value != 0: - new_dict[k] = new_value - else: - new_dict[k] = v - # what if our new key was not previously in the dictionary? - # stick it in now - for k in new_keys: - if k not in new_dict.keys(): - new_dict[k] = new_values[new_keys.index(k)] - # note we don't want to do update, since we need to make sure we are - # getting rid of the old keys, when we moved the value to a new key - self.sparse_array = new_dict - # now we need to increase the length by the amount we increased our array by - self.length += slice_length - - def __delitem__(self, key): - # we probably need to move the keys if we are not deleting the last - # number, use pop in case it was a zero - if key == self.length - 1: - self.sparse_array.pop(key, None) - else: - # since we need to adjust all of the keys after the one we are - # deleting, probably most efficient to create a new dictionary - new_dict = {} - for k, v in self.sparse_array.items(): - if k >= key: - new_dict[k - 1] = v - else: - new_dict[k] = v - # note we don't want to do update, since we need to make sure we are - # getting rid of the old keys, when we moved the value to a new key - self.sparse_array = new_dict - # length is now one shorter - self.length -= 1 - diff --git a/source/solutions/sparse_array/sparse_array.py b/source/solutions/sparse_array/sparse_array.py deleted file mode 100644 index d9543cab..00000000 --- a/source/solutions/sparse_array/sparse_array.py +++ /dev/null @@ -1,75 +0,0 @@ - -""" -An example of emulating a sequence - -A SparseArray is like a list, but only stores the non-zero values - -It can be indexed, appended-to, and iterated through. - -This version does not support slicing. -""" - - -class SparseArray(object): - - def __init__(self, my_array=()): - """ - initilize a sparse array - - :param my_array: an initial sequence to start with - if there are zeros in it, they wil not be stored - """ - self.length = len(my_array) - # self.sparse_array is a dict that stores only the non-zero items - self.sparse_array = self._convert_to_sparse(my_array) - - def _convert_to_sparse(self, my_array): - sparse_array = {} - for index, number in enumerate(my_array): - if number: # remember that zeros are falsey. - sparse_array[index] = number - # or the dict comprehension method: - # sparse_array = {index:number for index, number in enumerate(my_array) if number} - return sparse_array - - def __len__(self): - return self.length - - def __getitem__(self, key): - # fixme: doesn't handle negative indexes properly - try: - return self.sparse_array[key] - except KeyError: - if key >= self.length: - raise IndexError('array index out of range') - return 0 - - def __setitem__(self, key, value): - if key > self.length: - raise IndexError('array assignment index out of range') - if value != 0: - self.sparse_array[key] = value - else: - # if the value is being set to zero, we probably need to - # remove a key from our dictionary. - self.sparse_array.pop(key, None) - - def __delitem__(self, key): - # we probably need to move the keys if we are not deleting the last - # number, use pop in case it was a zero - if key == self.length - 1: # it's the last item -- easy. - self.sparse_array.pop(key, None) - else: - # since we need to adjust all of the keys after the one we are - # deleting, probably most efficient to create a new dictionary - new_dict = {} - for k, v in self.sparse_array.items(): - if k >= key: - new_dict[k - 1] = v - else: - new_dict[k] = v - # note we don't want to do update, since we need to make sure we are - # getting rid of the old keys, when we moved the value to a new key - self.sparse_array = new_dict - # length is now one shorter - self.length -= 1 diff --git a/source/solutions/sparse_array/test_slice_sparse.py b/source/solutions/sparse_array/test_slice_sparse.py deleted file mode 100644 index 4a7994c8..00000000 --- a/source/solutions/sparse_array/test_slice_sparse.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -this version tests the solution with slicing tests -""" - - -import pytest -from slice_sparse import SparseArray - - -def set_up(): - my_array = [2, 0, 0, 0, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9] - my_sparse = SparseArray(my_array) - return (my_array, my_sparse) - - -def test_object_exists(): - my_array, my_sparse = set_up() - assert isinstance(my_sparse, SparseArray) - - -def test_get_non_zero_number(): - my_array, my_sparse = set_up() - assert my_sparse[4] == 3 - - -def test_get_zero(): - my_array, my_sparse = set_up() - assert my_sparse[1] == 0 - - -def test_get_element_not_in_array(): - my_array, my_sparse = set_up() - with pytest.raises(IndexError): - my_sparse[14] - - -def test_str(): - my_array, my_sparse = set_up() - - -def test_get_slice(): - my_array, my_sparse = set_up() - assert my_sparse[2:4] == [0, 0] - - -def test_set_slice(): - my_array, my_sparse = set_up() - my_sparse[2:4] = [2, 3, 4] - assert my_sparse[:] == [2, 0, 2, 3, 4, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9] - - -def test_set_slice_over_end(): - # this slice goes over the end - my_array, my_sparse = set_up() - print(my_sparse) - my_sparse[2:4] = [2, 3, 4] - assert my_sparse[:] == [2, 0, 2, 3, 4, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9] - - -def test_get_length(): - my_array, my_sparse = set_up() - assert len(my_sparse) == 14 - - -def test_change_number_in_array(): - my_array, my_sparse = set_up() - my_sparse[0] = 3 - assert my_sparse[0] == 3 - # make sure others aren't changed - assert my_sparse[1] == 0 - # make sure still same length - assert len(my_sparse) == 14 - - -def test_change_number_in_array_to_zero(): - my_array, my_sparse = set_up() - my_sparse[4] = 0 - assert my_sparse[4] == 0 - # make sure still same length - assert len(my_sparse) == 14 - - -def test_change_number_in_array_from_zero(): - my_array, my_sparse = set_up() - my_sparse[1] = 4 - assert my_sparse[1] == 4 - # make sure still same length - assert len(my_sparse) == 14 - - -def test_change_slice(): - my_array, my_sparse = set_up() - my_sparse[1:3] = [2, 3] - assert my_sparse[1:3] == [2, 3] - - -def test_delete_number(): - my_array, my_sparse = set_up() - del(my_sparse[4]) - # if we delete the 4 position, should now be zero - assert my_sparse[4] == 0 - # should have smaller length - assert len(my_sparse) == 13 - - -def test_delete_zero(): - my_array, my_sparse = set_up() - del my_sparse[5] - # should still be zero, but should have shorter length - assert my_sparse[5] == 0 - assert len(my_sparse) == 13 - - -def test_delete_last_number(): - my_array, my_sparse = set_up() - del(my_sparse[13]) - # should get an error - with pytest.raises(IndexError): - my_sparse[13] - assert len(my_sparse) == 13 - - -def test_indices_change(): - my_array, my_sparse = set_up() - del(my_sparse[3]) - # next index should have changed - # my_sparse[4] was 3 now - # my_sparse[3] should be 3 - assert (my_sparse[3] == 3) diff --git a/source/solutions/sparse_array/test_sparse_array.py b/source/solutions/sparse_array/test_sparse_array.py deleted file mode 100644 index 06862292..00000000 --- a/source/solutions/sparse_array/test_sparse_array.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -This tests the solution that does not support slicing -""" - -import pytest -from sparse_array import SparseArray - - -def set_up(): - my_array = [2, 0, 0, 0, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9] - my_sparse = SparseArray(my_array) - return (my_array, my_sparse) - - -def test_object_exists(): - my_array, my_sparse = set_up() - assert isinstance(my_sparse, SparseArray) - - -def test_get_non_zero_number(): - my_array, my_sparse = set_up() - assert my_sparse[4] == 3 - - -def test_get_zero(): - my_array, my_sparse = set_up() - assert my_sparse[1] == 0 - - -def test_get_element_not_in_array(): - my_array, my_sparse = set_up() - with pytest.raises(IndexError): - my_sparse[14] - - -def test_get_length(): - my_array, my_sparse = set_up() - assert len(my_sparse) == 14 - - -def test_change_number_in_array(): - my_array, my_sparse = set_up() - my_sparse[0] = 3 - assert my_sparse[0] == 3 - # make sure others aren't changed - assert my_sparse[1] == 0 - # make sure still same length - assert len(my_sparse) == 14 - - -def test_change_number_in_array_to_zero(): - my_array, my_sparse = set_up() - my_sparse[4] = 0 - assert my_sparse[4] == 0 - # make sure still same length - assert len(my_sparse) == 14 - - -def test_change_number_in_array_from_zero(): - my_array, my_sparse = set_up() - my_sparse[1] = 4 - assert my_sparse[1] == 4 - # make sure still same length - assert len(my_sparse) == 14 - - -def test_delete_number(): - my_array, my_sparse = set_up() - del(my_sparse[4]) - # if we delete the 4 position, should now be zero - assert my_sparse[4] == 0 - # should have smaller length - assert len(my_sparse) == 13 - - -def test_delete_zero(): - my_array, my_sparse = set_up() - del(my_sparse[5]) - # should still be zero, but should have shorter length - assert my_sparse[5] == 0 - assert len(my_sparse) == 13 - - -def test_delete_last_number(): - my_array, my_sparse = set_up() - del(my_sparse[13]) - # should get an error? - with pytest.raises(IndexError): - my_sparse[13] - assert len(my_sparse) == 13 - - -def test_indices_change(): - my_array, my_sparse = set_up() - del(my_sparse[3]) - # next index should have changed - # my_sparse[4] was 3 now - # my_sparse[3] should be 3 - assert (my_sparse[3] == 3) - - -# this is a way to tell pytest that you expect this test to fail -@pytest.mark.xfail -def test_get_slice(): - my_array, my_sparse = set_up() - assert my_sparse[2:4] == [0, 0] - - -@pytest.mark.xfail -def test_set_slice(): - my_array, my_sparse = set_up() - my_sparse[2:4] = [2, 3, 4] - assert my_sparse[:] == [2, 0, 2, 3, 4, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9] From 753dac30161f5a6771810bf263a353f98f5bc4ac Mon Sep 17 00:00:00 2001 From: Chris Barker <PythonCHB@gmail.com> Date: Tue, 4 Dec 2018 17:22:06 -0800 Subject: [PATCH 40/87] a bit of copy editing --- source/exercises/mailroom-oo.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/exercises/mailroom-oo.rst b/source/exercises/mailroom-oo.rst index 74d61e02..0d98b1b5 100644 --- a/source/exercises/mailroom-oo.rst +++ b/source/exercises/mailroom-oo.rst @@ -15,7 +15,7 @@ But this time, we want to use an OO approach to better structure the code to mak It was quite reasonable to build the simple mailroom program using a single module, a simple data structure, and functions that manipulate -that data structure. +that data structure. In fact, you've already done that :-) But if one were to expand the program with additional functionality, it would start to get a bit unwieldy and hard to maintain. So it's a pretty good candidate for an object-oriented approach. @@ -23,7 +23,7 @@ would start to get a bit unwieldy and hard to maintain. So it's a pretty good ca As you design appropriate classes, keep in mind these three guidelines for good code structure: -1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that they that work on that data are within one unit. The rest of the code doesn't need to know about the data structure you are using. +1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everyting that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using. 2) Separation of Concerns: The user-interaction code should be cleanly separated from the data handling code. From f138cce7dd10ecaa7069d3f3cab77ef9ab404ee0 Mon Sep 17 00:00:00 2001 From: Chris Barker <PythonCHB@gmail.com> Date: Sat, 8 Dec 2018 14:33:31 -0800 Subject: [PATCH 41/87] some extra notes on OO mailroom --- source/exercises/mailroom-oo.rst | 87 ++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/source/exercises/mailroom-oo.rst b/source/exercises/mailroom-oo.rst index 0d98b1b5..4cc2b1a5 100644 --- a/source/exercises/mailroom-oo.rst +++ b/source/exercises/mailroom-oo.rst @@ -1,11 +1,12 @@ .. _exercise_mailroom_oo: +########################## Mailroom - Object Oriented -========================== +########################## Making Mailroom Object Oriented. -Goal: Refactor the mailroom program using classes to help organize the code. +**Goal:** Refactor the mailroom program using classes to help organize the code. The functionality is the same as the earlier mailroom: @@ -35,7 +36,7 @@ As you design appropriate classes, keep in mind these three guidelines for good The Program ------------ +=========== See: :ref:`exercise_mailroom_part1` to remind yourself what the program needs to do. @@ -131,7 +132,85 @@ At this point we have done a great job refactoring the more complex code out of The ``Donor`` and ``DonorCollection`` classes should now have close to 100 percent code coverage. -For the moment, don't worry about testing most of the command line interface code. That requires simulating use input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved. +For the moment, don't worry about testing most of the command line interface code. That requires simulating user input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved. + +Exercise Guidelines +=================== + +OO mailroom is the final project for 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. + +With that in mind: + +Functionality +------------- + +* The logic is correct -- i.e. the program works :-) + +* The logic is robust -- you are handling obvious expected errors reasonably: + + - User inputting a non-number as a donation + + - Trying to make a negative donation + + - User getting capitalization or spacing or ??? wrong with a name. + + - maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name. + +Code structure: +--------------- + +* Classes should have clear purpose and encapsulation: only the code within a class should know exactly how the data are stored, for instance. + +* Anything that only needs to know about one donor should be in the ``Donor`` class + +* Anything that needs to know about the collection should be in a ``DonorCollection`` class. + +* Any user interaction should be outside the "logic" code. (Sometimes called the "Model", or "Business logic") + + - 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. + + - The logic code should be 100% testable (without mocking input() or any fancy stuff like that) + +Testing: + +* All logic code should be tested. + +* Tests should be isolated to test one thing each + +* Tests should (reasonably) check for handling of weird input. + +* Tests should be isolated -- that is, they will work if run by themselves, and in any order. + + - This means they should not rely on any global state. + + - you'll probably find this easier with a well structured OO approach -- that is, you can test an individual donor functionality without knowing about the rest of the donors. + + +Now the "soft" stuff: +--------------------- + +* Style: conform to PEP8! (or another consistent style) + + - You can use 95 or some other reasonable number for line length + +* docstrings: functions and classes should all have good docstrings. They can very very short if the function does something simple. + +* 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 From cc6b92877f0c7961157a0d6d2e2a98ef16f48bb4 Mon Sep 17 00:00:00 2001 From: Chris Barker <PythonCHB@gmail.com> Date: Wed, 26 Dec 2018 15:28:24 -0800 Subject: [PATCH 42/87] updated for_instructors notes. --- source/for_instructors/code_review.rst | 14 ++++++------ source/for_instructors/github.rst | 30 ++++++++++++++++++++++++++ source/for_instructors/solutions.rst | 2 +- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/source/for_instructors/code_review.rst b/source/for_instructors/code_review.rst index 776f9bea..ff1f1e32 100644 --- a/source/for_instructors/code_review.rst +++ b/source/for_instructors/code_review.rst @@ -2,7 +2,7 @@ Reviewing Students' Code ######################## -One of the most important services the program provides is code review. While for teh most part the students will know if they have solved the problem (i.e. their code works) there is al ot they can learn about clean style, robust logic, appropriate data structures, pythonicity, etc. +One of the most important services the program provides is code review. While for the most part the students will know if they have solved the problem (i.e. their code works) there is a lot they can learn about clean style, robust logic, appropriate data structures, Pythonicity, etc. And they can only learn that if their code is reviewed and critiqued. @@ -11,7 +11,7 @@ Scoring Assignments Exactly how to score the exercises is up to the individual instructor. But we do need to provide some score, so there is a clear requirement to passing the class. -As the course is pass/fail, I don't think it's important to distinguish between decent, good, and excellent work. So I tend to give full credit for a good-faith effort at an exercise, and let me review be the feedback they need. +As the course is pass/fail, I don't think it's important to distinguish between decent, good, and excellent work. So I tend to give full credit for a good-faith effort at an exercise, and let the review be the feedback they need. Reviewing Rubric ================ @@ -38,16 +38,18 @@ Is the code Pythonic? - looping through a sequence rather than indexing - EAFTP Exception handling - - ... + - iterating rather that making copies + - no mutables in default arguments Style Issues: ------------- - PEP 8 compliance - variable names: + - meaningful names -- not "key", "value", "foo" - one letter names only for index variables etc. - - verbs for functions. methods + - verbs for functions and methods - nouns for objects - - plural for sequences of objects. - - ... + - plural for sequences of objects / singular for single items + diff --git a/source/for_instructors/github.rst b/source/for_instructors/github.rst index b46fde41..b5bc4bbb 100644 --- a/source/for_instructors/github.rst +++ b/source/for_instructors/github.rst @@ -9,6 +9,36 @@ gitHub provides a good platform for code review, and it also provides the studen The class repo ============== +There is a gitHub org for the class repos here: + +https://github.com/UWPCE-PythonCert-ClassRepos + +And a template here: + +https://github.com/UWPCE-PythonCert-ClassRepos/ClassRepoTemplate + +Before the class starts, the instructor should make a ocpy of that TEmplate repo, and crate a new one labeled something like: + +``Fall2018-PY210A`` or ``Sp2018-Online`` + +Make sure your students know which repo to fork and use for your class -- probably by updating Canvas or EdX for your instance. + +Class Repo Structure +-------------------- + +The class repo has three directories in it:: + + students + examples + solutions + +The ``students`` dir is where each student should create a directory for their own work. They can then submit a gitHub PR to add their dir to the class repo, and to have their work reviewed when it is ready to be turned in. + +The ``examples`` dir can be used to put examples of various sorts -- either from the main PythonCertDevel repo -- or anything you work up for class. It is a good place to put code developed during class as well, so everyone can have access in the future. + +The ``solutions`` dir can be used to post solutions to the exercises. How and when (and if) you do that is up the the individual instructor, and may be different depending on the modality. i.e. for self-paced inline there may no appropriate time to post solutions. + + Example Code ============ diff --git a/source/for_instructors/solutions.rst b/source/for_instructors/solutions.rst index b98b888f..86a15b64 100644 --- a/source/for_instructors/solutions.rst +++ b/source/for_instructors/solutions.rst @@ -4,7 +4,7 @@ Exercise Solutions A complete set of solutions is available in the PythonCertDevel Repo. -I've found that students really like to see clean, nicely written solutions to the exercises -- when I started teaching, I would go over the students' code in class, and correct and critique it. But I got a lot of requests for how **I** would have written the code. So I ended up developing a full set of solutions to share with the class. +I've found that students really like to see clean, nicely written solutions to the exercises -- when I started teaching, I would go over the students' code in class, and correct and critique it. But I got a lot of requests for how *I* would have written the code. So I ended up developing a full set of solutions to share with the class. When / How to Share Solutions ============================= From d92444a119ece763cd960e723a92f05f00544f05 Mon Sep 17 00:00:00 2001 From: Chris Barker <PythonCHB@gmail.com> Date: Fri, 28 Dec 2018 16:27:08 -0800 Subject: [PATCH 43/87] fleshing out the OO mailroom exercise a bit more. --- source/exercises/mailroom-oo.rst | 93 +++++++++++++++++++------------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/source/exercises/mailroom-oo.rst b/source/exercises/mailroom-oo.rst index 4cc2b1a5..9df501ad 100644 --- a/source/exercises/mailroom-oo.rst +++ b/source/exercises/mailroom-oo.rst @@ -24,13 +24,13 @@ would start to get a bit unwieldy and hard to maintain. So it's a pretty good ca As you design appropriate classes, keep in mind these three guidelines for good code structure: -1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everyting that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using. +1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everything that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using. 2) Separation of Concerns: The user-interaction code should be cleanly separated from the data handling code. https://en.wikipedia.org/wiki/Separation_of_concerns - There should be no ``input`` functions in the classes that hold the data! + 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. 3) As always: **DRY** (Don't Repeat Yourself): Anywhere you see repeated code; refactor it! @@ -48,23 +48,24 @@ One of the hardest parts of OO design (particularly in Python) is to know how "l There are no hard and fast rules, but here are some guidelines: -For this assignment it's OK to go with simple tuples. However, in order for the code to be more flexible in the future, for example, if new "fields" were added to the donor object, it's probably better to use a more structured data type, so you don't have to worry about changing the order or number of fields. +For this simple problem, simple tuples could work fine. However, in order for the code to be more flexible in the future: for example, if new "fields" were added to the donor object, it's probably better to use a more structured data type, so you don't have to worry about changing the order or number of fields. -So now you have to think about using a dict or class. Again for flexibility, I think 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. +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["first_name"]`` vs ``data.first_name``. Which one is more readable? Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDE to provide auto-completion, which reduces number of bugs and helps to produce code faster. +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. Below are more detailed suggestions on breaking down your existing code into multiple modules that will be part of a single mailroom program. -Modules vs. Classes +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. Optionally, you could organize your code into modules, which helps to keep code organized and re-usable. -What is a module? A module is a python file that can be imported in other files. +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: @@ -72,28 +73,25 @@ Here is an example file structure for ``mailroom_oo`` package that contains 3 mo .. code-block:: bash └── mailroom_oo - ├── __init__.py ├── cli_main.py ├── donor_models.py └── test_mailroom_oo.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. -Note that ``__init__.py`` is an empty file that tells Python that this directory should be treated as a package so that you can import modules. - -Donor Class -........... +``Donor`` Class +............... **Class responsible for donor data encapsulation** -This class will hold all the information about a single donor, and have attributes, properties, and methods to provide access to the donor-specific information that is needed. -Remember, if you are writing code that only accesses information about a single donor, then it should most likely live in this class. +This class will hold all the information about a *single* donor, and have attributes, properties, and methods to provide access to the donor-specific information that is needed. +Any code that only accesses information about a single donor should be part of this class. + -DonorCollection Class -..................... +``DonorCollection`` Class +......................... **Class responsible for donor collection data encapsulation** @@ -101,24 +99,35 @@ This class will hold all of the donor objects, as well as methods to add a new d Your class for the collection of donors will also hold the code that generates reports about multiple donors. +In short: if the functionality involves more than one donor -- it belongs in this class. + +Note that the ``DonorCollection`` class should be holding, and working with, ``Donor`` objects -- it should NOT work directly with a list of donations, etc. + +**Examples:** + +Generating a thank you letter to a donor only requires knowledge of that one donor -- so that code belongs in the ``Donor`` class. + +Generating a report about all the donors requires knowledge of all the donors, so that code belongs in the ``DonorCollection`` class. + 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``. 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. +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``. +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? -* main "switch dictionary" to map user selection to the program features; in general, you will have a method for each of the mailroom functions. -* ``input`` function calls to gather user input -* ``print`` statements to print to console +A set of user-interaction menu functions -- to handle each of the modes of the program. -.. note:: Technically, console print statements don't belong in your data classes. However, for some features of this program, such as "send letters," we are simply printing instead of "sending," so it is ok for this feature to reside in the data class. But do keep integration of console print statements with data classes to a minimum. Ideally, the data class methods return a string, and the UI code does the printing. +These will include ``input()`` function calls to gather user input, and ``print()`` functions to print results to console. +.. note:: Console print statements don't belong in your data classes. So for features such as "send letters," in which we are simply printing instead of "sending", the data class methods should return a string, and let the UI code do the printing. This will mean there may be very simple functions in the UI code that simply call a method and print the results -- but that does keep flexibility for other ways of handling user interaction. -Why is this separation of data and method so important? +.. rubric:: Why is this separation of data and method so important? The idea here is that we should be able to fairly easy replace this CLI program with a different type of interface, such as a GUI (Graphical User Interface), without having to make any changes to our data classes. @@ -130,10 +139,21 @@ Test-Driven Development At this point we have done a great job refactoring the more complex code out of data-holding classes and we are left with simple classes that are more straightforward to unit test. As you build your classes, update the tests you already have to the logic code to the new API. Ideally, update the tests first, then the code. -The ``Donor`` and ``DonorCollection`` classes should now have close to 100 percent code coverage. +The ``Donor`` and ``DonorCollection`` classes should now have 100 percent code coverage, which means that every line of code in your ``donor_models.py`` file will be run at least once when your tests are run. For the moment, don't worry about testing most of the command line interface code. That requires simulating user input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved. +.. rubric:: refactoring non-OO code + +In this case, you already have working code without an OO structure. You should be able to re-use a fair bit of your existing code. +However, you should still start with the OO structure/design. +That is, rather than take a non-OO function and try to make it a method of a class, decide what method you need, and what it's API should be, and then see if you have code you can use to fill in that function. + +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). + + Exercise Guidelines =================== @@ -156,10 +176,9 @@ Functionality - User getting capitalization or spacing or ??? wrong with a name. - - maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name. + - Maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name. -Code structure: ---------------- +.. rubric:: Code structure * Classes should have clear purpose and encapsulation: only the code within a class should know exactly how the data are stored, for instance. @@ -175,7 +194,7 @@ Code structure: - The logic code should be 100% testable (without mocking input() or any fancy stuff like that) -Testing: +.. rubric:: Testing * All logic code should be tested. @@ -190,21 +209,23 @@ Testing: - you'll probably find this easier with a well structured OO approach -- that is, you can test an individual donor functionality without knowing about the rest of the donors. -Now the "soft" stuff: ---------------------- +.. rubric:: The "soft" stuff: -* Style: conform to PEP8! (or another consistent style) +Style: + - conform to PEP8! (or another consistent style) - - You can use 95 or some other reasonable number for line length + - You can use 95 or some other reasonable number for line length -* docstrings: functions and classes should all have good docstrings. They can very very short if the function does something simple. +Docstrings: + Functions and classes should all have good docstrings. They can be very short if the function does something simple. -* Naming: all classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed. +Naming: + All classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed. -Extra ideas: +Extra Ideas: ------------ -In case, you are bored -- what features can you add? +In case you are bored -- what features can you add? * How about an html report using your html_render code? From 8b1e84a9593f7c31b9032562d90b36c7628e1187 Mon Sep 17 00:00:00 2001 From: Chris Barker <PythonCHB@gmail.com> Date: Tue, 8 Jan 2019 19:31:22 -0800 Subject: [PATCH 44/87] a bit more clarification in the Subclassing page. --- source/modules/SubclassingAndInheritance.rst | 119 +++++++++++++++---- 1 file changed, 95 insertions(+), 24 deletions(-) diff --git a/source/modules/SubclassingAndInheritance.rst b/source/modules/SubclassingAndInheritance.rst index edc7d154..220b19a9 100644 --- a/source/modules/SubclassingAndInheritance.rst +++ b/source/modules/SubclassingAndInheritance.rst @@ -22,11 +22,11 @@ The resulting classes are known as derived classes or subclasses. Subclassing ----------- -A subclass "inherits" all the attributes (methods, etc) of the parent class. +A subclass "inherits" all the attributes (methods, etc) of the parent class. This means that a subclass will have everything that its "parents" have. -You can then change ("override") some or all of the attributes to change the behavior. +You can then change ("override") some or all of the attributes to change the behavior. You can also add new attributes to extend the behavior. -You can also add new attributes to extend the behavior. You create a subclass by passing the superclass to the class statement. +You create a subclass by passing the superclass to the ``class`` statement. The simplest subclass in Python: @@ -35,33 +35,74 @@ The simplest subclass in Python: class A_subclass(The_superclass): pass -``A_subclass`` now has exactly the same behavior as ``The_superclass`` +``A_subclass`` now has exactly the same behavior as ``The_superclass`` -- all the same attributes and methods. Overriding attributes --------------------- Overriding is as simple as creating a new attribute with the same name: -.. code-block:: python +.. code-block:: ipython - class Circle: - color = "red" + In [1]: class Circle: + ...: color = "red" + ...: - ... +We now have a class with a class attribute, ``color``, with the value: "red". All instances of ``Circle`` will be red: - class NewCircle(Circle): - color = "blue" - >>> nc = NewCircle - >>> print(nc.color) - blue +.. code-block:: ipython + + In [2]: c = Circle() + + In [3]: c.color + Out[3]: 'red' + +If we create a subclass of Circle, and set that same class attribute: + +.. code-block:: ipython + + In [4]: class NewCircle(Circle): + ...: color = "blue" + ...: + + In [5]: nc = NewCircle() + + In [6]: nc.color + Out[6]: 'blue' + +We now have a class that is all the same, except that its instances have the color blue. + +Note that any methods that refer to that attribute, will get the new value, even if the methods themselves have not changed: + +.. code-block:: ipython + + In [10]: class Circle: + ...: color = "red" + ...: + ...: def describe(self): + ...: return f"I am a {self.color} circle" + ...: + + In [11]: class NewCircle(Circle): + ...: color = "blue" + ...: + + In [12]: c = Circle() + In [13]: c.describe() + Out[13]: 'I am a red circle' -all the ``self`` instances will have the new attribute. + In [14]: nc = NewCircle() + + In [15]: nc.describe() + Out[15]: 'I am a blue circle' + +Note that this is *why* self is passed in to every method -- when you write the method, you don't know exactly what class ``self`` will be -- it is an instance of the class at the time the method is called. Overriding methods ------------------ -Same thing, but with methods (remember, a method *is* an attribute in Python) +Overriding methods is exactly the same thing, but with methods (remember, a method *is* an attribute in Python -- one that happens to be a function) .. code-block:: python @@ -79,7 +120,7 @@ Same thing, but with methods (remember, a method *is* an attribute in Python) self.diameter = self.diameter * math.sqrt(2) -all the instances will have the new method. +all the instances of the new class will have the new method -- similar, but different, behavior. Note that both these methods are requiring that the class instance has a ``diameter`` attribute. **Here's a program design suggestion:** @@ -88,12 +129,15 @@ all the instances will have the new method. If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand. If you violate this rule, your code will collapse like (sorry) a house of cards. +-- from *Think Python* + + Overriding ``__init__`` ----------------------- ``__init__`` is a common method to override. -You often need to call the super class ``__init__`` as well. +You often need to call the super class ``__init__`` as well, so that any initialization required is performed: .. code-block:: python @@ -110,11 +154,32 @@ You often need to call the super class ``__init__`` as well. Exception to: "don't change the method signature" rule. +Often when you override ``__init__``, the new class may take an extra parameter or two. In this case, you will want to keep the signature as similar as possible, and cleanly define what is part of the subclass. A common idiom in this case is this: + +.. code-block:: python + + class A_Subclass(A_Superclass): + + def __init__(self, param1, param2, *args, **kwargs): + self.param1 = param1 + self.init_something(param2) + super().__init__(*args, **kwargs) + +That is: -Using the superclasses' methods + * Put the extra parameters in the beginning of the list -- usually as required positional parameters. + + * Accept ``*args`` and ``**kwargs`` + + * Pass everything else on to the superclass' __init__ + +Using ``*args`` and ``**kwargs`` is a way to make it clear that the rest is simply the signature of the superclass. It is also flexible if the superclass (or others up in the hierarchy) changes -- it could completely change its signature, and this subclass would still work. + + +Using the superclass' methods ------------------------------- -You can also call the superclass' other methods: +In a subclass, you can access everything in the superclass: all attributes and other methods: .. code-block:: python @@ -130,13 +195,13 @@ You can also call the superclass' other methods: return Circle.get_area(self, self.radius*2) -Note that there is nothing special about ``__init__`` except that it gets called automatically when you instantiate an instance. Otherwise, it is the same as any other method -- it gets ``self`` as the first argument, it can or can not call the superclasses methods, etc. +Note that there is nothing special about ``__init__`` except that it gets called automatically when you instantiate an instance. Otherwise, it is the same as any other method -- it gets ``self`` as the first argument, it can or can not call the superclass' methods, etc. "Favor Object Composition Over Class Inheritance" ------------------------------------------------- -That is a quotation from the "Design Patterns" book -- kind of one of the gospels of OO programming. +That is a quotation from the "Design Patterns" book -- one of the gospels of OO programming. But what does it mean? @@ -178,6 +243,8 @@ You only want to subclass list if your class could be used anywhere a list can b Attribute Resolution Order -------------------------- +Once there is a potentially large hierarchy of subclasses, how do you know which one will be used? + When you access an attribute: ``an_instance.something`` @@ -192,6 +259,8 @@ Python looks for it in this order: It can get more complicated, particularly when there are multiple superclasses (multiple inheritance), but when there is a simple inheritance structure (the usual case) -- it's fairly straightforward. +This is often referred to as "method resolution order" (MRO), because it's more complicated with methods, and in some languages, methods and attributes are more distinct than in Python. In Python, it can be thought of as "name resolution" -- everything in Python is about names and namespaces. + If you want to know more of the gory details -- here's some reading: https://www.python.org/download/releases/2.3/mro/ @@ -222,7 +291,7 @@ That's about it -- really! Type-Based Dispatch ------------------- -You'll see code that looks like this: +Occasionally you'll see code that looks like this: .. code-block:: python @@ -231,12 +300,12 @@ You'll see code that looks like this: else: Do_something_else -When it's called for, Python provides these utilties: +When it's called for, Python provides these utilities: * ``isinstance()`` * ``issubclass()`` -But it is very rarely called for! Between Duck typing, polymorphism, and EAFP, you rarely need to check for type directly. +But it is *very* rarely called for! Between Duck Typing, polymorphism, and EAFP, you rarely need to check for type directly. Wrap Up ------- @@ -254,3 +323,5 @@ OO can be a very powerful approach, but don't be a slave to what OO is *supposed Let OO work for you, not *create* work for you. +And the biggest way to do that is to support code re-use. + From c8c7b7b74a4959f677524b6cca2566611ead9573 Mon Sep 17 00:00:00 2001 From: Chris Barker <PythonCHB@gmail.com> Date: Tue, 8 Jan 2019 19:37:46 -0800 Subject: [PATCH 45/87] more .gitignore for output files --- source/solutions/Lesson07/.gitignore | 2 ++ source/solutions/Lesson07/sample_output.html | 27 +++++++++++++++++++ .../solutions/Lesson09/mailroom_oo/.gitignore | 2 ++ 3 files changed, 31 insertions(+) create mode 100644 source/solutions/Lesson07/.gitignore create mode 100644 source/solutions/Lesson07/sample_output.html create mode 100644 source/solutions/Lesson09/mailroom_oo/.gitignore diff --git a/source/solutions/Lesson07/.gitignore b/source/solutions/Lesson07/.gitignore new file mode 100644 index 00000000..dfec25c4 --- /dev/null +++ b/source/solutions/Lesson07/.gitignore @@ -0,0 +1,2 @@ +test_html_output?.html + diff --git a/source/solutions/Lesson07/sample_output.html b/source/solutions/Lesson07/sample_output.html new file mode 100644 index 00000000..9c2e675d --- /dev/null +++ b/source/solutions/Lesson07/sample_output.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8" /> + <title>Python Class Sample page + + +

    Python Class - Html rendering example

    +

    + Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

    +
    +
    + + \ No newline at end of file diff --git a/source/solutions/Lesson09/mailroom_oo/.gitignore b/source/solutions/Lesson09/mailroom_oo/.gitignore new file mode 100644 index 00000000..960fe795 --- /dev/null +++ b/source/solutions/Lesson09/mailroom_oo/.gitignore @@ -0,0 +1,2 @@ +*.txt + From 22ce3a29284dcc796ddde4bf501c1cf130fd8b22 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Sat, 19 Jan 2019 22:06:20 -0800 Subject: [PATCH 46/87] Minor fixes in IteratorsAndGenerators.rst --- source/modules/IteratorsAndGenerators.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/source/modules/IteratorsAndGenerators.rst b/source/modules/IteratorsAndGenerators.rst index 61b0cb2d..f0298ea0 100644 --- a/source/modules/IteratorsAndGenerators.rst +++ b/source/modules/IteratorsAndGenerators.rst @@ -115,7 +115,7 @@ https://docs.python.org/3/library/stdtypes.html#iterator-types Iterables --------- -To make an object iterable, you simply have to implement the __getitem__ method. +To make an object iterable, you simply have to implement the ``__getitem__`` method. .. code-block:: python @@ -131,8 +131,8 @@ To make an object iterable, you simply have to implement the __getitem__ method. How do you get the iterator object from an "iterable"? -The iter function will make any iterable an iterator. It first looks for the __iter__ -method, and if none is found, uses get_item to create the iterator. +The ``iter`` function will make any iterable an iterator. It first looks for the ``__iter__`` +method, and if none is found, uses ``__getitem__`` to create the iterator. The ``iter()`` function: @@ -199,7 +199,7 @@ It works, and is fairly efficient, but what about: for triple in zip(words[:-2], words[1:-1], words[2:]): -zip() returns an iterable -- it does not build up the whole list. +``zip()`` returns an iterable -- it does not build up the whole list. So this is quite efficient. but we are still slicing: ([1:]), which produces a copy -- so we are creating three copies of @@ -323,7 +323,7 @@ An "iterator" is anything that conforms to the "iterator protocol": - Has a ``__next__()`` method that returns objects. - Raises ``StopIteration`` when their are no more objects to be returned. - Has a ``__iter__()`` method that returns an iterator -- usually itself. - - sometimes the __iter__() method re-sets the iteration... + - sometimes the ``__iter__()`` method re-sets the iteration... https://docs.python.org/3/glossary.html#term-iterator @@ -430,7 +430,7 @@ Really just a shorthand for an iterator class that does the book keeping for you To master yield, you must understand that when you call the function, the code you have written in the function body does not run. The function only returns the generator object. The actual code in the function is run -when next() is called on the generator itself. +when ``next()`` is called on the generator itself. And note that each time you call the "generator function" you get a new instance of a generator object that saves state separately from other instances. @@ -473,7 +473,7 @@ Note: A generator function can also be a method in a class In fact, this is a nice way to provide different ways to iterate over the data in a class in multiple ways. -This is done by the dict protocol with dict.keys() and dict.values(). +This is done by the dict protocol with ``dict.keys()`` and ``dict.values()``. More about iterators and generators: @@ -509,11 +509,11 @@ Keep in mind -- if all you need to do with the results is loop over it Other uses for ``yield`` ------------------------ -The yield keyword and generator functions were designed with classic "generators" in mind. +The ``yield`` keyword and generator functions were designed with classic "generators" in mind. That is -- objects that generate values on the fly. -But, as we alluded to earlier, yield can be used for other things as well. +But, as we alluded to earlier, ``yield`` can be used for other things as well. Anytime you want to return a value, and then hold state until later, ``yield`` can be used. @@ -530,7 +530,7 @@ Anytime you want to return a value, and then hold state until later, # do the teardown something_with(value) -In this case, the yield isn't in any sort of loop or anything. +In this case, the ``yield`` isn't in any sort of loop or anything. It will only get run once. But the generator will maintain state, so the value can be used after the yield to do the teardown. From 97a8a01cda193e3366d20af2728183103261d860 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Sun, 20 Jan 2019 11:46:16 -0800 Subject: [PATCH 47/87] Minor formatting fixes in Closures.rst --- source/modules/Closures.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/source/modules/Closures.rst b/source/modules/Closures.rst index 74ef6359..2e15e161 100644 --- a/source/modules/Closures.rst +++ b/source/modules/Closures.rst @@ -162,9 +162,9 @@ So there are multiple scopes in play at any point -- the local scope, and all th this is in outer this is in inner -Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. name1 is only defined in the global scope, so that one is found. +Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. ``name1`` is only defined in the global scope, so that one is found. -The global keyword +The ``global`` keyword ------------------ global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope: @@ -192,7 +192,7 @@ global names can be accessed from within functions, but not if that same name is The problem here is that ``x += 5`` is the same as ``x = x + 5``, so it is creating a local name, but it can't be incremented, because it hasn't had a value set yet. -The global keyword tells python that you want to use the global name, rather than create a new, local name: +The ``global`` keyword tells python that you want to use the global name, rather than create a new, local name: .. code-block:: ipython @@ -207,9 +207,9 @@ The global keyword tells python that you want to use the global name, rather tha In [42]: x Out[42]: 10 -**NOTE:** the use of global is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code! +**NOTE:** The use of ``global`` is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code! -nonlocal keyword +``nonlocal`` keyword ---------------- The other limitation with ``global`` is that there is only one global namespace, so what if you are in a nested scope, and want to get at the value outside the current scope, but not all the way up at the global scope: @@ -229,7 +229,7 @@ That's not going to work as the inner x hasn't been initialized: ``UnboundLocalError: local variable 'x' referenced before assignment`` -But if we use ``global``, we'll get the global x: +But if we use ``global``, we'll get the global ``x``: .. code-block:: ipython @@ -257,7 +257,7 @@ But if we use ``global``, we'll get the global x: In [9]: x Out[9]: 15 -so the global x is getting changed, but not the one in the outer scope. +so the global ``x`` is getting changed, but not the one in the ``outer`` scope. This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``. What it means is that the name should be looked for outside the local scope, but only as far as you need to go to find it: @@ -275,9 +275,9 @@ This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``. In [11]: outer() x in outer is: 15 -So the x in the outer function scope is the one being changed. +So the ``x`` in the ``outer`` function scope is the one being changed. -While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is referring to a name in a scope that is closely defined like the above example. In fact, nonlocal will not go all the way up to the global scope to find a name: +While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is referring to a name in a scope that is closely defined like the above example. In fact, ``nonlocal`` will not go all the way up to the global scope to find a name: .. code-block:: ipython @@ -404,9 +404,9 @@ But what happens if we call ``counter()`` multiple times? In [44]: c2() Out[44]: 11 -So each time ``counter()`` is called, a new ``incr`` function is created. But also, a new namespace is created, that holds the count name. So the new ``incr`` function is holding a reference to that new count name. +So each time ``counter()`` is called, a new ``incr`` function is created. But also, a new namespace is created, that holds the ``count`` name. So the new ``incr`` function is holding a reference to that new ``count`` name. -This is what makes in a "closure" -- it carries with it the scope in which it was created. +This is what makes it a "closure" -- it carries with it the scope in which it was created. The returned ``incr`` function is a "curried" function -- a function with some parameters pre-specified. @@ -451,7 +451,7 @@ So to compute the scale, I could pass that half-life in each time I called the f def scale(time, half_life): return 0.5 ** (time / (half_life)) -But this is a bit klunky -- I need to keep passing that half_life around, even though it isn't changing. And there are places, like ``map`` that require a function that takes only one argument! +But this is a bit klunky -- I need to keep passing that ``half_life`` around, even though it isn't changing. And there are places, like ``map`` that require a function that takes only one argument! What if I could create a function, on the fly, that had a particular half-life "baked in"? @@ -527,7 +527,7 @@ https://docs.python.org/3.5/library/functools.html Creating a curried function turns out to be common enough that the ``functools.partial`` function provides an optimized way to do it: -What functools.partial does is: +What ``functools.partial`` does is: * Makes a new version of a function with one or more arguments already filled in. * The new version of a function documents itself. From 37b13e2d9e5bd654a781d0cc443e6534c44e4796 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Tue, 22 Jan 2019 20:05:24 -0800 Subject: [PATCH 48/87] Minor formatting fixes in Decorators.rst --- source/modules/Decorators.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/source/modules/Decorators.rst b/source/modules/Decorators.rst index 60299383..4234f114 100644 --- a/source/modules/Decorators.rst +++ b/source/modules/Decorators.rst @@ -186,7 +186,7 @@ And we can apply it with the regular calling and rebinding syntax: In [6]: other_func Out[6]: .inner> -Notice that other_func is now the "inner" function, which lives in the "my_decorator" namespace... +Notice that ``other_func`` is now the "inner" function, which lives in the "my_decorator" namespace... And this is the same with the decoration syntax: @@ -203,7 +203,7 @@ And this is the same with the decoration syntax: In [9]: other_func Out[9]: .inner> -Notice that other_func is the "inner" function here as well. +Notice that ``other_func`` is the "inner" function here as well. Decorators have the power to replace the decorated function with a different one! @@ -411,7 +411,7 @@ The ``classmethod()`` builtin can do the same thing: property() ----------- -Remember the property() built in? +Remember the ``property()`` builtin? Perhaps most commonly, you'll see the ``property()`` builtin used this way. @@ -502,8 +502,8 @@ A decorator that wraps an html `

    ` tag around the output of any decorated func @p_decorate - def get_fullname(first_name, last_name): - return f"{first_name} {last_name}" + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" In [124]: get_fullname('Chris', 'Barker') Out[124]: '

    Chris Barker

    ' @@ -532,8 +532,8 @@ Can you make a version that will wrap any other tag -- specified as a parameter .. code-block:: ipython @add_tag('p') - def get_fullname(first_name, last_name): - return f"{first_name} {last_name}" + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" In [124]: get_fullname('Chris', 'Barker') Out[124]: '

    Chris Barker

    ' @@ -545,21 +545,21 @@ But: .. code-block:: ipython @add_tag('div') - def get_fullname(first_name, last_name): - return f"{first_name} {last_name}" + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" In [124]: get_fullname('Chris', 'Barker') Out[124]: '
    Chris Barker
    ' and you could pass any tag in. -This can be ackomplished either with a closure --nesting antoher level of functions in the decorator, or with a callable class, like the memoize example. Maybe try both, and decide which you like better. +This can be accomplished either with a closure --nesting another level of functions in the decorator, or with a callable class, like the memoize example. Maybe try both, and decide which you like better. Further Reading: ---------------- -*Fluent Python* by Luciano Ramalho, chapter 7. +*Fluent Python* by Luciano Ramalho, Chapter 7. Another good overview: From 4ba89106ed67626af56fc04a56d93f1b5b6fddeb Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Tue, 22 Jan 2019 20:33:52 -0800 Subject: [PATCH 49/87] Minor fixes in ContextManager.rst --- .gitignore | 6 +++++- source/modules/ContextManagers.rst | 14 ++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 053ae06c..e6c77b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ instance/ # Sphinx documentation docs/_build/ +source/_build/ # PyBuilder target/ @@ -104,4 +105,7 @@ ENV/ .mypy_cache/ # emacs -*.*~ \ No newline at end of file +*.*~ + +# vscode +.vscode/ diff --git a/source/modules/ContextManagers.rst b/source/modules/ContextManagers.rst index d0784ba7..60e5d8c0 100644 --- a/source/modules/ContextManagers.rst +++ b/source/modules/ContextManagers.rst @@ -104,9 +104,9 @@ If the resource in questions has a ``.close()`` method, then you can simply use # and here, it will be closed automatically But what if the thing doesn't have a ``close()`` method, or you're creating -the thing and it shouldn't have a close() method? +the thing and it shouldn't have a ``close()`` method? -(full confession: urlib.request was not a context manager in py2 -- but it is in py3 -- but the issue still comes up with third-party packages and your own code!) +(full confession: ``urlib.request`` was not a context manager in py2 -- but it is in py3 -- but the issue still comes up with third-party packages and your own code!) Do It Yourself -------------- @@ -158,12 +158,10 @@ clarify the order in which things happen: .. code-block:: ipython - In [2]: %paste - In [46]: with Context(True) as foo: - ....: print('This is in the context') - ....: raise RuntimeError('this is the error message') - - ## -- End pasted text -- + In [46]: with Context(True) as foo: + ....: print('This is in the context') + ....: raise RuntimeError('this is the error message') + ....: __init__(True) __enter__() This is in the context From a455b0993c40ed0a9e921dacf11c2ec40abf5cb7 Mon Sep 17 00:00:00 2001 From: Hosung Song Date: Tue, 22 Jan 2019 20:42:42 -0800 Subject: [PATCH 50/87] Minor fixes in context-managers-exercise.rst --- source/exercises/context-managers-exercise.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/exercises/context-managers-exercise.rst b/source/exercises/context-managers-exercise.rst index 2c826466..67e2b505 100644 --- a/source/exercises/context-managers-exercise.rst +++ b/source/exercises/context-managers-exercise.rst @@ -22,7 +22,7 @@ run all the code inside the context: ...: This code took 0.206805 seconds -NOTE: the time module has what you need: +NOTE: the ``time`` module has what you need: .. code-block:: python @@ -30,7 +30,7 @@ NOTE: the time module has what you need: start = time.clock() # some code here - elapsed = time.clock() = start + elapsed = time.clock() - start ``time.clock()`` returns the number of seconds that this process has been running. You can also use ``time.time()``, which gives the "wall time", rather than the process time. ``time()`` will vary more depending on how busy the system is. But you may want to use it if you want to measure how long it takes to download something, for instance. @@ -38,13 +38,13 @@ Extra Credit ------------ Allow the ``Timer`` context manager to take a file-like -object as an argument (the default should be sys.stdout). The results of the +object as an argument (the default should be ``sys.stdout``). The results of the timing should be printed to the file-like object. You could also pass in a name for this particular context, so the message in the file-like object is labeled -- kind of a poor man's logging system. Extra Extra Credit ------------------ -Implement this a a generator, wrapped by the: +Implement this as a generator, wrapped by the: ``contextlib.contextmanager`` @@ -101,7 +101,7 @@ tests fail when an assert fails: assert some_expression, "a message" -you get a failure when some_expression evaluates as false. +you get a failure when ``some_expression`` evaluates as false. This is more-or-less the same as this code: @@ -110,7 +110,7 @@ This is more-or-less the same as this code: if some_expression: raise AssertionError("a message") -The reason it exists is not so much to save a bit of typing (though that's nice), but that assertions are designed for tests, ans thus can be turned of for an entire python process -- and, indeed are turned off when you turn on optimization. +The reason it exists is not so much to save a bit of typing (though that's nice), but that assertions are designed for tests, and thus can be turned off for an entire python process -- and, indeed are turned off when you turn on optimization. So in your context manager, you can raise an AssertionError, or force one with an assert: From f1b2c1c7a4cb64ff24b558b131bcab1d8e8ae4db Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak Date: Tue, 29 Jan 2019 17:03:19 -0800 Subject: [PATCH 51/87] Update string_formatting.rst (#181) Update domain for string format cookbook, a redirect is already in place, but the old domain will be expiring --- source/exercises/string_formatting.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/exercises/string_formatting.rst b/source/exercises/string_formatting.rst index d480f3dd..9879cb58 100644 --- a/source/exercises/string_formatting.rst +++ b/source/exercises/string_formatting.rst @@ -222,7 +222,7 @@ https://pyformat.info/ A nice "Cookbook": -https://mkaz.tech/python-string-format.html +https://mkaz.blog/code/python-string-format-cookbook/ Submitting Your Work From 33e0ccc7a1d7b48726c8409a01029ebec2a91554 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 18 Feb 2019 09:31:01 -0800 Subject: [PATCH 52/87] some formatting and copy editing. --- source/modules/Closures.rst | 84 +++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/source/modules/Closures.rst b/source/modules/Closures.rst index 2e15e161..17e70703 100644 --- a/source/modules/Closures.rst +++ b/source/modules/Closures.rst @@ -14,14 +14,16 @@ In order to get a handle on all this, it's important to understand variable scop "Scope" is the word for where the names in your code are accessible. Another word for a scope is namespace. -global ------- +Global Scope +------------ -The simplest is the global scope. This is where all the names defined right in your code file are (or in the interpreter). +The simplest is the global scope. This is where all the names defined right in your code file (module) are. When running in an interactavive interpreter, it is in the global namespace as well. You can get the global namespace with the ``globals()`` function, but ... -The Python interpreter defines a handful of names when it starts up, and iPython defines a whole bunch more. Most of those start with an underscore, so you can filter them out for a more reasonable result: +The Python interpreter defines a handful of names when it starts up, and iPython defines a whole bunch more. +Recall that a convention in Python is that names that start with an underscore are "special" in some way -- double underscore names have a special meaning to Python, and single underscore names are considered "private". +Most of the extra names defined by the Python interpreter or iPython that are designed for internal use start with an underscore. These can really "clutter up" the namespace, but they can be filtered out for a more reasonable result: .. code-block:: python @@ -31,7 +33,7 @@ The Python interpreter defines a handful of names when it starts up, and iPython if not (name.startswith("_") or name in ipy_names): print(name) -And run that in a raw interpreter: +Try running that in a newly started interpreter: .. code-block:: ipython @@ -47,6 +49,8 @@ And run that in a raw interpreter: The only name left is "print_globals" itself -- created when we defined the function. +.. note:: Try running ``globals()`` by itself to see all the cruft iPython adds. Also note that ``globals`` returns not just the names, but a dictionary, where the keys are the names, and the items are the values bound to those names. + If we add a name or two, they show up in the global scope: .. code-block:: ipython @@ -60,7 +64,7 @@ If we add a name or two, they show up in the global scope: x this -names are created by assignment, and by ``def`` and ``class`` statements. we already saw a ``def``. +names are created by assignment, and by ``def`` and ``class`` statements. We already saw a ``def``, here is a ``class`` definition. .. code-block:: ipython @@ -75,14 +79,14 @@ names are created by assignment, and by ``def`` and ``class`` statements. we alr test TestClass -local ------ +Local Scope +----------- So that's the global scope -- what creates a new scope? A new, "local" scope is created by a function or class definition: -And there is a built-in function to get the names in the local scope, too, so we can use it to show us the names in a function's local namespace. There isn't a lot of cruft in the local namespace, so we don't need a special function to print it. +There is a built-in function to get the names in the local scope, too, so we can use it to show us the names in a function's local namespace. There isn't a lot of cruft in the local namespace, so we don't need a special function to print it. Note that ``locals()`` and ``globals()`` returns a dict of the names and the objects they are bound to, so we can print the keys to get the names: @@ -136,10 +140,32 @@ Turns out that this holds true for functions defined within functions also: outer scope: dict_keys(['inner', 'y', 'x']) inner scope: dict_keys(['z', 'w']) +Function Parameters +------------------- + +The other way you can define names in a function's local namespace is with function parameters: + + +.. code-block:: ipython + + In [14]: def fun_with_parameters(a, b=0): + ...: print("local names are:", locals().keys()) + ...: + ...: + + In [15]: fun_with_parameters(4) + local names are: dict_keys(['a', 'b']) + +Notice that no other names have been defined in the function, but both of the parameters (positional and keyword) are local names. + + Finding Names ------------- -So there are multiple scopes in play at any point -- the local scope, and all the surrounding scopes. When you use a name, python checks in the local scope first, then moves out one by one until it finds the name. So if you define a new name inside a function, it "overrides" the name in any of the outer scopes. But the outer one will be found. +At any point, there are multiple scopes in play: the local scope, and all the surrounding scopes. +When you use a name, python checks in the local scope first, then moves out one by one until it finds the name. +If you define a new name inside a function, it "overrides" the name in any of the outer scopes. +But any names not defined in an inner scope will be found by looking in the enclosing scopes. .. code-block:: ipython @@ -165,9 +191,9 @@ So there are multiple scopes in play at any point -- the local scope, and all th Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. ``name1`` is only defined in the global scope, so that one is found. The ``global`` keyword ------------------- +---------------------- -global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope: +Global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope: .. code-block:: ipython @@ -210,7 +236,7 @@ The ``global`` keyword tells python that you want to use the global name, rather **NOTE:** The use of ``global`` is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code! ``nonlocal`` keyword ----------------- +-------------------- The other limitation with ``global`` is that there is only one global namespace, so what if you are in a nested scope, and want to get at the value outside the current scope, but not all the way up at the global scope: @@ -257,7 +283,7 @@ But if we use ``global``, we'll get the global ``x``: In [9]: x Out[9]: 15 -so the global ``x`` is getting changed, but not the one in the ``outer`` scope. +This indicates that the global ``x`` is getting changed, but not the one in the ``outer`` scope. This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``. What it means is that the name should be looked for outside the local scope, but only as far as you need to go to find it: @@ -295,7 +321,7 @@ While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is But it will go up multiple levels in nested scopes: -.. code-block: ipython +.. code-block:: ipython In [16]: def outer(): ...: x = 10 @@ -311,20 +337,6 @@ But it will go up multiple levels in nested scopes: In [17]: outer() x in outer is: 20 -function parameters -------------------- - -A side note: function parameters are in a function's local scope, just as though they were created there: - -.. code-block:: ipython - - In [28]: def fun(x, y, z): - ...: print(locals().keys()) - ...: - - In [29]: fun(1,2,3) - dict_keys(['z', 'y', 'x']) - Closures ======== @@ -354,7 +366,7 @@ So after we define a function within a function, we can actually return that fun return count return incr -So this looks a lot like the previous examples, but we are returning the function that was defined inside the function. +This looks a lot like the previous examples, but we are returning the function that was defined inside the function. Which means is can be used elsewhere. What's going on here? ..................... @@ -404,9 +416,9 @@ But what happens if we call ``counter()`` multiple times? In [44]: c2() Out[44]: 11 -So each time ``counter()`` is called, a new ``incr`` function is created. But also, a new namespace is created, that holds the ``count`` name. So the new ``incr`` function is holding a reference to that new ``count`` name. +So each time ``counter()`` is called, a new ``incr`` function is created. Along with the new function, a new namespace is created that holds the ``count`` name. So the new ``incr`` function is holding a reference to that new ``count`` name. -This is what makes it a "closure" -- it carries with it the scope in which it was created. +This is what makes it a "closure" -- it carries with it the scope in which it was created (or enclosed - I guess that's where the word closure comes from). The returned ``incr`` function is a "curried" function -- a function with some parameters pre-specified. @@ -414,8 +426,6 @@ Let's experiment a bit more with these ideas: :download:`play_with_scope.py <../examples/closures_currying/play_with_scope.py>` -.. :download:`capitalize.zip <../examples/packaging/capitalize.zip>` - Currying ======== @@ -423,7 +433,7 @@ Currying `Currying on Wikipedia `_ -The idea behind currying is that you may have a function with a number of parameters, and you want to make a specialized version of that function with a couple parameters pre-set. +The idea behind currying is that you may have a function with a number of parameters, and you want to make a specialized version of that function with a couple of parameters pre-set. Real world Example @@ -442,7 +452,9 @@ So I wanted a function that would compute how much the concentration would reduc The trick is, how much the concentration would be reduced depends on both time and the half life. And for a given material, and given flow conditions in the river, that half life is pre-determined. Once you know the half-life, the scale is given by: -scale = 0.5 ** (time / (half_life)) +.. code-block:: python + + scale = 0.5 ** (time / (half_life)) So to compute the scale, I could pass that half-life in each time I called the function: From fb727c5180db21edd27f83f3be29cfde8f453978 Mon Sep 17 00:00:00 2001 From: "Christopher H.Barker, PhD" Date: Tue, 19 Feb 2019 18:37:16 -0800 Subject: [PATCH 53/87] Remove cigar (#182) * changed the "cigar_party" (potentially offensive) examples to "walnut_party" * added a .gitignore so output files would not be tracked * adding a bit more to the testing module * fleshing out the unit testing module a bit * fix indentation issue --- source/examples/testing/cigar_party.py | 15 - source/examples/testing/test_cigar_party.py | 61 ---- source/examples/testing/test_random_pytest.py | 44 ++- .../examples/testing/test_random_unitest.py | 4 +- source/examples/testing/test_walnut_party.py | 65 +++++ source/examples/testing/walnut_party.py | 15 + source/exercises/unit_testing.rst | 81 ++++-- source/modules/Testing.rst | 272 ++++++++++++++---- .../Lesson01/codingbat/Logic-1/cigar_party.py | 45 --- .../codingbat/Logic-1/walnut_party.py | 49 ++++ source/solutions/Lesson06/.gitignore | 3 + source/solutions/Lesson06/cigar_party.py | 15 - source/solutions/Lesson06/test_cigar_party.py | 64 ----- .../solutions/Lesson06/test_walnut_party.py | 63 ++++ source/solutions/Lesson06/walnut_party.py | 14 + .../codingbat/Logic-1/cigar_party.py | 45 --- 16 files changed, 513 insertions(+), 342 deletions(-) delete mode 100644 source/examples/testing/cigar_party.py delete mode 100644 source/examples/testing/test_cigar_party.py create mode 100644 source/examples/testing/test_walnut_party.py create mode 100644 source/examples/testing/walnut_party.py delete mode 100755 source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py create mode 100755 source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py create mode 100644 source/solutions/Lesson06/.gitignore delete mode 100644 source/solutions/Lesson06/cigar_party.py delete mode 100644 source/solutions/Lesson06/test_cigar_party.py create mode 100644 source/solutions/Lesson06/test_walnut_party.py create mode 100644 source/solutions/Lesson06/walnut_party.py delete mode 100755 source/solutions/codingbat/Logic-1/cigar_party.py diff --git a/source/examples/testing/cigar_party.py b/source/examples/testing/cigar_party.py deleted file mode 100644 index e6863f46..00000000 --- a/source/examples/testing/cigar_party.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -def cigar_party(cigars, is_weekend): - pass diff --git a/source/examples/testing/test_cigar_party.py b/source/examples/testing/test_cigar_party.py deleted file mode 100644 index 260d5f47..00000000 --- a/source/examples/testing/test_cigar_party.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -# you can change this import to test different versions -from cigar_party import cigar_party -# from cigar_party import cigar_party2 as cigar_party -# from cigar_party import cigar_party3 as cigar_party - - -def test_1(): - assert cigar_party(30, False) is False - - -def test_2(): - assert cigar_party(50, False) is True - - -def test_3(): - assert cigar_party(70, True) is True - - -def test_4(): - assert cigar_party(30, True) is False - - -def test_5(): - assert cigar_party(50, True) is True - - -def test_6(): - assert cigar_party(60, False) is True - - -def test_7(): - assert cigar_party(61, False) is False - - -def test_8(): - assert cigar_party(40, False) is True - - -def test_9(): - assert cigar_party(39, False) is False - - -def test_10(): - assert cigar_party(40, True) is True - - -def test_11(): - assert cigar_party(39, True) is False diff --git a/source/examples/testing/test_random_pytest.py b/source/examples/testing/test_random_pytest.py index 6250d1b4..b9a65afd 100644 --- a/source/examples/testing/test_random_pytest.py +++ b/source/examples/testing/test_random_pytest.py @@ -8,32 +8,46 @@ import pytest -seq = list(range(10)) +example_seq = list(range(10)) + + +def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + + +def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq def test_shuffle(): - # make sure the shuffled sequence does not lose any elements + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) random.shuffle(seq) - seq.sort() # IFyou comment this out, it will fail, so you can see output + # seq.sort() # If you comment this out, it will fail, so you can see output print("seq:", seq) # only see output if it fails assert seq == list(range(10)) def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ with pytest.raises(TypeError): random.shuffle((1, 2, 3)) - -def test_choice(): - element = random.choice(seq) - assert (element in seq) - - -def test_sample(): - for element in random.sample(seq, 5): - assert element in seq - - def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ with pytest.raises(ValueError): - random.sample(seq, 20) + random.sample(example_seq, 20) diff --git a/source/examples/testing/test_random_unitest.py b/source/examples/testing/test_random_unitest.py index f825be5b..b8a1b712 100644 --- a/source/examples/testing/test_random_unitest.py +++ b/source/examples/testing/test_random_unitest.py @@ -8,7 +8,9 @@ def setUp(self): self.seq = list(range(10)) def test_shuffle(self): - # make sure the shuffled sequence does not lose any elements + """ + make sure the shuffled sequence does not lose any elements + """ random.shuffle(self.seq) self.seq.sort() self.assertEqual(self.seq, list(range(10))) diff --git a/source/examples/testing/test_walnut_party.py b/source/examples/testing/test_walnut_party.py new file mode 100644 index 00000000..62f226dc --- /dev/null +++ b/source/examples/testing/test_walnut_party.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +""" +test code for the walnut party example + +Adapted from the "coding bat" site: https://codingbat.com/python + +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +# you can change this import to test different versions +from walnut_party import walnut_party +# from walnut_party import walnut_party2 as walnut_party +# from walnut_party import walnut_party3 as walnut_party + + +def test_1(): + assert walnut_party(30, False) is False + + +def test_2(): + assert walnut_party(50, False) is True + + +def test_3(): + assert walnut_party(70, True) is True + + +def test_4(): + assert walnut_party(30, True) is False + + +def test_5(): + assert walnut_party(50, True) is True + + +def test_6(): + assert walnut_party(60, False) is True + + +def test_7(): + assert walnut_party(61, False) is False + + +def test_8(): + assert walnut_party(40, False) is True + + +def test_9(): + assert walnut_party(39, False) is False + + +def test_10(): + assert walnut_party(40, True) is True + + +def test_11(): + assert walnut_party(39, True) is False diff --git a/source/examples/testing/walnut_party.py b/source/examples/testing/walnut_party.py new file mode 100644 index 00000000..9b195b28 --- /dev/null +++ b/source/examples/testing/walnut_party.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +def walnut_party(walnuts, is_weekend): + pass diff --git a/source/exercises/unit_testing.rst b/source/exercises/unit_testing.rst index 72ba4057..b71653dc 100644 --- a/source/exercises/unit_testing.rst +++ b/source/exercises/unit_testing.rst @@ -8,57 +8,89 @@ Preparation ----------- In order to do unit testing, you need a framework in which to write and run your tests. -Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact a kind of unit test. +Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact, a kind of unit test. But as you build larger systems, you'll want a more structured way to write and run your tests. +We will use the pytest testing system for this class. +If you have not already done so -- install pytest like so: + +.. code-block:: bash + + $ python3 -m pip install pytest + +Once this is complete, you should have a ``pytest`` command you can run +at the command line: + +.. code-block:: bash + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/DrMartins, inifile: + plugins: cov-2.6.0 + collected 0 items + + ========================= no tests ran in 0.01 seconds ========================= + +If you already HAVE some tests -- you may see something different! Test Driven Development ----------------------- -Download this module: - -:download:`cigar_party.py ` +Download these files, and save them in your own students directory in the class repo: -(This is the `"cigar party" `_ problem from the codingbat site) +:download:`test_walnut_party.py <../examples/testing/test_walnut_party.py>` -and this test file: +and: -:download:`test_cigar_party.py ` +:download:`walnut_party.py <../examples/testing/walnut_party.py>` -Put them in the same directory, and make that directory your working directory. +(This is the adapted from the codingbat site: http://codingbat.com/prob/p195669) -Then try running the test file with pytest: +In the directory where you put the files, run: .. code-block:: bash - $ pytest test_cigar_party + $ pytest test_walnut_party.py + +You will get a LOT of test failures! What you've done here is the first step in what is called: - **Test Driven Development**. + **Test Driven Development** A bunch of tests exist, but the code to make them pass does not yet exist. The red you see in the terminal when we run our tests is a goad to us to write the code that fixes these tests. -Let's do that next! +The tests all failed because currently ``walnut_party()`` looks like: -Test Driven development ------------------------ +.. code-block:: python + + def walnut_party(walnuts, is_weekend): + pass + +A totally do nothing function. + + +Making tests pass +----------------- Open: -``test_cigar_party.py`` +``test_walnut_party.py`` and: -``cigar_party.py`` +``walnut_party.py`` In your editor. -Now edit ``cigar_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. +Now edit the function in ``walnut_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. + +When the tests pass -- you are done! That's the beauty of test-driven development. Doing your own: --------------- @@ -69,16 +101,23 @@ Pick another example from codingbat: Do a bit of test-driven development on it: - * run something on the web site. - * write a few tests using the examples from the site. +* Run something on the web site. +* Write a few tests using the examples from the site. +* Then write the function, and fix it 'till it passes the tests. -These tests should be in a file names ``test_something.py`` -- I usually name the test file the same as the module it tests, +These tests should be in a file named ``test_something.py`` -- I usually name the test file the same as the module it tests, with ``test_`` prepended. - * then write the function, and fix it 'till it passes the tests. +.. note:: + Technically, you can name your test files anything you want. But there are two reasons to use standard naming conventions. + One is that it is clear to anyone looking at the code what is and isn't a test module. The other is that pytest, and other testing systems, use `naming conventions `_ to find your test files. + If you name your test files: ``test_something.py`` then pytest will find them for you. And if you use the name of the module being tested: + ``test_name_of_tested_module.py`` then it will be clear which test files belong to which modules. + Do at least two of these to get the hang of the process. Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner? + Give it a shot -- you'll know if it still works if the tests still pass! diff --git a/source/modules/Testing.rst b/source/modules/Testing.rst index 7a37ae3b..f9baa7e6 100644 --- a/source/modules/Testing.rst +++ b/source/modules/Testing.rst @@ -26,9 +26,9 @@ block. * You can't do anything else when the file is executed without running tests. - This is not optimal. +This is not optimal. - Python provides testing systems to help. +Python provides testing systems to help. Standard Library: ``unittest`` @@ -38,11 +38,9 @@ The original testing system in Python. ``import unittest`` -More or less a port of ``JUnit`` from Java +More or less a port of `JUnit `_ from Java -A bit verbose: you have to write classes & methods - -(And we haven't covered that yet!) +A bit verbose: you have to write classes & methods (And we haven't covered that yet!) But here's a bit of an introduction, as you will see this in others' code. @@ -108,32 +106,31 @@ in ``test_my_mod.py``: Advantages of ``unittest`` -------------------------- +The ``unittest`` module is pretty full featured - The ``unittest`` module is pretty full featured - - It comes with the standard Python distribution, no installation required. +It comes with the standard Python distribution, no installation required. - It provides a wide variety of assertions for testing all sorts of situations. +It provides a wide variety of assertions for testing all sorts of situations. - It allows for a setup and tear down workflow both before and after all tests and before and after each test. +It allows for a setup and tear down workflow both before and after all tests and before and after each test. - It's well known and well understood. +It's well known and well understood. Disadvantages of ``unittest`` ----------------------------- - It's Object Oriented, and quite "heavyweight". +It's Object Oriented, and quite "heavyweight". - - modeled after Java's ``JUnit`` and it shows... + - modeled after Java's ``JUnit`` and it shows... - It uses the framework design pattern, so knowing how to use the features means learning what to override. +It uses the framework design pattern, so knowing how to use the features means learning what to override. - Needing to override means you have to be cautious. +Needing to override means you have to be cautious. - Test discovery is both inflexible and brittle. +Test discovery is both inflexible and brittle. - And there is no built-in parameterized testing. +And there is no built-in parameterized testing. Other Options @@ -141,22 +138,24 @@ Other Options There are several other options for running tests in Python. -* `Nose`: https://nose.readthedocs.org/ +* **Nose2**: https://github.com/nose-devs/nose2 -* `pytest`: http://pytest.org/latest/ +* **pytest**: http://pytest.org/latest/ * ... (many frameworks supply their own test runners: e.g. django) -Nose was the most common test runner when I first started learning testing, but it has been in maintaince mode for a while. +Nose was the most common test runner when I first started learning testing, but it has been in maintenance mode for a while. Even the nose2 site recommends that you consider pytest. pytest has become the defacto standard test runner for those that want a more "pythonic" test framework. -It is very capable and widely used. +pytest is very capable and widely used. For a great description of the strengths of pytest, see: `The Cleaning Hand of Pytest `_ +So we will use pytest for the rest of this class. + Installing ``pytest`` --------------------- @@ -173,7 +172,7 @@ at the command line: $ pytest -If you have any tests in your repository, that will find and run them. +If you have any tests in your repository, that command will find and run them (If you have followed the proper naming conventions). **Do you?** @@ -182,12 +181,18 @@ Pre-existing Tests Let's take a look at some examples. -in ``/examples/testing`` +Create a directory to try this out, and download: + +:download:`test_random_unitest.py <../examples/testing/test_random_unitest.py>` + +In the directory you created for that file, run: .. code-block:: bash $ pytest +It should find that test file and run it. + You can also run pytest on a particular test file: .. code-block:: bash @@ -205,17 +210,20 @@ Take a few minutes to look these files over. What is Happening Here? ----------------------- -You should have gotten results that look something like this:: +You should have gotten results that look something like this: + +.. code-block:: bash - MacBook-Pro:Session06 Chris$ pytest test_random_unitest.py + $ pytest ============================= test session starts ============================== - platform darwin -- Python 3.6.2, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 - rootdir: /Users/Chris/PythonStuff/UWPCE/IntroPython-2017/examples/Session06, inifile: + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/test_temp, inifile: + plugins: cov-2.6.0 collected 3 items - test_random_unitest.py ... + test_random_unitest.py ... [100%] - =========================== 3 passed in 0.02 seconds =========================== + =========================== 3 passed in 0.06 seconds =========================== When you run the ``pytest`` command, ``pytest`` starts in your current @@ -246,60 +254,204 @@ It will run ``unittest`` tests for you, so can be used as a test runner. But in addition to finding and running tests, it makes writing tests simple, and provides a bunch of nifty utilities to support more complex testing. +Now download this file: -Test Driven Development ------------------------ +:download:`test_random_pytest.py <../examples/testing/test_random_pytest.py>` -Download these files, and save them in your own students directory in the class repo: +And run pytest again: -:download:`test_cigar_party.py <../examples/testing/test_cigar_party.py>` -and: -:download:`cigar_party.py <../examples/testing/cigar_party.py>` +.. code-block:: bash -then, in dir where you put the files, run:: + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/test_temp, inifile: + plugins: cov-2.6.0 + collected 8 items - $ pytest test_cigar_party.py + test_random_pytest.py ..... [ 62%] + test_random_unitest.py ... [100%] -You will get a LOT of test failures! + =========================== 8 passed in 0.07 seconds =========================== -What we've just done here is the first step in what is called: +Note that it ran the tests in both the test files. - **Test Driven Development**. +Take a look at ``test_random_pytest.py`` -- It is essentially the same tests -- but written in native pytest style -- simple test functions. -The idea is that you write the tests first, and then write the code that passes the tests. In this case, the writing the tests part has been done for you: +pytest tests +------------ -A bunch of tests exist, but the code to make them pass does not yet exist. +The beauty of pytest is that it takes advantage of Python's dynamic nature -- you don't need to use any particular structure to write tests. -The red you see in the terminal when we run the tests is a goad to you to write the code that fixes these tests. +Any function named appropriately is a test. -The tests all failed because ``cigar_party()`` looks like: +If the function doesn't raise an error or an assertion, the test passes. It's that simple. + +Let's take a look at ``test_random_pytest.py`` to see how this works. .. code-block:: python - def cigar_party(cigars, is_weekend): - pass + import random + import pytest + +The ``random`` module is imported becasue that's what we are testing. +``pytest`` only needs to be imported if you are using its utilities -- more on this in a moment. -A totally do nothing function! +.. code-block:: python -Put real code in ``cigar_party.py`` until all the tests pass. + seq = list(range(10)) -When the tests pass -- you are done! That's the beauty of test-driven development. +Here we create a simple little sequence to use for testing. We put it in the global namespace so other functions can access it. -Trying it yourself ------------------- +Now the first tests -- simply by naming it ``test_something``, pytest will run it as a test: + +.. code-block:: python + + def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + +This is pretty straightforward. We make a random choice from the sequence, +and then assert that the selected element is, indeed, in the original sequence. + +.. code-block:: python + + def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq + +And this is pretty much the same thing, except that it loops to make sure that every item returned by ``.sample`` is in the original sequence. + +Note that this will result in 5 separate assertions -- that is fine, you can have as many assertions as you like in one test function. But the test will fail on the first failed assertion -- so you only want to have closely related assertions in each test function. + +.. code-block:: python + + def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + assert seq == list(range(10)) + +This test is designed to make sure that ``random.shuffle`` only re-arranges the items, but doesn't add or lose any. + +In this case, the global ``example_seq`` isn't used, because ``shuffle()`` will change the sequence -- tests should never rely on or alter global state. So a new sequence is created for the test. This also allows the test to know exactly what the results should be at the end. + +Then the "real work" -- calling ``random.shuffle`` on the sequence -- this should re-arrange the elements without adding or losing any. + +Calling ``.sort()`` again should put the elements back in the order they started + +So we can then test that after shuffling and re-sorting, we have the same sequence back: -Try it a bit more, writing the tests yourself: +.. code-block:: python + + assert seq == list(range(10)) + +If that assertion passes, the test will pass. + +``print()`` and test failures +............................. + +Try commenting out the sort line: + +.. code-block:: python + + # seq.sort() # If you comment this out, it will fail, so you can see output + +And run again to see what happens. This is what I got: + +.. code-block:: bash + + $ pytest test_random_pytest.py + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/PythonStuff/UWPCE/PythonCertDevel/source/examples/testing, inifile: + plugins: cov-2.6.0 + collected 5 items + + test_random_pytest.py F.... [100%] + + =================================== FAILURES =================================== + _________________________________ test_shuffle _________________________________ + + def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + # seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + > assert seq == list(range(10)) + E assert [4, 8, 9, 3, 2, 0, ...] == [0, 1, 2, 3, 4, 5, ...] + E At index 0 diff: 4 != 0 + E Use -v to get the full diff + + test_random_pytest.py:22: AssertionError + ----------------------------- Captured stdout call ----------------------------- + seq: [4, 8, 9, 3, 2, 0, 7, 5, 6, 1] + ====================== 1 failed, 4 passed in 0.40 seconds ====================== + +You get a lot of information when test fails. It's usually enough to tell you what went wrong. -Pick an example from codingbat: +Note that pytest didn't print out the results of the print statement when the test passed, but when it failed, it printed it (under "Captured stdout call"). This means you can put diagnostic print calls in your tests, and they will not clutter up the output when they are not needed. - `codingbat `_ +Testing for Exceptions +...................... -Do a bit of test-driven development on it: +One of the things you might want to test about your code is that it raises an exception when it should -- and that the exception it raises is the correct one. + +In this example, if you try to call ``random.shuffle`` with an immutable sequence, such as a tuple, it should raise a ``TypeError``. Since raising an exception will generally stop the code (and cause a test to fail), we can't use an assertion to test for this. + +pytest provides a "context manager", ``pytest.raises``, that can be used to test for exceptions. The test will pass if and only if the specified Exception is raised by the enclosed code. You use it like so: + +.. code-block:: python + + def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ + with pytest.raises(TypeError): + random.shuffle((1, 2, 3)) + +The ``with`` block is how you use a context manager -- it will run the code enclosed, and perform various actions at the end of the code, or when an exception is raised. +This is the same ``with`` as used to open files. In that case, it is used to assure that the file is properly closed when you are done with it. In this case, the ``pytest.raises`` context manager captures any exceptions, and raises an ``AssertionError`` if no exception is raised, or if the wrong exception is raised. + +In this case, the test will only pass if a ``TypeError`` is raised by the call to ``random.shuffle`` with a tuple as an argument. + +The next test: + +.. code-block:: python + + def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ + with pytest.raises(ValueError): + random.sample(example_seq, 20) + +is very similar, except that this time, a ValueError has to be raised for the test to pass. + +pytest provides a number of other features for fixtures, parameterized tests, test classes, configuration, shared resources, etc. +But simple test functions like this will get you very far. + + +Test Driven Development +----------------------- - * run something on the web site. - * write a few tests using the examples from the site. - * then write the function, and fix it 'till it passes the tests. +Test Driven Development or "TDD", is a development process where you write tests to assure that your code works, *before* you write the actual code. -Do at least two of these... +This is a very powerful approach, as it forces you to think carefully about exactly what your code should do before you start to write it. It also means that you know when you code is working, and you can refactor it in the future with assurance that you haven't broken it. +Give this exercise a try to get the idea: +:ref:`exercise_unit_testing` diff --git a/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py b/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py deleted file mode 100755 index ad7df22c..00000000 --- a/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python - - -def cigar_party(cigars, is_weekend): - """ - basic solution - """ - if is_weekend and cigars >= 40: - return True - elif 40 <= cigars <= 60: - return True - return False - - -def cigar_party2(cigars, is_weekend): - """ - some direct return of bool result - """ - if is_weekend: - return (cigars >= 40) - return (cigars >= 40 and cigars <= 60) - - -def cigar_party3(cigars, is_weekend): - """ - conditional expression - """ - return (cigars >= 40) if is_weekend else (cigars >= 40 and cigars <= 60) - -if __name__ == "__main__": - # some tests - - assert cigar_party(30, False) is False - assert cigar_party(50, False) is True - assert cigar_party(70, True) is True - assert cigar_party(30, True) is False - assert cigar_party(50, True) is True - assert cigar_party(60, False) is True - assert cigar_party(61, False) is False - assert cigar_party(40, False) is True - assert cigar_party(39, False) is False - assert cigar_party(40, True) is True - assert cigar_party(39, True) is False - - print("All tests passed") diff --git a/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py new file mode 100755 index 00000000..3bdae1d8 --- /dev/null +++ b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +""" +adapted from coding bat: https://codingbat.com/python +""" + + +def walnut_party(walnuts, is_weekend): + """ + basic solution + """ + if is_weekend and walnuts >= 40: + return True + elif 40 <= walnuts <= 60: + return True + return False + + +def walnut_party2(walnuts, is_weekend): + """ + Direct return of bool result + """ + if is_weekend: + return (walnuts >= 40) + return (walnuts >= 40 and walnuts <= 60) + + +def walnut_party3(walnuts, is_weekend): + """ + Conditional expression + """ + return (walnuts >= 40) if is_weekend else (walnuts >= 40 and walnuts <= 60) + +if __name__ == "__main__": + # some tests + + assert walnut_party(30, False) is False + assert walnut_party(50, False) is True + assert walnut_party(70, True) is True + assert walnut_party(30, True) is False + assert walnut_party(50, True) is True + assert walnut_party(60, False) is True + assert walnut_party(61, False) is False + assert walnut_party(40, False) is True + assert walnut_party(39, False) is False + assert walnut_party(40, True) is True + assert walnut_party(39, True) is False + + print("All tests passed") diff --git a/source/solutions/Lesson06/.gitignore b/source/solutions/Lesson06/.gitignore new file mode 100644 index 00000000..8035ab50 --- /dev/null +++ b/source/solutions/Lesson06/.gitignore @@ -0,0 +1,3 @@ +*.txt + + diff --git a/source/solutions/Lesson06/cigar_party.py b/source/solutions/Lesson06/cigar_party.py deleted file mode 100644 index 992d99bd..00000000 --- a/source/solutions/Lesson06/cigar_party.py +++ /dev/null @@ -1,15 +0,0 @@ - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -def cigar_party(num, weekend): - return num >= 40 and (num <= 60 or weekend) - diff --git a/source/solutions/Lesson06/test_cigar_party.py b/source/solutions/Lesson06/test_cigar_party.py deleted file mode 100644 index 80bc0920..00000000 --- a/source/solutions/Lesson06/test_cigar_party.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -# you can change this import to test different versions -from cigar_party import cigar_party -# from cigar_party import cigar_party2 as cigar_party -# from cigar_party import cigar_party3 as cigar_party - - -def test_1(): - assert cigar_party(30, False) is False - - -def test_2(): - - assert cigar_party(50, False) is True - - -def test_3(): - - assert cigar_party(70, True) is True - - -def test_4(): - assert cigar_party(30, True) is False - - -def test_5(): - assert cigar_party(50, True) is True - - -def test_6(): - assert cigar_party(60, False) is True - - -def test_7(): - assert cigar_party(61, False) is False - - -def test_8(): - assert cigar_party(40, False) is True - - -def test_9(): - assert cigar_party(39, False) is False - - -def test_10(): - assert cigar_party(40, True) is True - - -def test_11(): - assert cigar_party(39, True) is False - diff --git a/source/solutions/Lesson06/test_walnut_party.py b/source/solutions/Lesson06/test_walnut_party.py new file mode 100644 index 00000000..5bd7ae45 --- /dev/null +++ b/source/solutions/Lesson06/test_walnut_party.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +# you can change this import to test different versions +from walnut_party import walnut_party +# from walnut_party import walnut_party2 as walnut_party +# from walnut_party import walnut_party3 as walnut_party + + +def test_1(): + assert walnut_party(30, False) is False + + +def test_2(): + + assert walnut_party(50, False) is True + + +def test_3(): + + assert walnut_party(70, True) is True + + +def test_4(): + assert walnut_party(30, True) is False + + +def test_5(): + assert walnut_party(50, True) is True + + +def test_6(): + assert walnut_party(60, False) is True + + +def test_7(): + assert walnut_party(61, False) is False + + +def test_8(): + assert walnut_party(40, False) is True + + +def test_9(): + assert walnut_party(39, False) is False + + +def test_10(): + assert walnut_party(40, True) is True + + +def test_11(): + assert walnut_party(39, True) is False diff --git a/source/solutions/Lesson06/walnut_party.py b/source/solutions/Lesson06/walnut_party.py new file mode 100644 index 00000000..781f71be --- /dev/null +++ b/source/solutions/Lesson06/walnut_party.py @@ -0,0 +1,14 @@ + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +def walnut_party(num, weekend): + return num >= 40 and (num <= 60 or weekend) diff --git a/source/solutions/codingbat/Logic-1/cigar_party.py b/source/solutions/codingbat/Logic-1/cigar_party.py deleted file mode 100755 index ad7df22c..00000000 --- a/source/solutions/codingbat/Logic-1/cigar_party.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python - - -def cigar_party(cigars, is_weekend): - """ - basic solution - """ - if is_weekend and cigars >= 40: - return True - elif 40 <= cigars <= 60: - return True - return False - - -def cigar_party2(cigars, is_weekend): - """ - some direct return of bool result - """ - if is_weekend: - return (cigars >= 40) - return (cigars >= 40 and cigars <= 60) - - -def cigar_party3(cigars, is_weekend): - """ - conditional expression - """ - return (cigars >= 40) if is_weekend else (cigars >= 40 and cigars <= 60) - -if __name__ == "__main__": - # some tests - - assert cigar_party(30, False) is False - assert cigar_party(50, False) is True - assert cigar_party(70, True) is True - assert cigar_party(30, True) is False - assert cigar_party(50, True) is True - assert cigar_party(60, False) is True - assert cigar_party(61, False) is False - assert cigar_party(40, False) is True - assert cigar_party(39, False) is False - assert cigar_party(40, True) is True - assert cigar_party(39, True) is False - - print("All tests passed") From 736e9d5e6a50dd167e8c2e9def522fee6090d069 Mon Sep 17 00:00:00 2001 From: "Christopher H.Barker, PhD" Date: Mon, 4 Mar 2019 12:47:50 -0800 Subject: [PATCH 54/87] added Fluent Python to the learning page. --- source/references/learning.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/references/learning.rst b/source/references/learning.rst index c50f4196..f66ebbdd 100644 --- a/source/references/learning.rst +++ b/source/references/learning.rst @@ -109,6 +109,8 @@ Advanced Books (http://chimera.labs.oreilly.com/books/1230000000393): The Python Cookbook provides practical solutions to various programming tasks, along with excellent explanations of how they work. Written by David Beazley -- Python author and instructor extraordinaire. Also available fully online: (http://chimera.labs.oreilly.com/books/1230000000393/index.html) +* **Fluent Python** + (http://shop.oreilly.com/product/0636920032519.do): Fluent Python is an excellent deeper dive into the inner workings of Python. Written for folks that already understand the basics of Python, it goes provides explainations of the more advanced topics in Python. If we were going to use a textbook for an advanced Python class, this would be it. Evaluating Your Options ----------------------------- From 43302d0ff3ff6ad6f419b72b570c74d1ad77c56d Mon Sep 17 00:00:00 2001 From: Natasha Date: Wed, 20 Mar 2019 18:34:29 -0700 Subject: [PATCH 55/87] fix print_grid(11) to print_grid(9) (#183) --- source/exercises/grid_printer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/exercises/grid_printer.rst b/source/exercises/grid_printer.rst index 861869ff..87804fdc 100644 --- a/source/exercises/grid_printer.rst +++ b/source/exercises/grid_printer.rst @@ -128,7 +128,7 @@ One of the points of writing functions is so you can write code that does simila Write a function ``print_grid(n)`` that takes one integer argument and prints a grid just like before, *BUT* the size of the grid is given by the argument. -For example, ``print_grid(11)`` prints the grid at the top of this page. +For example, ``print_grid(9)`` prints the grid at the top of this page. ``print_grid(3)`` would print a smaller grid:: From ccd1695b82ab3d938336fc8b5f1182a4def9870a Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Tue, 2 Apr 2019 22:20:11 -0700 Subject: [PATCH 56/87] added a bit more about Comprehensions and a reference. --- source/modules/Comprehensions.rst | 45 ++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst index a594c669..19408c2c 100644 --- a/source/modules/Comprehensions.rst +++ b/source/modules/Comprehensions.rst @@ -88,7 +88,40 @@ Comprehensions and map() Comprehensions are another way of expressing the "map" pattern from functional programming. -Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. +Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. IF youare not familiar with ``map()``, you can saftly skip this, but if you are: + +.. code-block:: python + + map(a_function, an_iterable) + +is the same as: + +.. code-block:: python + + [a_function(item), for item in an_iterable] + +In this case, the comprehension is a tad wordier than ``map()``. BUt comprehensions really shine when you do'nt already have a handy function to pass to map: + +.. code-block:: python + + [x**2 for x in an_iterable] + +To use ``map()``, you need a function: + +.. code-block:: python + + def square(x): + return x**2 + + map(square, an_iterable) + +There are shortcuts of course, including ``lambda`` (stay tuned for more about that): + +.. code-block:: python + + map(lambda x: x**2, an_iterable) + +But is that easier to read or write? What about filter? @@ -130,6 +163,8 @@ This is expressing the "filter" pattern and the "map" pattern at the same time - Get creative.... +How do I see all the built in Exceptions? + .. code-block:: python [name for name in dir(__builtin__) if "Error" in name] @@ -431,3 +466,11 @@ If you are going to immediately loop through the items created by the comprehens The "official" term is "generator expression" -- that is what you will see in the Python docs, and a lot of online discussions. I've used the term "generator comprehension" here to better make clear the association with list comprehensions. +References +---------- + +This is a nice intro to comprehensions from Trey Hunner: + +https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/ + +Trey writes a lot of good stuff -- I recommned browsing his site. From 4cc1a1c71e4a8442ab85c8a895424ce858b44966 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Tue, 2 Apr 2019 22:25:33 -0700 Subject: [PATCH 57/87] another link about comprehensions --- source/modules/Comprehensions.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst index 19408c2c..a813b5ab 100644 --- a/source/modules/Comprehensions.rst +++ b/source/modules/Comprehensions.rst @@ -473,4 +473,8 @@ This is a nice intro to comprehensions from Trey Hunner: https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/ +Once you've got the hang of it, you may want to read this so you don't overdo it :-) + +https://treyhunner.com/2019/03/abusing-and-overusing-list-comprehensions-in-python/ + Trey writes a lot of good stuff -- I recommned browsing his site. From 16e2806dd5fc7419d6c0becd01d93b345e404899 Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Mon, 8 Apr 2019 00:33:52 -0700 Subject: [PATCH 58/87] Create vsc_as_ide.rst --- .../dev_environment/vsc_as_ide.rst | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 source/supplemental/dev_environment/vsc_as_ide.rst diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst new file mode 100644 index 00000000..a92f7dcb --- /dev/null +++ b/source/supplemental/dev_environment/vsc_as_ide.rst @@ -0,0 +1,69 @@ +.. _vsc_as_ide: + +########################################## +Using Visual Studio Code as a lightweight Python IDE +########################################## + +Visual Studio Code is an extensible and customizable text editor from Microsoft that provides a very minimal layout with additional tooling such as an excellent built-in terminal. + + +Requirements +============ + +Any IDE should ease your development experience by providing the following: + +* It should provide excellent, configurable syntax colorization. +* It should allow for robust tab completion. +* It should offer the ability to jump to the definition of symbols in other files. +* It should perform automatic code linting to help avoid silly mistakes. +* It should be able to interact with a Python interpreter such that when debugging, the editor will follow along with the debugger. + +Visual Studio Code requires that you perform some setup out of the box (see below for details). + + +Which Version? +============== + +There's just the latest version available to download. + +This ensures that all recent bug fixes and updates have been made. + +Visual Studio Code runs on Macs, Windows, and Linux flavors like Ubuntu, Debian, Red Hat, etc. + +Also, Visual Studio Code performs updates on itself, so there's no need to download newer versions of the app... you should already have it. + +Installation +============ + +Check out this solid video_ that will walk you through the process of setting up Visual Studio Code for Python in detail. + +.. _video: https://www.youtube.com/watch?v=TILIcrrVABg/ + +Go to the Visual Studio Code website_. + +.. _website: https://code.visualstudio.com/ + +Scroll down to the bottom of the page and you'll see links for installers to all the major OS platforms. + +Download your flavor and run the installer. + + +Basic Settings +============== + +Visual Studio Code can be used out of the box with no setup as a text editor. It automatically +recognizes file types and helpfully highlights text accordingly. To use in this manner, +write your Python files in Visual Studio Code, then run them in your Python command prompt +or Visual Studio Code's own built in terminal (Ctrl + \`) + + +Extending the Editor +==================== + +After you've install Visual Studio Code, there are many ways to extend it for working with Python. + +The video linked above goes into this much deeper. + +There is also a great tutorial for setting up Python here_. + +.. _here: https://code.visualstudio.com/docs/python/python-tutorial From b697855aad57a44f3c569b4c517218164dfdcfb7 Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Mon, 8 Apr 2019 00:36:01 -0700 Subject: [PATCH 59/87] Update vsc_as_ide.rst @ChrisBarker-NOAA Here's a starter page. I plan add more on linting and debugging soon. Ran out of time and the video / tutorial from Microsoft is pretty solid. Thanks --- source/supplemental/dev_environment/vsc_as_ide.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst index a92f7dcb..5539cc33 100644 --- a/source/supplemental/dev_environment/vsc_as_ide.rst +++ b/source/supplemental/dev_environment/vsc_as_ide.rst @@ -67,3 +67,4 @@ The video linked above goes into this much deeper. There is also a great tutorial for setting up Python here_. .. _here: https://code.visualstudio.com/docs/python/python-tutorial + From 4c172739bf369c7f178fb49c26b5485914f390a8 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 8 Apr 2019 21:44:12 -0700 Subject: [PATCH 60/87] a bit of clean up of the editor tutorials -- and adding links to the visual studio code page. --- source/modules/Class_introduction.rst | 12 +++++----- source/supplemental/dev_environment/index.rst | 8 +++++-- .../dev_environment/sublime_as_ide.rst | 23 +++++++++---------- .../dev_environment/vsc_as_ide.rst | 8 +++---- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/source/modules/Class_introduction.rst b/source/modules/Class_introduction.rst index 308326bd..363e0421 100644 --- a/source/modules/Class_introduction.rst +++ b/source/modules/Class_introduction.rst @@ -9,7 +9,7 @@ In which you are introduced to this class, your instructors, your environment an .. image:: /_static/python.png :align: center - :width: 38% + :width: 80% `xkcd.com/353`_ @@ -74,9 +74,9 @@ Interrupt us with questions -- please! Homework: --------- -* Homework will primarily be reading, a handful of videos, and links to optional external materials -- videos, blog posts, etc. +* Homework will be reading: a handful of videos, and links to optional external materials -- videos, blog posts, etc. -* Exercises will be started in class -- but you can finish them at home. +* Exercises will be started in class -- but you can finish them at home (and you will need time to do that!) * You are adults -- it's up to you to do the homework. But if you don't code, you won't learn to code. And we can't give you a certificate if you haven't demonstrated that you've done the work. @@ -87,11 +87,11 @@ There is a video about that, and we will show you in the first class as well. Communication ------------- -**MS Teams:** +**Mailing List** -There has been an MS Team channel set up for this class. You should have been invited to join -- if not, let your instructors know. +There should have been a Mailing List set up for this class. You should have been invited to join -- if not, let your instructors know. Also let them know if you would prefer a different email address. -Anything Python related is fair game. Keep general discussion about the class or Python in the main channel, and maybe set up separate channels for particular assignments. +Anything Python related is fair game. Questions and discussion about the assignments are encouraged. We highly encourage you to work together. You will learn at a much deeper level if you work together, and it gets you ready to collaborate with colleagues. diff --git a/source/supplemental/dev_environment/index.rst b/source/supplemental/dev_environment/index.rst index 8eb96522..9f98c5cf 100644 --- a/source/supplemental/dev_environment/index.rst +++ b/source/supplemental/dev_environment/index.rst @@ -278,13 +278,16 @@ http://www.sublimetext.com/ :ref:`sublime_as_ide` - "Atom" is another good open source option. https://atom.io/ :ref:`atom_as_ide` +"Visual Studio Code" is a relatively new cross platfrom offering from Microsoft -- a lot of folks seem to like it: + +:ref:`vsc_as_ide` + And, of course, vim or Emacs on Linux, if you are familiar with those. Why No IDE? @@ -377,7 +380,7 @@ You should be able to run git on the command line: .. code-block:: bash $ git --version - git version 2.11.0 (Apple Git-81) + git version 2.20.1 (Apple Git-117) It should be version >= 2 @@ -404,6 +407,7 @@ Specific Documentation sublime_as_ide atom_as_ide + vsc_as_ide command_line shell ipython diff --git a/source/supplemental/dev_environment/sublime_as_ide.rst b/source/supplemental/dev_environment/sublime_as_ide.rst index 65f0208e..1f96594a 100644 --- a/source/supplemental/dev_environment/sublime_as_ide.rst +++ b/source/supplemental/dev_environment/sublime_as_ide.rst @@ -34,6 +34,7 @@ And some more advanced features that you may want later: Which Version? ============== + Use version 3 -- it is updated now and again, so make sure to get the latest. *Use Sublime Version 3* @@ -42,7 +43,9 @@ Use version 3 -- it is updated now and again, so make sure to get the latest. Basic Settings ============== -All configuration in Sublime Text is done via `JSON `_. It's simple to learn. go and read that link then return here. [Note that JSON is very similar to Python dict and list literals] +All configuration in Sublime Text is done via `JSON `_. It's simple to learn. Go and read that link then return here. + +.. note:: JSON is very similar to Python dict and list literals. Though it has its root in Javascript, it is also used in a wide variety of applications, and is well supported by Python. But a key difference is that it does not allow trailing commas after items in lists -- so be careful. There are a number of `different levels of configuration `_ in Sublime Text. You will most often work on settings at the user level. @@ -86,7 +89,7 @@ Here's a reasonable set of preliminary settings (theme, color scheme and font ar **NOTE:** Especially important is the setting ``translate_tabs_to_spaces``, which ensures that any time you hit a tab key, the single character is replaced by four characters. In Python this is **vital**! -If you do nothing else, add this for your config! +If you do nothing else, add ``translate_tabs_to_spaces`` to your config! Extending the Editor ==================== @@ -125,7 +128,7 @@ Anaconda There are a bunch of Python-related plugins available. However, Anaconda is a nice package that provides most of the features you want, so plan on using just that one. -Not to be confused with the Scientific Python distribution -- the Anaconda sublime plugin is a full featured package to turn Sublime into a pretty full IDE: +Not to be confused with the scientific Python distribution -- the Anaconda sublime plugin is a full featured package to turn Sublime into a pretty full IDE: http://damnwidget.github.io/anaconda/ @@ -144,7 +147,9 @@ A few settings you'll want to change There are a few setting you may want to change: -* max line length for the linter: default is 72, which is pretty short these day. I use 95 +* max line length for the linter: default is 72, which is pretty short these day. I use 95:: + + "pep8_max_line_length": 95, White Space Management @@ -171,13 +176,7 @@ You'll probably want to wait on this until you start using a debugger, but it's The final requirement for a reasonable IDE experience is to be able to follow a debugging session in the file where the code exists. -There is no plugin for Sublime Text that supports this. But there is a Python package you can install. - -The package is called `PDBSublimeTextSupport `_ and its simple to install with ``pip``: - -.. code-block:: bash - - $ python -m pip install PDBSublimeTextSupport -With that package installed in the Python that is used for your project, any breakpoint you set will automatically pop to the surface in Sublime Text. And as you step through the code, you will see the current line in your Sublime Text file move along with you. +This: https://packagecontrol.io/packages/Python%20Debugger +Looks promising as a debugger plugin for sublime. diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst index 5539cc33..cd35aee0 100644 --- a/source/supplemental/dev_environment/vsc_as_ide.rst +++ b/source/supplemental/dev_environment/vsc_as_ide.rst @@ -1,8 +1,8 @@ .. _vsc_as_ide: -########################################## +#################################################### Using Visual Studio Code as a lightweight Python IDE -########################################## +#################################################### Visual Studio Code is an extensible and customizable text editor from Microsoft that provides a very minimal layout with additional tooling such as an excellent built-in terminal. @@ -53,8 +53,8 @@ Basic Settings Visual Studio Code can be used out of the box with no setup as a text editor. It automatically recognizes file types and helpfully highlights text accordingly. To use in this manner, -write your Python files in Visual Studio Code, then run them in your Python command prompt -or Visual Studio Code's own built in terminal (Ctrl + \`) +write your Python files in Visual Studio Code, then run them in your Python command prompt +or Visual Studio Code's own built in terminal: Ctrl + \` (control-backtick) Extending the Editor From 7da2e15ecd99c0029fa631aef8c9b4641fad8788 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 8 Apr 2019 22:13:59 -0700 Subject: [PATCH 61/87] another coding bat --- source/solutions/codingbat/List-2/sum13.py | 64 ++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 source/solutions/codingbat/List-2/sum13.py diff --git a/source/solutions/codingbat/List-2/sum13.py b/source/solutions/codingbat/List-2/sum13.py new file mode 100644 index 00000000..e2e77ad9 --- /dev/null +++ b/source/solutions/codingbat/List-2/sum13.py @@ -0,0 +1,64 @@ +def sum13(l): + tot = 0 + prev = 0 + for i in range(len(l)): + if l[i] != 13 and prev != 13: + tot += l[i] + prev = l[i] + return tot + + +# def sum13(l): +# prev, tot = 0, 0 +# for i in l: +# if i != 13 and prev != 13: +# tot += i +# prev = i +# return tot + + +# def sum13(l): +# tot = 0 +# i = 0 +# while i < len(l): +# if l[i] != 13: +# tot += l[i] +# i += 1 +# else: +# i += 2 +# return tot + + +# def sum13(l): +# tot = 0 +# l_iter = iter(l) +# for i in l_iter: +# if i == 13: +# try: +# next(l_iter) +# except StopIteration: +# break +# else: +# tot += i +# return tot + + +if __name__ == "__main__": + + assert sum13([1, 2, 2, 1]) == 6 + assert sum13([1, 1]) == 2 + assert sum13([1, 2, 2, 1, 13]) == 6 + assert sum13([1, 2, 13, 2, 1, 13]) == 4 + assert sum13([13, 1, 2, 13, 2, 1, 13]) == 3 + assert sum13([]) == 0 + assert sum13([13]) == 0 + assert sum13([13, 13]) == 0 + assert sum13([13, 0, 13]) == 0 + assert sum13([13, 1, 13]) == 0 + assert sum13([5, 7, 2]) == 14 + assert sum13([5, 13, 2]) == 5 + assert sum13([0]) == 0 + assert sum13([13, 0]) == 0 + + print("all asserts passed") + From c4226fe3f4eb58a85316633b8246db3668d6c142 Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Tue, 9 Apr 2019 23:47:51 -0700 Subject: [PATCH 62/87] Update Adding info on default Git editor --- source/supplemental/dev_environment/vsc_as_ide.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst index cd35aee0..eb43e57d 100644 --- a/source/supplemental/dev_environment/vsc_as_ide.rst +++ b/source/supplemental/dev_environment/vsc_as_ide.rst @@ -68,3 +68,6 @@ There is also a great tutorial for setting up Python here_. .. _here: https://code.visualstudio.com/docs/python/python-tutorial +I also recommend setting up Visual Studio Code as your default_ Git editor +.. _default: https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git + From ca0d4d7b75d7b2dc4a86dbf9098ae4974e2c35ae Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Tue, 9 Apr 2019 23:48:43 -0700 Subject: [PATCH 63/87] another update --- source/supplemental/dev_environment/vsc_as_ide.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst index eb43e57d..21af2f4c 100644 --- a/source/supplemental/dev_environment/vsc_as_ide.rst +++ b/source/supplemental/dev_environment/vsc_as_ide.rst @@ -68,6 +68,7 @@ There is also a great tutorial for setting up Python here_. .. _here: https://code.visualstudio.com/docs/python/python-tutorial -I also recommend setting up Visual Studio Code as your default_ Git editor +I also recommend setting up Visual Studio Code as your default_ Git editor. + .. _default: https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git From c572010f1d24335a48d139d39a2eaaaaf8a75365 Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Tue, 9 Apr 2019 23:50:19 -0700 Subject: [PATCH 64/87] Update vsc_as_ide.rst --- source/supplemental/dev_environment/vsc_as_ide.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst index 21af2f4c..98a0826b 100644 --- a/source/supplemental/dev_environment/vsc_as_ide.rst +++ b/source/supplemental/dev_environment/vsc_as_ide.rst @@ -68,6 +68,10 @@ There is also a great tutorial for setting up Python here_. .. _here: https://code.visualstudio.com/docs/python/python-tutorial +If you're on a Mac, be sure to set up your path_ for easy integration with the terminal. + +.. _path: https://code.visualstudio.com/docs/setup/mac + I also recommend setting up Visual Studio Code as your default_ Git editor. .. _default: https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git From ac442bfd64c2b7a3303a28a2236b4edee02206ec Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 13 Apr 2019 12:30:28 -0700 Subject: [PATCH 65/87] changed references to "lesson**" from "session**" for the exercise submissions. --- source/exercises/dict_lab.rst | 2 +- source/exercises/except_exercise.rst | 2 +- source/exercises/fib_and_lucas.rst | 2 +- source/exercises/list_lab.rst | 2 +- source/exercises/python_pushups.rst | 9 +++++++-- source/exercises/rot13.rst | 2 +- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/source/exercises/dict_lab.rst b/source/exercises/dict_lab.rst index c887fa33..e048e032 100644 --- a/source/exercises/dict_lab.rst +++ b/source/exercises/dict_lab.rst @@ -15,7 +15,7 @@ Learn the basic ins and outs of Python dictionaries and sets. Procedure --------- -In your student dir in the class repo, create a ``session04`` dir and put in a new ``dict_lab.py`` file. +In your student dir in the class repo, create a ``lesson04`` dir and put in a new ``dict_lab.py`` file. The file should be an executable Python script. That is to say that one should be able to run the script directly like so: diff --git a/source/exercises/except_exercise.rst b/source/exercises/except_exercise.rst index 62104ccf..c4b0fdc1 100644 --- a/source/exercises/except_exercise.rst +++ b/source/exercises/except_exercise.rst @@ -9,7 +9,7 @@ This is a little exercise that shows you how to handle exceptions in a way that Procedure ========= -Here are two files that you should put in your ``session05`` directory in the class repo. +Here are two files that you should put in your ``lesson05`` directory in the class repo. :download:`except_exercise.py` diff --git a/source/exercises/fib_and_lucas.rst b/source/exercises/fib_and_lucas.rst index 189aaa29..ba0e5c21 100644 --- a/source/exercises/fib_and_lucas.rst +++ b/source/exercises/fib_and_lucas.rst @@ -25,7 +25,7 @@ We will write a function that computes this series -- then generalize it. Step 1 ------ -* Create a new module ``series.py`` in the ``session02`` folder in your student folder. +* Create a new module ``series.py`` in the ``lesson02`` folder in your student folder. - In it, add a function called ``fibonacci``. diff --git a/source/exercises/list_lab.rst b/source/exercises/list_lab.rst index 205acf9d..f4c78606 100644 --- a/source/exercises/list_lab.rst +++ b/source/exercises/list_lab.rst @@ -29,7 +29,7 @@ to query the user for info at the command line, you use: Procedure --------- -In your student dir in the class repo, create a ``session03`` dir and put in a new ``list_lab.py`` file. +In your student dir in the class repo, create a ``lesson03`` dir and put in a new ``list_lab.py`` file. The file should be an executable Python script. That is to say that one should be able to run the script directly like so: diff --git a/source/exercises/python_pushups.rst b/source/exercises/python_pushups.rst index 146ef766..7586521b 100644 --- a/source/exercises/python_pushups.rst +++ b/source/exercises/python_pushups.rst @@ -11,8 +11,8 @@ Task 1: Explore Errors * Create a new directory in your working dir for the class:: - $ mkdir session01 - $ cd session01 + $ mkdir lesson01 + $ cd lesson01 * Add a new file to it called ``break_me.py`` @@ -38,3 +38,8 @@ exercises at "Coding Bat": http://codingbat.com/python There are 8 sets of puzzles. Do as many as you can, but try to get to at least the "Warmups". +While the codingbat site has a great interface for submitting your solution and see if it works, we suggest you write your code in your regular text editor and get it to run on your machine first. + +Put at least one solution in the ``lesson1`` folder you created in git and submit it via pull request. + + diff --git a/source/exercises/rot13.rst b/source/exercises/rot13.rst index 8000d19b..26dbcc47 100644 --- a/source/exercises/rot13.rst +++ b/source/exercises/rot13.rst @@ -22,7 +22,7 @@ circle, so it wraps around). The task -------- -Add a python module named ``rot13.py`` to the session03 dir in your student dir. This module should provide at least one function called ``rot13`` that takes any amount of text and returns that same text encrypted by ROT13. +Add a python module named ``rot13.py`` to the lesson03 dir in your student dir. This module should provide at least one function called ``rot13`` that takes any amount of text and returns that same text encrypted by ROT13. This function should preserve whitespace, punctuation and capitalization. From b0264b51db2aa6514436e54edfa42f995a3ef38f Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 13 Apr 2019 13:57:39 -0700 Subject: [PATCH 66/87] updated gitbash instructions to use .bash_profile --- .../supplemental/installing/python_for_windows.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/source/supplemental/installing/python_for_windows.rst b/source/supplemental/installing/python_for_windows.rst index b35f77c5..e2579c2e 100644 --- a/source/supplemental/installing/python_for_windows.rst +++ b/source/supplemental/installing/python_for_windows.rst @@ -40,7 +40,7 @@ Terminal If you are confident in your use of the "DOS Box" or "powershell", command lines, feel free to use one of those. However, your life may be easier if you install "Git Bash", as then you can follow unix-style terminal instructions exactly, and do not have to translate. Also, your instructors are more experienced with Bash. -From now on, if you hear the terms "bash", "shell" or "terminal", or "commandline" know that this is the application that is being referred to. We will use those terms interchangably to mean ANY command line. +From now on, if you hear the terms "bash", "shell" or "terminal", or "command line" know that this is the application that is being referred to. We will use those terms interchangeably to mean ANY command line. When you install Git Bash, you are installing git (and a git gui) as well, thus killing two birds with one stone! @@ -51,7 +51,7 @@ You can go through the rest of the prompts using default values. Once you are do You can use this git with the DOS box or Powershell as well. -This is also a good bet for running Python -- If you use the Git Bash shell, you can use the same commands as Linux and OS-X users. Regardless of which shell you choose, you will need to add Python to your environment. It is possible that this was done during the installation of Python. If you type 'which python' into your terminal, and get back the answer '/c/python34/python', then you are good to go, otherwise (which shouldn't happen if you checked the "Add Python 3.7 to PATH" checkbox when you installed Python above), follow the instructions here: +This is also a good bet for running Python -- If you use the Git Bash shell, you can use the same commands as Linux and OS-X users. Regardless of which shell you choose, you will need to add Python to your environment. It is possible that this was done during the installation of Python. If you type 'which python' into your terminal, and get back the answer '/c/python37/python', then you are good to go, otherwise (which shouldn't happen if you checked the "Add Python 3.7 to PATH" checkbox when you installed Python above), follow the instructions here: http://www.computerhope.com/issues/ch000549.htm @@ -101,7 +101,7 @@ Note: if you have trouble running ``python`` command in your gitbash (it hangs), :: - $ echo "alias python='winpty python'" >> ~/.bashrc + $ echo "alias python='winpty python'" >> ~/.bash_profile You will need to close the current bash window and restart a new one to get this alias. Then from now on, you can just type ``python`` and it should work on git bash (no more hanging) as well. @@ -128,7 +128,7 @@ It should download and install the latest ``pip``. You can now use pip to install other packages. - The first thing you may want to do is update pip itself: +The first thing you may want to do is update pip itself: .. code-block:: bash @@ -171,7 +171,7 @@ You should now be able to run ``iPython`` from the git bash shell or "DOS Box" o IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help. (or from the DOS box or PowerShell prompt) -We will use this as our default Python interpreter. +We will use this in class as our default Python interpreter. Testing it out @@ -187,7 +187,7 @@ All available from the command line. To try it out, you should be able to run all of these commands, and get something like the following results: -(recall that you can get out of the python or iPython command lines with ``ctrl+Z``) +(recall that you can get out of the python or iPython command lines with ``ctrl+Z`` (ifthat doesn't work, try ``ctrl+D`` -- the *nix version)) For Python: @@ -209,7 +209,7 @@ For iPython: Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. - + In [1]: Do you really want to exit ([y]/n)? y From 41a68459428003b11661913222b2982315a7d2a7 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 15 Apr 2019 22:14:00 -0700 Subject: [PATCH 67/87] another coding bat solution --- .../codingbat/String-2/count_code.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 source/solutions/codingbat/String-2/count_code.py diff --git a/source/solutions/codingbat/String-2/count_code.py b/source/solutions/codingbat/String-2/count_code.py new file mode 100644 index 00000000..e58198a2 --- /dev/null +++ b/source/solutions/codingbat/String-2/count_code.py @@ -0,0 +1,20 @@ +def count_code(str): + count = 0 + for i in range(len(str) - 3): + if str[i:i + 2] == "co" and str[i + 3] == "e": + count += 1 + return count + + +assert count_code('aaacodebbb') == 1 +assert count_code('codexxcode') == 2 +assert count_code('cozexxcope') == 2 +assert count_code('cozexxcope') == 2 +assert count_code('cozfxxcope') == 1 +assert count_code('xxcozeyycop') == 1 +assert count_code('cozcop') == 0 + + + +print("all checks passed") + From 03a159b695dba284eadd9742f9c5d92915bd5836 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Tue, 16 Apr 2019 17:45:05 -0700 Subject: [PATCH 68/87] updated packaging page a bit --- source/modules/Packaging.rst | 34 +++++++++++-------- .../installing/python_for_windows.rst | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/source/modules/Packaging.rst b/source/modules/Packaging.rst index 522ddbf5..492beb9b 100644 --- a/source/modules/Packaging.rst +++ b/source/modules/Packaging.rst @@ -37,25 +37,25 @@ of python files or other package directories:: module_b.py The ``__init__.py`` can be totally empty -- or it can have arbitrary python code in it. -The code will be run when the package is imported -- just like a module, +The code will be run when the package is imported -- just like a module. -modules inside packages are *not* automatically imported. So, with the above structure:: +Modules inside packages are *not* automatically imported. So, with the above structure:: import a_package -will run the code in ``a_package/__init__.py``. Any names defined in the -``__init__.py`` will be available in:: +will run the code in ``a_package/__init__.py``. Any names defined in the ``__init__.py`` will be available in:: a_package.a_name -but:: +But:: a_package.module_a -will not exist. To get submodules, you need to explicitly import them: +will not exist. To get submodules, you need to explicitly import them like so: import a_package.module_a + More on Importing ----------------- @@ -77,11 +77,11 @@ or a few names from a package:: x, y) -And you can rename stuff as you import it:: +You also can optionally rename stuff as you import it:: import numpy as np -This is a common pattern for using large packages and not having to type a lot... +This is a common pattern for using large packages (maybe with long names) and not having to type a lot. ``import *`` @@ -91,16 +91,16 @@ This is a common pattern for using large packages and not having to type a lot.. from something import * -means: "import all the names in the module" +Means: "import all the names in the module, "something". -You really don't want to do that! It is an old pattern that is now an anti-pattern +You really don't want to do that! It is an old pattern that is now an anti-pattern. But if you do encounter it, it doesn't actually import all the names -- it imports the ones defined in the module's ``__all__`` variable. ``__all__`` is a list of names that you want ``import *`` to import. -So the module author can control it, and not expect all sorts of builtins and other modules. +So the module author can control it, and not accidentally override builtins or bring a lot of extraneous names into your namespace. -But really -- don't use it! +But really -- **don't use ``import *``** Relative imports @@ -149,7 +149,7 @@ Similarly to \*nix shells: ".." means "the package above this one" -Note that you have to use the "from" form when using relative imports. +Note that you have to use the ``from`` form of import when using relative imports. **Caveats:** @@ -185,8 +185,13 @@ There is debate about which is the "one way to do it" -- a bit unpythonic, but y sys.modules ----------- +``sys.modules`` is simply a dictionary that stores all teh already imported modules. +The keys are the module names, and the values are the module objects themselves: + .. code-block:: ipython + In [3]: import sys + In [4]: type(sys.modules) Out[4]: dict @@ -231,8 +236,7 @@ So, more or less, when you import a module, the interpreter: * Looks to see if the module is already in ``sys.modules``. -* If it is, it binds a name to the existing module in the current - module's namespace. +* If it is, it binds a name to the existing module in the current module's namespace. * If it isn't: diff --git a/source/supplemental/installing/python_for_windows.rst b/source/supplemental/installing/python_for_windows.rst index e2579c2e..1dca6fd8 100644 --- a/source/supplemental/installing/python_for_windows.rst +++ b/source/supplemental/installing/python_for_windows.rst @@ -187,7 +187,7 @@ All available from the command line. To try it out, you should be able to run all of these commands, and get something like the following results: -(recall that you can get out of the python or iPython command lines with ``ctrl+Z`` (ifthat doesn't work, try ``ctrl+D`` -- the *nix version)) +(recall that you can get out of the python or iPython command lines with ``ctrl+Z`` (if that doesn't work, try ``ctrl+D`` -- the \*nix version)) For Python: From e1f1e7693dcd1fb7d708d5b1d760ff35689ba1e5 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 20 Apr 2019 14:40:06 -0700 Subject: [PATCH 69/87] updated series template --- source/exercises/series_template.py | 17 ++++- .../solutions/Lesson02/series_non_recusive.py | 73 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 source/solutions/Lesson02/series_non_recusive.py diff --git a/source/exercises/series_template.py b/source/exercises/series_template.py index 0a87f509..fb161d70 100644 --- a/source/exercises/series_template.py +++ b/source/exercises/series_template.py @@ -21,11 +21,16 @@ def sum_series(n, n0=0, n1=1): :param n0=0: value of zeroth element in the series :param n1=1: value of first element in the series - + This function should generalize the fibonacci() and the lucas(), so that this function works for any first two numbers for a sum series. Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). And sum_series(n, 2, 1) should be equivalent to lucas(n). + + sum_series(n, 3, 2) should generate antoehr series with no specific name + + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies """ pass @@ -45,9 +50,19 @@ def sum_series(n, n0=0, n1=1): assert lucas(4) == 7 + # test that sum_series matches fibonacci assert sum_series(5) == fibonacci(5) + assert sum_series(7, 0, 1) == fibonacci(7) # test if sum_series matched lucas assert sum_series(5, 2, 1) == lucas(5) + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + print("tests passed") diff --git a/source/solutions/Lesson02/series_non_recusive.py b/source/solutions/Lesson02/series_non_recusive.py new file mode 100644 index 00000000..2ac4e070 --- /dev/null +++ b/source/solutions/Lesson02/series_non_recusive.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +""" +a template for the series assignment +""" + + +def fibonacci(n): + """ compute the nth Fibonacci number """ + a, b = 0, 1 + if n == 0: + return a + for _ in range(n - 1): + a, b = b, a + b + return b + + +def lucas(n): + """ compute the nth Lucas number """ + pass + + +def sum_series(n, n0=0, n1=1): + """ + compute the nth value of a summation series. + + :param n0=0: value of zeroth element in the series + :param n1=1: value of first element in the series + + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). + + sum_series(n, 3, 2) should generate antoehr series with no specific name + + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies + """ + pass + +if __name__ == "__main__": + # run some tests + assert fibonacci(0) == 0 + assert fibonacci(1) == 1 + assert fibonacci(2) == 1 + assert fibonacci(3) == 2 + assert fibonacci(4) == 3 + assert fibonacci(5) == 5 + assert fibonacci(6) == 8 + assert fibonacci(7) == 13 + + assert lucas(0) == 2 + assert lucas(1) == 1 + + assert lucas(4) == 7 + + # test that sum_series matches fibonacci + assert sum_series(5) == fibonacci(5) + assert sum_series(7, 0, 1) == fibonacci(7) + + # test if sum_series matched lucas + assert sum_series(5, 2, 1) == lucas(5) + + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + + print("tests passed") From c9c3ebff08271fef1f12ac4e1d741576104abf05 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 20 Apr 2019 14:42:09 -0700 Subject: [PATCH 70/87] added a non-recusive series solution --- source/solutions/Lesson02/series.py | 42 ++++++++++++++++--- .../solutions/Lesson02/series_non_recusive.py | 16 +++++-- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/source/solutions/Lesson02/series.py b/source/solutions/Lesson02/series.py index 082e4c1e..3bf35665 100755 --- a/source/solutions/Lesson02/series.py +++ b/source/solutions/Lesson02/series.py @@ -3,14 +3,25 @@ """ series.py -solutions to the Fibonacci Series and Lucas numbers +solutions to the Fibonacci Series, Lucas numbers and +generalized sum_series + +These solutions use recursion +-- calling a funciton from within that function. + +These series are defined "recusively", so it's a +really natural way to express the solution. However, +recursion can be substantially less efficient.n + +See series_non_recursive.py for a more efficient way +to do it. """ def fibonacci(n): """ compute the nth Fibonacci number """ - if n < 0: + if n < 0: # check for negative number -- just in case. return None elif n == 0: return 0 @@ -35,15 +46,22 @@ def lucas(n): def sum_series(n, n0=0, n1=1): """ - compute the nth value of a summation series. + Compute the nth value of a summation series. :param n0=0: value of zeroth element in the series :param n1=1: value of first element in the series - if n0 == 0 and n1 == 1, the result is the Fibbonacci series + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). + + sum_series(n, 3, 2) should generate antoehr series with no specific name - if n0 == 2 and n1 == 1, the result is the Lucas series + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies """ + if n < 0: return None if n == 0: @@ -55,6 +73,7 @@ def sum_series(n, n0=0, n1=1): # Can you re-define fibonacci and lucas by using sum_series? + if __name__ == "__main__": # run some tests @@ -94,4 +113,17 @@ def sum_series(n, n0=0, n1=1): for n in range(0, 10): assert sum_series(n, 2, 1) == lucas(n) + # test if sum_series works for negative value + # (it should return None) + + assert sum_series(-1, 3, 2) is None + + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + print("tests passed") diff --git a/source/solutions/Lesson02/series_non_recusive.py b/source/solutions/Lesson02/series_non_recusive.py index 2ac4e070..b6925b92 100644 --- a/source/solutions/Lesson02/series_non_recusive.py +++ b/source/solutions/Lesson02/series_non_recusive.py @@ -4,7 +4,6 @@ a template for the series assignment """ - def fibonacci(n): """ compute the nth Fibonacci number """ a, b = 0, 1 @@ -17,7 +16,12 @@ def fibonacci(n): def lucas(n): """ compute the nth Lucas number """ - pass + a, b = 2, 1 # notice that all I had to change from fib were these values? + if n == 0: + return a + for _ in range(n - 1): + a, b = b, a + b + return b def sum_series(n, n0=0, n1=1): @@ -37,7 +41,13 @@ def sum_series(n, n0=0, n1=1): The defaults are set to 0, 1, so if you don't pass in any values, you'll get the fibonacci sercies """ - pass + a, b = n0, n1 # notice that all I had to change from fib were these values? + if n == 0: + return a + for _ in range(n - 1): + a, b = b, a + b + return b + if __name__ == "__main__": # run some tests From 46c8594bcfda893f584fefec8472c1d8ced72abe Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 20 Apr 2019 15:28:44 -0700 Subject: [PATCH 71/87] clarifications on the sum_series assignment --- source/exercises/fib_and_lucas.rst | 62 ++++++++++++++----------- source/solutions/Lesson02/print_grid.py | 3 ++ 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/source/exercises/fib_and_lucas.rst b/source/exercises/fib_and_lucas.rst index ba0e5c21..b0243936 100644 --- a/source/exercises/fib_and_lucas.rst +++ b/source/exercises/fib_and_lucas.rst @@ -12,12 +12,15 @@ Goal: The `Fibonacci Series`_ is a numeric series starting with the integers 0 and 1. -In this series, the next integer is determined by summing the previous two. +In this series, the next integer is determined by summing the previous two + This gives us:: 0, 1, 1, 2, 3, 5, 8, 13, ... +.. note: 0+1 is 1; 1+1 is 2; 1+2 is 3; 2+3 is 5; 3+5 is 8; and so on forever... + We will write a function that computes this series -- then generalize it. .. _Fibonacci Series: http://en.wikipedia.org/wiki/Fibbonaci_Series @@ -31,12 +34,11 @@ Step 1 - The function should have one parameter ``n``. - - The function should return the ``nth`` value in the fibonacci series - (starting with zero index). + - The function should return the ``nth`` value in the fibonacci series (starting with zero index). * Ensure that your function has a well-formed ``docstring`` -Note that the fibinacci series is naturally recursive -- the value is +Note that the fibonacci series is naturally recursive -- the value is defined by previous values: fib(n) = fib(n-2) + fib(n-1) @@ -58,50 +60,58 @@ In your ``series.py`` module, add a new function ``lucas`` that returns the Ensure that your function has a well-formed ``docstring`` +YOu should find it's *very* similar to the ``fibonacci()`` function. + Generalizing ------------ -Both the *fibonacci series* and the *lucas numbers* are based on an identical -formula. +Both the *fibonacci series* and the *lucas numbers* are based on an identical formula: + +fib(n) = fib(n-2) + fib(n-1) + +That's why the code is so similar. + +This formula creates a class of series that are all related -- each with a different two starting numbers. + +Add a third function called ``sum_series`` that can compute all of these related series. + +It should have one required parameter and two optional parameters. +The required parameter will determine which element in the +series to print. +The two optional parameters will have default values of 0 and 1 and will determine the first two values for the series to be produced. + +Calling this function with no optional parameters will produce numbers from the *fibonacci series* (because 0 and 1 are the defaults). -Add a third function called ``sum_series`` with one required parameter and two -optional parameters. The required parameter will determine which element in the -series to print. The two optional parameters will have default values of 0 and -1 and will determine the first two values for the series to be produced. +Calling it with the optional arguments 2 and 1 will +produce values from the *lucas numbers*. -Calling this function with no optional parameters will produce numbers from the -*fibonacci series*. Calling it with the optional arguments 2 and 1 will -produce values from the *lucas numbers*. Other values for the optional -parameters will produce other series. +Other values for the optional parameters will produce other series. **Note:** While you *could* check the input arguments, and then call one of the functions you wrote, the idea of this exercise is to make a general -function, rather than one specialized. So you should reimplement the code +function, rather than one specialized. So you should re-implement the code in this function. -In fact, you could go back and reimplement your fibonacci and lucas -functions to call this one with particular arguments. +In fact, you could go back and re-implement your fibonacci and lucas +functions to call ``sum-series`` with particular arguments. Ensure that your function has a well-formed ``docstring`` Tests... -------- -Add a block of code to the end of your ``series.py`` -module. Use the block to write a series of ``assert`` statements that +Add a block of code to the end of your ``series.py`` module. +Use the block to write a series of ``assert`` statements that demonstrate that your three functions work properly. Use comments in this block to inform the observer what your tests do. -We have created a template for you to use, to clarify what we mean by these -tests: +We have created a template for you to use to clarify what we mean by these asserts: :download:`series_template.py <../exercises/series_template.py>` -Add your new module to your git clone and commit frequently while working on -your implementation. Include good commit messages that explain concisely both -*what* you are doing and *why*. +Add your new module to your personal git repo and commit frequently while working on your implementation. +Include good commit messages that explain concisely both *what* you are doing and *why*. -When you are finished, push your changes to your fork of the class repository -in GitHub and make a pull request. +When you are finished, push your changes to your fork of the class repository in GitHub and make a pull request. diff --git a/source/solutions/Lesson02/print_grid.py b/source/solutions/Lesson02/print_grid.py index a270eaf7..651b3bf8 100755 --- a/source/solutions/Lesson02/print_grid.py +++ b/source/solutions/Lesson02/print_grid.py @@ -14,6 +14,9 @@ def print_grid_trivial(): """ Did anyone come up with the most trivial possible solution? + + Note that this may the the wy to go, oif this all you need to do: + print something specific once. Why write fancy code for that? """ print(""" + - - - - + - - - - + From d227300b5bc743fc51c281f0ec923cc0f861800a Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 20 Apr 2019 17:25:45 -0700 Subject: [PATCH 72/87] updated string notes --- source/modules/Strings.rst | 165 ++++++++++++++++++++++++++++--------- source/modules/Unicode.rst | 86 +++++++++++++------ 2 files changed, 183 insertions(+), 68 deletions(-) diff --git a/source/modules/Strings.rst b/source/modules/Strings.rst index d476e186..ac712fea 100644 --- a/source/modules/Strings.rst +++ b/source/modules/Strings.rst @@ -9,7 +9,26 @@ Strings Strings ======= -A "String" is a computerese word for a piece of text -- a "string" of characters. +.. admonition:: Joke + + A piece of string is new in town, and looking for a drink. He sees a local bar, walks in, sits down, and orders a beer. The bartender looks at him askance, and says: "wait a minute, are you a piece of string?". "Why yes", the string replies. The bartender growls back: "We don't serve your kind in here -- you get out!". + + Disappointed, the piece of string leaves and heads down the street to the next bar, but this time, he barely gets to a seat before being yelled at -- "we don't want any string in here -- you get out!" + + A third bar, and he has a similar encounter. Now he's getting pretty distraught and confused -- "what have they got against string in this town?", he asks himself. But he's also tired and thirsty, so he gets an idea. + + The piece of string twists himself all up, winding himself around and around. Then he reaches up and fluffs up the top of his head. + + Thus prepared, he heads into yet another bar. This time is different. He walks in, sits down, the bartender takes his order -- all good. But then just as the bartender is putting his beer down he stops, and looks hard at him: "wait a minute! you're a piece of string, aren't you?". + + Full of confidence, the string replies: "Nope, I'm a frayed knot." + +A "String" is a computerese word for a piece of text -- a "string" of characters. Why "string"? "String" can be used to mean "(a linear sequence (as of characters, words, proteins, etc.)" +(`definition of string `_) + +So a string is a sequence of individual letters or characters. + +In Python, each character can be a `Unicode `_ character -- that is, any character in any language in the world. Having this built in by default in Python (3) means that you can get very far simply ignoring it -- anything you can type on your computer can be used in strings in Python. If you do need to work with non-English characters, or data encoded in non-utf-8, particularly on Python 2, here are some notes about that: :ref:`unicode`. But for the most part, in Python3 -- strings are text, and text is string, and that's that. Creating strings: ----------------- @@ -18,6 +37,8 @@ A string literal creates a string type. (we've seen this already...) +A literal can be delineated with single or double quotes, alone or in triples. + :: "this is a string" @@ -26,45 +47,52 @@ A string literal creates a string type. """and this also""" - '''and even this''' + '''and even this + triple quotes preserve newlines + so this is three lines''' -You can also use ``str()`` +You can also use call the string object (``str()``) to "make" a string out of other data types. .. code-block:: ipython In [256]: str(34) Out[256]: '34' -And strings can be read from files or other sources of I/O. +Strings can also be read from files or other sources of I/O. String Methods =============== -String objects have a lot of methods. +The python string object is very powerful with lots of methods for common text manipulation. "methods" are functions defined on an object itself (more on that when we get to OO programming). But it means that you have many ways to manipulate text built right into the string objects themselves. -Here are just a few: +Note that strings are "immutable" --they can not be changed once they have been created. So the string methods all return new objects, rather than change the string in place. + +Here are just a few of the more common string methods: Splitting and Joining Strings ----------------------------- -``split`` and ``join``: +``split`` and ``join`` can be used to break up a string into pieces, or make one big string out of multiple smaller pieces: .. code-block:: ipython - In [167]: csv = "comma, separated, values" - In [168]: csv.split(', ') + In [167]: csv = "comma,separated,values" + + In [168]: csv.split(',') Out[168]: ['comma', 'separated', 'values'] - In [169]: psv = '|'.join(csv.split(', ')) + + In [169]: psv = '|'.join(csv.split(',')) + In [170]: psv Out[170]: 'comma|separated|values' -It may seem odd at first that ``.join()`` is a string method, rather than, say, a method on lists. But, in fact it makes a lot of sense. Lists (and tuples, and other sequences) can hold any type of data -- and "joining" arbitrary data types doesn't make any sense. Joining is strictly a string activity. +It may seem odd at first that ``.join()`` is a string method, rather than, say, a method on lists. But in fact, it makes a lot of sense. Lists (and tuples, and other sequences) can hold any type of data -- and "joining" arbitrary data types doesn't make any sense. Joining is strictly a string activity. And you need a string so you can join the parts -- therefore, we need a string object in there somewhere anyway. -Lastly, having join() be a string method means that it can join strings in ANY iterable object -- not just the built-in sequence types. +Lastly, having join() be a string method means that it can join strings in ANY iterable object -- not just lists or other built-in sequence types. -So it does make sense -- but even if not, that's the way it is. +So it does make sense -- but even if doesn't make sense to you, that's the way it is -- so remember that you call ``.join()`` on the string you want to join things with. So to be clear: if you have a bunch of strings in a sequence and you want to put them together, you create a string with the character (or characters) you want to join them with, and call join() on that object: @@ -80,10 +108,17 @@ So to be clear: if you have a bunch of strings in a sequence and you want to put In [23]: "".join(["these", "are", "some", "strings"]) Out[23]: 'thesearesomestrings' -Building up a long string. +Maybe not very common, but you can join with a longer string as well: + +.. code-block:: ipython + + In [5]: " --#-- ".join(["these", "are", "some", "strings"]) + Out[5]: 'these --#-- are --#-- some --#-- strings' + +Building up a Long String. -------------------------- -The obvious thing to do is something like: +An obvious thing to do is something like: .. code-block:: python @@ -91,7 +126,7 @@ The obvious thing to do is something like: for piece in list_of_stuff: msg += piece -But: strings are immutable -- Python needs to create a new string each time you add a piece -- not efficient: +But: strings are immutable -- Python needs to create a new string each time you add a piece, which is not very efficient. So it's better to gather all the pieces together in a list, and then join them together: .. code-block:: python @@ -100,8 +135,7 @@ But: strings are immutable -- Python needs to create a new string each time you msg.append(piece) " ".join(msg) -appending to lists is efficient -- and so is the join() method of strings. - +appending to lists is efficient -- and so is the ``join()`` method of strings. Case Switching -------------- @@ -109,29 +143,38 @@ Case Switching .. code-block:: ipython In [171]: sample = 'A long string of words' + In [172]: sample.upper() Out[172]: 'A LONG STRING OF WORDS' + In [173]: sample.lower() Out[173]: 'a long string of words' + In [174]: sample.swapcase() Out[174]: 'a LONG STRING OF WORDS' + In [175]: sample.title() Out[175]: 'A Long String Of Words' -Testing --------- +Testing for certain classes of characters +----------------------------------------- .. code-block:: ipython In [181]: number = "12345" + In [182]: number.isnumeric() Out[182]: True + In [183]: number.isalnum() Out[183]: True + In [184]: number.isalpha() Out[184]: False + In [185]: fancy = "Th!$ $tr!ng h@$ $ymb0l$" + In [186]: fancy.isalnum() Out[186]: False @@ -139,6 +182,10 @@ Testing String Literals ----------------- +Sometimes when you are creating a string, you want to put an non-normal character in there --one that isn't strictly a letter or symbol, such as newlines, etc. + +To do that, python support a set of "escape" sequences -- when a character follows a backslash, it gets interpreted as having a particular meaning. + Common Escape Sequences:: \\ Backslash (\) @@ -149,8 +196,10 @@ Common Escape Sequences:: \t ASCII Horizontal Tab (TAB) \ooo Character with octal value ooo \xhh Character with hex value hh + \uxxxx Charactor with Unicode code point value xxxx + \N{char-name} Charactor with Unicdoe name char_name -for example -- for tab-separated values: +For example -- for tab-separated values: .. code-block:: ipython @@ -160,11 +209,14 @@ for example -- for tab-separated values: these are separated by tabs https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + https://docs.python.org/3/library/stdtypes.html#string-methods Raw Strings ------------ +There are times when you want a literal backslash in your string: Windows file paths, regular expressions. Tomake this easy, Pyhton support "raw" strings -- string literals where the backslash does not have special meaning: + Add an ``r`` in front of the string literal: **Escape Sequences Ignored** @@ -174,6 +226,7 @@ Add an ``r`` in front of the string literal: In [408]: print("this\nthat") this that + In [409]: print(r"this\nthat") this\nthat @@ -184,10 +237,33 @@ Add an ``r`` in front of the string literal: In [415]: r"\" SyntaxError: EOL while scanning string literal -putting a backslash right before the end quote confuses the interpreter! +Putting a backslash right before the end quote confuses the interpreter! + +Raw strings can be very handy for things like regular expressions that need embedded backslashes. + +Building Long String Literals +----------------------------- + +If you put two string literals next to each other in the code, Python will join them into one when compiling: -This can be very handy for things like regular expressions that need embedded backslashes. +.. code-block:: ipython + In [6]: "this" "that" + Out[6]: 'thisthat' + +(note: no comma in between!) +THis may not look useful, but when combined with the fact that Python joins together lines when inside a parentheses, it can be a nice way to make larger string literals: + +.. code-block:: ipython + + In [7]: print("This is the first line\n" + ...: "And here is another line\n" + ...: "If I don't put in a newline" + ...: "I can get an very long line in, without making the" + ...: "line of code too long.") + This is the first line + And here is another line + If I don't put in a newlineI can get an very long line in, without making the line of code too long. Ordinal values -------------- @@ -196,27 +272,33 @@ Characters in strings are stored as numeric values: * "ASCII" values: 1-127 -* Unicode values -- 1 - 1,114,111 (!!!) +* Unicode "code points" -- 1 - 1,114,111 (!!!) -Unicode supports a LOT of characters --every character in every language known to man -- and then some :-) +Unicode supports a LOT of characters -- every character in every language known to man -- and then some :-). The Unicode code poitns for the characters in the ASCII character set are the same as ASCII -- so handy for us English speakers. -To get the value: +To get the value, use ``ord()``: .. code-block:: ipython In [109]: for i in 'Chris': .....: print(ord(i), end=' ') 67 104 114 105 115 + +To get the character from the code point, use ``chr()``: + +.. code-block:: ipython + In [110]: for i in (67,104,114,105,115): .....: print(chr(i), end='') Chris -For the English language, stick with ASCII, otherwise use full Unicode: it's easy with Python3 -- more on that in a later lesson. - +For the English language, stick with ASCII, otherwise use, full Unicode: it's easy with Python3 -Building Strings from data +Building Strings from Data -------------------------- +We often have some data in Python variables -- maybe strings, maybe numbers -- and we often want to combine that data with text to make a custom message of some sort. + You could, but please don't(!), do this: .. code-block:: python @@ -225,11 +307,12 @@ You could, but please don't(!), do this: (I know -- we did that in the grid_printing exercise) -Do this instead: +Why not? It's slow and not very flexible. Python provides a few ways to "format" text, so you can do this instead: -.. code-block:: python +.. code-block:: ipython - 'Hello {}!'.format(name) + In [11]: 'Hello {}!'.format(name) + Out[11]: 'Hello Chris!' It's much faster and safer, and easier to modify as code gets complicated. @@ -249,12 +332,12 @@ This is very similar to C-style string formatting (`sprintf`). It's still around, and handy --- but ... -The "new" ``format()`` method is more powerful and flexible, so we'll focus on that in this class. +The "new" ``format()`` method is more powerful and flexible, so we'll focus on that in this class. And there is now the newer "f-strings" (see below) which provide a lot of that "quick and dirty" convenience, while using the same formatting codes as ``.format()`` String Formatting ----------------- -The string ``format()`` method: +The string ``.format()`` method: .. code-block:: ipython @@ -292,7 +375,7 @@ The counts must agree: IndexError: tuple index out of range -Named placeholders: +Named Placeholders: ------------------- .. code-block:: ipython @@ -320,7 +403,7 @@ The format operator works with string variables, too: In [82]: s.format(a, b, a/b) Out[82]: '12 / 3 = 4.000000' -So you can dynamically build a format string, and then use it in multiple places in the code. +So you can save a format string, or even built it up dynamically, and then use it in multiple places in the code. Complex Formatting @@ -390,13 +473,13 @@ For this most simple example:: f"some text: {expression}" -`expression` is any valid python expression(remember that an expression is a combination of values and operators and names that produces a value). +`expression` is any valid python expression (remember that an expression is a combination of values and operators and names that produces a value). The expression is evaluated, and then, if it is not a string, it is converted to one, so it's really:: f"some text: {str(expression)}" -Let's see how that works in practice: +Let's see how this works in practice: .. code-block:: ipython @@ -439,14 +522,14 @@ And it has to be an expression, not a statement -- so you can't put a for loop o You can see how this can be a very powerful and quick way to get things done. -f-string use +F-string Use ------------ -F-strings are a very new Python feature. They will cause a syntax error in any Python version older than 3.6 -- and 3.6 was first released on December 23, 2016 -- less than a year from this writing. +F-strings are a fairly new Python feature. They will cause a syntax error in any Python version older than 3.6 -- 3.6 was first released on December 23, 2016 -- only a couple years from this writing. So there is not much out there in the wild, and I have yet to see it in production code. -They are not currently used in any of the examples in this course. +They are not currently used in many of the examples in this course. Nevertheless, they are a nifty feature that could be very useful, so feel free to use them where it makes you code cleaner and clearer. diff --git a/source/modules/Unicode.rst b/source/modules/Unicode.rst index e2c0f49f..9f2ca514 100644 --- a/source/modules/Unicode.rst +++ b/source/modules/Unicode.rst @@ -36,11 +36,11 @@ What the heck is Unicode anyway? * MacRoman, Windows 1252, etc... - * There is now "latin-1", but still a lot of old files around. + * There is now "latin-1", a 1-byte encoding suitable for European languages -- but still a lot of old files around that use the old ones. * Non-Western European languages required totally incompatible 1-byte encodings -* No way to mix languages with different alphabets. +* This means there was no way to mix languages with different alphabets in the same document (web page, etc.) Enter Unicode @@ -52,14 +52,15 @@ The Unicode idea is pretty simple: But how do you express that in bytes? * Early days: we can fit all the code points in a two byte integer (65536 characters) - * Turns out that didn't work -- now need 32 bit integer to hold all of unicode "raw" (UTC-4) + * Turns out that didn't work -- 65536 is not enough for all languages. So we now need 32 bit integer to hold all of Unicode "raw" (UTC-4). + * But it's a waste of space to use 4 full bytes for each character, when so many don't require that much space. Enter "encodings": * An encoding is a way to map specific bytes to a code point. * Each code point can be represented by one or more bytes. - * Each encoding is different -- if you don't know the encoding, you don't know how to interpret the bytes! (though maybe you can guess...) + * Each encoding is different -- if you don't know the encoding, you don't know how to interpret the bytes! (though maybe you can guess) Unicode @@ -79,7 +80,7 @@ http://www.joelonsoftware.com/articles/Unicode.html * Python provides some abstractions to make it easier to deal with bytes -**Unicode is a biggie** +**Unicode is a Biggie** Actually, dealing with numbers rather than bytes is big @@ -92,12 +93,12 @@ Mechanics What are strings? ----------------- -Py2 strings were simply sequences of bytes. When text was one per charater that worked fine. +Py2 strings were simply sequences of bytes. When text was one per character that worked fine. -Py3 strings (or Unicode strings in py2) are sequences of platonic characters. +Py3 strings (or Unicode strings in py2) are sequences of "platonic characters". It's almost one code point per character -- there are complications -with combined characters: accents, etc -- but we can mostly ignore those. +with combined characters: accents, etc -- but we can mostly ignore those -- you will get far thiking of a code point as a character. Platonic characters cannot be written to disk or network! @@ -153,12 +154,12 @@ If you need to deal with the actual bytes for some reason, you may need to conve And can get even more confusing with py2 strings being *both* text and bytes! -This is actually one of teh biggest differences between Python 2 and Python 3. As an ordinary user (particulary one that used English...), you may not notice -- text is text, and things generally "just work", but under the hood it is very different, and folks writting libraries for things like internet protocols struggle with the differences. +This is actually one of the biggest differences between Python 2 and Python 3. As an ordinary user (particularly one that used English...), you may not notice -- text is text, and things generally "just work", but under the hood it is very different, and folks writing libraries for things like Internet protocols struggle with the differences. -Using unicode in Py2 +Using Unicode in Py2 --------------------- -IF you do need to write Python2 code, you really should use Unicode. +If you do need to write Python2 code, you really should use Unicode. Here are the basics: @@ -215,23 +216,24 @@ Encoding and Decoding Unicode Literals ------------------ -1) Use unicode in your source files: +1) Use Unicode in your source files: .. code-block:: python # -*- coding: utf-8 -*- -2) Escape the unicode characters: +(This is only required on Py2 -- the UTF-8 encoding is default for Python 3) + +2) Escape the Unicode characters: .. code-block:: python print u"The integral sign: \u222B" print u"The integral sign: \N{integral}" -Lots of tables of code points online: +Lots of tables of code points are available online: -One example: - http://inamidst.com/stuff/unidata/ +One example: http://inamidst.com/stuff/unidata/ :download:`hello_unicode.py <../examples/unicode/hello_unicode.py>`. @@ -258,10 +260,10 @@ Python has a default encoding (usually ascii) The default encoding will get used in unexpected places! -Using Unicode everywhere +Using Unicode Everywhere ------------------------- -Python 2.6 and above have a nice feature to make it easier to use unicode everywhere +Python 2.6 and above have a nice feature to make it easier to use Unicode everywhere .. code-block:: python @@ -280,9 +282,9 @@ After running that line, the ``u''`` is assumed In [5]: type(s) Out[5]: unicode -NOTE: You can still get py2 strings from other sources! +NOTE: You can still get py2 strings from other sources! So you still need to think about ``str`` vs ``unicdode`` -This is a really good idea if you want to write code compatible with Python2 and 3 +This is a really good idea if you want to write code compatible with Python2 and 3. Encodings ---------- @@ -310,11 +312,11 @@ Probably the one you'll use most -- most common in Internet protocols (xml, JSON Nice properties: -* ASCII compatible: First 127 characters are the same +* ASCII compatible: First 127 characters are the same as ASCII * Any ascii string is a utf-8 string -* Compact for mostly-english text. +* Compact for mostly-English text. Gotchas: @@ -327,7 +329,7 @@ UTF-16 Kind of like UTF-8, except it uses at least 16bits (2 bytes) for each character: NOT ASCII compatible. -But is still needs more than two bytes for some code points, so you still can't process it as one per character. +But is still needs more than two bytes for some code points, so you still can't process it as two bytes per character. In C/C++ held in a "wide char" or "wide string". @@ -341,7 +343,7 @@ There is a lot of criticism on the net about UTF-16 -- it's kind of the worst of * You can't assume every character is the same number of bytes * It takes up more memory than UTF-8 -`UTF Considered Harmful `_ +`UTF-16 Considered Harmful `_ But to be fair: @@ -393,6 +395,8 @@ use io.open: (https://docs.python.org/2/library/io.html#module-interface) +.. note: This is all for Python 2 -- the built in ``open`` in Py3 does utf-8 by default. + Encodings Built-in to Python: http://docs.python.org/2/library/codecs.html#standard-encodings @@ -436,14 +440,14 @@ Exception messages: Unicode in Python 3 ---------------------- -The "string" object is unicode. +The "string" object **is** Unicode (always). Py3 has two distinct concepts: -* "text" -- uses the str object (which is always unicode!) +* "text" -- uses the str object (which is always Unicode!) * "binary data" -- uses bytes or bytearray -Everything that's about text is unicode. +Everything that's about text is Unicode. Everything that requires binary data uses bytes. @@ -451,6 +455,34 @@ It's all much cleaner. (by the way, the recent implementations are very efficient...) +So you can pretty much ignore encodings and all that for most basic text processing. +If you do find yourself needing to deal with binary data, you ay need to encode/decode stuff yourself. IN which case, Python provides an ``.encode()`` method on strings that encode the string to a bytes object with the encoding you select: + +.. code-block:: ipython + + In [3]: this_in_utf16 = "this".encode('utf-16') + + In [4]: this_in_utf16 + Out[4]: b'\xff\xfet\x00h\x00i\x00s\x00' + +And bytes objects have a ``.decode`` method that decodes the bytes and makes a string object: + + In [5]: this_in_utf16.decode('utf-16') + Out[5]: 'this' + +It's all quite simple an robust. + +.. note:: + During the long and painful transition from Python2 to Python3, the Unicode-always string type was a major source of complaints. There are many rants and `well thought out posts `_ about it still available on the internet. It was enough to think that Python had made a huge mistake. + + But there are a couple key points to remember: + + * The primary people struggling were those that wrote (or wored with) libraries that had to deal with protocols that used both binary and text data in the same data stream. + + * As of Python 3.4 or so, the python string object had grown the features it needed to support even those ugly binary+text use cases. + + For a typical user, the Python3 text model is MUCH easier to deal with and less error prone. + Exercises ========= From 62896ee875dde86e4863cf134feec0a9ec852c12 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 27 Apr 2019 13:53:35 -0700 Subject: [PATCH 73/87] updated the Naming Things page. --- source/modules/NamingThings.rst | 43 +++++++++++++++++++ source/solutions/Lesson02/fizz_buzz.py | 20 ++++++++- .../solutions/Lesson02/fizz_buzz_one_liner.py | 6 +-- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/source/modules/NamingThings.rst b/source/modules/NamingThings.rst index 362ada94..e0cc5a8b 100644 --- a/source/modules/NamingThings.rst +++ b/source/modules/NamingThings.rst @@ -91,6 +91,49 @@ And then singular for a single item in that collection: line = line.replace(",", " ") .... +What about Hungarian Notation? +------------------------------ + +`Hungarian Notation `_ +is a naming system where the data type is part of the name: + +.. code-block:: python + + strFirstName = "Chris" + + listDonations = [400.0, 125.0, 1000.0] + + int_num_days = 30 + +This method is not recommended nor widely used in the Python community. + +One reason is Python's dynamic typing -- it really isn't important what type a value is, but rather, what it means. +And you may end up refactoring the code to use a different type, and then do you want to have to rename everything? +Or worse, the type in the name no longer matches the actual type in the code -- and that's really bad. I have seen code like this: + +.. code-block:: python + + strNumber = input("How many would you like?") + strNumber = int(strNumber) + + for i in range(strNumber): + ... + +So you have a name used for a string, then it gets converted to an integer, and the data type no longer matches the name. Wouldn't you be better off if that had never been named with the type in the first place? + +While widely used in some circles, it is generally considered bad style in the Python community -- so: + + **Do not use Hungarian Notation** + +More About Naming Things +------------------------ + Here's a nice talk about naming: `Jack Diederich: Name things Once `_ + +One note about that talk -- Jack is mostly encouraging people to not use names that are too long and unnecessarily specific. +However, with beginners, it's often tempting to use names that are too *short* and *non-specific*, like "x" and "item" -- so you need to strike a balance. + + + diff --git a/source/solutions/Lesson02/fizz_buzz.py b/source/solutions/Lesson02/fizz_buzz.py index 6204fc8c..45b9fd73 100755 --- a/source/solutions/Lesson02/fizz_buzz.py +++ b/source/solutions/Lesson02/fizz_buzz.py @@ -18,15 +18,31 @@ def fizzbuzz1(n): print(i) +def fizzbuzz1b(n): + """ + Save one computation -- if it's a multiple of 3 and 5, it's a + multiple of 15 + """ + for i in range(1, n + 1): + if i % 15 == 0: + print("FizzBuzz") + elif i % 3 == 0: + print("Fizz") + elif i % 5 == 0: + print("Buzz") + else: + print(i) + + def fizzbuzz2(n): """ Why evaluate i%3 and i%5 twice? """ for i in range(1, n + 1): msg = '' - if i % 3 == 0: + if not i % 3: msg += "Fizz" - if i % 5 == 0: + if not i % 5: msg += "Buzz" if msg: print(msg) diff --git a/source/solutions/Lesson02/fizz_buzz_one_liner.py b/source/solutions/Lesson02/fizz_buzz_one_liner.py index f77dde9f..494c5ecd 100644 --- a/source/solutions/Lesson02/fizz_buzz_one_liner.py +++ b/source/solutions/Lesson02/fizz_buzz_one_liner.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # This One Liner solution to the Fizz Buzz problem -# was found by a student on the internet +# was found by a student on the Internet for i in range(1,101): print([i,'Fizz','Buzz','FizzBuzz'][(i%3==0)+2*(i%5==0)]) # this is a good example of why the most compact code is not always the # best -- readability counts! -# And this is pretty impenatrable. +# And this is pretty impenetrable. # but it's also pretty nifty logic, so below, -# It's unpacked to make it easeir to understand. +# It's unpacked to make it easier to understand. # first, add some white space to make it pep8 compatible, and more readable. From 9b76e0e9e6bc8742918a5f4215015f3b756ef623 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 5 May 2019 11:44:55 -0700 Subject: [PATCH 74/87] a bit of clarification --- source/exercises/dict_lab.rst | 9 ++++++++- source/exercises/file_lab.rst | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/source/exercises/dict_lab.rst b/source/exercises/dict_lab.rst index e048e032..6e1d100a 100644 --- a/source/exercises/dict_lab.rst +++ b/source/exercises/dict_lab.rst @@ -76,11 +76,18 @@ Dictionaries 2 * Using the dictionary from item 1: Make a dictionary using the same keys but with the number of 't's in each value as the value (consider upper and lower case?). + The result should look something like:: + + {"name": 0 + "city": 2 + "cake": 2 + } + Sets ---- * Create sets s2, s3 and s4 that contain numbers from zero through twenty, - divisible by 2, 3 and 4. + divisible by 2, 3 and 4 (figure out a way to compute those -- don't just type them in). * Display the sets. diff --git a/source/exercises/file_lab.rst b/source/exercises/file_lab.rst index a7b26bb3..a05fcdca 100644 --- a/source/exercises/file_lab.rst +++ b/source/exercises/file_lab.rst @@ -29,7 +29,9 @@ Paths and File Processing writing). Note that for binary files, you can't use ``readline()`` -- lines don't have any meaning for binary files. - - Test it with both text and binary files (maybe jpeg or something of your choosing). + - Test it with both text and binary files (maybe a jpeg or something of your choosing). + + - This should only be a few lines of code :-) File reading and parsing From 2f5ed8edc582fe2d5432464c8ff335cbef2f66c7 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 5 May 2019 11:46:23 -0700 Subject: [PATCH 75/87] typo --- source/exercises/dict_lab.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/exercises/dict_lab.rst b/source/exercises/dict_lab.rst index 6e1d100a..9c863c54 100644 --- a/source/exercises/dict_lab.rst +++ b/source/exercises/dict_lab.rst @@ -102,5 +102,5 @@ Sets 2 * Create a frozenset with the letters in 'marathon'. -* display the union and intersection of the two sets. +* Display the union and intersection of the two sets. From 178bbe711588f37d9e6e21672f86991fb7907064 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 5 May 2019 12:06:25 -0700 Subject: [PATCH 76/87] a bit of clarification and simiplification --- source/examples/file_exercise/students.txt | 4 ++-- source/exercises/file_lab.rst | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/source/examples/file_exercise/students.txt b/source/examples/file_exercise/students.txt index dc8508d5..a85853a8 100644 --- a/source/examples/file_exercise/students.txt +++ b/source/examples/file_exercise/students.txt @@ -23,8 +23,8 @@ Mitchell, Joni: php, mysql, python Ramone, John: Johnny, rex, db King, Carol: r Waters, Muddy: perl, python -Star, Richard: Ringo, Tom, shell, python, vb -Smith, Patricia: Patti, Gene, python +Star, Richard: Ringo, shell, python, vb +Smith, Patricia: Patti, python Morrison, Jim: fortran, perl, sql, python Marley, Robert: Bob, c, c++, lisp Simon, Paul: bash, python, sql diff --git a/source/exercises/file_lab.rst b/source/exercises/file_lab.rst index a05fcdca..9aa4dfd9 100644 --- a/source/exercises/file_lab.rst +++ b/source/exercises/file_lab.rst @@ -16,13 +16,10 @@ Paths and File Processing ========================= * Write a program which prints the full path for all files in the current - directory, one per line + directory, one per line. Use either the ``os`` module or ``pathlib``. * Write a program which copies a file from a source, to a destination - (without using shutil, or the OS copy command). - - - Advanced: make it work for any size file: i.e. don't read the entire - contents of the file into memory at once. + (without using shutil, or the OS copy command (you are essentially writing a simple version of the OS copy command)). - This should work for any kind of file, so you need to open the files in binary mode: ``open(filename, 'rb')`` (or ``'wb'`` for @@ -31,6 +28,9 @@ Paths and File Processing - Test it with both text and binary files (maybe a jpeg or something of your choosing). + - Advanced: make it work for any size file: i.e. don't read the entire + contents of the file into memory at once. + - This should only be a few lines of code :-) @@ -43,9 +43,9 @@ Download this text file: In it, you will find a list of names and what programming languages they have used in the past. This may be similar to a list generated at the beginning of this class. -Write a little script that reads that file, and generates a list of all the languages that have been used. +Write a little script that reads that file and generates a list of all the languages that have been used. -What might be the best data structure to use to keep track of bunch of values without duplication? +What might be the best data structure to use to keep track of bunch of values (the languages) without duplication? The file format: ---------------- @@ -62,5 +62,7 @@ So a colon after the name, then the nickname, and then one or more languages. However, like real data files, the file is NOT well-formed. Only some lines have nicknames, and other small differences, so you will need to write some code to make sure you get it all correct. +How can you tell the difference between a nickname and a language? + Extra challenge: keep track of how many students specified each language. From d088f58b1771d07728beea4e7a535d43dc984235 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 5 May 2019 12:30:00 -0700 Subject: [PATCH 77/87] updating mailroom solution --- source/solutions/Lesson04/mailroom2.py | 59 +++-- .../mailroom/mailroom_basic/mailroom.py | 159 ------------- .../mailroom/mailroom_basic/mailroom2.py | 220 ------------------ 3 files changed, 40 insertions(+), 398 deletions(-) delete mode 100755 source/solutions/mailroom/mailroom_basic/mailroom.py delete mode 100755 source/solutions/mailroom/mailroom_basic/mailroom2.py diff --git a/source/solutions/Lesson04/mailroom2.py b/source/solutions/Lesson04/mailroom2.py index c4ca1790..a586655e 100755 --- a/source/solutions/Lesson04/mailroom2.py +++ b/source/solutions/Lesson04/mailroom2.py @@ -2,8 +2,11 @@ """ mailroom assignment -This version uses a dict for the main db, and exception handling to -check input +This version uses a dict for the main db, and a dict to"switch" on the user's +input choices. + +it also write the thank you letters to files. + """ import sys @@ -20,6 +23,7 @@ # This makes it easier to have a 'normalized' key. # you could get a bit fancier by having each "record" be a dict, with # "name" and "donations" as keys. + def get_donor_db(): return {'william gates iii': ("William Gates III", [653772.32, 12.17]), 'jeff bezos': ("Jeff Bezos", [877.33]), @@ -65,20 +69,7 @@ def add_donor(name): return donor -def main_menu_selection(): - """ - Print out the main application menu and then read the user input. - """ - action = input(dedent(''' - Choose an action: - - 1 - Send a Thank You - 2 - Create a Report - 3 - Send letters to everyone - 4 - Quit - > ''')) - return action.strip() def gen_letter(donor): @@ -195,7 +186,7 @@ def save_letters_to_disk(): letter = gen_letter(donor) # I don't like spaces in filenames... filename = donor[0].replace(" ", "_") + ".txt" - print("writting letter to:", donor[0]) + print("writing letter to:", donor[0]) open(filename, 'w').write(letter) @@ -213,18 +204,48 @@ def quit(): sys.exit(0) -if __name__ == "__main__": +def main_menu(): + """ + Run the main menu for mailroom + """ + prompt = dedent(''' + Choose an action: - donor_db = get_donor_db() + 1 - Send a Thank You + 2 - Create a Report + 3 - Send letters to everyone + 4 - Quit + + > ''') selection_dict = {"1": send_thank_you, "2": print_donor_report, "3": save_letters_to_disk, "4": quit} + run_menu(prompt, selection_dict) + + +def run_menu(prompt, selection_dict): + """ + run an interactive menu + + :param prompt: What you want to ask the user + + :param selection_dict: Dict of possible user impots mapped to + the actions to take. + """ while True: - selection = main_menu_selection() + + selection = input(prompt).strip() try: + # This calls teh function in the selection_dict selection_dict[selection]() except KeyError: print("error: menu selection is invalid!") + +if __name__ == "__main__": + + donor_db = get_donor_db() + + main_menu() diff --git a/source/solutions/mailroom/mailroom_basic/mailroom.py b/source/solutions/mailroom/mailroom_basic/mailroom.py deleted file mode 100755 index 9fd134a0..00000000 --- a/source/solutions/mailroom/mailroom_basic/mailroom.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 - -""" -Mailroom Exercise -- as of Session 3 -- no dictionaries or Exceptions -""" - -from textwrap import dedent # nifty utility! -import math - -# In memory representation of the donor database -# using a tuple for each donor -# -- kind of like a record in a database table -# the donations are in a list -- so you can add to them -# Note the mutable inside an immutable - -donor_db = [("William Gates, III", [653772.32, 12.17]), - ("Jeff Bezos", [877.33]), - ("Paul Allen", [663.23, 43.87, 1.32]), - ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]), - ] - -#loop through the donor list and print the 0th element of the list -def print_donors(): - print("Donor list:\n") - for donor in donor_db: - print(donor[0]) - - -def find_donor(name): - """ - find a donor in the donor db - - :param: the name of the donor - - :returns: The donor data structure -- None if not in the donor_db - """ - for donor in donor_db: - # do a case-insenstive compare - if name.strip().lower() == donor[0].lower(): - return donor - return None - - -def main_menu_selection(): - """ - Print out the main application menu and then read the user input. - """ - action = input(dedent(''' - Choose an action: - - 't' - Send a Thank You - 'r' - Create a Report - 'q' - Quit - - > ''')) - return action.strip() - - -def gen_letter(donor): - """ - Generate a thank you letter for the donor - - :param: donor tuple - - :returns: string with letter - """ - return dedent(''' - Dear {} - - Thank you for your very kind donation of ${:.2f}. - It will be put to very good use. - - Sincerely, - -The Team - '''.format(donor[0], donor[1][-1])) - - -def send_thank_you(): - """ - Execute the logic to record a donation and generate a thank you message. - """ - # Read a valid donor to send a thank you from, handling special commands to - # let the user navigate as defined. - while True: - name = input("Enter a donor's name " - "(or 'list' to see all donors or 'menu' to exit)> ").strip() - if name == "list": - print_donors() - elif name == "menu": - return - else: - break - - # Now prompt the user for a donation amount to apply. Since this is - # also an exit point to the main menu, we want to make sure this is - # done before mutating the donors list object. - while True: - amount_str = input("Enter a donation amount (or 'menu' to exit) > ").strip() - if amount_str == "menu": - return - # Make sure amount is a valid amount before leaving the input loop - amount = float(amount_str) - # NOTE: this is getting a bit carried away... - if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: - print("error: donation amount is invalid\n") - continue # not really needed, but makes it more clear - else: - break - - # If this is a new user, ensure that the database has the necessary data structure. - donor = find_donor(name) - if donor is None: - donor = (name, []) - donor_db.append(donor) - - # Record the donation - # Note how the donor object can be manipulated while it is in the donors list. - donor[1].append(amount) - - print(gen_letter(donor)) - - -def sort_key(item): - return item[1] - - -def print_donor_report(): - """ - Generate the report of the donors and amounts donated. - """ - # First, reduce the raw data into a summary list view - report_rows = [] - for (name, gifts) in donor_db: - total_gifts = sum(gifts) - num_gifts = len(gifts) - avg_gift = total_gifts / num_gifts - report_rows.append((name, total_gifts, num_gifts, avg_gift)) - - # sort the report data - report_rows.sort(key=sort_key) - # print it out in with a nice format. - print("{:25s} | {:11s} | {:9s} | {:12s}".format( - "Donor Name", "Total Given", "Num Gifts", "Average Gift")) - print("-" * 66) - for row in report_rows: - print("{:25s} {:11.2f} {:9d} {:12.2f}".format(*row)) - -if __name__ == "__main__": - running = True - while running: - selection = main_menu_selection() - if selection == "t": - send_thank_you() - elif selection == "r": - print_donor_report() - elif selection == "q": - running = False - else: - print("error: menu selection is invalid!") diff --git a/source/solutions/mailroom/mailroom_basic/mailroom2.py b/source/solutions/mailroom/mailroom_basic/mailroom2.py deleted file mode 100755 index 81d3a97e..00000000 --- a/source/solutions/mailroom/mailroom_basic/mailroom2.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python -""" -mailroom assignment - -This version uses a dict for the main db, and exception handling to -check input -""" - -import sys -import math - -# handy utility to make pretty printing easier -from textwrap import dedent - - -# In memory representation of the donor database -# using a tuple for each donor -# -- kind of like a record in a database table -# using a dict with a lower case version of the donor's name as the key -# This makes it easier to have a 'normalized' key. -# you could get a bit fancier by having each "record" be a dict, with -# "name" and "donations" as keys. -def get_donor_db(): - return {'william gates iii': ("William Gates III", [653772.32, 12.17]), - 'jeff bezos': ("Jeff Bezos", [877.33]), - 'paul allen': ("Paul Allen", [663.23, 43.87, 1.32]), - 'mark zuckerberg': ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]), - } - - -def list_donors(): - """ - creates a list of the donors as a string, so they can be printed - - Not calling print from here makes it more flexible and easier to - test - """ - listing = ["Donor list:"] - for donor in donor_db.values(): - listing.append(donor[0]) - return "\n".join(listing) - - -def find_donor(name): - """ - find a donor in the donor db - - :param: the name of the donor - - :returns: The donor data structure -- None if not in the donor_db - """ - key = name.strip().lower() - return donor_db.get(key) - - -def add_donor(name): - """ - Add a new donor to the donor db - - :param: the name of the donor - - :returns: the new Donor data structure - """ - name = name.strip() - donor = (name, []) - donor_db[name.lower()] = donor - return donor - - -def main_menu_selection(): - """ - Print out the main application menu and then read the user input. - """ - action = input(dedent(''' - Choose an action: - - 1 - Send a Thank You - 2 - Create a Report - 3 - Send letters to everyone - 4 - Quit - - > ''')) - return action.strip() - - -def gen_letter(donor): - """ - Generate a thank you letter for the donor - - :param: donor tuple - - :returns: string with letter - - note: This doesn't actually write to a file -- that's a separate - function. This makes it more flexible and easier to test. - """ - return dedent('''Dear {0:s}, - - Thank you for your very kind donation of ${1:.2f}. - It will be put to very good use. - - Sincerely, - -The Team - '''.format(donor[0], donor[1][-1])) - - -def send_thank_you(): - """ - Execute the logic to record a donation and generate a thank you message. - """ - # Read a valid donor to send a thank you from, handling special commands to - # let the user navigate as defined. - while True: - name = input("Enter a donor's name (or list to see all donors or 'menu' to exit)> ").strip() - if name == "list": - print(list_donors()) - elif name == "menu": - return - else: - break - - # Now prompt the user for a donation amount to apply. Since this is - # also an exit point to the main menu, we want to make sure this is - # done before mutating the db. - while True: - amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip() - if amount_str == "menu": - return - # Make sure amount is a valid amount before leaving the input loop - try: - amount = float(amount_str) - # extra check here -- unlikely that someone will type "NaN", but - # it IS possible, and it is a valid floating point number: - # http://en.wikipedia.org/wiki/NaN - if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: - raise ValueError - # in this case, the ValueError could be raised by the float() call, or by the NaN-check - except ValueError: - print("error: donation amount is invalid\n") - else: - break - - # If this is a new user, ensure that the database has the necessary - # data structure. - donor = find_donor(name) - if donor is None: - donor = add_donor(name) - - # Record the donation - donor[1].append(amount) - print(gen_letter(donor)) - - -def sort_key(item): - # used to sort on name in donor_db - return item[1] - - -def generate_donor_report(): - """ - Generate the report of the donors and amounts donated. - - :returns: the donor report as a string. - """ - # First, reduce the raw data into a summary list view - report_rows = [] - for (name, gifts) in donor_db.values(): - total_gifts = sum(gifts) - num_gifts = len(gifts) - avg_gift = total_gifts / num_gifts - report_rows.append((name, total_gifts, num_gifts, avg_gift)) - - # sort the report data - report_rows.sort(key=sort_key) - report = [] - report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name", - "Total Given", - "Num Gifts", - "Average Gift")) - report.append("-" * 66) - for row in report_rows: - report.append("{:25s} ${:10.2f} {:9d} ${:11.2f}".format(*row)) - return "\n".join(report) - - -def save_letters_to_disk(): - """ - make a letter for each donor, and save it to disk. - """ - for donor in donor_db.values(): - letter = gen_letter(donor) - # I don't like spaces in filenames... - filename = donor[0].replace(" ", "_") + ".txt" - open(filename, 'w').write(letter) - - -def print_donor_report(): - print(generate_donor_report()) - - -def quit(): - sys.exit(0) - -if __name__ == "__main__": - - donor_db = get_donor_db() - - running = True - - selection_dict = {"1": send_thank_you, - "2": print_donor_report, - "3": save_letters_to_disk, - "4": quit} - - while True: - selection = main_menu_selection() - try: - selection_dict[selection]() - except KeyError: - print("error: menu selection is invalid!") From c68903262340f5e3b1425738000cedf7bdf0b908 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 5 May 2019 13:30:26 -0700 Subject: [PATCH 78/87] updated mailroom2 solution --- source/solutions/Lesson04/mailroom2.py | 98 ++++++++++++++------------ 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/source/solutions/Lesson04/mailroom2.py b/source/solutions/Lesson04/mailroom2.py index a586655e..1a118435 100755 --- a/source/solutions/Lesson04/mailroom2.py +++ b/source/solutions/Lesson04/mailroom2.py @@ -45,6 +45,14 @@ def list_donors(): return "\n".join(listing) +def print_donor_list(): + """ + Doesn't do much, but keeps the printing separate + """ + print(list_donors()) + print() + + def find_donor(name): """ Find a donor in the donor db @@ -68,10 +76,6 @@ def add_donor(name): donor_db[name.lower()] = donor return donor - - - - def gen_letter(donor): """ Generate a thank you letter for the donor @@ -92,35 +96,35 @@ def gen_letter(donor): '''.format(donor[0], donor[1][-1])) -def take_donation(name): +def take_donation(): """ Ask user for donation amount, and then add it to the DB """ # Now prompt the user for a donation amount to apply. Since this is # also an exit point to the main menu, we want to make sure this is # done before mutating the db. + print("in take_donation") + name = input("Enter a donor name (new or existing): \n >") while True: - amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip() - if amount_str == "menu": + amount_str = input("Enter a donation amount (or to exit)> ").strip() + if not amount_str: + # if they provide no input, go back to previous menu return # Make sure amount is a valid amount before leaving the input loop - try: - amount = float(amount_str) - # extra check here -- unlikely that someone will type "NaN", but - # it IS possible, and it is a valid floating point number: - # http://en.wikipedia.org/wiki/NaN - if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: - raise ValueError - # in this case, the ValueError could be raised by the float() call, or by the NaN-check - except ValueError: + amount = float(amount_str) + # extra check here -- unlikely that someone will type "NaN", but + # it IS possible, and it is a valid floating point number: + # http://en.wikipedia.org/wiki/NaN + if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: print("error: donation amount is invalid\n") + continue else: break - # If this is a new user, ensure that the database has the necessary - # data structure. donor = find_donor(name) + # If the donor is not found, it's a new donor if donor is None: + # add the new donor to the database donor = add_donor(name) # Record the donation @@ -129,23 +133,6 @@ def take_donation(name): print(gen_letter(donor)) -def send_thank_you(): - """ - Execute the logic to record a donation and generate a thank you message. - """ - # Read a valid donor to send a thank you from, handling special commands to - # let the user navigate as defined. - while True: - name = input("Enter a donor's name or 'list' to see all donors or " - "'menu' to exit to main menu > ").strip() - if name == "list": - print(list_donors()) - elif name == "menu": - return - else: - take_donation(name) - - def sort_key(item): # used to sort on name in donor_db return item[1] @@ -194,6 +181,10 @@ def print_donor_report(): print(generate_donor_report()) +def return_to_menu(): + ''' Return True to trigger exit out of sub-loop''' + return True + def quit(): """ quit the program @@ -203,6 +194,21 @@ def quit(): """ sys.exit(0) +def send_thank_you(): + """ + Execute the logic to record a donation and generate a thank you message. + """ + # Read a valid donor to send a thank you from, handling special commands to + # let the user navigate as defined. + prompt = ("To send a thank you, select one:\n\n" + "(1) Update donor and send thank-you\n" + "(2) List all existing DONORS\n" + "(3) Return to main menu\n > ") + selection_dict = {"1": take_donation, + "2": print_donor_list, + "3": return_to_menu, + } + run_menu(prompt, selection_dict) def main_menu(): """ @@ -211,10 +217,10 @@ def main_menu(): prompt = dedent(''' Choose an action: - 1 - Send a Thank You - 2 - Create a Report - 3 - Send letters to everyone - 4 - Quit + (1) - Send a Thank You + (2) - Create a Report + (3) - Send letters to everyone + (4) - Quit > ''') @@ -236,13 +242,15 @@ def run_menu(prompt, selection_dict): the actions to take. """ while True: - - selection = input(prompt).strip() - try: - # This calls teh function in the selection_dict - selection_dict[selection]() - except KeyError: + selection = input(prompt).strip().lower() + action = selection_dict.get(selection, None) + if action is None: print("error: menu selection is invalid!") + else: + if action(): + # break out of the loop if action returns True + break + if __name__ == "__main__": From 05cb383caaa16f053ae344433dd23773353818e4 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 5 May 2019 21:46:25 -0700 Subject: [PATCH 79/87] a bit of copy editing. --- source/modules/Comprehensions.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst index a813b5ab..397933f4 100644 --- a/source/modules/Comprehensions.rst +++ b/source/modules/Comprehensions.rst @@ -88,7 +88,7 @@ Comprehensions and map() Comprehensions are another way of expressing the "map" pattern from functional programming. -Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. IF youare not familiar with ``map()``, you can saftly skip this, but if you are: +Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. If you are not familiar with ``map()``, you can safely skip this, but if you are: .. code-block:: python @@ -100,7 +100,7 @@ is the same as: [a_function(item), for item in an_iterable] -In this case, the comprehension is a tad wordier than ``map()``. BUt comprehensions really shine when you do'nt already have a handy function to pass to map: +In this case, the comprehension is a tad wordier than ``map()``. But comprehensions really shine when you don't already have a handy function to pass to map: .. code-block:: python @@ -144,7 +144,7 @@ This kind of "filtering" loop can be achieved by adding a conditional to the com new_list = [expr for var in a_list if something_is_true] -This is expressing the "filter" pattern and the "map" pattern at the same time -- one reason I like the comprehension sytax so much. +This is expressing the "filter" pattern and the "map" pattern at the same time -- one reason I like the comprehension syntax so much. .. rubric:: Examples: @@ -477,4 +477,4 @@ Once you've got the hang of it, you may want to read this so you don't overdo it https://treyhunner.com/2019/03/abusing-and-overusing-list-comprehensions-in-python/ -Trey writes a lot of good stuff -- I recommned browsing his site. +Trey writes a lot of good stuff -- I recommend browsing his site. From 0a6ed5ec3850adfdbec7a81a08df89c007497a44 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 12 May 2019 10:35:27 -0700 Subject: [PATCH 80/87] added mailroom 3 solution --- source/solutions/Lesson05/mailroom3.py | 253 +++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100755 source/solutions/Lesson05/mailroom3.py diff --git a/source/solutions/Lesson05/mailroom3.py b/source/solutions/Lesson05/mailroom3.py new file mode 100755 index 00000000..958cee05 --- /dev/null +++ b/source/solutions/Lesson05/mailroom3.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python +""" +mailroom assignment + +This version uses a dict for the main db, and a dict to"switch" on the user's +input choices. + +it also write the thank you letters to files. + +""" + +import sys +import math +from operator import itemgetter + +# handy utility to make pretty printing easier +from textwrap import dedent + + +# In memory representation of the donor database +# using a tuple for each donor +# -- kind of like a record in a database table +# using a dict with a lower case version of the donor's name as the key +# This makes it easier to have a 'normalized' key. +# you could get a bit fancier by having each "record" be a dict, with +# "name" and "donations" as keys. + +def get_donor_db(): + return {'william gates iii': ("William Gates III", [653772.32, 12.17]), + 'jeff bezos': ("Jeff Bezos", [877.33]), + 'paul allen': ("Paul Allen", [663.23, 43.87, 1.32]), + 'mark zuckerberg': ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]), + } + + +def list_donors(): + """ + Create a list of the donors as a string, so they can be printed + + Not calling print from here makes it more flexible and easier to + test + """ + listing = ["Donor list:"] + for donor in donor_db.values(): + listing.append(donor[0]) + return "\n".join(listing) + + +def print_donor_list(): + """ + Doesn't do much, but keeps the printing separate + """ + print(list_donors()) + print() + + +def find_donor(name): + """ + Find a donor in the donor db + + :param: the name of the donor + :returns: The donor data structure -- None if not in the donor_db + """ + key = name.strip().lower() + return donor_db.get(key) + + +def add_donor(name): + """ + Add a new donor to the donor db + + :param: the name of the donor + :returns: the new Donor data structure + """ + name = name.strip() + donor = (name, []) + donor_db[name.lower()] = donor + return donor + + +def gen_letter(donor): + """ + Generate a thank you letter for the donor + + :param: donor tuple + :returns: string with letter + + note: This doesn't actually write to a file -- that's a separate + function. This makes it more flexible and easier to test. + """ + return dedent('''Dear {0:s}, + + Thank you for your very kind donation of ${1:.2f}. + It will be put to very good use. + + Sincerely, + -The Team + '''.format(donor[0], donor[1][-1])) + + +def take_donation(): + """ + Ask user for donation amount, and then add it to the DB + """ + # Now prompt the user for a donation amount to apply. Since this is + # also an exit point to the main menu, we want to make sure this is + # done before mutating the db. + print("in take_donation") + name = input("Enter a donor name (new or existing): \n >") + while True: + amount_str = input("Enter a donation amount (or to exit)> ").strip() + if not amount_str: + # if they provide no input, go back to previous menu + return + # Make sure amount is a valid amount before leaving the input loop + try: + amount = float(amount_str) + # extra check here -- unlikely that someone will type "NaN", but + # it IS possible, and it is a valid floating point number: + # http://en.wikipedia.org/wiki/NaN + if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: + raise ValueError + except ValueError: + print("error: donation amount is invalid\n") + continue + else: + break + + donor = find_donor(name) + # If the donor is not found, it's a new donor + if donor is None: + # add the new donor to the database + donor = add_donor(name) + + # Record the donation + donor[1].append(amount) + # print the thank you letter + print(gen_letter(donor)) + + +def sort_key(item): + # used to sort on name in donor_db + return item[1] + + +def generate_donor_report(): + """ + Generate the report of the donors and amounts donated. + + :returns: the donor report as a string. + """ + # First, reduce the raw data into a summary list view + report_rows = [] + for (name, gifts) in donor_db.values(): + total_gifts = sum(gifts) + num_gifts = len(gifts) + avg_gift = total_gifts / num_gifts + report_rows.append((name, total_gifts, num_gifts, avg_gift)) + + # sort the report data + report_rows.sort(key=itemgetter(1), reverse=True) + report = [] + report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name", + "Total Given", + "Num Gifts", + "Average Gift")) + report.append("-" * 66) + for row in report_rows: + report.append("{:25s} ${:10.2f} {:9d} ${:11.2f}".format(*row)) + return "\n".join(report) + + +def save_letters_to_disk(): + """ + make a letter for each donor, and save it to disk. + """ + for donor in donor_db.values(): + letter = gen_letter(donor) + # I don't like spaces in filenames... + filename = donor[0].replace(" ", "_") + ".txt" + print("writing letter to:", donor[0]) + open(filename, 'w').write(letter) + + +def print_donor_report(): + print(generate_donor_report()) + + +def return_to_menu(): + ''' Return True to trigger exit out of sub-loop''' + return True + + +def send_thank_you(): + """ + Execute the logic to record a donation and generate a thank you message. + """ + # Read a valid donor to send a thank you from, handling special commands to + # let the user navigate as defined. + prompt = ("To send a thank you, select one:\n\n" + "(1) Update donor and send thank-you\n" + "(2) List all existing DONORS\n" + "(3) Return to main menu\n > ") + selection_dict = {"1": take_donation, + "2": print_donor_list, + "3": return_to_menu, + } + run_menu(prompt, selection_dict) + +def main_menu(): + """ + Run the main menu for mailroom + """ + prompt = dedent(''' + Choose an action: + + (1) - Send a Thank You + (2) - Create a Report + (3) - Send letters to everyone + (4) - Quit + + > ''') + + selection_dict = {"1": send_thank_you, + "2": print_donor_report, + "3": save_letters_to_disk, + "4": quit} + + run_menu(prompt, selection_dict) + + +def run_menu(prompt, selection_dict): + """ + run an interactive menu + + :param prompt: What you want to ask the user + + :param selection_dict: Dict of possible user impots mapped to + the actions to take. + """ + while True: + selection = input(prompt).strip().lower() + try: + if selection_dict[selection](): + # break out of the loop if action returns True + break + except KeyError: + print("error: menu selection is invalid!") + + +if __name__ == "__main__": + donor_db = get_donor_db() + main_menu() From 0d724986f85c9b25854e57298c26b11395c35809 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sun, 12 May 2019 10:41:32 -0700 Subject: [PATCH 81/87] clean up the solutions a bit --- .../Lesson05/dict_set_with_comps_solution.py | 2 +- source/solutions/Lesson05/fizz_buzz_comprehension.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/source/solutions/Lesson05/dict_set_with_comps_solution.py b/source/solutions/Lesson05/dict_set_with_comps_solution.py index a60375eb..6bb42b2e 100644 --- a/source/solutions/Lesson05/dict_set_with_comps_solution.py +++ b/source/solutions/Lesson05/dict_set_with_comps_solution.py @@ -22,7 +22,7 @@ # 2. Using a list comprehension, build a dictionary of numbers from zero # to fifteen and the hexadecimal equivalent (string is fine). -print(dict([(i, hex(i)) for i in range(16)])) +print(dict(((i, hex(i)) for i in range(16)))) # 3. Do the previous entirely with a dict comprehension -- should be a one-liner diff --git a/source/solutions/Lesson05/fizz_buzz_comprehension.py b/source/solutions/Lesson05/fizz_buzz_comprehension.py index 44588fde..a29fbf4d 100755 --- a/source/solutions/Lesson05/fizz_buzz_comprehension.py +++ b/source/solutions/Lesson05/fizz_buzz_comprehension.py @@ -1,6 +1,15 @@ #!/usr/bin/env python3 +""" +doing all of fizzbuzz in a comprehension -fb = [[str(i), 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)] for i in range(1, 101)] +Is this a good idea? + -- probably not, but it's kind of cool + + Note that it uses a generator comprehension, so it won't actually get + computed until the join() call +""" + +fb = ([str(i), 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)] for i in range(1, 101)) print('\n'.join(fb)) From cae42970e2697d137c383bf55b1b951216771683 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Tue, 14 May 2019 07:42:29 -0700 Subject: [PATCH 82/87] added ignore for files generated by mailroom3 --- source/solutions/Lesson05/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 source/solutions/Lesson05/.gitignore diff --git a/source/solutions/Lesson05/.gitignore b/source/solutions/Lesson05/.gitignore new file mode 100644 index 00000000..88917bc7 --- /dev/null +++ b/source/solutions/Lesson05/.gitignore @@ -0,0 +1,4 @@ +Jeff_Bezos.txt +Mark_Zuckerberg.txt +Paul_Allen.txt +William_Gates_III.txt From 0e62a10b5dcfb72133ddcfaade30f5a7e6abe364 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Sat, 18 May 2019 17:03:05 -0400 Subject: [PATCH 83/87] updated to the latest mailroom3 used in previous lesson solutions --- .../Lesson06/{mailroom2.py => mailroom3.py} | 131 +++++++++++------- .../{test_mailroom2.py => test_mailroom3.py} | 7 +- 2 files changed, 88 insertions(+), 50 deletions(-) rename source/solutions/Lesson06/{mailroom2.py => mailroom3.py} (68%) rename source/solutions/Lesson06/{test_mailroom2.py => test_mailroom3.py} (94%) diff --git a/source/solutions/Lesson06/mailroom2.py b/source/solutions/Lesson06/mailroom3.py similarity index 68% rename from source/solutions/Lesson06/mailroom2.py rename to source/solutions/Lesson06/mailroom3.py index 49dfd81d..958cee05 100755 --- a/source/solutions/Lesson06/mailroom2.py +++ b/source/solutions/Lesson06/mailroom3.py @@ -2,12 +2,16 @@ """ mailroom assignment -This version uses a dict for the main db, and exception handling to -check input, and has been factored to be amenable to testing. +This version uses a dict for the main db, and a dict to"switch" on the user's +input choices. + +it also write the thank you letters to files. + """ import sys import math +from operator import itemgetter # handy utility to make pretty printing easier from textwrap import dedent @@ -20,6 +24,7 @@ # This makes it easier to have a 'normalized' key. # you could get a bit fancier by having each "record" be a dict, with # "name" and "donations" as keys. + def get_donor_db(): return {'william gates iii': ("William Gates III", [653772.32, 12.17]), 'jeff bezos': ("Jeff Bezos", [877.33]), @@ -30,7 +35,7 @@ def get_donor_db(): def list_donors(): """ - creates a list of the donors as a string, so they can be printed + Create a list of the donors as a string, so they can be printed Not calling print from here makes it more flexible and easier to test @@ -41,12 +46,19 @@ def list_donors(): return "\n".join(listing) +def print_donor_list(): + """ + Doesn't do much, but keeps the printing separate + """ + print(list_donors()) + print() + + def find_donor(name): """ - find a donor in the donor db + Find a donor in the donor db :param: the name of the donor - :returns: The donor data structure -- None if not in the donor_db """ key = name.strip().lower() @@ -58,7 +70,6 @@ def add_donor(name): Add a new donor to the donor db :param: the name of the donor - :returns: the new Donor data structure """ name = name.strip() @@ -67,28 +78,11 @@ def add_donor(name): return donor -def main_menu_selection(): - """ - Print out the main application menu and then read the user input. - """ - action = input(dedent(''' - Choose an action: - - 1 - Send a Thank You - 2 - Create a Report - 3 - Send letters to everyone - 4 - Quit - - > ''')) - return action.strip() - - def gen_letter(donor): """ Generate a thank you letter for the donor :param: donor tuple - :returns: string with letter note: This doesn't actually write to a file -- that's a separate @@ -104,27 +98,19 @@ def gen_letter(donor): '''.format(donor[0], donor[1][-1])) -def send_thank_you(): +def take_donation(): """ - Execute the logic to record a donation and generate a thank you message. + Ask user for donation amount, and then add it to the DB """ - # Read a valid donor to send a thank you from, handling special commands to - # let the user navigate as defined. - while True: - name = input("Enter a donor's name (or list to see all donors or 'menu' to exit)> ").strip() - if name == "list": - print(list_donors()) - elif name == "menu": - return - else: - break - # Now prompt the user for a donation amount to apply. Since this is # also an exit point to the main menu, we want to make sure this is # done before mutating the db. + print("in take_donation") + name = input("Enter a donor name (new or existing): \n >") while True: - amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip() - if amount_str == "menu": + amount_str = input("Enter a donation amount (or to exit)> ").strip() + if not amount_str: + # if they provide no input, go back to previous menu return # Make sure amount is a valid amount before leaving the input loop try: @@ -134,20 +120,21 @@ def send_thank_you(): # http://en.wikipedia.org/wiki/NaN if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: raise ValueError - # in this case, the ValueError could be raised by the float() call, or by the NaN-check except ValueError: print("error: donation amount is invalid\n") + continue else: break - # If this is a new user, ensure that the database has the necessary - # data structure. donor = find_donor(name) + # If the donor is not found, it's a new donor if donor is None: + # add the new donor to the database donor = add_donor(name) # Record the donation donor[1].append(amount) + # print the thank you letter print(gen_letter(donor)) @@ -171,7 +158,7 @@ def generate_donor_report(): report_rows.append((name, total_gifts, num_gifts, avg_gift)) # sort the report data - report_rows.sort(key=sort_key) + report_rows.sort(key=itemgetter(1), reverse=True) report = [] report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name", "Total Given", @@ -191,6 +178,7 @@ def save_letters_to_disk(): letter = gen_letter(donor) # I don't like spaces in filenames... filename = donor[0].replace(" ", "_") + ".txt" + print("writing letter to:", donor[0]) open(filename, 'w').write(letter) @@ -198,23 +186,68 @@ def print_donor_report(): print(generate_donor_report()) -def quit(): - sys.exit(0) +def return_to_menu(): + ''' Return True to trigger exit out of sub-loop''' + return True -if __name__ == "__main__": - donor_db = get_donor_db() +def send_thank_you(): + """ + Execute the logic to record a donation and generate a thank you message. + """ + # Read a valid donor to send a thank you from, handling special commands to + # let the user navigate as defined. + prompt = ("To send a thank you, select one:\n\n" + "(1) Update donor and send thank-you\n" + "(2) List all existing DONORS\n" + "(3) Return to main menu\n > ") + selection_dict = {"1": take_donation, + "2": print_donor_list, + "3": return_to_menu, + } + run_menu(prompt, selection_dict) + +def main_menu(): + """ + Run the main menu for mailroom + """ + prompt = dedent(''' + Choose an action: - running = True + (1) - Send a Thank You + (2) - Create a Report + (3) - Send letters to everyone + (4) - Quit + + > ''') selection_dict = {"1": send_thank_you, "2": print_donor_report, "3": save_letters_to_disk, "4": quit} + run_menu(prompt, selection_dict) + + +def run_menu(prompt, selection_dict): + """ + run an interactive menu + + :param prompt: What you want to ask the user + + :param selection_dict: Dict of possible user impots mapped to + the actions to take. + """ while True: - selection = main_menu_selection() + selection = input(prompt).strip().lower() try: - selection_dict[selection]() + if selection_dict[selection](): + # break out of the loop if action returns True + break except KeyError: print("error: menu selection is invalid!") + + +if __name__ == "__main__": + donor_db = get_donor_db() + main_menu() diff --git a/source/solutions/Lesson06/test_mailroom2.py b/source/solutions/Lesson06/test_mailroom3.py similarity index 94% rename from source/solutions/Lesson06/test_mailroom2.py rename to source/solutions/Lesson06/test_mailroom3.py index e633e575..49cfad52 100644 --- a/source/solutions/Lesson06/test_mailroom2.py +++ b/source/solutions/Lesson06/test_mailroom3.py @@ -5,7 +5,7 @@ """ import os -import mailroom2 as mailroom +import mailroom3 as mailroom # so that it's there for the tests mailroom.donor_db = mailroom.get_donor_db() @@ -50,6 +50,11 @@ def test_gen_letter(): def test_add_donor(): + """ + adds a new donor + + then tests that the donor is added, and that a donation is properly recorded. + """ name = "Fred Flintstone " donor = mailroom.add_donor(name) From 9155414b6c46661d30f818693d46fd97277597d3 Mon Sep 17 00:00:00 2001 From: smckellips Date: Tue, 28 May 2019 17:08:41 -0700 Subject: [PATCH 84/87] Misc typos (#185) * Text update * Text Update --- source/exercises/html_renderer.rst | 4 ++-- source/exercises/html_renderer_tutorial.rst | 2 +- source/modules/Strings.rst | 6 +++--- source/scripts/dict_as_switch.rst | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/source/exercises/html_renderer.rst b/source/exercises/html_renderer.rst index 8175b30b..8761e90d 100644 --- a/source/exercises/html_renderer.rst +++ b/source/exercises/html_renderer.rst @@ -30,7 +30,7 @@ If you don't know html -- just look at the example and copy that. And you can re The exercise is broken down into a number of steps -- each requiring a few more OO concepts in Python. -The goal of the code is render html. The goal of the *exercise* is to build up a simple object hierarchy with: +The goal of the code is to render html. The goal of the *exercise* is to build up a simple object hierarchy with: * classes * class attributes @@ -219,7 +219,7 @@ Part B: Now it gets fun! -Now that you have multipel types of elements, it's worth looking a bit at how html works. A given element can hold text, but it can *also* hold other elements. So we need to update our ``Element`` classes to support that. +Now that you have multiple types of elements, it's worth looking a bit at how html works. A given element can hold text, but it can *also* hold other elements. So we need to update our ``Element`` classes to support that. Extend the ``Element.render()`` method so that it can render other elements inside the tag in addition to strings. A recursion-like approach should do it. i.e. it can call the ``render()`` method of the elements it contains. diff --git a/source/exercises/html_renderer_tutorial.rst b/source/exercises/html_renderer_tutorial.rst index e86de07a..762688c0 100644 --- a/source/exercises/html_renderer_tutorial.rst +++ b/source/exercises/html_renderer_tutorial.rst @@ -831,7 +831,7 @@ Now we are getting a little more interesting. This is easy; you know how to do that, yes? -But the training wheels are off -- you are going to need to write your own tests now. So before you create the ``Head`` element class, write a test for it. You should be able to copy and paste one of the previous tests, and just change the name of the class and the tag value. Remember to give yo test a new name, or it will simply replace the previous test. +But the training wheels are off -- you are going to need to write your own tests now. So before you create the ``Head`` element class, write a test for it. You should be able to copy and paste one of the previous tests, and just change the name of the class and the tag value. Remember to give your test a new name, or it will simply replace the previous test. I like to run the tests as soon as I make a new one. If nothing else, I can make sure I have one more test. diff --git a/source/modules/Strings.rst b/source/modules/Strings.rst index ac712fea..cafbfcc7 100644 --- a/source/modules/Strings.rst +++ b/source/modules/Strings.rst @@ -196,8 +196,8 @@ Common Escape Sequences:: \t ASCII Horizontal Tab (TAB) \ooo Character with octal value ooo \xhh Character with hex value hh - \uxxxx Charactor with Unicode code point value xxxx - \N{char-name} Charactor with Unicdoe name char_name + \uxxxx Character with Unicode code point value xxxx + \N{char-name} Character with Unicdoe name char_name For example -- for tab-separated values: @@ -215,7 +215,7 @@ https://docs.python.org/3/library/stdtypes.html#string-methods Raw Strings ------------ -There are times when you want a literal backslash in your string: Windows file paths, regular expressions. Tomake this easy, Pyhton support "raw" strings -- string literals where the backslash does not have special meaning: +There are times when you want a literal backslash in your string: Windows file paths, regular expressions. To make this easy, Python support "raw" strings -- string literals where the backslash does not have special meaning: Add an ``r`` in front of the string literal: diff --git a/source/scripts/dict_as_switch.rst b/source/scripts/dict_as_switch.rst index 872bbf87..171f8d0d 100644 --- a/source/scripts/dict_as_switch.rst +++ b/source/scripts/dict_as_switch.rst @@ -105,7 +105,7 @@ Perhaps a little silly for only two options, but I hope you get the idea. And if you establish a protocol for what those functions return, -you can use the return value -- perhaps as simple as a True or FAlse to indicate success. +you can use the return value -- perhaps as simple as a True or False to indicate success. Or a sentinel value to indicate it's time to break out of a loop. From bce41c45765708d82ec9c1f4fce1f5a2f20bc05e Mon Sep 17 00:00:00 2001 From: "Christopher H.Barker, PhD" Date: Wed, 5 Jun 2019 23:03:33 -0300 Subject: [PATCH 85/87] removed "wait for classmethod" --- source/exercises/circle_class.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/source/exercises/circle_class.rst b/source/exercises/circle_class.rst index 26377e95..b2c47af9 100644 --- a/source/exercises/circle_class.rst +++ b/source/exercises/circle_class.rst @@ -32,8 +32,7 @@ You will use: - properties. - a bunch of "magic methods". - - a classmethod (after you've learned about them...). - + - a classmethod. General Instructions: --------------------- @@ -121,8 +120,6 @@ The user should not be able to set the area: Step 5: ------- -**NOTE:** wait on this one 'till we learn about class methods.. - Add an "alternate constructor" that lets the user create a Circle directly with the diameter: @@ -134,6 +131,8 @@ with the diameter: >> print(c.radius) 4 +Hint: This is a good use case for a ``classmethod`` + Step 6: ------- From 8e369daa2cbf93895b173870dc147ce7179b6727 Mon Sep 17 00:00:00 2001 From: "Christopher H.Barker, PhD" Date: Mon, 5 Aug 2019 09:50:05 -0700 Subject: [PATCH 86/87] fixed some typos --- source/modules/Tutorial.rst | 56 +++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/source/modules/Tutorial.rst b/source/modules/Tutorial.rst index 1f9a33e3..e5eb5740 100644 --- a/source/modules/Tutorial.rst +++ b/source/modules/Tutorial.rst @@ -39,7 +39,7 @@ There are a number of ways to run python code: - At the interpreter, often referred to as a REPL (Read, Evaluate, Print Loop) - At an enhanced interpreter such as iPython - In a browser-based interactive system such as the Jupyter Notebook -- From and IDE, such as IDLE or PyCharm +- From an IDE, such as IDLE or PyCharm - Calling python from the command line to run a file. While working with an interactive interpreter can be an excellent way to explore Python (and I highly recommend it), For this tutorial, to get you used to "real" production development, you will write, edit, and save your code in a programmer's text editor, and run it from the command line. @@ -47,7 +47,8 @@ While working with an interactive interpreter can be an excellent way to explore A Programmer's Text Editor -------------------------- -A bit here about an editor, and recommendations on selecting one, with pointers to documentation about editor configuration. +See These notes for getting set up with an editor and Python itself: :ref:`setting_up_dev_environment` + The Python Interpreter ---------------------- @@ -66,19 +67,19 @@ These each have their own special uses for interaction the the Java VM or CLR, o [link to setting up your environment here] -For this tutorial, you will need cPython version 3.6, installed and running so that when you type "python" at your command line, it starts up: +For this tutorial, you will need cPython version 3.7, installed and running so that when you type "python" at your command line, it starts up: .. code-block:: bash MacBook-Pro:~ Chris$ python - Python 3.6.2 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + Python 3.7.4 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> -Your result may be slightly different, but it should say Python 3.6. *something* there at the top, and give you the command prompt (``>>>``) at the end. +Your result may be slightly different, but it should say Python 3.7. *something* there at the top, and give you the command prompt (``>>>``) at the end. -You can get out of it by typing ctrl+D (on OS_X and Linux) or ctrl+Z (On Windows), or typing ``exit()`` and hitting . +You can get out of it by typing ctrl+D (on OS-X and Linux) or ctrl+Z (On Windows), or typing ``exit()`` and then . Your first "program" -------------------- @@ -120,7 +121,7 @@ In this case, python ran the one line of code you put in that file, which told i The print function ------------------ -you can display jsut about anything in Python with the ``print()`` function. Simply type:: +you can display just about anything in Python with the ``print()`` function. Simply type:: print(what you want to print) @@ -129,20 +130,20 @@ examples: print(45) print("this is a bit of text") -you can print more than one thing by separating them with parentheses:: +you can print more than one thing by separating them with commas, inside the parenthesis:: - print("the value of pi is:", 3.1459, "to two decimal places") + print("the value of pi is:", 3.1459, "to four decimal places") Text in Python -------------- -Text in python is supported by the "str" datatype, which is short for "string". The text datatype is often referred to called "strings" in computer science because it is strings of characters. +Text in python is supported by the "str" datatype, which is short for "string". The text datatype is often referred to as "strings" in computer science because it is a series, or string of characters. -In Python3, strings can be any length, and contain any character (even in virtually any language). This is because they support "Unicode" which is a system for representing all the characters of virtually all the languages used on earth. +In Python3, strings can be any length, and contain any character (in virtually any language). This is because they support "Unicode" which is a system for representing all the characters of virtually all the languages used on earth. There are many complications to full support of Unicode, but for the most part, in Python it "just works". Any text you can put in your text editor should work fine. -To create a str, you simply type what you want surrounded by either double or, single quotes (the apostrophe). +To create a str, you simply type what you want surrounded by either double or single quotes (the apostrophe). Type this in a new file, called ``strings.py``: @@ -161,13 +162,13 @@ run the file, and you should get something like this:: MacBook-Pro:tutorial Chris$ python strings.py This is a basic string This is exactly the same string - you want to use double quotes if there's an apostrophe, like this: ' in the string - and you can use single quotes if you want to "quote" a word + You want to use double quotes if there's an apostrophe, like this: ' in the string + You can use single quotes if you want to "quote" a word Numbers in Python ----------------- -Python support two types of numbers in Python: integers (int) -- or whole numbers: +Python supports two types of numbers: integers (int) -- or "whole numbers", with no fractional part: .. code-block:: python @@ -180,18 +181,18 @@ integers can be negative or positive and as large as you want: >>> print(12345678987654321234567890987654321234567898765) 12345678987654321234567890987654321234567898765 -"real numbers" are called "floating point" (float) numbers. They are internally stored as binary, but you wirte them as regular decimal numbers: +"real numbers" are called "floating point" (float) numbers. They are internally stored as binary, but you write them as regular decimal (base 10) numbers: .. code-block:: python 2.3, 3.0, 3.2459, -23.21 -For the most part, Python will convert from integer to floating point numbers for you. +Note tht while the integer`3` and the float `3.0` have the same value, they are different types of numbers. But for the most part, Python will convert from integer to floating point numbers for you, so this distiction is rarely important. Math ---- -Being a computer language, python, of course supports the regular math functions. type the following into a file named math.py and run it: +Being a computer language, Python, of course, supports the regular math functions. Type the following into a file named math.py and run it: .. code-block:: python @@ -204,12 +205,12 @@ Being a computer language, python, of course supports the regular math functions print("twelve divided by 5 is:") print(12 // 5) -What is the difference between ``12 / 5`` and ``12 // 5`` ? +What is the difference between ``12 / 5`` and ``12 // 5`` ? Run your this code and find out. -Order of operations +Order of Operations ------------------- -Python follows the standard rules of operator precedence -- which operations are performed first when there are a bunch in a row: +Python follows the standard rules of "operator precedence" from algebra -- which operations are performed first when there are a bunch in a row: https://en.wikipedia.org/wiki/Order_of_operations @@ -237,7 +238,7 @@ Variables Directly printing things is not all that useful -- though Python does make a good calculator! -Do do anything more complicated, you need to store values to be used later. We do this by "assigning" them to a variable. SAve the follwing in a variables.py file: +To do anything more complicated, you need to store values to be used later. We do this by "assigning" them to a "variable", essentially givng them a name. Save the follwing in a ``variables.py`` file: .. code-block:: python @@ -249,7 +250,7 @@ Do do anything more complicated, you need to store values to be used later. We d The equals sign: ``=`` is the "assignment operator". It assigns a value to a name, and then when you use the name in the future, Python will replace it with the value it is assigned to when it is used. -names can (and generally should) be long and descriptive, and can contain letters, numbers (but not at the beginning) and some symbols, like the underscore character: +Names can (and generally should) be long and descriptive, and can contain letters, numbers (but not at the beginning) and only a few symbols, like the underscore character: .. code-block:: python @@ -289,7 +290,14 @@ And this? print ("that") # I think we need this line too print("the other") -comments can come after running code on a line. +comments can come after running code on a line as well. USing the hash to "comment out" parts of code is used in two ways: + +1) To add a little extra description to some code, to explain what it doing. + +2) To temporarily disable some code + + + From 3b2c6223f4e73770d603738475d372499f3ed00b Mon Sep 17 00:00:00 2001 From: Natasha Date: Sun, 31 May 2020 18:55:14 -0700 Subject: [PATCH 87/87] OO intro assignment (#189) * oo intro * oo intro --- source/exercises/oo_intro.py | 103 ++++++++++++++++++++++++++++++++++ source/exercises/oo_intro.rst | 30 ++++++++++ 2 files changed, 133 insertions(+) create mode 100644 source/exercises/oo_intro.py create mode 100644 source/exercises/oo_intro.rst diff --git a/source/exercises/oo_intro.py b/source/exercises/oo_intro.py new file mode 100644 index 00000000..522e62a0 --- /dev/null +++ b/source/exercises/oo_intro.py @@ -0,0 +1,103 @@ +import math +import operator +from uuid import uuid4 + + +class Row: + """This class represents a single row with ID, first name, last name and state attributes""" + def __init__(self, fname: str, lname: str, state: str): + self.row_id = str(uuid4()) # randomly generated unique ID + self.fname = fname + self.lname = lname + self.state = state + + def __str__(self): + return f"| {self.row_id} | {self.fname + ' ' + self.lname:<15} | {self.state} |" + + +class Report: + def __init__(self, limit: int): + self.limit = limit + self.rows = [] + + def add_row(self, row: Row) -> None: + """Add a row object to the list""" + pass + + def remove_row(self, row_id: str) -> None: + """Remove a row object by the row ID""" + pass + + def size(self) -> int: + """Return how many total rows the report has""" + pass + + def get_number_of_pages(self) -> int: + """Get how many pages the report has; this will be based on limit variable. + If your limit=4 and rows list has 6 records then there are two pages: page1 has 4 records, page2 has 2 records + hint: you'll want to round up + """ + pass + + def get_paged_rows(self, sort_field: str, page: int) -> list: + """Return a list of rows for a specific page number + :param sort_field: field to sort on, "name" or "-name" (descending) + :param page: specific page for returning data + :return: list of row objects for specific page + + Hints: + 1. you'll want to determine if sort is reversed or not (remember that sorted() takes in param for that) + this is based on if the fields start with a minus sign for DESCENDING sort + 2. when sorting on passed in field you can use handy `operator` library with `attrgetter` method (look up official docs) + 3. to actually determine what rows belong on the specific page you'll be using list slicing (remember lesson 03?) + here is an illustration to help with the code logic: + our list has 6 rows => [, , , , , ] + for page=2 we expect to get => [, ] + with slicing you'll want to offset your list by 4 in this case + (extra hint: we can define offset as `offset = (page - 1) * self.limit`) + + """ + pass + + +if __name__ == "__main__": + + report = Report(4) + + report.add_row(Row("natasha", "smith", "WA")) + report.add_row(Row("devin", "lei", "WA")) + report.add_row(Row("bob", "li", "CA")) + report.add_row(Row("tracy", "jones", "OR")) + report.add_row(Row("johny", "jakes", "WA")) + report.add_row(Row("derek", "wright", "WA")) + + + def run_report(sort_field): + print(f"... PAGED REPORT SORTED BY: '{sort_field}'...") + page = 1 + while True: + rows = report.get_paged_rows(sort_field, page=page) + + if not rows: + break + + input(f"Press ENTER to see page {page}") + + print(f"PAGE: {page} of {report.get_number_of_pages()}") + print("---------------------------------------------------------------") + + for row in rows: + print(row) + + print("---------------------------------------------------------------") + + page += 1 + + + run_report("fname") + + print(f"\n\nRemoving student: {report.rows[1].fname} [{report.rows[1].row_id}]... \n\n") + + report.remove_row(report.rows[1].row_id) + + run_report("-fname") diff --git a/source/exercises/oo_intro.rst b/source/exercises/oo_intro.rst new file mode 100644 index 00000000..703b3df3 --- /dev/null +++ b/source/exercises/oo_intro.rst @@ -0,0 +1,30 @@ +.. _oo_intro: + +######################## +OO Intro - Report Class +######################## + +This assignment uses Object Oriented Programming to design a class that can be used to manage data reporting. + +We have done some reporting in our mailroom program, but that report was pretty simple and used simple functions to accomplish the work. + +We will explore here, how one can utilize OO to improve and enhance reporting capabilities. + + +Procedure +========= + +You will use :download:`oo_intro.py` file as a starting point for your code. + +You will notice that ``Report`` class will have attributes and methods defined for you, including input parameters (and their types) as well as expected output. You will need to fill out the code for each defined method and docstrings contain additional information on what is expected. + +The ``Report`` class uses another class that is fully defined for you, the ``Row`` class. This class represents a single row in your report, and the report class will hold a list of the row instances. +There are big advantages to using a class like ``Row`` in contrast to a simple dictionary, this design creates a clear contract on what's expected to be as part of a row, where with a dictionary it is easy to misspell or miss a key. +A class like ``Row`` is often called a dataclass (since it only holds "data" and doesn't actually have any logic to it). + +This is such a popular pattern that python introduced dataclasses to make this even simpler and you can read more about them here: +https://realpython.com/python-data-classes/. You do not use them in this assignment but you should know that they exist and why. + + + +And at last, you will of course want to include unit tests covering all of your class methods. \ No newline at end of file