Skip to content

Commit

Permalink
refactored and added docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
narfdotpl committed Oct 26, 2010
1 parent 0ab5275 commit dc52a2d
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 74 deletions.
2 changes: 1 addition & 1 deletion interssection/__init__.py
@@ -1,4 +1,4 @@
#!/usr/bin/env python
# encoding: utf-8

from interssection import *
from core import *
63 changes: 40 additions & 23 deletions interssection/interssection.py → interssection/core.py
@@ -1,19 +1,26 @@
#!/usr/bin/env python
# encoding: utf-8

import datetime
import uuid
"""
Core module providing Feed class.
"""

from feedparser import parse

from templates import render_as_atom
from interssection.templates import render_as_atom
from interssection.utils import get_iso8601_datetime, get_urn


__all__ = ['__author__', 'Feed']
__author__ = 'Maciej Konieczny <hello@narf.pl>'


class MetaFeed(type):
"""
Metaclass that adds methods to support set operations on feeds.
All frozenset methods are added apart from `copy()` and
`__contains__(elem)`.
"""

def __new__(cls, class_name, base_classes, attributes_dict):
for method_name in ['__and__', '__ge__', '__gt__', '__le__', '__len__',
Expand Down Expand Up @@ -41,8 +48,14 @@ def decorated_method(self, *args, **kwargs):


def create_set_method(method_name):
"""
Return method that accepts Feed instances as arguments and calls method
of given name on arguments' private frozenset attributes to produce
return value.
"""

def method(*feeds):
# prepare feeds
# prepare feeds (they're lazy)
for feed in feeds:
if feed._entry_ids is None:
feed._entries_by_id = dict((e.id, e) for e in feed._entries)
Expand All @@ -52,27 +65,39 @@ def method(*feeds):
first_set = feeds[0]._entry_ids
other_sets = (feed._entry_ids for feed in feeds[1:])

# run method
# run method on id sets
result = getattr(first_set, method_name)(*other_sets)

# return bools, etc.
# return bools and ints
if not isinstance(result, frozenset):
return result

# create resultant feed
# gather entries for new feed
entries_by_id = {}
for feed in feeds:
entries_by_id.update(feed._entries_by_id)
entries = [entries_by_id[id] for id in result]

# set feed title
title = (' ' + method_name + ' ').join(feed.title for feed in feeds)

# return new feed
return Feed(id=get_urn(), title=title, updated=get_iso8601_datetime(),
author='interssection', entries=entries)

return method


class Feed(object):
"""
Class that reads Atom and RSS feeds and lets you treat them like sets.
Feed objects have to be instantiated with single string argument that
can be either feed URL or raw XML.
Feed instances have two attributes: `id` and `title`, and support all
frozenset methods apart from `copy()` and `__contains__(elem)`.
"""

__metaclass__ = MetaFeed

Expand All @@ -82,7 +107,7 @@ def __init__(self, string_or_url=None, **feed_attributes):
Read feed from string or URL.
"""

# set initial...
# book place for ids frozenset (don't generate it now, be lazy)
self._entry_ids = None

# pretend it's `def __init__(self, string_or_url):`
Expand All @@ -91,12 +116,12 @@ def __init__(self, string_or_url=None, **feed_attributes):
message = '__init__() takes exactly 2 arguments (1 given)'
raise TypeError(message)

# use backdoor
# use backdoor to set attributes without parsing any string
for name in ['id', 'title', 'updated', 'author', 'entries']:
setattr(self, '_' + name, feed_attributes[name])
return

# accept only string (type) argument
# accept only string argument (url is also of string type)
if not isinstance(string_or_url, basestring):
raise TypeError('String expected, {} given.'
.format(string_or_url.__class__.__name__))
Expand Down Expand Up @@ -137,19 +162,11 @@ def __str__(self):
Render as Atom 1.0.
"""

# cache rendered template
if self._xml is None:
# pick feed attributes
context = {'entries': self._entries}
for name in ['id', 'title', 'updated', 'author']:
context[name] = getattr(self, '_' + name)

# context = {'id': self._id, ...}
context = dict((name, getattr(self, '_' + name)) for name in
['id', 'title', 'updated', 'author', 'entries'])
self._xml = render_as_atom(context)

return self._xml


def get_iso8601_datetime():
return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')

def get_urn():
return uuid.uuid4().urn
18 changes: 17 additions & 1 deletion interssection/templates/__init__.py
@@ -1,4 +1,20 @@
#!/usr/bin/env python
# encoding: utf-8

from templates import *
from jinja2 import Environment, PackageLoader


__all__ = ['render_as_atom']


# create environment
env = Environment(loader=PackageLoader('interssection', 'templates'),
extensions=['jinja2.ext.autoescape'],
autoescape=True)

# get template
template = env.get_template('atom.xml')


def render_as_atom(context):
return template.render(context)
20 changes: 0 additions & 20 deletions interssection/templates/templates.py

This file was deleted.

13 changes: 13 additions & 0 deletions interssection/utils.py
@@ -0,0 +1,13 @@
#!/usr/bin/env python
# encoding: utf-8

import datetime
import uuid


def get_iso8601_datetime():
return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')


def get_urn():
return uuid.uuid4().urn
26 changes: 7 additions & 19 deletions tests/feeds/__init__.py
Expand Up @@ -4,23 +4,11 @@
from os.path import dirname, join, realpath


# read atom feeds
# read atom feeds (updating locals is ugly but efficient)
_current_dir = dirname(realpath(__file__))

with open(join(_current_dir, 'atom12.xml')) as f:
atom12 = f.read()

with open(join(_current_dir, 'atom23.xml')) as f:
atom23 = f.read()

with open(join(_current_dir, 'atom34.xml')) as f:
atom34 = f.read()

with open(join(_current_dir, 'atom-min-entry.xml')) as f:
atom_min_entry = f.read()

with open(join(_current_dir, 'atom-min-feed.xml')) as f:
atom_min_feed = f.read()

with open(join(_current_dir, 'atom-html.xml')) as f:
atom_html = f.read()
_locals = locals()
for variable in ['atom12', 'atom23', 'atom34', 'atom_min_entry',
'atom_min_feed', 'atom_html']:
filename = variable.replace('_', '-') + '.xml'
with open(join(_current_dir, filename)) as f:
_locals[variable] = f.read()
3 changes: 0 additions & 3 deletions tests/server.py
Expand Up @@ -5,9 +5,6 @@
from wsgiref.simple_server import make_server


__author__ = 'Maciej Konieczny <hello@narf.pl>'


def serve_atom(host, port, content):
"""
Run HTTP server in separate thread on given host and port, serve given
Expand Down
5 changes: 1 addition & 4 deletions tests/testsuite.py
Expand Up @@ -14,9 +14,6 @@
from tests.validator import validate


__author__ = 'Maciej Konieczny <hello@narf.pl>'


# validate test feeds
for feed in [atom12, atom23, atom34, atom_min_entry, atom_min_feed, atom_html]:
validate(feed)
Expand Down Expand Up @@ -67,7 +64,7 @@ def test_raise_error_on_no_argument(self):
Feed()

def test_raise_error_on_incorrect_argument(self):
for item in [0, 2.72, ('foo',), ['bar'], {'baz': 'quak'}]:
for item in [0, 2.72, ('foo',), ['bar'], {'baz': 'quux'}]:
with self.assertRaises(TypeError):
Feed(item)

Expand Down
3 changes: 0 additions & 3 deletions tests/validator.py
Expand Up @@ -7,9 +7,6 @@
from feedvalidator.logging import Error


__author__ = 'Maciej Konieczny <hello@narf.pl>'


class ValidationError(Exception):

def __init__(self, event):
Expand Down

0 comments on commit dc52a2d

Please sign in to comment.