Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for multiple categories/subcategories #118

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
2 changes: 1 addition & 1 deletion podgen/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
124 changes: 63 additions & 61 deletions podgen/category.py
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
:license: FreeBSD and LGPL, see license.* for more details.
Expand All @@ -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.
Expand All @@ -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 &amp; hobbies", "Video games")
>>> d.category
>>
>> d = Category("games &amp; hobbies", "Video games")
>> d.categories
Games & Hobbies
>>> d.subcategory
>> d.subcategory
Video Games
"""

Expand Down Expand Up @@ -212,57 +212,68 @@ 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,
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("&amp;", "&").lower()
for actual_category in available_categories:
if actual_category.lower() == search_category:
# We found it
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
Expand All @@ -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
42 changes: 21 additions & 21 deletions podgen/podcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def __init__(self, **kwargs):
:RSS: itunes:block
"""

self.__category = None
self.__categories = None

self.__image = None

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
56 changes: 28 additions & 28 deletions podgen/tests/test_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,54 +35,54 @@ 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 &amp; Family", "Pets &amp; Animals")
self.assertEqual(c.category, "Kids & Family")
self.assertEqual(c.subcategory, "Pets & Animals")
c = Category([("Kids &amp; Family", "Pets &amp; 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
with warnings.catch_warnings(record=True) as w:
# 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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading