Skip to content

Commit 518cb11

Browse files
committed
Provide an example on how to extend PodGen
Also, make it possible to define new namespaces to be used.
1 parent 74b6d5f commit 518cb11

File tree

3 files changed

+195
-13
lines changed

3 files changed

+195
-13
lines changed

doc/extending.rst

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
Adding new tags
2+
===============
3+
4+
Are there XML tags you want to use that aren't supported by PodGen? If so, you
5+
should be able to add them in using inheritance.
6+
7+
.. note::
8+
9+
There hasn't been a focus on making it easy to extend PodGen.
10+
Future versions may provide better support for this.
11+
12+
.. note::
13+
14+
Feel free to add a feature request to GitHub Issues if you think PodGen
15+
should support a certain tag out of the box.
16+
17+
18+
Quick How-to
19+
------------
20+
21+
#. Create new class that extends Podcast.
22+
#. Add the new attribute.
23+
#. Override :meth:`.Podcast._create_rss`, call super()._create_rss(),
24+
add the new tag to its result and return the new tree.
25+
26+
If you'll use RSS elements from another namespace, you must make sure you
27+
update the _nsmap attribute of Podcast (you cannot define new namespaces from
28+
an episode!). It is a dictionary with the prefix as key and the
29+
URI for that namespace as value. To use a namespace, you must put the URI inside
30+
curly braces, with the tag name following right after (outside the braces).
31+
For example::
32+
33+
"{%s}link" % self._nsmap['atom'] # This will render as atom:link
34+
35+
The `lxml API documentation`_ is a pain to read, so just look at the source code
36+
for PodGen to figure out how to do things. The example below may help, too.
37+
38+
.. _lxml API documentation: http://lxml.de/api/index.html
39+
40+
You can do the same with Episode, if you replace _create_rss() with
41+
rss_entry() above.
42+
43+
Example: Adding a ttl field
44+
---------------------------
45+
46+
The examples here assume version 3 of Python is used.
47+
48+
Using traditional inheritance
49+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50+
51+
::
52+
53+
from lxml import etree
54+
55+
from podgen import Podcast
56+
57+
58+
class PodcastWithTtl(Podcast):
59+
"""This is an extension of Podcast, which supports ttl.
60+
61+
You gain access to ttl by creating a new instance of this class instead
62+
of Podcast.
63+
"""
64+
def __init__(self, *args, **kwargs):
65+
# Initialize the ttl value
66+
self.__ttl = None
67+
68+
# Has the user passed in ttl value as a keyword?
69+
if 'ttl' in kwargs:
70+
self.ttl = kwargs['ttl']
71+
kwargs.pop('ttl') # avoid TypeError from super()
72+
73+
# Call Podcast's constructor
74+
super().__init__(*args, **kwargs)
75+
76+
@property
77+
def ttl(self):
78+
"""Your suggestion for how many minutes podcatchers should wait
79+
before refreshing the feed.
80+
81+
ttl stands for "time to live".
82+
83+
:type: :obj:`int`
84+
:RSS: ttl
85+
"""
86+
return self.__ttl
87+
88+
@ttl.setter
89+
def ttl(self, ttl):
90+
try:
91+
ttl_int = int(ttl)
92+
except ValueError:
93+
raise TypeError("ttl expects an integer, got %s" % ttl)
94+
95+
if ttl_int < 0:
96+
raise ValueError("Negative ttl values aren't accepted, got %s"
97+
% ttl_int)
98+
self.__ttl = ttl_int
99+
100+
def _create_rss(self):
101+
rss = super()._create_rss()
102+
channel = rss.find("channel")
103+
if self.__ttl is not None:
104+
ttl = etree.SubElement(channel, 'ttl')
105+
ttl.text = str(self.__ttl)
106+
107+
return rss
108+
109+
# How to use the new class (normally, you would put this somewhere else)
110+
myPodcast = PodcastWithTtl(name="Test", website="http://example.org",
111+
explicit=False, description="Testing ttl")
112+
myPodcast.ttl = 90
113+
print(myPodcast)
114+
115+
116+
Using mixins
117+
^^^^^^^^^^^^
118+
119+
To use mixins, you cannot make the class with the ttl functionality inherit
120+
Podcast. Instead, it must inherit nothing. Other than that, the code will be
121+
the same, so it doesn't make sense to repeat it here.
122+
123+
::
124+
125+
class TtlMixin(object):
126+
# ...
127+
128+
# How to use the new mixin
129+
class PodcastWithTtl(TtlMixin, Podcast):
130+
def __init__(*args, **kwargs):
131+
super().__init__(*args, **kwargs)
132+
133+
myPodcast = PodcastWithTtl(name="Test", website="http://example.org",
134+
explicit=False, description="Testing ttl")
135+
myPodcast.ttl = 90
136+
print(myPodcast)
137+
138+
Note the order of the mixins in the class declaration. You should read it as
139+
the path Python takes when looking for a method. First Python checks
140+
PodcastWithTtl, then TtlMixin, finally Podcast. This is also the order the
141+
methods are called when chained together using super(). If you had Podcast
142+
first, Podcast's _create_rss() method would be run first, and since it never
143+
calls super()._create_rss(), the TtlMixin's _create_rss would never be run.
144+
Therefore, you should always have Podcast last in that list.
145+
146+
Which approach is best?
147+
^^^^^^^^^^^^^^^^^^^^^^^
148+
149+
The advantage of mixins isn't really displayed here, but it will become
150+
apparent as you add more and more extensions. Say you define 5 different mixins,
151+
which all add exactly one more attribute to Podcast. If you used traditional
152+
inheritance, you would have to make sure each of those 5 subclasses made up a
153+
tree. That is, class 1 would inherit Podcast. Class 2 would have to inherit
154+
class 1, class 3 would have to inherit class 2 and so on. If two of the classes
155+
had the same superclass, you would be screwed.
156+
157+
By using mixins, you can put them together however you want. Perhaps for one
158+
podcast you only need ttl, while for another podcast you want to use the
159+
textInput element in addition to ttl, and another podcast requires the
160+
textInput element together with the comments element. Using traditional
161+
inheritance, you would have to duplicate code for textInput in two classes. Not
162+
so with mixins::
163+
164+
class PodcastWithTtl(TtlMixin, Podcast):
165+
def __init__(*args, **kwargs):
166+
super().__init__(*args, **kwargs)
167+
168+
class PodcastWithTtlAndTextInput(TtlMixin, TextInputMixin, Podcast):
169+
def __init__(*args, **kwargs):
170+
super().__init__(*args, **kwargs)
171+
172+
class PodcastWithTextInputAndComments(TextInputMixin, CommentsMixin,
173+
Podcast):
174+
def __init__(*args, **kwargs):
175+
super().__init__(*args, **kwargs)
176+
177+
If the list of attributes you want to use varies between different podcasts,
178+
mixins are the way to go. On the other hand, mixins are overkill if you are okay
179+
with one giant class with all the attributes 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)