Skip to content

Commit 511cae1

Browse files
authored
Merge pull request #54 from UWPCE-PythonCert/html_tutorial
moved tag rendering out of loops in examples
2 parents 0b172a4 + 3442eb7 commit 511cae1

File tree

1 file changed

+33
-25
lines changed

1 file changed

+33
-25
lines changed

source/exercises/html_renderer/html_renderer_tutorial.rst

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@ But this one failed:
110110
assert file_contents.startswith("<html>")
111111
assert file_contents.endswith("</html>")
112112
113-
OK -- this one really does something real -- it tries to render an html element -- which did NOT pass -- so it's time to put some real functionality in the Element class.
113+
OK -- this one really does something real -- it tries to render an html element -- which did NOT pass -- so it's time to put some real functionality in the ``Element`` class.
114114

115115
This is the code:
116116

117117
.. code-block:: python
118118
119-
class Element(object):
119+
class Element:
120120
121121
def __init__(self, content=None):
122122
pass
@@ -139,7 +139,7 @@ So we need to add a tiny bit of code:
139139

140140
.. code-block:: python
141141
142-
class Element(object):
142+
class Element:
143143
144144
tag = "html"
145145
@@ -492,7 +492,8 @@ Part A
492492
.. rubric:: Instructions:
493493

494494

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

497498
So this is very straightforward. We have a class that represents an element, and the only difference between basic elements is that they have a different tag. For example::
498499

@@ -509,7 +510,9 @@ and::
509510
</p>
510511

511512

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

514517
Before we do that -- let's do some test-driven development. Uncomment the next few tests in ``test_html_render.py``: ``test_html``, ``test_body``, and ``test_p``, and run the tests::
515518

@@ -546,7 +549,8 @@ Before we do that -- let's do some test-driven development. Uncomment the next f
546549
====================== 3 failed, 4 passed in 0.08 seconds ======================
547550

548551
So we have three failures. Of course we do, because we haven't written any new code yet!
549-
Yes, this is pedantic, and there is no real reason to run tests you know are going to fail. But there is a reason to *write* tests that you know are going to fail, and you have to run them to know that you have written them correctly.
552+
Yes, this is pedantic, and there is no real reason to run tests you know are going to fail.
553+
But there is a reason to *write* tests that you know are going to fail, and you have to run them to know that you have written them correctly.
550554

551555
Now we can write the code for those three new element types. Try to do that yourself first, before you read on.
552556

@@ -664,24 +668,28 @@ Uncomment ``test_subelement`` in the test file, and run the tests::
664668
out_file = <_io.StringIO object at 0x10325b5e8>
665669

666670
def render(self, out_file):
671+
out_file.write("<{}>\n".format(self.tag))
667672
# loop through the list of contents:
668673
for content in self.contents:
669-
out_file.write("<{}>\n".format(self.tag))
670674
> out_file.write(content)
671675
E TypeError: string argument expected, got 'P'
672676

673677
html_render.py:26: TypeError
674678
====================== 1 failed, 7 passed in 0.11 seconds ======================
675679

676-
Again, the new test failed; no surprise because we haven't written any new code yet. But do read the report carefully; it did not fail on an assert, but rather with a ``TypeError``. The code itself raised an exception before it could produce results to test.
680+
Again, the new test failed; no surprise because we haven't written any new code yet.
681+
But do read the error report carefully; it did not fail on an assert, but rather with a ``TypeError``. The code itself raised an exception before it could produce results to test.
677682

678683
So now it's time to write the code. Look at where the exception was raised: line 26 in my code, inside the ``render()`` method. The line number will likely be different in your code, but it probably failed on the render method. Looking closer at the error::
679684

680685
> out_file.write(content)
681686
E TypeError: string argument expected, got 'P'
682687

683-
It occurred in the file ``write`` method, complaining that it expected to be writing a string to the file, but it got a ``'P'``. ``'P'`` is the name of the paragraph element class.
684-
So we need a way to write an element to a file. How might we do that? Inside the element's render method, we need to render an element...
688+
It occurred in the file ``write`` method, complaining that it expected to be writing a string to the file, but it got a ``'P'``.
689+
``'P'`` is the name of the paragraph element class.
690+
So we need a way to write an element to a file. How might we do that?
691+
692+
Inside the element's render method, we need to render an element...
685693

