From 518cb111298a205bace219a8b859a8a66d38c5b2 Mon Sep 17 00:00:00 2001 From: Thorben Dahl Date: Sat, 9 Jul 2016 01:37:51 +0200 Subject: [PATCH 1/2] Provide an example on how to extend PodGen Also, make it possible to define new namespaces to be used. --- doc/extending.rst | 179 ++++++++++++++++++++++++++++++++++++++++++++++ doc/index.rst | 1 + podgen/podcast.py | 28 ++++---- 3 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 doc/extending.rst diff --git a/doc/extending.rst b/doc/extending.rst new file mode 100644 index 0000000..d015f6f --- /dev/null +++ b/doc/extending.rst @@ -0,0 +1,179 @@ +Adding new tags +=============== + +Are there XML tags you want to use that aren't supported by PodGen? If so, you +should be able to add them in using inheritance. + +.. 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 tag out of the box. + + +Quick How-to +------------ + +#. Create new class that extends Podcast. +#. Add the new attribute. +#. Override :meth:`.Podcast._create_rss`, call super()._create_rss(), + add the new tag to its result and return the new tree. + +If you'll use RSS elements from another namespace, you must make sure you +update the _nsmap attribute of 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 to figure out how to do things. The example below may help, too. + +.. _lxml API documentation: http://lxml.de/api/index.html + +You can do the same with Episode, if you replace _create_rss() with +rss_entry() above. + +Example: Adding a ttl field +--------------------------- + +The examples here assume version 3 of Python is used. + +Using traditional inheritance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + from lxml import etree + + 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) + + @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 + """ + return self.__ttl + + @ttl.setter + def ttl(self, ttl): + try: + ttl_int = int(ttl) + except ValueError: + raise TypeError("ttl expects an integer, got %s" % ttl) + + if ttl_int < 0: + raise ValueError("Negative ttl values aren't accepted, got %s" + % ttl_int) + self.__ttl = ttl_int + + def _create_rss(self): + rss = super()._create_rss() + channel = rss.find("channel") + if self.__ttl is not None: + ttl = etree.SubElement(channel, 'ttl') + ttl.text = str(self.__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 + print(myPodcast) + + +Using mixins +^^^^^^^^^^^^ + +To use mixins, you cannot make the class with the ttl functionality inherit +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, finally Podcast. This is also the order the +methods are called when chained together using super(). If you had Podcast +first, Podcast's _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 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 attribute to 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 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 would be 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 attributes 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 attributes 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' From 2bebb27175a0549490de3ac2f922964040ddba3f Mon Sep 17 00:00:00 2001 From: Thorben Dahl Date: Tue, 12 Jul 2016 13:53:16 +0200 Subject: [PATCH 2/2] Explain the example better, small improvements to extending.rst --- doc/extending.rst | 121 +++++++++++++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 38 deletions(-) diff --git a/doc/extending.rst b/doc/extending.rst index d015f6f..b8b321a 100644 --- a/doc/extending.rst +++ b/doc/extending.rst @@ -1,8 +1,12 @@ Adding new tags =============== -Are there XML tags you want to use that aren't supported by PodGen? If so, you -should be able to add them in using inheritance. +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:: @@ -11,47 +15,67 @@ should be able to add them in using inheritance. .. note:: - Feel free to add a feature request to GitHub Issues if you think PodGen - should support a certain tag out of the box. + 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 Podcast. +#. Create new class that extends :class:`.Podcast`. #. Add the new attribute. -#. Override :meth:`.Podcast._create_rss`, call super()._create_rss(), - add the new tag to its result and return the new tree. +#. 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 _nsmap attribute of 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:: +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 to figure out how to do things. The example below may help, too. +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 -You can do the same with Episode, if you replace _create_rss() with -rss_entry() above. - -Example: Adding a ttl field ---------------------------- +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 @@ -73,6 +97,9 @@ Using traditional inheritance # 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 @@ -83,42 +110,59 @@ Using traditional inheritance :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 + 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 -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. +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. :: @@ -137,28 +181,29 @@ the same, so it doesn't make sense to repeat it here. 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, finally Podcast. This is also the order the -methods are called when chained together using super(). If you had Podcast -first, Podcast's _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 Podcast last in that list. +``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 attribute to Podcast. If you used traditional +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 Podcast. Class 2 would have to inherit +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 would be screwed. +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 +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): @@ -174,6 +219,6 @@ so with mixins:: def __init__(*args, **kwargs): super().__init__(*args, **kwargs) -If the list of attributes you want to use varies between different podcasts, +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 attributes you need. +with one giant class with all the elements you need.