diff --git a/doc/extending.rst b/doc/extending.rst new file mode 100644 index 0000000..b8b321a --- /dev/null +++ b/doc/extending.rst @@ -0,0 +1,224 @@ +Adding new tags +=============== + +Are there XML elements you want to use that aren't supported by PodGen? If so, +you should be able to add them in using inheritance. + +.. warning:: + + This is an advanced topic. + +.. note:: + + There hasn't been a focus on making it easy to extend PodGen. + Future versions may provide better support for this. + +.. note:: + + Feel free to add a feature request to `GitHub Issues`_ if you think PodGen + should support a certain element out of the box. + +.. _GitHub Issues: https://github.com/tobinus/python-podgen/issues + + +Quick How-to +------------ + +#. Create new class that extends :class:`.Podcast`. +#. Add the new attribute. +#. Override :meth:`~.Podcast._create_rss`, call ``super()._create_rss()``, + add the new element to its result and return the new tree. + +You can do the same with :class:`.Episode`, if you replace +:meth:`~.Podcast._create_rss` with :meth:`~Episode.rss_entry` above. + +There are plenty of small quirks you have to keep in mind. You are strongly +encouraged to read the example below. + +Using namespaces +^^^^^^^^^^^^^^^^ + +If you'll use RSS elements from another namespace, you must make sure you +update the :attr:`~.Podcast._nsmap` attribute of :class:`.Podcast` +(you cannot define new namespaces from an episode!). It is a dictionary with the +prefix as key and the URI for that namespace as value. To use a namespace, you +must put the URI inside curly braces, with the tag name following right after +(outside the braces). For example:: + + "{%s}link" % self._nsmap['atom'] # This will render as atom:link + +The `lxml API documentation`_ is a pain to read, so just look at the `source code +for PodGen`_ and the example below. + +.. _lxml API documentation: http://lxml.de/api/index.html +.. _source code for PodGen: https://github.com/tobinus/python-podgen/blob/master/podgen/podcast.py + +Example: Adding a ttl element +----------------------------- + +The examples here assume version 3 of Python is used. + +``ttl`` is an RSS element and stands for "time to live", and can only be an +integer which indicates how many minutes the podcatcher can rely on its copy of +the feed before refreshing (or something like that). There is confusion as to +what it is supposed to mean (max refresh frequency? min refresh frequency?), +which is why it is not included in PodGen. If you use it, you should treat it as +the **recommended** update period (source: `RSS Best Practices`_). + +.. _RSS Best Practices: http://www.rssboard.org/rss-profile#element-channel-ttl + +Using traditional inheritance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + # The module used to create the XML tree and generate the XML + from lxml import etree + + # The class we will extend + from podgen import Podcast + + + class PodcastWithTtl(Podcast): + """This is an extension of Podcast, which supports ttl. + + You gain access to ttl by creating a new instance of this class instead + of Podcast. + """ + def __init__(self, *args, **kwargs): + # Initialize the ttl value + self.__ttl = None + + # Has the user passed in ttl value as a keyword? + if 'ttl' in kwargs: + self.ttl = kwargs['ttl'] + kwargs.pop('ttl') # avoid TypeError from super() + + # Call Podcast's constructor + super().__init__(*args, **kwargs) + + # If we were to use another namespace, we would add this here: + # self._nsmap['prefix'] = "URI" + + @property + def ttl(self): + """Your suggestion for how many minutes podcatchers should wait + before refreshing the feed. + + ttl stands for "time to live". + + :type: :obj:`int` + :RSS: ttl + """ + # By using @property and @ttl.setter, we encapsulate the ttl field + # so that we can check the value that is assigned to it. + # If you don't need this, you could just rename self.__ttl to + # self.ttl and remove those two methods. + return self.__ttl + + @ttl.setter + def ttl(self, ttl): + # Try to convert to int + try: + ttl_int = int(ttl) + except ValueError: + raise TypeError("ttl expects an integer, got %s" % ttl) + # Is this negative? + if ttl_int < 0: + raise ValueError("Negative ttl values aren't accepted, got %s" + % ttl_int) + # All checks passed + self.__ttl = ttl_int + + def _create_rss(self): + # Let Podcast generate the lxml etree (adding the standard elements) + rss = super()._create_rss() + # We must get the channel element, since we want to add subelements + # to it. + channel = rss.find("channel") + # Only add the ttl element if it has been populated. + if self.__ttl is not None: + # First create our new subelement of channel. + ttl = etree.SubElement(channel, 'ttl') + # If we were to use another namespace, we would instead do this: + # ttl = etree.SubElement(channel, + # '{%s}ttl' % self._nsmap['prefix']) + + # Then, fill it with the ttl value + ttl.text = str(self.__ttl) + + # Return the new etree, now with ttl + return rss + + # How to use the new class (normally, you would put this somewhere else) + myPodcast = PodcastWithTtl(name="Test", website="http://example.org", + explicit=False, description="Testing ttl") + myPodcast.ttl = 90 # or set ttl=90 in the constructor + print(myPodcast) + + +Using mixins +^^^^^^^^^^^^ + +To use mixins, you cannot make the class with the ``ttl`` functionality inherit +:class:`.Podcast`. Instead, it must inherit nothing. Other than that, the code +will be the same, so it doesn't make sense to repeat it here. + +:: + + class TtlMixin(object): + # ... + + # How to use the new mixin + class PodcastWithTtl(TtlMixin, Podcast): + def __init__(*args, **kwargs): + super().__init__(*args, **kwargs) + + myPodcast = PodcastWithTtl(name="Test", website="http://example.org", + explicit=False, description="Testing ttl") + myPodcast.ttl = 90 + print(myPodcast) + +Note the order of the mixins in the class declaration. You should read it as +the path Python takes when looking for a method. First Python checks +``PodcastWithTtl``, then ``TtlMixin`` and finally :class:`.Podcast`. This is +also the order the methods are called when chained together using :func:`super`. +If you had Podcast first, :meth:`.Podcast._create_rss` method would be run +first, and since it never calls ``super()._create_rss()``, the ``TtlMixin``'s +``_create_rss`` would never be run. Therefore, you should always have +:class:`.Podcast` last in that list. + +Which approach is best? +^^^^^^^^^^^^^^^^^^^^^^^ + +The advantage of mixins isn't really displayed here, but it will become +apparent as you add more and more extensions. Say you define 5 different mixins, +which all add exactly one more element to :class:`.Podcast`. If you used traditional +inheritance, you would have to make sure each of those 5 subclasses made up a +tree. That is, class 1 would inherit :class:`.Podcast`. Class 2 would have to inherit +class 1, class 3 would have to inherit class 2 and so on. If two of the classes +had the same superclass, you could get screwed. + +By using mixins, you can put them together however you want. Perhaps for one +podcast you only need ``ttl``, while for another podcast you want to use the +``textInput`` element in addition to ``ttl``, and another podcast requires the +``textInput`` element together with the ``comments`` element. Using traditional +inheritance, you would have to duplicate code for ``textInput`` in two classes. Not +so with mixins:: + + class PodcastWithTtl(TtlMixin, Podcast): + def __init__(*args, **kwargs): + super().__init__(*args, **kwargs) + + class PodcastWithTtlAndTextInput(TtlMixin, TextInputMixin, Podcast): + def __init__(*args, **kwargs): + super().__init__(*args, **kwargs) + + class PodcastWithTextInputAndComments(TextInputMixin, CommentsMixin, + Podcast): + def __init__(*args, **kwargs): + super().__init__(*args, **kwargs) + +If the list of elements you want to use varies between different podcasts, +mixins are the way to go. On the other hand, mixins are overkill if you are okay +with one giant class with all the elements you need. diff --git a/doc/index.rst b/doc/index.rst index 5e01b6b..6984f1e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -57,5 +57,6 @@ User Guide user/basic_usage_guide/part_2 user/basic_usage_guide/part_3 user/example + extending contributing api diff --git a/podgen/podcast.py b/podgen/podcast.py index b80628a..b4e5cbb 100644 --- a/podgen/podcast.py +++ b/podgen/podcast.py @@ -73,6 +73,16 @@ def __init__(self, **kwargs): self.__episode_class = Episode """The internal value used by self.Episode.""" + self._nsmap = { + 'atom': 'http://www.w3.org/2005/Atom', + 'content': 'http://purl.org/rss/1.0/modules/content/', + 'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd', + 'dc': 'http://purl.org/dc/elements/1.1/' + } + """A dictionary which maps namespace prefixes to their namespace URI. + Add a new entry here if you want to use that namespace. + """ + ## RSS # http://www.rssboard.org/rss-specification # Mandatory: @@ -414,17 +424,9 @@ def _create_rss(self): :returns: The root element (ie. the rss element) of the feed. :rtype: lxml.etree.Element """ + ITUNES_NS = self._nsmap['itunes'] - nsmap = { - 'atom': 'http://www.w3.org/2005/Atom', - 'content': 'http://purl.org/rss/1.0/modules/content/', - 'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd', - 'dc': 'http://purl.org/dc/elements/1.1/' - } - - ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' - - feed = etree.Element('rss', version='2.0', nsmap=nsmap ) + feed = etree.Element('rss', version='2.0', nsmap=self._nsmap) channel = etree.SubElement(feed, 'channel') if not (self.name and self.website and self.description and self.explicit is not None): @@ -484,7 +486,7 @@ def _create_rss(self): # author without email) for a in self.authors or []: author = etree.SubElement(channel, - '{%s}creator' % nsmap['dc']) + '{%s}creator' % self._nsmap['dc']) if a.name and a.email: author.text = "%s <%s>" % (a.name, a.email) elif a.name: @@ -566,13 +568,13 @@ def _create_rss(self): subtitle.text = self.subtitle if self.feed_url: - link_to_self = etree.SubElement(channel, '{%s}link' % nsmap['atom']) + link_to_self = etree.SubElement(channel, '{%s}link' % self._nsmap['atom']) link_to_self.attrib['href'] = self.feed_url link_to_self.attrib['rel'] = 'self' link_to_self.attrib['type'] = 'application/rss+xml' if self.pubsubhubbub: - link_to_hub = etree.SubElement(channel, '{%s}link' % nsmap['atom']) + link_to_hub = etree.SubElement(channel, '{%s}link' % self._nsmap['atom']) link_to_hub.attrib['href'] = self.pubsubhubbub link_to_hub.attrib['rel'] = 'hub'