From 8b25dbd60c4c0195cbf17b5ecc5c520fac2a2642 Mon Sep 17 00:00:00 2001 From: Michalis Fragkiadakis Date: Fri, 6 Nov 2020 00:01:14 +0200 Subject: [PATCH 1/3] Support for multiple categories/subcategories --- doc/conf.py | 2 +- podgen/category.py | 124 +++++++++++++++++----------------- podgen/podcast.py | 42 ++++++------ podgen/tests/test_category.py | 56 +++++++-------- podgen/tests/test_podcast.py | 13 ++-- podgen/warnings.py | 2 +- 6 files changed, 121 insertions(+), 118 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 3e8bdc9..fac00b0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -279,7 +279,7 @@ def iad_add_directive_header(self, sig): # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, -# dir menu entry, description, category) +# dir menu entry, description, categories) texinfo_documents = [ ('index', 'pyPodGen.tex', u'PodGen Documentation', u'Lars Kiesow, Thorben Dahl', 'Lernfunk3', 'One line description of project.', diff --git a/podgen/category.py b/podgen/category.py index 883c9bd..cbdcebc 100644 --- a/podgen/category.py +++ b/podgen/category.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """ - podgen.category + podgen.categories ~~~~~~~~~~~~~~~ - This module contains Category, which represents a single iTunes category. + This module contains Category, which represents a single iTunes categories. :copyright: 2016, Thorben Dahl :license: FreeBSD and LGPL, see license.* for more details. @@ -17,11 +17,11 @@ class Category(object): - """Immutable class representing an Apple Podcasts category. + """Immutable class representing an Apple Podcasts categories. - By using this class, you can be sure that the chosen category is a - valid category, that it is formatted correctly and you will be warned - when using an old category. + By using this class, you can be sure that the chosen categories is a + valid categories, that it is formatted correctly and you will be warned + when using an old categories. See https://help.apple.com/itc/podcasts_connect/#/itc9267a2f12 for an overview of the available categories and their subcategories. @@ -35,22 +35,22 @@ class Category(object): .. note:: The categories are case-insensitive, and you may escape ampersands. - The category and subcategory will end up properly capitalized and + The categories and subcategory will end up properly capitalized and with unescaped ampersands. Example:: - >>> from podgen import Category - >>> c = Category("Music") - >>> c.category + >> from podgen import Category + >> c = Category("Music") + >> c.categories Music - >>> c.subcategory + >> c.subcategory None - >>> - >>> d = Category("games & hobbies", "Video games") - >>> d.category + >> + >> d = Category("games & hobbies", "Video games") + >> d.categories Games & Hobbies - >>> d.subcategory + >> d.subcategory Video Games """ @@ -212,41 +212,52 @@ class Category(object): ], } - def __init__(self, category, subcategory=None): + def __init__(self, categories): """Create new Category object. See the class description of :class:ยด~podgen.category.Category`. - :param category: Category of the podcast. - :type category: str - :param subcategory: (Optional) Subcategory of the podcast. - :type subcategory: str or None + :param categories: Categories and subcategories of the podcast. + :type categories: list of tuples """ - if not category: - raise TypeError("category must be provided, was \"%s\"" % category) + if not categories: + raise TypeError("categories must be provided, was \"%s\"" % categories) + canonical_categories = [] try: - canonical_category, canonical_subcategory = self._look_up_category( - category, - subcategory, - self._categories, - ) + for category in categories: + try: + subcategory = category[1] + except IndexError: + subcategory = None + canonical_category, canonical_subcategory = self._look_up_category( + category[0], + subcategory, + self._categories, + ) + canonical_categories.append((canonical_category, canonical_subcategory)) except ValueError: - # Maybe this is a legacy category? - canonical_category, canonical_subcategory = self._look_up_category( - category, - subcategory, - self._legacy_categories, - ) - # Okay, it is, warn about this - warnings.warn( - 'The category ("%s", "%s") is a legacy category. Please switch ' - 'to one of the new Apple Podcast categories.' % - (canonical_category, canonical_subcategory), - category=LegacyCategoryWarning, - stacklevel=2 - ) + # Maybe this is a legacy categories? + for category in categories: + try: + subcategory = category[1] + except IndexError: + subcategory = None + canonical_category, canonical_subcategory = self._look_up_category( + category[0], + subcategory, + self._legacy_categories, + ) + # Okay, it is, warn about this + warnings.warn( + 'The categories ("%s", "%s") is a legacy categories. Please switch ' + 'to one of the new Apple Podcast categories.' % + (canonical_category, canonical_subcategory), + category=LegacyCategoryWarning, + stacklevel=2 + ) + canonical_categories.append((canonical_category, canonical_subcategory)) - self.__category = canonical_category - self.__subcategory = canonical_subcategory + self.__categories = canonical_categories + # self.__subcategory = canonical_subcategory def _look_up_category( self, @@ -254,7 +265,7 @@ def _look_up_category( subcategory, available_categories ): - # Do a case-insensitive search for the category + # Do a case-insensitive search for the categories search_category = category.strip().replace("&", "&").lower() for actual_category in available_categories: if actual_category.lower() == search_category: @@ -262,7 +273,7 @@ def _look_up_category( canonical_category = actual_category break else: # no break - raise ValueError('Invalid category "%s"' % category) + raise ValueError('Invalid categories "%s"' % category) # Do a case-insensitive search for the subcategory, if provided canonical_subcategory = None @@ -274,29 +285,20 @@ def _look_up_category( canonical_subcategory = actual_subcategory break else: # no break - raise ValueError('Invalid subcategory "%s" under category "%s"' + raise ValueError('Invalid subcategory "%s" under categories "%s"' % (subcategory, canonical_category)) return canonical_category, canonical_subcategory @property - def category(self): - """The category represented by this object. Read-only. + def categories(self): + """The categories represented by this object. Read-only. - :type: :obj:`str` + :type: :obj:`list` """ - return self.__category - # Make this attribute read-only by not implementing setter - - @property - def subcategory(self): - """The subcategory this object represents. Read-only. - - :type: :obj:`str` - """ - return self.__subcategory + return self.__categories # Make this attribute read-only by not implementing setter def __repr__(self): - return 'Category(category=%s, subcategory=%s)' % \ - (self.category, self.subcategory) + return 'Category(categories=%s)' % \ + self.categories diff --git a/podgen/podcast.py b/podgen/podcast.py index 41e96a3..79c37d3 100644 --- a/podgen/podcast.py +++ b/podgen/podcast.py @@ -225,7 +225,7 @@ def __init__(self, **kwargs): :RSS: itunes:block """ - self.__category = None + self.__categories = None self.__image = None @@ -582,12 +582,13 @@ def _create_rss(self): block = etree.SubElement(channel, '{%s}block' % ITUNES_NS) block.text = 'Yes' - if self.category: - category = etree.SubElement(channel, '{%s}category' % ITUNES_NS) - category.attrib['text'] = self.category.category - if self.category.subcategory: - subcategory = etree.SubElement(category, '{%s}category' % ITUNES_NS) - subcategory.attrib['text'] = self.category.subcategory + if self.categories: + for cat in self.categories.categories: + category = etree.SubElement(channel, '{%s}category' % ITUNES_NS) + category.attrib['text'] = cat[0] + if cat[1]: + subcategory = etree.SubElement(category, '{%s}category' % ITUNES_NS) + subcategory.attrib['text'] = cat[1] if self.image: image = etree.SubElement(channel, '{%s}image' % ITUNES_NS) @@ -1069,27 +1070,26 @@ def web_master(self, web_master): self.__web_master = web_master @property - def category(self): - """The iTunes category, which appears in the category column + def categories(self): + """The iTunes categories, which appears in the categories column and in iTunes Store listings. :type: :class:`podgen.Category` - :RSS: itunes:category + :RSS: itunes:categories """ - return self.__category - - @category.setter - def category(self, category): - if category is not None: - # Check that the category quacks like a duck - if hasattr(category, "category") and \ - hasattr(category, "subcategory"): - self.__category = category + return self.__categories + + @categories.setter + def categories(self, categories): + if categories is not None: + # Check that the categories quacks like a duck + if hasattr(categories, "categories"): + self.__categories = categories else: raise TypeError("A Category(-like) object must be used, got " - "%s" % category) + "%s" % categories) else: - self.__category = None + self.__categories = None @property def image(self): diff --git a/podgen/tests/test_category.py b/podgen/tests/test_category.py index 9b2a7c6..a8052c5 100644 --- a/podgen/tests/test_category.py +++ b/podgen/tests/test_category.py @@ -35,45 +35,45 @@ def test_constructorWithSubcategory(self): # Replacement of assertWarns in Python 2.7 warnings.simplefilter("always", LegacyCategoryWarning) - c = Category("Arts", "Food") - self.assertEqual(c.category, "Arts") - self.assertEqual(c.subcategory, "Food") + c = Category([("Arts", "Food")]) + self.assertEqual(c.categories[0][0], "Arts") + self.assertEqual(c.categories[0][1], "Food") # No warning should be given # Replacement of assertWarns in Python 2.7 self.assertEqual(len(w), 0); def test_constructorWithoutSubcategory(self): - c = Category("Arts") - self.assertEqual(c.category, "Arts") - self.assertTrue(c.subcategory is None) + c = Category([("Arts",)]) + self.assertEqual(c.categories[0][0], "Arts") + self.assertTrue(c.categories[0][1] is None) def test_constructorInvalidCategory(self): - self.assertRaises(ValueError, Category, "Farts", "Food") + self.assertRaises(ValueError, Category, [("Farts", "Food")]) def test_constructorInvalidSubcategory(self): - self.assertRaises(ValueError, Category, "Arts", "Flood") + self.assertRaises(ValueError, Category, [("Arts", "Flood")]) def test_constructorSubcategoryWithoutCategory(self): - self.assertRaises((ValueError, TypeError), Category, None, "Food") + self.assertRaises((ValueError, TypeError, AttributeError), Category, [(None, "Food")]) def test_constructorCaseInsensitive(self): - c = Category("arTS", "FOOD") - self.assertEqual(c.category, "Arts") - self.assertEqual(c.subcategory, "Food") + c = Category([("arTS", "FOOD")]) + self.assertEqual(c.categories[0][0], "Arts") + self.assertEqual(c.categories[0][1], "Food") def test_immutable(self): - c = Category("Arts", "Food") - self.assertRaises(AttributeError, setattr, c, "category", "Fiction") - self.assertEqual(c.category, "Arts") + c = Category([("Arts", "Food")]) + self.assertRaises(AttributeError, setattr, c, "categories", [("Fiction",)]) + self.assertEqual(c.categories[0][0], "Arts") - self.assertRaises(AttributeError, setattr, c, "subcategory", "Science Fiction") - self.assertEqual(c.subcategory, "Food") + self.assertRaises(AttributeError, setattr, c, "categories", [("Fiction", "Science Fiction")]) + self.assertEqual(c.categories[0][1], "Food") def test_escapedIsAccepted(self): - c = Category("Kids & Family", "Pets & Animals") - self.assertEqual(c.category, "Kids & Family") - self.assertEqual(c.subcategory, "Pets & Animals") + c = Category([("Kids & Family", "Pets & Animals")]) + self.assertEqual(c.categories[0][0], "Kids & Family") + self.assertEqual(c.categories[0][1], "Pets & Animals") def test_oldCategoryIsAcceptedWithWarning(self): # Replacement of assertWarns in Python 2.7 @@ -81,8 +81,8 @@ def test_oldCategoryIsAcceptedWithWarning(self): # Replacement of assertWarns in Python 2.7 warnings.simplefilter("always", LegacyCategoryWarning) - c = Category("Government & Organizations") - self.assertEqual(c.category, "Government & Organizations") + c = Category([("Government & Organizations",)]) + self.assertEqual(c.categories[0][0], "Government & Organizations") # Replacement of assertWarns in Python 2.7 self.assertEqual(len(w), 1) @@ -94,9 +94,9 @@ def test_oldSubcategoryIsAcceptedWithWarnings(self): # Replacement of assertWarns in Python 2.7 warnings.simplefilter("always", LegacyCategoryWarning) - c = Category("Technology", "Podcasting") - self.assertEqual(c.category, "Technology") - self.assertEqual(c.subcategory, "Podcasting") + c = Category([("Technology", "Podcasting")]) + self.assertEqual(c.categories[0][0], "Technology") + self.assertEqual(c.categories[0][1], "Podcasting") # Replacement of assertWarns in Python 2.7 self.assertEqual(len(w), 1) @@ -108,9 +108,9 @@ def test_oldCategorySubcategoryIsAcceptedWithWarnings(self): # Replacement of assertWarns in Python 2.7 warnings.simplefilter("always", LegacyCategoryWarning) - c = Category("Science & Medicine", "Medicine") - self.assertEqual(c.category, "Science & Medicine") - self.assertEqual(c.subcategory, "Medicine") + c = Category([("Science & Medicine", "Medicine")]) + self.assertEqual(c.categories[0][0], "Science & Medicine") + self.assertEqual(c.categories[0][1], "Medicine") # Replacement of assertWarns in Python 2.7 self.assertEqual(len(w), 1) diff --git a/podgen/tests/test_podcast.py b/podgen/tests/test_podcast.py index fa81180..ddab5ab 100644 --- a/podgen/tests/test_podcast.py +++ b/podgen/tests/test_podcast.py @@ -427,18 +427,18 @@ def test_webMaster(self): channel.find("webMaster").text) def test_categoryWithoutSubcategory(self): - c = Category("Arts") + c = Category([("Arts",)]) self.fg.category = c channel = self.fg._create_rss().find("channel") itunes_category = channel.find("{%s}category" % self.nsItunes) assert itunes_category is not None - self.assertEqual(itunes_category.get("text"), c.category) + self.assertEqual(itunes_category.get("text"), c.categories[0][0]) assert itunes_category.find("{%s}category" % self.nsItunes) is None def test_categoryWithSubcategory(self): - c = Category("Arts", "Food") + c = Category([("Arts", "Food")]) self.fg.category = c channel = self.fg._create_rss().find("channel") itunes_category = channel.find("{%s}category" % self.nsItunes) @@ -446,11 +446,11 @@ def test_categoryWithSubcategory(self): itunes_subcategory = itunes_category\ .find("{%s}category" % self.nsItunes) assert itunes_subcategory is not None - self.assertEqual(itunes_subcategory.get("text"), c.subcategory) + self.assertEqual(itunes_subcategory.get("text"), c.categories[0][1]) def test_categoryChecks(self): - c = ("Arts", "Food") - self.assertRaises(TypeError, setattr, self.fg, "category", c) + c = ([("Arts", "Food")]) + self.assertRaises(TypeError, setattr, self.fg, "categories", c) def test_explicitIsExplicit(self): self.fg.explicit = True @@ -635,5 +635,6 @@ def test_isSerialWhenTrue(self): # Test that its contents is correct self.assertEqual(podcast_type.text, "serial") + if __name__ == '__main__': unittest.main() diff --git a/podgen/warnings.py b/podgen/warnings.py index 9f3c3a3..733422b 100644 --- a/podgen/warnings.py +++ b/podgen/warnings.py @@ -31,7 +31,7 @@ class NotRecommendedWarning(PodgenWarning): class LegacyCategoryWarning(PodgenWarning): """ - Indicates that the category created is an old category. It will still be + Indicates that the categories created is an old categories. It will still be accepted by Apple Podcasts, but it would be wise to use the new categories since they may have more relevant options for your podcast. From b7e3173298afac07a9f997c9ff059db09f1c94f2 Mon Sep 17 00:00:00 2001 From: Michalis Fragkiadakis Date: Fri, 6 Nov 2020 13:42:50 +0200 Subject: [PATCH 2/3] Fixed bug in test_podcast.py that failed the categories assertion --- podgen/tests/test_podcast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/podgen/tests/test_podcast.py b/podgen/tests/test_podcast.py index ddab5ab..dd3696a 100644 --- a/podgen/tests/test_podcast.py +++ b/podgen/tests/test_podcast.py @@ -428,7 +428,7 @@ def test_webMaster(self): def test_categoryWithoutSubcategory(self): c = Category([("Arts",)]) - self.fg.category = c + self.fg.categories = c channel = self.fg._create_rss().find("channel") itunes_category = channel.find("{%s}category" % self.nsItunes) assert itunes_category is not None @@ -439,7 +439,7 @@ def test_categoryWithoutSubcategory(self): def test_categoryWithSubcategory(self): c = Category([("Arts", "Food")]) - self.fg.category = c + self.fg.categories = c channel = self.fg._create_rss().find("channel") itunes_category = channel.find("{%s}category" % self.nsItunes) assert itunes_category is not None From bb45ab557d42722c174c982a88e4137c1b199654 Mon Sep 17 00:00:00 2001 From: Michalis Fragkiadakis Date: Fri, 6 Nov 2020 13:47:08 +0200 Subject: [PATCH 3/3] Updated __main__.py to the new format --- podgen/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podgen/__main__.py b/podgen/__main__.py index 05371e9..5a00874 100644 --- a/podgen/__main__.py +++ b/podgen/__main__.py @@ -56,7 +56,7 @@ def main(): p.is_serial = True p.language = 'de' p.feed_url = 'http://example.com/feeds/myfeed.rss' - p.category = Category('Leisure', 'Aviation') + p.category = Category([('Leisure', 'Aviation')]) p.explicit = False p.complete = False p.new_feed_url = 'http://example.com/new-feed.rss'