diff --git a/.gitignore b/.gitignore index b6f6775..af64cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea/ venv *.pyc *.pyo @@ -10,3 +11,7 @@ feedgen/tests/tmp_Rssfeed.xml tmp_Atomfeed.xml tmp_Rssfeed.xml + +# testing artifacts +.coverage +*.egg-info/ diff --git a/feedgen/ext/geo_entry.py b/feedgen/ext/geo_entry.py index 8c9dd15..2ad6611 100644 --- a/feedgen/ext/geo_entry.py +++ b/feedgen/ext/geo_entry.py @@ -9,18 +9,73 @@ :license: FreeBSD and LGPL, see license.* for more details. ''' +import numbers +import warnings from lxml import etree from feedgen.ext.base import BaseEntryExtension +class GeoRSSPolygonInteriorWarning(Warning): + """ + Simple placeholder for warning about ignored polygon interiors. + + Stores the original geom on a ``geom`` attribute (if required warnings are + raised as errors). + """ + + def __init__(self, geom, *args, **kwargs): + self.geom = geom + super(GeoRSSPolygonInteriorWarning, self).__init__(*args, **kwargs) + + def __str__(self): + return '{:d} interiors of polygon ignored'.format( + len(self.geom.__geo_interface__['coordinates']) - 1 + ) # ignore exterior in count + + +class GeoRSSGeometryError(ValueError): + """ + Subclass of ValueError for a GeoRSS geometry error + + Only some geometries are supported in Simple GeoRSS, so if not raise an + error. Offending geometry is stored on the ``geom`` attribute. + """ + + def __init__(self, geom, *args, **kwargs): + self.geom = geom + super(GeoRSSGeometryError, self).__init__(*args, **kwargs) + + def __str__(self): + msg = "Geometry of type '{}' not in Point, Linestring or Polygon" + return msg.format( + self.geom.__geo_interface__['type'] + ) + + class GeoEntryExtension(BaseEntryExtension): '''FeedEntry extension for Simple GeoRSS. ''' def __init__(self): - # Simple GeoRSS tag + '''Simple GeoRSS tag''' + # geometries self.__point = None + self.__line = None + self.__polygon = None + self.__box = None + + # additional properties + self.__featuretypetag = None + self.__relationshiptag = None + self.__featurename = None + + # elevation + self.__elev = None + self.__floor = None + + # radius + self.__radius = None def extend_file(self, entry): '''Add additional fields to an RSS item. @@ -34,6 +89,48 @@ class GeoEntryExtension(BaseEntryExtension): point = etree.SubElement(entry, '{%s}point' % GEO_NS) point.text = self.__point + if self.__line: + line = etree.SubElement(entry, '{%s}line' % GEO_NS) + line.text = self.__line + + if self.__polygon: + polygon = etree.SubElement(entry, '{%s}polygon' % GEO_NS) + polygon.text = self.__polygon + + if self.__box: + box = etree.SubElement(entry, '{%s}box' % GEO_NS) + box.text = self.__box + + if self.__featuretypetag: + featuretypetag = etree.SubElement( + entry, + '{%s}featuretypetag' % GEO_NS + ) + featuretypetag.text = self.__featuretypetag + + if self.__relationshiptag: + relationshiptag = etree.SubElement( + entry, + '{%s}relationshiptag' % GEO_NS + ) + relationshiptag.text = self.__relationshiptag + + if self.__featurename: + featurename = etree.SubElement(entry, '{%s}featurename' % GEO_NS) + featurename.text = self.__featurename + + if self.__elev: + elevation = etree.SubElement(entry, '{%s}elev' % GEO_NS) + elevation.text = str(self.__elev) + + if self.__floor: + floor = etree.SubElement(entry, '{%s}floor' % GEO_NS) + floor.text = str(self.__floor) + + if self.__radius: + radius = etree.SubElement(entry, '{%s}radius' % GEO_NS) + radius.text = str(self.__radius) + return entry def extend_rss(self, entry): @@ -53,3 +150,186 @@ class GeoEntryExtension(BaseEntryExtension): self.__point = point return self.__point + + def line(self, line=None): + '''Get or set the georss:line of the entry + + :param point: The GeoRSS formatted line (i.e. "45.256 -110.45 46.46 + -109.48 43.84 -109.86") + :return: The current georss:line of the entry + ''' + if line is not None: + self.__line = line + + return self.__line + + def polygon(self, polygon=None): + '''Get or set the georss:polygon of the entry + + :param polygon: The GeoRSS formatted polygon (i.e. "45.256 -110.45 + 46.46 -109.48 43.84 -109.86 45.256 -110.45") + :return: The current georss:polygon of the entry + ''' + if polygon is not None: + self.__polygon = polygon + + return self.__polygon + + def box(self, box=None): + ''' + Get or set the georss:box of the entry + + :param box: The GeoRSS formatted box (i.e. "42.943 -71.032 43.039 + -69.856") + :return: The current georss:box of the entry + ''' + if box is not None: + self.__box = box + + return self.__box + + def featuretypetag(self, featuretypetag=None): + ''' + Get or set the georss:featuretypetag of the entry + + :param featuretypetag: The GeoRSS feaaturertyptag (e.g. "city") + :return: The current georss:featurertypetag + ''' + if featuretypetag is not None: + self.__featuretypetag = featuretypetag + + return self.__featuretypetag + + def relationshiptag(self, relationshiptag=None): + ''' + Get or set the georss:relationshiptag of the entry + + :param relationshiptag: The GeoRSS relationshiptag (e.g. + "is-centred-at") + :return: the current georss:relationshiptag + ''' + if relationshiptag is not None: + self.__relationshiptag = relationshiptag + + return self.__relationshiptag + + def featurename(self, featurename=None): + ''' + Get or set the georss:featurename of the entry + + :param featuretypetag: The GeoRSS featurename (e.g. "Footscray") + :return: the current georss:featurename + ''' + if featurename is not None: + self.__featurename = featurename + + return self.__featurename + + def elev(self, elev=None): + ''' + Get or set the georss:elev of the entry + + :param elev: The GeoRSS elevation (e.g. 100.3) + :type elev: numbers.Number + :return: the current georss:elev + ''' + if elev is not None: + if not isinstance(elev, numbers.Number): + raise ValueError("elev tag must be numeric: {}".format(elev)) + + self.__elev = elev + + return self.__elev + + def floor(self, floor=None): + ''' + Get or set the georss:floor of the entry + + :param floor: The GeoRSS floor (e.g. 4) + :type floor: int + :return: the current georss:floor + ''' + if floor is not None: + if not isinstance(floor, int): + raise ValueError("floor tag must be int: {}".format(floor)) + + self.__floor = floor + + return self.__floor + + def radius(self, radius=None): + ''' + Get or set the georss:radius of the entry + + :param radius: The GeoRSS radius (e.g. 100.3) + :type radius: numbers.Number + :return: the current georss:radius + ''' + if radius is not None: + if not isinstance(radius, numbers.Number): + raise ValueError( + "radius tag must be numeric: {}".format(radius) + ) + + self.__radius = radius + + return self.__radius + + def geom_from_geo_interface(self, geom): + ''' + Generate a georss geometry from some Python object with a + ``__geo_interface__`` property (see the `geo_interface specification by + Sean Gillies`_geointerface ) + + Note only a subset of GeoJSON (see `geojson.org`_geojson ) can be + easily converted to GeoRSS: + + - Point + - LineString + - Polygon (if there are holes / donuts in the polygons a warning will + be generaated + + Other GeoJson types will raise a ``ValueError``. + + .. note:: The geometry is assumed to be x, y as longitude, latitude in + the WGS84 projection. + + .. _geointerface: https://gist.github.com/sgillies/2217756 + .. _geojson: https://geojson.org/ + + :param geom: Geometry object with a __geo_interface__ property + :return: the formatted GeoRSS geometry + ''' + geojson = geom.__geo_interface__ + + if geojson['type'] not in ('Point', 'LineString', 'Polygon'): + raise GeoRSSGeometryError(geom) + + if geojson['type'] == 'Point': + + coords = '{:f} {:f}'.format( + geojson['coordinates'][1], # latitude is y + geojson['coordinates'][0] + ) + return self.point(coords) + + elif geojson['type'] == 'LineString': + + coords = ' '.join( + '{:f} {:f}'.format(vertex[1], vertex[0]) + for vertex in + geojson['coordinates'] + ) + return self.line(coords) + + elif geojson['type'] == 'Polygon': + + if len(geojson['coordinates']) > 1: + warnings.warn(GeoRSSPolygonInteriorWarning(geom)) + + coords = ' '.join( + '{:f} {:f}'.format(vertex[1], vertex[0]) + for vertex in + geojson['coordinates'][0] + ) + return self.polygon(coords) diff --git a/tests/test_extension.py b/tests/test_extension.py deleted file mode 100644 index db85c08..0000000 --- a/tests/test_extension.py +++ /dev/null @@ -1,300 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests for extensions -""" - -import unittest - -from lxml import etree - -from feedgen.feed import FeedGenerator - - -class TestExtensionSyndication(unittest.TestCase): - - SYN_NS = {'sy': 'http://purl.org/rss/1.0/modules/syndication/'} - - def setUp(self): - self.fg = FeedGenerator() - self.fg.load_extension('syndication') - self.fg.title('title') - self.fg.link(href='http://example.com', rel='self') - self.fg.description('description') - - def test_update_period(self): - for period_type in ('hourly', 'daily', 'weekly', 'monthly', 'yearly'): - self.fg.syndication.update_period(period_type) - root = etree.fromstring(self.fg.rss_str()) - a = root.xpath('/rss/channel/sy:UpdatePeriod', - namespaces=self.SYN_NS) - assert a[0].text == period_type - - def test_update_frequency(self): - for frequency in (1, 100, 2000, 100000): - self.fg.syndication.update_frequency(frequency) - root = etree.fromstring(self.fg.rss_str()) - a = root.xpath('/rss/channel/sy:UpdateFrequency', - namespaces=self.SYN_NS) - assert a[0].text == str(frequency) - - def test_update_base(self): - base = '2000-01-01T12:00+00:00' - self.fg.syndication.update_base(base) - root = etree.fromstring(self.fg.rss_str()) - a = root.xpath('/rss/channel/sy:UpdateBase', namespaces=self.SYN_NS) - assert a[0].text == base - - -class TestExtensionPodcast(unittest.TestCase): - - def setUp(self): - self.fg = FeedGenerator() - self.fg.load_extension('podcast') - self.fg.title('title') - self.fg.link(href='http://example.com', rel='self') - self.fg.description('description') - - def test_category_new(self): - self.fg.podcast.itunes_category([{'cat': 'Technology', - 'sub': 'Podcasting'}]) - self.fg.podcast.itunes_explicit('no') - self.fg.podcast.itunes_complete('no') - self.fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss') - self.fg.podcast.itunes_owner('John Doe', 'john@example.com') - ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'} - root = etree.fromstring(self.fg.rss_str()) - cat = root.xpath('/rss/channel/itunes:category/@text', namespaces=ns) - scat = root.xpath('/rss/channel/itunes:category/itunes:category/@text', - namespaces=ns) - assert cat[0] == 'Technology' - assert scat[0] == 'Podcasting' - - def test_category(self): - self.fg.podcast.itunes_category('Technology', 'Podcasting') - self.fg.podcast.itunes_explicit('no') - self.fg.podcast.itunes_complete('no') - self.fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss') - self.fg.podcast.itunes_owner('John Doe', 'john@example.com') - ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'} - root = etree.fromstring(self.fg.rss_str()) - cat = root.xpath('/rss/channel/itunes:category/@text', namespaces=ns) - scat = root.xpath('/rss/channel/itunes:category/itunes:category/@text', - namespaces=ns) - assert cat[0] == 'Technology' - assert scat[0] == 'Podcasting' - - def test_podcastItems(self): - fg = self.fg - fg.podcast.itunes_author('Lars Kiesow') - fg.podcast.itunes_block('x') - fg.podcast.itunes_complete(False) - fg.podcast.itunes_explicit('no') - fg.podcast.itunes_image('x.png') - fg.podcast.itunes_subtitle('x') - fg.podcast.itunes_summary('x') - assert fg.podcast.itunes_author() == 'Lars Kiesow' - assert fg.podcast.itunes_block() == 'x' - assert fg.podcast.itunes_complete() == 'no' - assert fg.podcast.itunes_explicit() == 'no' - assert fg.podcast.itunes_image() == 'x.png' - assert fg.podcast.itunes_subtitle() == 'x' - assert fg.podcast.itunes_summary() == 'x' - - # Check that we have the item in the resulting XML - ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'} - root = etree.fromstring(self.fg.rss_str()) - author = root.xpath('/rss/channel/itunes:author/text()', namespaces=ns) - assert author == ['Lars Kiesow'] - - def test_podcastEntryItems(self): - fe = self.fg.add_item() - fe.title('y') - fe.podcast.itunes_author('Lars Kiesow') - fe.podcast.itunes_block('x') - fe.podcast.itunes_duration('00:01:30') - fe.podcast.itunes_explicit('no') - fe.podcast.itunes_image('x.png') - fe.podcast.itunes_is_closed_captioned('yes') - fe.podcast.itunes_order(1) - fe.podcast.itunes_subtitle('x') - fe.podcast.itunes_summary('x') - assert fe.podcast.itunes_author() == 'Lars Kiesow' - assert fe.podcast.itunes_block() == 'x' - assert fe.podcast.itunes_duration() == '00:01:30' - assert fe.podcast.itunes_explicit() == 'no' - assert fe.podcast.itunes_image() == 'x.png' - assert fe.podcast.itunes_is_closed_captioned() - assert fe.podcast.itunes_order() == 1 - assert fe.podcast.itunes_subtitle() == 'x' - assert fe.podcast.itunes_summary() == 'x' - - # Check that we have the item in the resulting XML - ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'} - root = etree.fromstring(self.fg.rss_str()) - author = root.xpath('/rss/channel/item/itunes:author/text()', - namespaces=ns) - assert author == ['Lars Kiesow'] - - -class TestExtensionGeo(unittest.TestCase): - - def setUp(self): - self.fg = FeedGenerator() - self.fg.load_extension('geo') - self.fg.title('title') - self.fg.link(href='http://example.com', rel='self') - self.fg.description('description') - - def test_geoEntryItems(self): - fe = self.fg.add_item() - fe.title('y') - fe.geo.point('42.36 -71.05') - - assert fe.geo.point() == '42.36 -71.05' - - # Check that we have the item in the resulting XML - ns = {'georss': 'http://www.georss.org/georss'} - root = etree.fromstring(self.fg.rss_str()) - point = root.xpath('/rss/channel/item/georss:point/text()', - namespaces=ns) - assert point == ['42.36 -71.05'] - - -class TestExtensionDc(unittest.TestCase): - - def setUp(self): - self.fg = FeedGenerator() - self.fg.load_extension('dc') - self.fg.title('title') - self.fg.link(href='http://example.com', rel='self') - self.fg.description('description') - - def test_entryLoadExtension(self): - fe = self.fg.add_item() - try: - fe.load_extension('dc') - except ImportError: - pass # Extension already loaded - - def test_elements(self): - for method in dir(self.fg.dc): - if method.startswith('dc_'): - m = getattr(self.fg.dc, method) - m(method) - assert m() == [method] - - self.fg.id('123') - assert self.fg.atom_str() - assert self.fg.rss_str() - - -class TestExtensionTorrent(unittest.TestCase): - - def setUp(self): - self.fg = FeedGenerator() - self.fg.load_extension('torrent') - self.fg.title('title') - self.fg.link(href='http://example.com', rel='self') - self.fg.description('description') - - def test_podcastEntryItems(self): - fe = self.fg.add_item() - fe.title('y') - fe.torrent.filename('file.xy') - fe.torrent.infohash('123') - fe.torrent.contentlength('23') - fe.torrent.seeds('1') - fe.torrent.peers('2') - fe.torrent.verified('1') - assert fe.torrent.filename() == 'file.xy' - assert fe.torrent.infohash() == '123' - assert fe.torrent.contentlength() == '23' - assert fe.torrent.seeds() == '1' - assert fe.torrent.peers() == '2' - assert fe.torrent.verified() == '1' - - # Check that we have the item in the resulting XML - ns = {'torrent': 'http://xmlns.ezrss.it/0.1/dtd/'} - root = etree.fromstring(self.fg.rss_str()) - filename = root.xpath('/rss/channel/item/torrent:filename/text()', - namespaces=ns) - assert filename == ['file.xy'] - - -class TestExtensionMedia(unittest.TestCase): - - def setUp(self): - self.fg = FeedGenerator() - self.fg.load_extension('media') - self.fg.id('id') - self.fg.title('title') - self.fg.link(href='http://example.com', rel='self') - self.fg.description('description') - - def test_media_content(self): - fe = self.fg.add_item() - fe.id('id') - fe.title('title') - fe.content('content') - fe.media.content(url='file1.xy') - fe.media.content(url='file2.xy') - fe.media.content(url='file1.xy', group=2) - fe.media.content(url='file2.xy', group=2) - fe.media.content(url='file.xy', group=None) - - ns = {'media': 'http://search.yahoo.com/mrss/', - 'a': 'http://www.w3.org/2005/Atom'} - # Check that we have the item in the resulting RSS - root = etree.fromstring(self.fg.rss_str()) - url = root.xpath('/rss/channel/item/media:group/media:content[1]/@url', - namespaces=ns) - assert url == ['file1.xy', 'file1.xy'] - - # There is one without a group - url = root.xpath('/rss/channel/item/media:content[1]/@url', - namespaces=ns) - assert url == ['file.xy'] - - # Check that we have the item in the resulting Atom feed - root = etree.fromstring(self.fg.atom_str()) - url = root.xpath('/a:feed/a:entry/media:group/media:content[1]/@url', - namespaces=ns) - assert url == ['file1.xy', 'file1.xy'] - - fe.media.content(content=[], replace=True) - assert fe.media.content() == [] - - def test_media_thumbnail(self): - fe = self.fg.add_item() - fe.id('id') - fe.title('title') - fe.content('content') - fe.media.thumbnail(url='file1.xy') - fe.media.thumbnail(url='file2.xy') - fe.media.thumbnail(url='file1.xy', group=2) - fe.media.thumbnail(url='file2.xy', group=2) - fe.media.thumbnail(url='file.xy', group=None) - - ns = {'media': 'http://search.yahoo.com/mrss/', - 'a': 'http://www.w3.org/2005/Atom'} - # Check that we have the item in the resulting RSS - root = etree.fromstring(self.fg.rss_str()) - url = root.xpath( - '/rss/channel/item/media:group/media:thumbnail[1]/@url', - namespaces=ns) - assert url == ['file1.xy', 'file1.xy'] - - # There is one without a group - url = root.xpath('/rss/channel/item/media:thumbnail[1]/@url', - namespaces=ns) - assert url == ['file.xy'] - - # Check that we have the item in the resulting Atom feed - root = etree.fromstring(self.fg.atom_str()) - url = root.xpath('/a:feed/a:entry/media:group/media:thumbnail[1]/@url', - namespaces=ns) - assert url == ['file1.xy', 'file1.xy'] - - fe.media.thumbnail(thumbnail=[], replace=True) - assert fe.media.thumbnail() == [] diff --git a/tests/test_extensions/__init__.py b/tests/test_extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_extensions/test_dc.py b/tests/test_extensions/test_dc.py new file mode 100644 index 0000000..1623804 --- /dev/null +++ b/tests/test_extensions/test_dc.py @@ -0,0 +1,31 @@ +import unittest + +from feedgen.feed import FeedGenerator + + +class TestExtensionDc(unittest.TestCase): + + def setUp(self): + self.fg = FeedGenerator() + self.fg.load_extension('dc') + self.fg.title('title') + self.fg.link(href='http://example.com', rel='self') + self.fg.description('description') + + def test_entryLoadExtension(self): + fe = self.fg.add_item() + try: + fe.load_extension('dc') + except ImportError: + pass # Extension already loaded + + def test_elements(self): + for method in dir(self.fg.dc): + if method.startswith('dc_'): + m = getattr(self.fg.dc, method) + m(method) + assert m() == [method] + + self.fg.id('123') + assert self.fg.atom_str() + assert self.fg.rss_str() diff --git a/tests/test_extensions/test_geo.py b/tests/test_extensions/test_geo.py new file mode 100644 index 0000000..6dd401b --- /dev/null +++ b/tests/test_extensions/test_geo.py @@ -0,0 +1,429 @@ +from itertools import chain +import unittest +import warnings + +from lxml import etree + +from feedgen.feed import FeedGenerator +from feedgen.ext.geo_entry import GeoRSSPolygonInteriorWarning, GeoRSSGeometryError # noqa: E501 + + +class Geom(object): + """ + Dummy geom to make testing easier + + When we use the geo-interface we need a class with a `__geo_interface__` + property. Makes it easier for the other tests as well. + + Ultimately this could be used to generate dummy geometries for testing + a wider variety of values (e.g. with the faker library, or the hypothesis + library) + """ + + def __init__(self, geom_type, coords): + self.geom_type = geom_type + self.coords = coords + + def __str__(self): + if self.geom_type == 'Point': + + coords = '{:f} {:f}'.format( + self.coords[1], # latitude is y + self.coords[0] + ) + return coords + + elif self.geom_type == 'LineString': + + coords = ' '.join( + '{:f} {:f}'.format(vertex[1], vertex[0]) + for vertex in + self.coords + ) + return coords + + elif self.geom_type == 'Polygon': + + coords = ' '.join( + '{:f} {:f}'.format(vertex[1], vertex[0]) + for vertex in + self.coords[0] + ) + return coords + + elif self.geom_type == 'Box': + # box not really supported by GeoJSON, but it's a handy cheat here + # for testing + coords = ' '.join( + '{:f} {:f}'.format(vertex[1], vertex[0]) + for vertex in + self.coords + ) + return coords[:2] + + else: + return 'Not a supported geometry' + + @property + def __geo_interface__(self): + return { + 'type': self.geom_type, + 'coordinates': self.coords + } + + +class TestExtensionGeo(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.point = Geom('Point', [-71.05, 42.36]) + cls.line = Geom('LineString', [[-71.05, 42.36], [-71.15, 42.46]]) + cls.polygon = Geom( + 'Polygon', + [[[-71.05, 42.36], [-71.15, 42.46], [-71.15, 42.36]]] + ) + cls.box = Geom('Box', [[-71.05, 42.36], [-71.15, 42.46]]) + cls.polygon_with_interior = Geom( + 'Polygon', + [ + [ # exterior + [0, 0], + [0, 1], + [1, 1], + [1, 0], + [0, 0] + ], + [ # interior + [0.25, 0.25], + [0.25, 0.75], + [0.75, 0.75], + [0.75, 0.25], + [0.25, 0.25] + ] + ] + ) + + def setUp(self): + self.fg = FeedGenerator() + self.fg.load_extension('geo') + self.fg.title('title') + self.fg.link(href='http://example.com', rel='self') + self.fg.description('description') + + def test_point(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.point(str(self.point)) + + self.assertEqual(fe.geo.point(), str(self.point)) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + point = root.xpath('/rss/channel/item/georss:point/text()', + namespaces=ns) + self.assertEqual(point, [str(self.point)]) + + def test_line(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.line(str(self.line)) + + self.assertEqual(fe.geo.line(), str(self.line)) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + line = root.xpath( + '/rss/channel/item/georss:line/text()', + namespaces=ns + ) + self.assertEqual(line, [str(self.line)]) + + def test_polygon(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.polygon(str(self.polygon)) + + self.assertEqual(fe.geo.polygon(), str(self.polygon)) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + poly = root.xpath( + '/rss/channel/item/georss:polygon/text()', + namespaces=ns + ) + self.assertEqual(poly, [str(self.polygon)]) + + def test_box(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.box(str(self.box)) + + self.assertEqual(fe.geo.box(), str(self.box)) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + box = root.xpath( + '/rss/channel/item/georss:box/text()', + namespaces=ns + ) + self.assertEqual(box, [str(self.box)]) + + def test_featuretypetag(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.featuretypetag('city') + + self.assertEqual(fe.geo.featuretypetag(), 'city') + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + featuretypetag = root.xpath( + '/rss/channel/item/georss:featuretypetag/text()', + namespaces=ns + ) + self.assertEqual(featuretypetag, ['city']) + + def test_relationshiptag(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.relationshiptag('is-centred-at') + + self.assertEqual(fe.geo.relationshiptag(), 'is-centred-at') + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + relationshiptag = root.xpath( + '/rss/channel/item/georss:relationshiptag/text()', + namespaces=ns + ) + self.assertEqual(relationshiptag, ['is-centred-at']) + + def test_featurename(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.featurename('Footscray') + + self.assertEqual(fe.geo.featurename(), 'Footscray') + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + featurename = root.xpath( + '/rss/channel/item/georss:featurename/text()', + namespaces=ns + ) + self.assertEqual(featurename, ['Footscray']) + + def test_elev(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.elev(100.3) + + self.assertEqual(fe.geo.elev(), 100.3) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + elev = root.xpath( + '/rss/channel/item/georss:elev/text()', + namespaces=ns + ) + self.assertEqual(elev, ['100.3']) + + def test_elev_fails_nonnumeric(self): + fe = self.fg.add_item() + fe.title('y') + + with self.assertRaises(ValueError): + fe.geo.elev('100.3') + + def test_floor(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.floor(4) + + self.assertEqual(fe.geo.floor(), 4) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + floor = root.xpath( + '/rss/channel/item/georss:floor/text()', + namespaces=ns + ) + self.assertEqual(floor, ['4']) + + def test_floor_fails_nonint(self): + fe = self.fg.add_item() + fe.title('y') + + with self.assertRaises(ValueError): + fe.geo.floor(100.3) + + with self.assertRaises(ValueError): + fe.geo.floor('4') + + def test_radius(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.radius(100.3) + + self.assertEqual(fe.geo.radius(), 100.3) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + radius = root.xpath( + '/rss/channel/item/georss:radius/text()', + namespaces=ns + ) + self.assertEqual(radius, ['100.3']) + + def test_radius_fails_nonnumeric(self): + fe = self.fg.add_item() + fe.title('y') + + with self.assertRaises(ValueError): + fe.geo.radius('100.3') + + def test_geom_from_geointerface_point(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.geom_from_geo_interface(self.point) + + self.assertEqual(fe.geo.point(), str(self.point)) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + point = root.xpath('/rss/channel/item/georss:point/text()', + namespaces=ns) + self.assertEqual(point, [str(self.point)]) + + coords = [float(c) for c in point[0].split()] + + try: + self.assertCountEqual( + coords, + self.point.coords + ) + except AttributeError: # was assertItemsEqual in Python 2.7 + self.assertItemsEqual( + coords, + self.point.coords + ) + + def test_geom_from_geointerface_line(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.geom_from_geo_interface(self.line) + + self.assertEqual(fe.geo.line(), str(self.line)) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + line = root.xpath('/rss/channel/item/georss:line/text()', + namespaces=ns) + self.assertEqual(line, [str(self.line)]) + + coords = [float(c) for c in line[0].split()] + + try: + self.assertCountEqual( + coords, + list(chain.from_iterable(self.line.coords)) + ) + except AttributeError: # was assertItemsEqual in Python 2.7 + self.assertItemsEqual( + coords, + list(chain.from_iterable(self.line.coords)) + ) + + def test_geom_from_geointerface_poly(self): + fe = self.fg.add_item() + fe.title('y') + fe.geo.geom_from_geo_interface(self.polygon) + + self.assertEqual(fe.geo.polygon(), str(self.polygon)) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + poly = root.xpath('/rss/channel/item/georss:polygon/text()', + namespaces=ns) + self.assertEqual(poly, [str(self.polygon)]) + + coords = [float(c) for c in poly[0].split()] + + try: + self.assertCountEqual( + coords, + list(chain.from_iterable(self.polygon.coords[0])) + ) + except AttributeError: # was assertItemsEqual in Python 2.7 + self.assertItemsEqual( + coords, + list(chain.from_iterable(self.polygon.coords[0])) + ) + + def test_geom_from_geointerface_fail_other_geom(self): + fe = self.fg.add_item() + fe.title('y') + + with self.assertRaises(GeoRSSGeometryError): + fe.geo.geom_from_geo_interface(self.box) + + def test_geom_from_geointerface_fail_requires_geo_interface(self): + fe = self.fg.add_item() + fe.title('y') + + with self.assertRaises(AttributeError): + fe.geo.geom_from_geo_interface(str(self.box)) + + def test_geom_from_geointerface_warn_poly_interior(self): + """ + Test complex polygons warn as expected. Taken from + + https://stackoverflow.com/a/3892301/379566 and + https://docs.python.org/2.7/library/warnings.html#testing-warnings + """ + fe = self.fg.add_item() + fe.title('y') + + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger a warning. + fe.geo.geom_from_geo_interface(self.polygon_with_interior) + # Verify some things + assert len(w) == 1 + assert issubclass(w[-1].category, GeoRSSPolygonInteriorWarning) + + self.assertEqual(fe.geo.polygon(), str(self.polygon_with_interior)) + + # Check that we have the item in the resulting XML + ns = {'georss': 'http://www.georss.org/georss'} + root = etree.fromstring(self.fg.rss_str()) + poly = root.xpath('/rss/channel/item/georss:polygon/text()', + namespaces=ns) + self.assertEqual(poly, [str(self.polygon_with_interior)]) + + coords = [float(c) for c in poly[0].split()] + + try: + self.assertCountEqual( + coords, + list(chain.from_iterable(self.polygon_with_interior.coords[0])) + ) + except AttributeError: # was assertItemsEqual in Python 2.7 + self.assertItemsEqual( + coords, + list(chain.from_iterable(self.polygon_with_interior.coords[0])) + ) diff --git a/tests/test_extensions/test_media.py b/tests/test_extensions/test_media.py new file mode 100644 index 0000000..7fd9e40 --- /dev/null +++ b/tests/test_extensions/test_media.py @@ -0,0 +1,83 @@ +import unittest + +from lxml import etree + +from feedgen.feed import FeedGenerator + + +class TestExtensionMedia(unittest.TestCase): + + def setUp(self): + self.fg = FeedGenerator() + self.fg.load_extension('media') + self.fg.id('id') + self.fg.title('title') + self.fg.link(href='http://example.com', rel='self') + self.fg.description('description') + + def test_media_content(self): + fe = self.fg.add_item() + fe.id('id') + fe.title('title') + fe.content('content') + fe.media.content(url='file1.xy') + fe.media.content(url='file2.xy') + fe.media.content(url='file1.xy', group=2) + fe.media.content(url='file2.xy', group=2) + fe.media.content(url='file.xy', group=None) + + ns = {'media': 'http://search.yahoo.com/mrss/', + 'a': 'http://www.w3.org/2005/Atom'} + # Check that we have the item in the resulting RSS + root = etree.fromstring(self.fg.rss_str()) + url = root.xpath('/rss/channel/item/media:group/media:content[1]/@url', + namespaces=ns) + assert url == ['file1.xy', 'file1.xy'] + + # There is one without a group + url = root.xpath('/rss/channel/item/media:content[1]/@url', + namespaces=ns) + assert url == ['file.xy'] + + # Check that we have the item in the resulting Atom feed + root = etree.fromstring(self.fg.atom_str()) + url = root.xpath('/a:feed/a:entry/media:group/media:content[1]/@url', + namespaces=ns) + assert url == ['file1.xy', 'file1.xy'] + + fe.media.content(content=[], replace=True) + assert fe.media.content() == [] + + def test_media_thumbnail(self): + fe = self.fg.add_item() + fe.id('id') + fe.title('title') + fe.content('content') + fe.media.thumbnail(url='file1.xy') + fe.media.thumbnail(url='file2.xy') + fe.media.thumbnail(url='file1.xy', group=2) + fe.media.thumbnail(url='file2.xy', group=2) + fe.media.thumbnail(url='file.xy', group=None) + + ns = {'media': 'http://search.yahoo.com/mrss/', + 'a': 'http://www.w3.org/2005/Atom'} + # Check that we have the item in the resulting RSS + root = etree.fromstring(self.fg.rss_str()) + url = root.xpath( + '/rss/channel/item/media:group/media:thumbnail[1]/@url', + namespaces=ns) + assert url == ['file1.xy', 'file1.xy'] + + # There is one without a group + url = root.xpath('/rss/channel/item/media:thumbnail[1]/@url', + namespaces=ns) + assert url == ['file.xy'] + + # Check that we have the item in the resulting Atom feed + root = etree.fromstring(self.fg.atom_str()) + url = root.xpath('/a:feed/a:entry/media:group/media:thumbnail[1]/@url', + namespaces=ns) + assert url == ['file1.xy', 'file1.xy'] + + fe.media.thumbnail(thumbnail=[], replace=True) + assert fe.media.thumbnail() == [] diff --git a/tests/test_extensions/test_podcast.py b/tests/test_extensions/test_podcast.py new file mode 100644 index 0000000..a41c96c --- /dev/null +++ b/tests/test_extensions/test_podcast.py @@ -0,0 +1,96 @@ +import unittest + +from lxml import etree + +from feedgen.feed import FeedGenerator + + +class TestExtensionPodcast(unittest.TestCase): + + def setUp(self): + self.fg = FeedGenerator() + self.fg.load_extension('podcast') + self.fg.title('title') + self.fg.link(href='http://example.com', rel='self') + self.fg.description('description') + + def test_category_new(self): + self.fg.podcast.itunes_category([{'cat': 'Technology', + 'sub': 'Podcasting'}]) + self.fg.podcast.itunes_explicit('no') + self.fg.podcast.itunes_complete('no') + self.fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss') + self.fg.podcast.itunes_owner('John Doe', 'john@example.com') + ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'} + root = etree.fromstring(self.fg.rss_str()) + cat = root.xpath('/rss/channel/itunes:category/@text', namespaces=ns) + scat = root.xpath('/rss/channel/itunes:category/itunes:category/@text', + namespaces=ns) + assert cat[0] == 'Technology' + assert scat[0] == 'Podcasting' + + def test_category(self): + self.fg.podcast.itunes_category('Technology', 'Podcasting') + self.fg.podcast.itunes_explicit('no') + self.fg.podcast.itunes_complete('no') + self.fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss') + self.fg.podcast.itunes_owner('John Doe', 'john@example.com') + ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'} + root = etree.fromstring(self.fg.rss_str()) + cat = root.xpath('/rss/channel/itunes:category/@text', namespaces=ns) + scat = root.xpath('/rss/channel/itunes:category/itunes:category/@text', + namespaces=ns) + assert cat[0] == 'Technology' + assert scat[0] == 'Podcasting' + + def test_podcastItems(self): + fg = self.fg + fg.podcast.itunes_author('Lars Kiesow') + fg.podcast.itunes_block('x') + fg.podcast.itunes_complete(False) + fg.podcast.itunes_explicit('no') + fg.podcast.itunes_image('x.png') + fg.podcast.itunes_subtitle('x') + fg.podcast.itunes_summary('x') + assert fg.podcast.itunes_author() == 'Lars Kiesow' + assert fg.podcast.itunes_block() == 'x' + assert fg.podcast.itunes_complete() == 'no' + assert fg.podcast.itunes_explicit() == 'no' + assert fg.podcast.itunes_image() == 'x.png' + assert fg.podcast.itunes_subtitle() == 'x' + assert fg.podcast.itunes_summary() == 'x' + + # Check that we have the item in the resulting XML + ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'} + root = etree.fromstring(self.fg.rss_str()) + author = root.xpath('/rss/channel/itunes:author/text()', namespaces=ns) + assert author == ['Lars Kiesow'] + + def test_podcastEntryItems(self): + fe = self.fg.add_item() + fe.title('y') + fe.podcast.itunes_author('Lars Kiesow') + fe.podcast.itunes_block('x') + fe.podcast.itunes_duration('00:01:30') + fe.podcast.itunes_explicit('no') + fe.podcast.itunes_image('x.png') + fe.podcast.itunes_is_closed_captioned('yes') + fe.podcast.itunes_order(1) + fe.podcast.itunes_subtitle('x') + fe.podcast.itunes_summary('x') + assert fe.podcast.itunes_author() == 'Lars Kiesow' + assert fe.podcast.itunes_block() == 'x' + assert fe.podcast.itunes_duration() == '00:01:30' + assert fe.podcast.itunes_explicit() == 'no' + assert fe.podcast.itunes_image() == 'x.png' + assert fe.podcast.itunes_is_closed_captioned() + assert fe.podcast.itunes_order() == 1 + assert fe.podcast.itunes_subtitle() == 'x' + assert fe.podcast.itunes_summary() == 'x' + + # Check that we have the item in the resulting XML + ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'} + root = etree.fromstring(self.fg.rss_str()) + author = root.xpath('/rss/channel/item/itunes:author/text()', + namespaces=ns) + assert author == ['Lars Kiesow'] diff --git a/tests/test_extensions/test_syndication.py b/tests/test_extensions/test_syndication.py new file mode 100644 index 0000000..7a187d7 --- /dev/null +++ b/tests/test_extensions/test_syndication.py @@ -0,0 +1,40 @@ +import unittest + +from lxml import etree + +from feedgen.feed import FeedGenerator + + +class TestExtensionSyndication(unittest.TestCase): + + SYN_NS = {'sy': 'http://purl.org/rss/1.0/modules/syndication/'} + + def setUp(self): + self.fg = FeedGenerator() + self.fg.load_extension('syndication') + self.fg.title('title') + self.fg.link(href='http://example.com', rel='self') + self.fg.description('description') + + def test_update_period(self): + for period_type in ('hourly', 'daily', 'weekly', 'monthly', 'yearly'): + self.fg.syndication.update_period(period_type) + root = etree.fromstring(self.fg.rss_str()) + a = root.xpath('/rss/channel/sy:UpdatePeriod', + namespaces=self.SYN_NS) + assert a[0].text == period_type + + def test_update_frequency(self): + for frequency in (1, 100, 2000, 100000): + self.fg.syndication.update_frequency(frequency) + root = etree.fromstring(self.fg.rss_str()) + a = root.xpath('/rss/channel/sy:UpdateFrequency', + namespaces=self.SYN_NS) + assert a[0].text == str(frequency) + + def test_update_base(self): + base = '2000-01-01T12:00+00:00' + self.fg.syndication.update_base(base) + root = etree.fromstring(self.fg.rss_str()) + a = root.xpath('/rss/channel/sy:UpdateBase', namespaces=self.SYN_NS) + assert a[0].text == base diff --git a/tests/test_extensions/test_torrent.py b/tests/test_extensions/test_torrent.py new file mode 100644 index 0000000..e996fde --- /dev/null +++ b/tests/test_extensions/test_torrent.py @@ -0,0 +1,38 @@ +import unittest + +from lxml import etree + +from feedgen.feed import FeedGenerator + + +class TestExtensionTorrent(unittest.TestCase): + + def setUp(self): + self.fg = FeedGenerator() + self.fg.load_extension('torrent') + self.fg.title('title') + self.fg.link(href='http://example.com', rel='self') + self.fg.description('description') + + def test_podcastEntryItems(self): + fe = self.fg.add_item() + fe.title('y') + fe.torrent.filename('file.xy') + fe.torrent.infohash('123') + fe.torrent.contentlength('23') + fe.torrent.seeds('1') + fe.torrent.peers('2') + fe.torrent.verified('1') + assert fe.torrent.filename() == 'file.xy' + assert fe.torrent.infohash() == '123' + assert fe.torrent.contentlength() == '23' + assert fe.torrent.seeds() == '1' + assert fe.torrent.peers() == '2' + assert fe.torrent.verified() == '1' + + # Check that we have the item in the resulting XML + ns = {'torrent': 'http://xmlns.ezrss.it/0.1/dtd/'} + root = etree.fromstring(self.fg.rss_str()) + filename = root.xpath('/rss/channel/item/torrent:filename/text()', + namespaces=ns) + assert filename == ['file.xy']