Skip to content

Commit c90f505

Browse files
authored
Merge pull request #38 from tobinus/extending-podgen-#13
Document how to add custom elements, closes #13
2 parents 74b6d5f + 2bebb27 commit c90f505

File tree

3 files changed

+240
-13
lines changed

3 files changed

+240
-13
lines changed

doc/extending.rst

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
Adding new tags
2+
===============
3+
4+
Are there XML elements you want to use that aren't supported by PodGen? If so,
5+
you should be able to add them in using inheritance.
6+
7+
.. warning::
8+
9+
This is an advanced topic.
10+
11+
.. note::
12+
13+
There hasn't been a focus on making it easy to extend PodGen.
14+
Future versions may provide better support for this.
15+
16+
.. note::
17+
18+
Feel free to add a feature request to `GitHub Issues`_ if you think PodGen
19+
should support a certain element out of the box.
20+
21+
.. _GitHub Issues: https://github.com/tobinus/python-podgen/issues
22+
23+
24+
Quick How-to
25+
------------
26+
27+
#. Create new class that extends :class:`.Podcast`.
28+
#. Add the new attribute.
29+
#. Override :meth:`~.Podcast._create_rss`, call ``super()._create_rss()``,
30+
add the new element to its result and return the new tree.
31+
32+
You can do the same with :class:`.Episode`, if you replace
33+
:meth:`~.Podcast._create_rss` with :meth:`~Episode.rss_entry` above.
34+
35+
There are plenty of small quirks you have to keep in mind. You are strongly
36+
encouraged to read the example below.
37+
38+
Using namespaces
39+
^^^^^^^^^^^^^^^^
40+
41+
If you'll use RSS elements from another namespace, you must make sure you
42+
update the :attr:`~.Podcast._nsmap` attribute of :class:`.Podcast`
43+
(you cannot define new namespaces from an episode!). It is a dictionary with the
44+
prefix as key and the URI for that namespace as value. To use a namespace, you
45+
must put the URI inside curly braces, with the tag name following right after
46+
(outside the braces). For example::
47+
48+
"{%s}link" % self._nsmap['atom'] # This will render as atom:link
49+
50+
The `lxml API documentation`_ is a pain to read, so just look at the `source code
51+
for PodGen`_ and the example below.
52+
53+
.. _lxml API documentation: http://lxml.de/api/index.html
54+
.. _source code for PodGen: https://github.com/tobinus/python-podgen/blob/master/podgen/podcast.py
55+
56+
Example: Adding a ttl element
57+
-----------------------------
58+
59+
The examples here assume version 3 of Python is used.
60+
61+
``ttl`` is an RSS element and stands for "time to live", and can only be an
62+
integer which indicates how many minutes the podcatcher can rely on its copy of
63+
the feed before refreshing (or something like that). There is confusion as to
64+
what it is supposed to mean (max refresh frequency? min refresh frequency?),
65+
which is why it is not included in PodGen. If you use it, you should treat it as
66+
the **recommended** update period (source: `RSS Best Practices`_).
67+
68+
.. _RSS Best Practices: http://www.rssboard.org/rss-profile#element-channel-ttl
69+
70+
Using traditional inheritance
71+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72+
73+
::
74+
75+
# The module used to create the XML tree and generate the XML
76+
from lxml import etree
77+
78+
# The class we will extend
79+
from podgen import Podcast
80+
81+
82+
class PodcastWithTtl(Podcast):
83+
"""This is an extension of Podcast, which supports ttl.
84+
85+
You gain access to ttl by creating a new instance of this class instead
86+
of Podcast.
87+
"""
88+
def __init__(self, *args, **kwargs):
89+
# Initialize the ttl value
90+
self.__ttl = None
91+
92+
# Has the user passed in ttl value as a keyword?
93+
if 'ttl' in kwargs:
94+
self.ttl = kwargs['ttl']
95+
kwargs.pop('ttl') # avoid TypeError from super()
96+
97+
# Call Podcast's constructor
98+
super().__init__(*args, **kwargs)
99+
100+
# If we were to use another namespace, we would add this here:
101+
# self._nsmap['prefix'] = "URI"
102+
103+
@property
104+
def ttl(self):
105+
"""Your suggestion for how many minutes podcatchers should wait
106+
before refreshing the feed.
107+
108+
ttl stands for "time to live".
109+
110+
:type: :obj:`int`
111+
:RSS: ttl
112+
"""
113+
# By using @property and @ttl.setter, we encapsulate the ttl field
114+
# so that we can check the value that is assigned to it.
115+
# If you don't need this, you could just rename self.__ttl to
116+
# self.ttl and remove those two methods.
117+
return self.__ttl
118+
119+
@ttl.setter
120+
def ttl(self, ttl):
121+
# Try to convert to int
122+
try:
123+
ttl_int = int(ttl)
124+
except ValueError:
125+
raise TypeError("ttl expects an integer, got %s" % ttl)
126+
# Is this negative?
127+
if ttl_int < 0:
128+
raise ValueError("Negative ttl values aren't accepted, got %s"
129+
% ttl_int)
130+
# All checks passed
131+
self.__ttl = ttl_int
132+
133+
def _create_rss(self):
134+
# Let Podcast generate the lxml etree (adding the standard elements)
135+
rss = super()._create_rss()
136+
# We must get the channel element, since we want to add subelements
137+
# to it.
138+
channel = rss.find("channel")
139+
# Only add the ttl element if it has been populated.
140+
if self.__ttl is not None:
141+
# First create our new subelement of channel.
142+
ttl = etree.SubElement(channel, 'ttl')
143+
# If we were to use another namespace, we would instead do this:
144+
# ttl = etree.SubElement(channel,
145+
# '{%s}ttl' % self._nsmap['prefix'])
146+
147+
# Then, fill it with the ttl value
148+
ttl.text = str(self.__ttl)
149+
150+
# Return the new etree, now with ttl
151+
return rss
152+
153+
# How to use the new class (normally, you would put this somewhere else)
154+
myPodcast = PodcastWithTtl(name="Test", website="http://example.org",
155+
explicit=False, description="Testing ttl")
156+
myPodcast.ttl = 90 # or set ttl=90 in the constructor
157+
print(myPodcast)
158+
159+
160+
Using mixins
161+
^^^^^^^^^^^^
162+
163+
To use mixins, you cannot make the class with the ``ttl`` functionality inherit
164+
:class:`.Podcast`. Instead, it must inherit nothing. Other than that, the code
165+
will be the same, so it doesn't make sense to repeat it here.
166+
167+
::
168+
169+
class TtlMixin(object):
170+
# ...
171+
172+
# How to use the new mixin
173+
class PodcastWithTtl(TtlMixin, Podcast):
174+
def __init__(*args, **kwargs):
175+
super().__init__(*args, **kwargs)
176+
177+
myPodcast = PodcastWithTtl(name="Test", website="http://example.org",
178+
explicit=False, description="Testing ttl")
179+
myPodcast.ttl = 90
180+
print(myPodcast)
181+
182+
Note the order of the mixins in the class declaration. You should read it as
183+
the path Python takes when looking for a method. First Python checks
184+
``PodcastWithTtl``, then ``TtlMixin`` and finally :class:`.Podcast`. This is
185+
also the order the methods are called when chained together using :func:`super`.
186+
If you had Podcast first, :meth:`.Podcast._create_rss` method would be run
187+
first, and since it never calls ``super()._create_rss()``, the ``TtlMixin``'s
188+
``_create_rss`` would never be run. Therefore, you should always have
189+
:class:`.Podcast` last in that list.
190+
191+
Which approach is best?
192+
^^^^^^^^^^^^^^^^^^^^^^^
193+
194+
The advantage of mixins isn't really displayed here, but it will become
195+
apparent as you add more and more extensions. Say you define 5 different mixins,
196+
which all add exactly one more element to :class:`.Podcast`. If you used traditional
197+
inheritance, you would have to make sure each of those 5 subclasses made up a
198+
tree. That is, class 1 would inherit :class:`.Podcast`. Class 2 would have to inherit
199+
class 1, class 3 would have to inherit class 2 and so on. If two of the classes
200+
had the same superclass, you could get screwed.
201+
202+
By using mixins, you can put them together however you want. Perhaps for one
203+
podcast you only need ``ttl``, while for another podcast you want to use the
204+
``textInput`` element in addition to ``ttl``, and another podcast requires the
205+
``textInput`` element together with the ``comments`` element. Using traditional
206+
inheritance, you would have to duplicate code for ``textInput`` in two classes. Not
207+
so with mixins::
208+
209+
class PodcastWithTtl(TtlMixin, Podcast):
210+
def __init__(*args, **kwargs):
211+
super().__init__(*args, **kwargs)
212+
213+
class PodcastWithTtlAndTextInput(TtlMixin, TextInputMixin, Podcast):
214+
def __init__(*args, **kwargs):
215+
super().__init__(*args, **kwargs)
216+
217+
class PodcastWithTextInputAndComments(TextInputMixin, CommentsMixin,
218+
Podcast):
219+
def __init__(*args, **kwargs):
220+
super().__init__(*args, **kwargs)
221+
222+
If the list of elements you want to use varies between different podcasts,
223+
mixins are the way to go. On the other hand, mixins are overkill if you are okay
224+
with one giant class with all the elements you need.

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ User Guide
5757
user/basic_usage_guide/part_2
5858
user/basic_usage_guide/part_3
5959
user/example
60+
extending
6061
contributing
6162
api