686694
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.
687695

@@ -696,25 +704,25 @@ it becomes clear -- we render an element by passing the output file to the eleme
696704
.. code-block:: python
697705
698706
def render(self, out_file):
707+
out_file.write("<{}>\n".format(self.tag))
699708
# loop through the list of contents:
700709
for content in self.contents:
701-
out_file.write("<{}>\n".format(self.tag))
702710
out_file.write(content)
703711
out_file.write("\n")
704-
out_file.write("</{}>\n".format(self.tag))
712+
out_file.write("</{}>\n".format(self.tag))
705713
706714
So let's update our render by replacing that ``out_file.write()`` call with a call to the content's ``render`` method:
707715

708716
.. code-block:: python
709717
710718
def render(self, out_file):
719+
out_file.write("<{}>\n".format(self.tag))
711720
# loop through the list of contents:
712721
for content in self.contents:
713-
out_file.write("<{}>\n".format(self.tag))
714722
# out_file.write(content)
715723
content.render(out_file)
716724
out_file.write("\n")
717-
out_file.write("</{}>\n".format(self.tag))
725+
out_file.write("</{}>\n".format(self.tag))
718726
719727
And let's see what happens when we run the tests::
720728

@@ -779,20 +787,21 @@ There are a number of approaches you can take. This is a good time to read the n
779787
You may want to try one of the more complex methods, but for now, we're going to use the one that suggests itself from the error.
780788

781789
We need to know whether we want to call a ``render()`` method, or simply write the content to the file. How would we know which to do? Again, look at the error:
790+
782791
We tried to call the render() method of a piece of content, but got an ``AttributeError``. So the way to know whether we can call a render method is to try to call it -- if it works, great! If not, we can catch the exception, and do something else. In this case, the something else is to try to write the content directly to the file:
783792

784793
.. code-block:: python
785794
786795
def render(self, out_file):
796+
out_file.write("<{}>\n".format(self.tag))
787797
# loop through the list of contents:
788798
for content in self.contents:
789-
out_file.write("<{}>\n".format(self.tag))
790799
try:
791800
content.render(out_file)
792801
except AttributeError:
793802
out_file.write(content)
794803
out_file.write("\n")
795-
out_file.write("</{}>\n".format(self.tag))
804+
out_file.write("</{}>\n".format(self.tag))
796805
797806
And run the tests again::
798807

@@ -815,7 +824,7 @@ So what are the downsides to this method? Well, there are two:
815824

816825
1. When we successfully call the ``render`` method, we have no idea if it's actually done the right thing -- it could do anything. If someone puts some completely unrelated object in the content list that happens to have a ``render`` method, this is not going to work. But what are the odds of that?
817826

818-
2. This is the bigger one: if the object *HAS* a render method, but that render method has something wrong with it, then it could conceivably raise an ``AttributeError`` itself, but it would not be the Attribute Error we are expecting. The trick here is that this is very hard to debug.
827+
2. This is the bigger one: if the object *HAS* a render method, but that render method has something wrong with it, then it could conceivably raise an ``AttributeError`` itself, but it would not be the ``AttributeError`` we are expecting. The trick here is that this can be very hard to debug.
819828

820829
However, we are saved by tests. If the render method works in all the other tests, It's not going to raise an ``AttributeError`` only in this case. Another reason to have a good test suite.
821830

@@ -965,14 +974,14 @@ So how do we get this test to pass? We need a new render method for ``OneLineTag
965974
class OneLineTag(Element):
966975
967976
def render(self, out_file):
977+
out_file.write("<{}>".format(self.tag))
968978
# loop through the list of contents:
969979
for content in self.contents:
970-
out_file.write("<{}>".format(self.tag))
971980
try:
972981
content.render(out_file)
973982
except AttributeError:
974983
out_file.write(content)
975-
out_file.write("</{}>\n".format(self.tag))
984+
out_file.write("</{}>\n".format(self.tag))
976985
977986
Notice that I left the newline in at the end of the closing tag -- we do want a newline there, so the next element won't get rendered on the same line. And the tests::
978987

