You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: source/exercises/html_renderer/html_renderer_tutorial.rst
+33-25Lines changed: 33 additions & 25 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -110,13 +110,13 @@ But this one failed:
110
110
assert file_contents.startswith("<html>")
111
111
assert file_contents.endswith("</html>")
112
112
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.
114
114
115
115
This is the code:
116
116
117
117
.. code-block:: python
118
118
119
-
classElement(object):
119
+
classElement:
120
120
121
121
def__init__(self, content=None):
122
122
pass
@@ -139,7 +139,7 @@ So we need to add a tiny bit of code:
139
139
140
140
.. code-block:: python
141
141
142
-
classElement(object):
142
+
classElement:
143
143
144
144
tag ="html"
145
145
@@ -492,7 +492,8 @@ Part A
492
492
.. rubric:: Instructions:
493
493
494
494
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)."
496
497
497
498
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::
498
499
@@ -509,7 +510,9 @@ and::
509
510
</p>
510
511
511
512
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.
513
516
514
517
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::
515
518
@@ -546,7 +549,8 @@ Before we do that -- let's do some test-driven development. Uncomment the next f
546
549
====================== 3 failed, 4 passed in 0.08 seconds ======================
547
550
548
551
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.
550
554
551
555
Now we can write the code for those three new element types. Try to do that yourself first, before you read on.
552
556
@@ -664,24 +668,28 @@ Uncomment ``test_subelement`` in the test file, and run the tests::
664
668
out_file = <_io.StringIO object at 0x10325b5e8>
665
669
666
670
def render(self, out_file):
671
+
out_file.write("<{}>\n".format(self.tag))
667
672
# loop through the list of contents:
668
673
for content in self.contents:
669
-
out_file.write("<{}>\n".format(self.tag))
670
674
> out_file.write(content)
671
675
E TypeError: string argument expected, got 'P'
672
676
673
677
html_render.py:26: TypeError
674
678
====================== 1 failed, 7 passed in 0.11 seconds ======================
675
679
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.
677
682
678
683
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::
679
684
680
685
> out_file.write(content)
681
686
E TypeError: string argument expected, got 'P'
682
687
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...
685
693
686
694
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.
687
695
@@ -696,25 +704,25 @@ it becomes clear -- we render an element by passing the output file to the eleme
696
704
.. code-block:: python
697
705
698
706
defrender(self, out_file):
707
+
out_file.write("<{}>\n".format(self.tag))
699
708
# loop through the list of contents:
700
709
for content inself.contents:
701
-
out_file.write("<{}>\n".format(self.tag))
702
710
out_file.write(content)
703
711
out_file.write("\n")
704
-
out_file.write("</{}>\n".format(self.tag))
712
+
out_file.write("</{}>\n".format(self.tag))
705
713
706
714
So let's update our render by replacing that ``out_file.write()`` call with a call to the content's ``render`` method:
707
715
708
716
.. code-block:: python
709
717
710
718
defrender(self, out_file):
719
+
out_file.write("<{}>\n".format(self.tag))
711
720
# loop through the list of contents:
712
721
for content inself.contents:
713
-
out_file.write("<{}>\n".format(self.tag))
714
722
# out_file.write(content)
715
723
content.render(out_file)
716
724
out_file.write("\n")
717
-
out_file.write("</{}>\n".format(self.tag))
725
+
out_file.write("</{}>\n".format(self.tag))
718
726
719
727
And let's see what happens when we run the tests::
720
728
@@ -779,20 +787,21 @@ There are a number of approaches you can take. This is a good time to read the n
779
787
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.
780
788
781
789
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
+
782
791
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:
783
792
784
793
.. code-block:: python
785
794
786
795
defrender(self, out_file):
796
+
out_file.write("<{}>\n".format(self.tag))
787
797
# loop through the list of contents:
788
798
for content inself.contents:
789
-
out_file.write("<{}>\n".format(self.tag))
790
799
try:
791
800
content.render(out_file)
792
801
exceptAttributeError:
793
802
out_file.write(content)
794
803
out_file.write("\n")
795
-
out_file.write("</{}>\n".format(self.tag))
804
+
out_file.write("</{}>\n".format(self.tag))
796
805
797
806
And run the tests again::
798
807
@@ -815,7 +824,7 @@ So what are the downsides to this method? Well, there are two:
815
824
816
825
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?
817
826
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.
819
828
820
829
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.
821
830
@@ -965,14 +974,14 @@ So how do we get this test to pass? We need a new render method for ``OneLineTag
965
974
classOneLineTag(Element):
966
975
967
976
defrender(self, out_file):
977
+
out_file.write("<{}>".format(self.tag))
968
978
# loop through the list of contents:
969
979
for content inself.contents:
970
-
out_file.write("<{}>".format(self.tag))
971
980
try:
972
981
content.render(out_file)
973
982
exceptAttributeError:
974
983
out_file.write(content)
975
-
out_file.write("</{}>\n".format(self.tag))
984
+
out_file.write("</{}>\n".format(self.tag))
976
985
977
986
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::
978
987
@@ -1109,7 +1118,7 @@ Now that we know how to initialize an element with attributes, and how it should
1109
1118
# but it starts the same:
1110
1119
assert file_contents.startswith("<p")
1111
1120
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? ::
@@ -1123,7 +1132,7 @@ Note that this doesn't (yet) test that the attributes are actually rendered, but
1123
1132
1124
1133
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.
1125
1134
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:
1127
1136
1128
1137
.. code-block:: python
1129
1138
@@ -1135,7 +1144,7 @@ And let's try to run the tests again::
1135
1144
1136
1145
========================== 12 passed in 0.07 seconds ===========================
1137
1146
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::
1139
1148
1140
1149
...
1141
1150
assert False
@@ -1167,7 +1176,6 @@ So we need to render the ``<``, then the ``p``, then a bunch of attribute name=v
1167
1176
.. code-block:: python
1168
1177
1169
1178
defrender(self, out_file):
1170
-
# loop through the list of contents:
1171
1179
open_tag = ["<{}".format(self.tag)]
1172
1180
open_tag.append(">\n")
1173
1181
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
1205
1213
A paragraph of text
1206
1214
</p>
1207
1215
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::
1209
1217
1210
1218
<p style="text-align: center"id="intro">
1211
1219
A paragraph of text
@@ -1315,7 +1323,7 @@ You'll need to override the ``render()`` method:
1315
1323
What needs to be there? Well, self closing tags can have attributes, same as other elements.
1316
1324
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)?
1317
1325
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:
0 commit comments