podgen/podcast.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ def __init__(self, **kwargs):
7373
self.__episode_class = Episode
7474
"""The internal value used by self.Episode."""
7575

76+
self._nsmap = {
77+
'atom': 'http://www.w3.org/2005/Atom',
78+
'content': 'http://purl.org/rss/1.0/modules/content/',
79+
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
80+
'dc': 'http://purl.org/dc/elements/1.1/'
81+
}
82+
"""A dictionary which maps namespace prefixes to their namespace URI.
83+
Add a new entry here if you want to use that namespace.
84+
"""
85+
7686
## RSS
7787
# http://www.rssboard.org/rss-specification
7888
# Mandatory:
@@ -414,17 +424,9 @@ def _create_rss(self):
414424
:returns: The root element (ie. the rss element) of the feed.
415425
:rtype: lxml.etree.Element
416426
"""
427+
ITUNES_NS = self._nsmap['itunes']
417428

418-
nsmap = {
419-
'atom': 'http://www.w3.org/2005/Atom',
420-
'content': 'http://purl.org/rss/1.0/modules/content/',
421-
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
422-
'dc': 'http://purl.org/dc/elements/1.1/'
423-
}
424-
425-
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
426-
427-
feed = etree.Element('rss', version='2.0', nsmap=nsmap )
429+
feed = etree.Element('rss', version='2.0', nsmap=self._nsmap)
428430
channel = etree.SubElement(feed, 'channel')
429431
if not (self.name and self.website and self.description
430432
and self.explicit is not None):
@@ -484,7 +486,7 @@ def _create_rss(self):
484486
# author without email)
485487
for a in self.authors or []:
486488
author = etree.SubElement(channel,
487-
'{%s}creator' % nsmap['dc'])
489+
'{%s}creator' % self._nsmap['dc'])
488490
if a.name and a.email:
489491
author.text = "%s <%s>" % (a.name, a.email)
490492
elif a.name:
@@ -566,13 +568,13 @@ def _create_rss(self):
566568
subtitle.text = self.subtitle
567569

568570
if self.feed_url:
569-
link_to_self = etree.SubElement(channel, '{%s}link' % nsmap['atom'])
571+
link_to_self = etree.SubElement(channel, '{%s}link' % self._nsmap['atom'])
570572
link_to_self.attrib['href'] = self.feed_url
571573
link_to_self.attrib['rel'] = 'self'
572574
link_to_self.attrib['type'] = 'application/rss+xml'
573575

574576
if self.pubsubhubbub:
575-
link_to_hub = etree.SubElement(channel, '{%s}link' % nsmap['atom'])
577+
link_to_hub = etree.SubElement(channel, '{%s}link' % self._nsmap['atom'])
576578
link_to_hub.attrib['href'] = self.pubsubhubbub
577579
link_to_hub.attrib['rel'] = 'hub'
578580

0 commit comments

Comments
 (0)