@@ -1109,7 +1118,7 @@ Now that we know how to initialize an element with attributes, and how it should
11091118
# but it starts the same:
11101119
assert file_contents.startswith("<p")
11111120
1112-
Note that this doesn't (yet) test that the attributes are actually rendered, but it does test that you can pass them in to constructor. What happens when we run this test? ::
1121+
Note that this doesn't (yet) test that the attributes are actually rendered, but it does test that you can pass them in to the constructor. What happens when we run this test? ::
11131122

11141123
=================================== FAILURES ===================================
11151124
_______________________________ test_attributes ________________________________
@@ -1123,7 +1132,7 @@ Note that this doesn't (yet) test that the attributes are actually rendered, but
11231132

11241133
Yes, the new test failed -- isn't TDD a bit hard on the ego? So many failures! But why? Well, we passed in the ``style`` and ``id`` attributes as keyword arguments, but the ``__init__`` doesn't expect those arguments. Hence the failure.
11251134

1126-
So should be add those two as keyword parameters? Well, no we shouldn't because those are two arbitrary attribute names; we need to support virtually any attribute name. So how do you write a method that will accept ANY keyword argument? Time for our old friend ``**kwargs``. ``**kwargs**`` will allow any keyword argument to be used, and will store them in the ``kwargs`` dict. So time to update the ``Element.__init__`` like so:
1135+
So should be add those two as keyword parameters? Well, no we shouldn't because those are two arbitrary attribute names; we need to support virtually any attribute name. So how do you write a method that will accept ANY keyword argument? Time for our old friend ``**kwargs``. ``**kwargs`` will allow any keyword argument to be used, and will store them in the ``kwargs`` dict. So time to update the ``Element.__init__`` like so:
11271136

11281137
.. code-block:: python
11291138
@@ -1135,7 +1144,7 @@ And let's try to run the tests again::
11351144

11361145
========================== 12 passed in 0.07 seconds ===========================
11371146

1138-
They passed! Great, but did we test whether the attributes get rendered in the tag correctly? No -- not yet, let's make sure to add that. It may be helpful to add and ``assert False`` in there, so we can see what our tag looks like while we work on it::
1147+
They passed! Great, but did we test whether the attributes get rendered in the tag correctly? No -- not yet, let's make sure to add that. It may be helpful to add an ``assert False`` in there, so we can see what our tag looks like while we work on it::
11391148

11401149
...
11411150
assert False
@@ -1167,7 +1176,6 @@ So we need to render the ``<``, then the ``p``, then a bunch of attribute name=v
11671176
.. code-block:: python
11681177
11691178
def render(self, out_file):
1170-
# loop through the list of contents:
11711179
open_tag = ["<{}".format(self.tag)]
11721180
open_tag.append(">\n")
11731181
out_file.write("".join(open_tag))
@@ -1205,7 +1213,7 @@ Note that I added a space after the ``p`` in the test. Now my test is failing on
12051213
A paragraph of text
12061214
</p>
12071215

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

12101218
<p style="text-align: center"id="intro">
12111219
A paragraph of text
@@ -1315,7 +1323,7 @@ You'll need to override the ``render()`` method:
13151323
What needs to be there? Well, self closing tags can have attributes, same as other elements.
13161324
So we need a lot of the same code here as with the other ``render()`` methods. You could copy and paste the ``Element.render()`` method, and edit it a bit. But that's a "Bad Idea" -- remember DRY (Don't Repeat Yourself)?
13171325
You really don't want two copies of that attribute rendering code you worked so hard on.
1318-
How do we avoid that? We take advantage of the power of subclassing. If you put the code to render the opening (and closing) tags in it's own method, then we can call that method from multiple render methods, something like:
1326+
How do we avoid that? We take advantage of the power of subclassing. If you put the code to render the opening (and closing) tags in its own method, then we can call that method from multiple render methods, something like:
13191327

13201328
.. code-block:: python
13211329

0 commit comments

Comments
 (0)