1
0
mirror of https://github.com/gryf/vimblogger_ft.git synced 2025-12-17 19:40:24 +01:00

Initial import

This commit is contained in:
2010-12-13 22:09:57 +01:00
commit cd6be2563c
12 changed files with 2331 additions and 0 deletions

View File

@@ -0,0 +1 @@
# module rst2blogger

View File

@@ -0,0 +1,214 @@
"""
File: blogger.py
Author: Roman 'gryf' Dobosz
Description: This is blogger activity connected module. It is using gdata[1]
blogger module to provide add/modify/delete articles interface.
[1] http://code.google.com/p/gdata-python-client
"""
import datetime
import re
import atom
from gdata.blogger.client import BloggerClient, BLOG_POST_URL
from gdata.blogger.data import BlogPost
class VimBlogger(object):
"""
Communicate with blogger through gdata.blogger modules
"""
DATE_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2})"
"T(\d{2}:\d{2}:\d{2})(\.\d{3})?[+-]"
"(\d{2}:\d{2})$")
DATE_FORMAT = "%Y-%m-%d"
TIME_FORMAT = "%H:%M:%S"
TZ_FORMAT = "%H:%M"
def __init__(self, blogname, login, password):
"""
Initialization.
"""
self.draft = True
self.blog_id = None
self.blog = None
self.client = BloggerClient()
self._authorize(login, password)
self.feed = self.client.get_blogs()
self._set_blog(blogname)
def get_articles(self, maxarticles=0):
"""
Return list of articles
"""
feed = self.client.get_posts(self.blog_id)
posts = []
for index, entry in enumerate(feed.entry):
if maxarticles and index >= maxarticles:
break
posts.append((entry.get_post_id(),
entry.title.text,
self._extract_date(entry.published.text)))
return posts
def create_article(self, html_doc, attrs=None):
"""
Create new article
html_doc is content of the article in HTML format, without headers,
preamble, doctype and body tags.
attrs is a dictionary that should hold title, date and tags.
return BlogPost object
"""
if not attrs:
attrs = {}
title = 'title' in attrs and attrs['title'] or ""
title = atom.data.Title(text=title, type="text")
html_doc = atom.data.Content(text=html_doc, type="html")
new_post = BlogPost(title=title, content=html_doc)
if 'tags' in attrs and attrs['tags']:
for tag in attrs['tags'].split(','):
new_post.add_label(tag.strip())
if 'date' in attrs and attrs['date'] and \
self._check_date(attrs['date']):
new_post.published = atom.data.Published(text=attrs['date'])
if self.draft:
new_post.control = atom.data.Control(\
draft=atom.data.Draft(text='yes'))
return self.client.post(new_post, BLOG_POST_URL % self.blog_id)
def update_article(self, html_doc, attrs):
"""
Update article.
html_doc is content of the article in HTML format, without headers,
preamble, doctype and body tags.
attrs is a dictionary that should hold title, date and tags.
return BlogPost object
"""
if "id" not in attrs:
raise Exception("Post Id not found in attributes!")
post = self._get_post(attrs['id'])
post.content = atom.data.Content(text=html_doc, type="html")
# update published date
if 'date' in attrs and attrs['date'] and \
self._check_date(attrs['date']):
post.published = atom.data.Published(text=attrs['date'])
if 'title' in attrs and attrs['title']:
post.title = atom.data.Title(text=attrs['title'], type="text")
#
# update tag list
if 'tags' in attrs:
tags = [tag.strip() for tag in attrs['tags'].split(',')]
for index, label in enumerate(post.category):
if label.term not in tags:
del(post.category[index])
for tag in tags:
self._add_tag(post, tag.strip())
return self.client.update(post)
def delete_article(self, post_id):
"""
Delete selected article
"""
if not post_id:
return "No article id provided"
post = self._get_post(post_id)
self.client.delete(post)
return None
def _get_post(self, post_id):
"""
Return post with specified ID
"""
post_href = self.blog.get_post_link().href
return self.client.get_feed(post_href + "/%s" % post_id,
desired_class=BlogPost)
def _add_tag(self, post, tag):
"""
post - BlogPost object
tag - string with tag/label to add
"""
for label in post.category:
if label.term == tag:
return
post.add_label(tag)
def _extract_date(self, date_string, time=False):
"""
Extract date from the string and optionally time
"""
if not self.DATE_PATTERN.match(date_string):
return False
if not time:
return self.DATE_PATTERN.match(date_string).groups()[0]
groups = self.DATE_PATTERN.match(date_string).groups()
return groups[0] + " " + groups[1]
def _check_date(self, date):
"""
Parse date as RFC 3339 format, for example:
2010-11-30T21:06:48.678+01:00
or
2010-11-30T21:06:48+01:00
Returns true, if date is acceptable, false otherwise
"""
if not self.DATE_PATTERN.match(date):
return False
groups = self.DATE_PATTERN.match(date).groups()
_date = groups[0]
_time = groups[1]
_tz = len(groups) == 3 and groups[2] or groups[3]
try:
datetime.datetime.strptime(_date, self.DATE_FORMAT)
datetime.datetime.strptime(_time, self.TIME_FORMAT)
datetime.datetime.strptime(_tz, self.TZ_FORMAT)
except ValueError:
return False
return True
def _authorize(self, login, password):
"""
Try to authorize in Google service.
Authorization is kept in client object. In case of wrong credentials,
exception is thrown.
"""
source = 'Vim rst2blogger interface'
service = 'blogger'
self.client.client_login(login,
password,
source=source,
service=service)
def _set_blog(self, blogname):
"""
Set correct blog, as defined in blogname
"""
for blog in self.feed.entry:
if blog.get_blog_name() == blogname:
self.blog_id = blog.get_blog_id()
self.blog = blog
break

View File

@@ -0,0 +1,204 @@
# vim: fileencoding=utf8
"""
File: main.py
Author: Roman 'gryf' Dobosz
Description: main file to provide fuctionality between vim and moudles rest
and blogger
"""
import webbrowser
from xml.dom import minidom
from xml.parsers.expat import ExpatError
import vim
from rst2blogger.rest import blogPreview, blogArticleString
from rst2blogger.blogger import VimBlogger
class Rst2Blogger(object):
"""
Provide convenient way to communicate between vim and blogger through reST
"""
def __init__(self):
vim.command('call setqflist([])')
self.buff = vim.current.buffer
self.docinfo_len = 0
self._set_docinfo_len()
self.login = vim.eval("g:blogger_login")
self.password = vim.eval("g:blogger_pass")
self.blogname = vim.eval("g:blogger_name")
self.buffer_encoding = vim.eval("&fileencoding")
self.vim_encoding = vim.eval("&encoding")
self.draft = int(vim.eval("g:blogger_draft"))
self.maxarticles = int(vim.eval("g:blogger_maxarticles"))
self.confirm_del = int(vim.eval("g:blogger_confirm_del"))
self.stylesheets = vim.eval("g:blogger_stylesheets")
def preview(self):
"""
Generate HTML Blogger article preview and (optionally) display it in
systems' web browser
"""
bufcontent = "\n".join(self.buff)
name = self.buff.name
name = name[:-4] + ".html"
html = blogPreview(bufcontent, self.stylesheets)
self._open_qf(self._check_html(html))
output_file = open(name, "w")
output_file.write(html)
output_file.close()
if vim.eval("g:blogger_browser"):
webbrowser.open(name)
return "Generated HTML has been opened in browser"
else:
return "Generated HTML has been written to %s" % name
def post(self):
"""
Do post article
"""
bufcontent = "\n".join(self.buff)
html, attrs = blogArticleString(bufcontent)
parse_msg = self._check_html(html, True)
if parse_msg:
self._open_qf(parse_msg)
return "There are errors in generated document"
if not self.password:
self.password = \
vim.eval('inputsecret("Enter your gmail password: ")')
blog = VimBlogger(self.blogname, self.login, self.password)
blog.draft = self.draft > 0
if 'id' in attrs and attrs['id']:
post = blog.update_article(html, attrs=attrs)
msg = unicode("Article '%s' has been updated" % post.title.text)
msg = msg.encode(self.vim_encoding)
else:
post = blog.create_article(html, attrs=attrs)
msg = "New article with id %s has been created" % \
post.get_post_id()
for item, value in (('id', post.get_post_id()),
('date', post.published.text),
('title', post.title.text),
('modified', post.updated.text),
('tags',
", ".join([cat.term for cat in post.category]))):
self._update_docinfo(item, value)
return msg
def delete(self):
"""
Get list of articles, display it to the user, make him choose one and
delete
"""
if not self.password:
self.password = \
vim.eval('inputsecret("Enter your gmail password: ")')
blog = VimBlogger(self.blogname, self.login, self.password)
posts = blog.get_articles(self.maxarticles)
msg = u"inputlist(["
for index, entries in enumerate(posts):
line = "%2d %s %s" % (index + 1,
entries[1],
entries[2])
msg += u'"' + line.replace('"', '\\"') + u'",'
msg = msg[:-1]
msg += u"])"
msg = unicode(msg).encode(self.vim_encoding)
choice = int(vim.eval(msg))
if choice:
art = posts[choice - 1]
msg = 'confirm("You are about to delete article \'%s\'. '
msg += 'Are you sure?"'
msg = unicode(msg % art[1]).encode(self.vim_encoding)
msg += ', "&No\n&Yes")'
if self.confirm_del:
choice = int(vim.eval(msg))
else:
choice = 2
if choice == 2:
blog.delete_article(art[0])
return "Article deleted"
return "No articles deleted"
def _update_docinfo(self, attr, val):
"""
Update current buffer with attributes value
"""
val = unicode(":%s: %s" % (attr.capitalize(), val))
val = val.encode(self.buffer_encoding)
if not self.docinfo_len:
self.buff.append(val, 0)
return
for num, line in enumerate(self.buff[:self.docinfo_len]):
if ':%s:' % attr in line.lower() and line.startswith(":"):
self.buff[num] = val
return
self.buff.append(val, 0)
self.docinfo_len += 1
def _set_docinfo_len(self):
"""
Set docinfo_len, which means number of lines from the beginning of the
buffer to the first empty line.
"""
for num, line in enumerate(self.buff):
if line and line.startswith(':'):
continue
elif not line:
self.docinfo_len = num
break
else:
self.docinfo_len = 0
break
def _open_qf(self, msg):
"""
Open VIm QuickFix window with message, if argument msg is non empty
string.
"""
if msg:
msg1 = "There are problems reported by XML parser:"
msg2 = "Check generated html for errors."
vim.command('call setqflist([{"text": "%s"}, {"text": "%s"}, '
'{"text": "%s"}])' % (msg1, msg, msg2))
vim.command('copen')
def _check_html(self, html, add_container=False):
"""
Check HTML generated document, by simply use minidom parser
If add_container is set to True, entire document is wrapped inside
additional div
returns empty string if parses succeed, else exception message.
"""
# minidom doesn't understand html entities like ' ' For checking
# purpose it is perfectly ok, to switch them with '&'
html = html.replace(" ", "&")
if add_container:
html = "<div>" + html + "</div>"
message = ""
try:
minidom.parseString(html)
except ExpatError as ex:
message = str(ex)
return message

View File

@@ -0,0 +1,287 @@
"""
File: rest.py
Author: Roman 'gryf' Dobosz
Description: This module is responsible for conversion between reST and HTML
with some goods added.
"""
import re
from docutils import core
from docutils import nodes
from docutils.parsers.rst import directives, Directive
from docutils.writers.html4css1 import Writer, HTMLTranslator
try:
from pygments import highlight
from pygments.lexers import get_lexer_by_name, TextLexer
from pygments.formatters import HtmlFormatter
class Pygments(Directive):
"""
Source code syntax highlighting.
"""
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
has_content = True
def run(self):
self.assert_has_content()
try:
lexer = get_lexer_by_name(self.arguments[0])
except ValueError:
# no lexer found - use the text one instead of an exception
lexer = TextLexer()
# take an arbitrary option if more than one is given
formatter = HtmlFormatter(noclasses=True)
parsed = highlight(u'\n'.join(self.content), lexer, formatter)
return [nodes.raw('', parsed, format='html')]
directives.register_directive('sourcecode', Pygments)
except ImportError:
pass
class Attrs(object):
ATTRS = {}
class CustomHTMLTranslator(HTMLTranslator):
"""
Base class for reST files translations.
There are couple of customizations for docinfo fields behaviour and
abbreviations and acronyms.
"""
def __init__(self, document):
"""
Set some nice defaults for articles translations
"""
HTMLTranslator.__init__(self, document)
self.initial_header_level = 4
def visit_section(self, node):
"""
Don't affect document, just keep track of the section levels
"""
self.section_level += 1
def depart_section(self, node):
self.section_level -= 1
def visit_meta(self, node):
pass
def depart_meta(self, node):
pass
def visit_document(self, node):
pass
def depart_document(self, node):
pass
def depart_docinfo(self, node):
"""
Reset body, remove unnecessary content.
"""
self.body = []
def visit_date(self, node):
pass
def depart_date(self, node):
pass
def visit_literal(self, node):
"""
This is almos the same as the original one from HTMLTranslator class.
The only difference is in used HTML tag: it uses 'code' instead of
'tt'
"""
self.body.append(self.starttag(node, 'code', ''))
text = node.astext()
for token in self.words_and_spaces.findall(text):
if token.strip():
# Protect text like "--an-option" and the regular expression
# ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
if self.sollbruchstelle.search(token):
self.body.append('<span class="pre">%s</span>'
% self.encode(token))
else:
self.body.append(self.encode(token))
elif token in ('\n', ' '):
# Allow breaks at whitespace:
self.body.append(token)
else:
# Protect runs of multiple spaces; the last space can wrap:
self.body.append('&nbsp;' * (len(token) - 1) + ' ')
self.body.append('</code>')
# Content already processed:
raise nodes.SkipNode
def visit_acronym(self, node):
"""
Define missing acronym HTML tag
"""
node_text = node.children[0].astext()
node_text = node_text.replace('\n', ' ')
patt = re.compile(r'^(.+)\s<(.+)>')
if patt.match(node_text):
node.children[0] = nodes.Text(patt.match(node_text).groups()[0])
self.body.append(\
self.starttag(node, 'acronym',
'', title=patt.match(node_text).groups()[1]))
else:
self.body.append(self.starttag(node, 'acronym', ''))
def visit_abbreviation(self, node):
"""
Define missing abbr HTML tag
"""
node_text = node.children[0].astext()
node_text = node_text.replace('\n', ' ')
patt = re.compile(r'^(.+)\s<(.+)>')
if patt.match(node_text):
node.children[0] = nodes.Text(patt.match(node_text).groups()[0])
self.body.append(\
self.starttag(node, 'abbr',
'', title=patt.match(node_text).groups()[1]))
else:
self.body.append(self.starttag(node, 'abbr', ''))
class NoHeaderHTMLTranslator(CustomHTMLTranslator):
"""
Special subclass for generating only body of an article
"""
def __init__(self, document):
"""
Remove all needless parts of HTML document.
"""
CustomHTMLTranslator.__init__(self, document)
self.head = []
self.meta = []
self.head_prefix = ['', '', '', '', '']
self.body_prefix = []
self.body_suffix = []
self.stylesheet = []
self.generator = ('')
def visit_field(self, node):
"""
Harvest docinfo fields and store it in global dictionary.
"""
key, val = [n.astext() for n in node]
Attrs.ATTRS[key.lower()] = val.strip()
def visit_date(self, node):
"""
Store published date in global dictionary.
"""
Attrs.ATTRS['date'] = node.astext()
class PreviewHTMLTranslator(CustomHTMLTranslator):
"""
Class for display article in the browser as a preview.
"""
CSS = []
def __init__(self, document):
"""
Alter levels for the heading tags, define custom, blog specific
stylesheets. Note, that style_custom is present only locally to adjust
way of display the page
"""
CustomHTMLTranslator.__init__(self, document)
self.initial_header_level = 1
self.section_level = 1
# order of css files is important
self.default_stylesheets = PreviewHTMLTranslator.CSS
self.stylesheet = [self.stylesheet_link % self.encode(css) \
for css in self.default_stylesheets]
self.body_ = []
def depart_docinfo(self, node):
"""
Overwrite body with some custom one. body_ will hold the first heading
with title of the document.
"""
self.body = self.body_
def visit_field(self, node):
"""
Make title visible as a heading
"""
key, node_ = [n.astext() for n in node]
key = key.lower()
if key == 'title':
self.head.append('<title>%s</title>\n' % self.encode(node_))
self.body_.append('<h1 class="post-title entry-title">'
'<a href="#">%s</a></h1>\n' % self.encode(node_))
class BlogBodyWriter(Writer):
"""
Custom Writer class for generating HTML partial with the article
"""
def __init__(self):
Writer.__init__(self)
self.translator_class = NoHeaderHTMLTranslator
def translate(self):
self.document.settings.output_encoding = "utf-8"
Writer.translate(self)
class BlogPreviewWriter(Writer):
"""
Custom Writer class for generating full HTML of the article
"""
def __init__(self, stylesheets=None):
Writer.__init__(self)
if not stylesheets:
stylesheets = []
self.translator_class = PreviewHTMLTranslator
self.translator_class.CSS = stylesheets
def translate(self):
self.document.settings.output_encoding = "utf-8"
Writer.translate(self)
def blogPreview(string, stylesheets=None):
"""
Returns full HTML of the article.
string argument is an article in reST
"""
if not stylesheets:
stylesheets = []
html_output = core.publish_string(string,
writer=BlogPreviewWriter(stylesheets))
html_output = html_output.strip()
html_output = html_output.replace("<!-- more -->", "\n<!-- more -->\n")
return html_output
def blogArticleString(string):
"""
Returns partial HTML of the article, and attribute dictionary
string argument is an article in reST
"""
# reset ATTRS
Attrs.ATTRS = {}
html_output = core.publish_string(string, writer=BlogBodyWriter())
html_output = html_output.strip()
html_output = html_output.replace("<!-- more -->", "\n<!-- more -->\n")
attrs = {}
for key in Attrs.ATTRS:
if Attrs.ATTRS[key]:
attrs[key] = Attrs.ATTRS[key]
return html_output, attrs

View File

@@ -0,0 +1 @@
# module rst2blogger.tests

View File

@@ -0,0 +1,267 @@
# vim: set fileencoding=utf-8
import sys
import os
from datetime import datetime
from tempfile import mkstemp
LOGIN = "John"
PASS = "secret"
REST_ARTICLE = u""":Title: Title — This is a test
:Date: 2010-12-12T12:36:36+01:00
:Tags: this is a test, Blogger, rest
.. meta::
:description: meta are completely ignored in blogger parsers
`Amet`, convallis sollicitudin, commodo a, purus. Nulla vitae eros a diam
blandit **mollis**. Proin luctus ``ls --color ~/`` feugiat eros.
.. more
Pellentesque habitant morbi tristique senectus et *netus* et malesuada fames
ac turpis egestas. Duis ultricies urna: ``easy_install pygments``. Etiam enim
urna, pharetra suscipit, varius et, congue quis, odio. Donec `NES <Nintendo
Entertainment System>`:acronym: lobortis, elit bibendum euismod faucibus,
velit nibh egestas libero, vitae pellentesque elit augue ut massa.
test empty `acronym`:acronym: and `abbrev`:abbreviation:
Section 1
---------
Nulla consequat erat at massa. Vivamus id mi. Morbi purus enim, dapibus a,
facilisis non, tincidunt at, enim. Vestibulum ante ipsum primis in faucibus
orci luctus et ultrices posuere cubilia Curae; `WTF? <What the
fcuk?>`:abbreviation: Duis imperdiet eleifend arcu. Cras magna ligula,
consequat at, tempor non, posuere.
Subsection 1.1
..............
.. sourcecode:: python
import vim
print vim.current.buffer.name
.. sourcecode:: unknown_lexer
Cras dignissim vulputate metus.
Phasellus eu quam. Quisque interdum cursus purus. In.
End.
"""
class Eval(object):
"""
Communication class
"""
value = ""
blog = None
gdata_delete = 0
class Dummy(sys.__class__):
"""
Dummy class, for faking modules and other objects, not directly needed
"""
def __getattr__(self, attrname):
""" The dummy class should have no attribute """
if attrname == 'util':
return Dummy("util")
return None
# fake vim module.
sys.modules["vim"] = Dummy("vim")
class MockBuffer(list):
"""
Vim buffer-like class
"""
def append(self, val, line=None):
"""
Override append method to mimic vim.buffer append behaviour
"""
if line is None:
super(MockBuffer, self).append(val)
else:
super(MockBuffer, self).insert(line, val)
class Mock(object):
"""
Generic all-purpose mock class
"""
pass
import vim
vim.command = lambda x: None
vim.current = Mock()
vim.current.buffer = MockBuffer(REST_ARTICLE.split("\n"))
fdesc, vim.current.buffer.name = mkstemp()
vim.current.buffer.name += ".rst"
os.close(fdesc) # close descriptor, only filename is needed
def mock_vim_eval(string):
ints = ("g:blogger_draft", "g:blogger_maxarticles",
"g:blogger_confirm_del")
if string in ints:
return "0"
elif string == "g:blogger_stylesheets":
return []
else:
return Eval.value
vim.eval = mock_vim_eval
class MockBlog(object):
"""
Mock blog class
"""
def __init__(self, name, id):
self.name = name
self.id = id
def get_blog_name(self):
return self.name
def get_blog_id(self):
return self.id
def get_post_link(self):
link = Mock()
link.href = "http://www.mock.org"
return link
def get_post_id(self):
return self.id
class MockPost(object):
"""
Mock class imitating posts
"""
def __init__(self):
self.category = Mock()
self.category = []
self.id = None
self.title = Mock()
self.title.text = ""
self.published = Mock()
self.published.text = ""
def add_label(self, label):
item = Mock()
item.term = label
self.category.append(item)
def get_post_id(self):
return self.id
class MockBlogFeed(object):
"""
Mock class for feed objects
"""
def __init__(self, *args, **kwargs):
self.entry = []
if Eval.blog:
for bid, bname in {1: 'one', 3: 'test', 7: 'blog_name'}.items():
blog = MockBlog(bname, bid)
self.entry.append(blog)
class MockPostFeed(object):
"""
Mock class for feed objects
"""
def __init__(self, *args, **kwargs):
self.entry = []
from atom.data import Id, Updated
from gdata.blogger.client import BloggerClient
BloggerClient.get_blogs = lambda x: MockBlogFeed()
from gdata.client import BadAuthentication
def mock_client_login(self, login, password, source=None, service=None):
"""
Mock method for client login.
"""
if login != LOGIN or password != PASS:
raise BadAuthentication("Incorrect username or password")
BloggerClient.client_login = mock_client_login
def mock_client_post(self, post, url=None):
"""
Mimic post method
"""
if Eval.value == 10:
return None
new_id = Id(text='1234567890')
post.id = new_id
date = datetime.utcnow()
milli = str(date.microsecond)[:3]
date = date.strftime("%Y-%m-%dT%H:%M:%S")
date = date + ".%s+00:00" % milli
post.updated = Updated(text=date)
return post
BloggerClient.post = mock_client_post
BloggerClient.update = mock_client_post
def mock_client_delete(self, post):
"""
Mock delete method
"""
if not post:
raise AttributeError("%s object has no attribute 'etag'" % type(post))
if Eval.gdata_delete:
return "404 Mock"
return None
BloggerClient.delete = mock_client_delete
def mock_client_get_posts(self, blog_id):
"""
Mock get_posts method
"""
posts = (('title1', 1, "2000-01-01T00:04:00.001+01:00"),
('title2', 2, "2001-01-01T00:02:19.001+01:00"),
('title3', 3, "2002-01-01T00:01:00.001+01:00"),
('title4', 4, "2006-01-01T00:02:00.001+02:00"))
feed = MockPostFeed()
for p in posts:
a = MockPost()
a.id = p[1]
a.title.text = p[0]
a.published.text = p[2]
feed.entry.append(a)
return feed
BloggerClient.get_posts = mock_client_get_posts
def mock_client_get_feed(self, uri, desired_class=None):
"""
Mock get_feed method
"""
post = MockPost()
post.add_label('test1')
return post
BloggerClient.get_feed = mock_client_get_feed
from gdata.blogger.data import BlogPost
def mock_get_post_id(self):
return self.id.text
BlogPost.get_post_id = mock_get_post_id

View File

@@ -0,0 +1,514 @@
import os
import sys
import unittest
this_dir = os.path.dirname(os.path.abspath(__file__))
this_dir = os.path.abspath(os.path.join(this_dir, "../.."))
sys.path.insert(0, this_dir)
from rst2blogger.tests import shared
from rst2blogger.blogger import VimBlogger
class TestCheckDates(unittest.TestCase):
"""
Tests for method VimBlogger._check_date
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimb = VimBlogger(None, shared.LOGIN, shared.PASS)
def test_happy_case_CET(self):
"""
Test on good date string on Central and East Europe
"""
date = "2000-01-01T00:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
def test_happy_case_HST(self):
"""
Test on good date string on Hawaii Time Zone
"""
date = "2000-01-01T00:00:00.001-10:00"
self.assertTrue(self.vimb._check_date(date))
def test_happy_case_GMT(self):
"""
Test UTC date string
"""
date = "2000-01-01T00:00:00.001-00:00"
self.assertTrue(self.vimb._check_date(date))
def test_without_milliseconds(self):
"""
Test on date string without milliseconds
"""
date = "2000-01-01T00:00:00+01:00"
self.assertTrue(self.vimb._check_date(date))
def test_wrong_tz_format(self):
"""
Test date with wrong timezone format (hour have no leading 0)
"""
date = "2000-01-01T00:00:00.001+1:00"
self.assertFalse(self.vimb._check_date(date))
# Test date with wrong timezone format (minute have only one digit)
date = "2000-01-01T00:00:00.001+01:0"
self.assertFalse(self.vimb._check_date(date))
# Test date with wrong timezone format (hours and minutes hasn't been
# separated by colon)
date = "2000-01-01T00:00:00.001+0100"
self.assertFalse(self.vimb._check_date(date))
def test_wrong_milliseconds(self):
"""
Test date with wrong format of milliseconds (.01 instead of .010)
"""
date = "2000-01-01T00:00:00.01+01:00"
self.assertFalse(self.vimb._check_date(date))
# Test date with wrong format of milliseconds (.1 instead of .100)
date = "2000-01-01T00:00:00.1+01:00"
self.assertFalse(self.vimb._check_date(date))
# Test date with spolied format (dot for milliseconds, but no digits)
date = "2000-01-01T00:00:00.+01:00"
self.assertFalse(self.vimb._check_date(date))
def test_good_milliseconds(self):
"""
Test date with correct format of milliseconds
"""
date = "2000-01-01T00:00:00.000+01:00"
self.assertTrue(self.vimb._check_date(date), date + " is incorrect")
date = "2000-01-01T00:00:00.999+01:00"
self.assertTrue(self.vimb._check_date(date), date + " is incorrect")
def test_wrong_hours(self):
"""
Test date with wrong hours value
"""
date = "2000-01-01T24:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
def test_good_hours(self):
"""
Test date with correct hours values
"""
date = "2000-01-01T00:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date), date + " is incorrect")
date = "2000-01-01T23:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date), date + " is incorrect")
def test_wrong_minutes(self):
"""
Test date with wrong minutes value
"""
date = "2000-01-01T00:60:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
date = "2000-01-01T00:000:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
date = "2000-01-01T00:1:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
def test_good_minutes(self):
"""
Test date with correct minutes values
"""
date = "2000-01-01T00:01:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
date = "2000-01-01T00:59:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
def test_wrong_seconds(self):
"""
Test date with wrong seconds value
"""
date = "2000-01-01T00:00:60.001+01:00"
self.assertFalse(self.vimb._check_date(date))
def test_good_seconds(self):
"""
Test date with good seconds values
"""
for second in range(60):
date = "2000-01-01T00:00:%0.2d.001+01:00" % second
self.assertTrue(self.vimb._check_date(date))
def test_wrong_days(self):
"""
Test date with incorrect days (january has always 31 days, no month
has lower number than 1)
"""
date = "2000-01-32T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
date = "2000-01-00T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
def test_good_days(self):
"""
Test date with correct days (january has always 31 days)
"""
date = "2000-01-01T00:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
date = "2000-01-31T00:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
def test_wrong_month(self):
"""
Test date with wrong month
"""
date = "2000-00-01T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
date = "2000-13-01T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
date = "2000-1-01T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
date = "2000-001-01T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
def test_good_month(self):
"""
Test date with correct months
"""
date = "2000-01-01T00:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
date = "2000-12-01T00:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
def test_wrong_year(self):
"""
Test date with wrong year
"""
date = "0000-01-01T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
date = "10000-01-01T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
date = "900-01-01T00:00:00.001+01:00"
self.assertFalse(self.vimb._check_date(date))
def test_good_year(self):
"""
Test date with correct years
"""
date = "0001-01-01T00:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
date = "9999-01-01T00:00:00.001+01:00"
self.assertTrue(self.vimb._check_date(date))
class TestAuthorize(unittest.TestCase):
"""
Test method VimBlogger._authorize
"""
def setUp(self):
"""
Create VimBlogger object (with good credentials, yes :>)
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
def test_happy_case(self):
"""
Try to login with good credentials
"""
self.assertTrue(self.vimob._authorize(shared.LOGIN,
shared.PASS) is None)
def test_wrong_login(self):
"""
Try to login with wrong login
"""
self.assertRaises(shared.BadAuthentication, self.vimob._authorize,
'joe', shared.PASS)
def test_wrong_pass(self):
"""
Try to login with wrong password
"""
self.assertRaises(shared.BadAuthentication, self.vimob._authorize,
'joe', shared.PASS)
class TestAddTag(unittest.TestCase):
"""
Test method VimBlogger._add_tag
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
self.post = shared.MockPost()
def test_add_tag(self):
"""
Add items to existing categories. List should be uniq.
"""
self.vimob._add_tag(self.post, 'item')
self.assertTrue(len(self.post.category) == 1)
# Item number should not change on the same label
self.vimob._add_tag(self.post, 'item')
self.assertTrue(len(self.post.category) == 1)
self.vimob._add_tag(self.post, 'item2')
self.assertTrue(len(self.post.category) == 2)
class TestExtractDate(unittest.TestCase):
"""
Test method VimBlogger._extract_date
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
def test_extract_date(self):
"""
Date should be already verified by _check_date method, so only
extraction is tested
"""
date = "2000-01-01T00:00:00.001-10:00"
# wrong scenario
self.assertFalse(self.vimob._extract_date('wrong_date_string'))
# only date should be returned
self.assertEqual(self.vimob._extract_date(date), "2000-01-01")
# date and time should be returned
self.assertEqual(self.vimob._extract_date(date, True),
"2000-01-01 00:00:00")
class TestGetPost(unittest.TestCase):
"""
Test method VimBlogger._get_post
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
self.vimob.blog = shared.Mock()
link = shared.Mock()
link.href = "mock.com"
link.feed = shared.Mock()
self.vimob.blog.get_post_link = lambda: link
def test_get_post(self):
"""
Nothing really to test here. Maybe in the future :)
"""
result = self.vimob._get_post('1234')
self.assertEqual(type(result), shared.MockPost)
class TestSetBlog(unittest.TestCase):
"""
Test method VimBlogger._set_blog
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
for bid, bname in {1: 'one', 3: 'test', 7: 'blog_name'}.items():
blog = shared.MockBlog(bname, bid)
self.vimob.feed.entry.append(blog)
def test_set_blog(self):
"""
Test setting a blog
"""
self.vimob._set_blog("no_valid_blog_name")
self.assertEqual(self.vimob.blog_id, None)
self.assertEqual(self.vimob.blog, None)
self.vimob._set_blog("blog_name")
self.assertEqual(self.vimob.blog_id, 7)
self.assertEqual(self.vimob.blog.get_blog_name(), 'blog_name')
self.vimob._set_blog("test")
self.assertEqual(self.vimob.blog_id, 3)
self.assertEqual(self.vimob.blog.get_blog_name(), 'test')
self.vimob._set_blog("one")
self.assertEqual(self.vimob.blog_id, 1)
self.assertEqual(self.vimob.blog.get_blog_name(), 'one')
class TestCreateArticle(unittest.TestCase):
"""
Test method VimBlogger.create_article
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
def test_create_simple_article(self):
"""
Test creation of article with minimum requirements
"""
html = "<p>article</p>"
post = self.vimob.create_article(html)
self.vimob.draft = True
self.assertEqual(post.id.text, '1234567890')
self.assertEqual(post.content.text, html)
self.assertEqual(post.published, None)
self.assertTrue(post.updated is not None)
self.assertEqual(post.title.text, "")
self.assertEqual(post.category, [])
self.assertEqual(post.control.draft.text, "yes")
def test_create_article(self):
"""
Test creation of article with full attrs
"""
html = u"<p>article \xe2\x80\x94 article</p>"
labels = "tag with spaces|vim|python|blogger".split("|")
attrs = {"title": u'Title \xe2\x80\x94 title',
"tags": ", ".join(labels),
"date": "2010-12-10T14:18:32+00:00"}
self.vimob.draft = False
post = self.vimob.create_article(html, attrs)
self.assertEqual(post.id.text, '1234567890')
self.assertEqual(post.content.text, html)
self.assertEqual(post.published.text, attrs['date'])
self.assertTrue(post.updated is not None)
self.assertEqual(post.title.text, attrs['title'])
self.assertEqual(len(post.category), 4)
for label in post.category:
self.assertTrue(label.term in labels)
del(labels[labels.index(label.term)])
self.assertEqual(post.control, None)
class TestDeleteArticle(unittest.TestCase):
"""
Test method VimBlogger.create_article
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
for bid, bname in {1: 'one', 3: 'test', 7: 'blog_name'}.items():
blog = shared.MockBlog(bname, bid)
self.vimob.feed.entry.append(blog)
self.vimob._set_blog('test')
def test_delete_non_existing_article(self):
"""
Test removing article without id
"""
self.assertEqual(self.vimob.delete_article(None),
"No article id provided")
def test_delete_article(self):
"""
Test removing article
"""
html = u"<p>article \xe2\x80\x94 article</p>"
labels = "tag with spaces|vim|python|blogger".split("|")
attrs = {"title": u'Title \xe2\x80\x94 title',
"tags": ", ".join(labels),
"date": "2010-12-10T14:18:32+00:00"}
self.vimob.draft = False
post = self.vimob.create_article(html, attrs)
self.assertEqual(self.vimob.delete_article(post.id.text), None)
class TestGetArticles(unittest.TestCase):
"""
Test method VimBlogger.get_articles
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
def test_get_articles(self):
"""
Test removing article without id
"""
articles = self.vimob.get_articles()
self.assertEqual(len(articles), 4)
articles = self.vimob.get_articles(maxarticles=2)
self.assertEqual(len(articles), 2)
class TestUpdateArticle(unittest.TestCase):
"""
Test method VimBlogger.update_article
"""
def setUp(self):
"""
Create VimBlogger object
"""
self.vimob = VimBlogger(None, shared.LOGIN, shared.PASS)
for bid, bname in {1: 'one', 3: 'test', 7: 'blog_name'}.items():
blog = shared.MockBlog(bname, bid)
self.vimob.feed.entry.append(blog)
self.vimob._set_blog('test')
def test_wrong_argument_types(self):
"""
Test update_article method with wrong argument types
"""
self.assertRaises(TypeError, self.vimob.update_article, None, None)
def test_no_id_in_attrs(self):
"""
Test update_article method with no id in attrs
"""
self.assertRaises(Exception, self.vimob.update_article,
'<p>update</p>', [])
def test_update(self):
"""
Test update_article method with no id in attrs
"""
attrs = {'id': 1234567890, 'title': 'update',
'date': '2001-01-01T00:02:19.001+01:00',
'tags': "tag1, tag2, tag3"}
post = self.vimob.update_article('<p>update</p>', attrs)
self.assertEqual(post.title.text, 'update')
self.assertEqual(post.id.text, '1234567890')
self.assertEqual(post.content.text, '<p>update</p>')
self.assertTrue(post.updated.text is not None)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,294 @@
# vim: set fileencoding=utf-8
import os
import sys
import unittest
import webbrowser
webbrowser.open = lambda x: None
this_dir = os.path.dirname(os.path.abspath(__file__))
this_dir = os.path.abspath(os.path.join(this_dir, "../.."))
sys.path.insert(0, this_dir)
from rst2blogger.tests.shared import LOGIN, PASS, Eval, MockBuffer
from rst2blogger.main import Rst2Blogger
from gdata.client import BadAuthentication
class TestRst2Blogger(unittest.TestCase):
"""
Tests for vim - rest - blogger interface
"""
def setUp(self):
"""
Create Rst2Blogger object
"""
self.obj = Rst2Blogger()
def test_object_creation(self):
"""
Create Rst2Blogger object and test it.
"""
self.assertTrue(self.obj is not None)
self.assertEqual(self.obj.docinfo_len, 3)
self.assertEqual(self.obj.login, "")
self.assertEqual(self.obj.password, "")
self.assertEqual(self.obj.blogname, "")
self.assertEqual(self.obj.buffer_encoding, "")
self.assertEqual(self.obj.vim_encoding, "")
self.assertEqual(self.obj.maxarticles, 0)
self.assertEqual(self.obj.draft, 0)
self.assertEqual(self.obj.confirm_del, 0)
self.assertEqual(self.obj.stylesheets, [])
class TestRst2BloggerSetDocinfoLen(unittest.TestCase):
"""
Test _set_docinfo_len method on different docinfo configurations
"""
def setUp(self):
"""
Create Rst2Blogger object
"""
self.obj = Rst2Blogger()
def test_set_docinfo_len(self):
"""
Test with no defined docinfo
"""
self.obj.buff = self.obj.buff[4:]
self.obj._set_docinfo_len()
self.assertEqual(self.obj.docinfo_len, 0)
def test_set_docinfo_len2(self):
"""
Test with one docinfo entry
"""
self.obj.buff = self.obj.buff[:1] + [''] + self.obj.buff[4:]
self.obj._set_docinfo_len()
self.assertEqual(self.obj.docinfo_len, 1)
def test_set_docinfo_len3(self):
"""
Test with wrong docinfo definition
"""
self.obj.buff = self.obj.buff[:1] + self.obj.buff[4:]
self.obj._set_docinfo_len()
self.assertEqual(self.obj.docinfo_len, 0)
class TestCheckHtml(unittest.TestCase):
"""
Check HTML parser
"""
def setUp(self):
"""
Create Rst2Blogger object
"""
self.obj = Rst2Blogger()
def test_check_html1(self):
"""
Parse (generated) html string, should return empty string
"""
html = "<html><head><title>test</title></head><body></body></html>"
self.assertEqual(self.obj._check_html(html), "")
self.assertEqual(self.obj._check_html(html, True), "")
def test_check_html2(self):
"""
Parse html fragment string
"""
html = "<p>first paragraph</p><p>another paragraph</p>"
self.assertEqual(self.obj._check_html(html),
"junk after document element: line 1, column 22")
self.assertEqual(self.obj._check_html(html, True), "")
def test_check_html3(self):
"""
Parse wrong html string (crossed tags)
"""
html = "<p>first paragraph<b></p>another paragraph</b>"
self.assertEqual(self.obj._check_html(html),
"mismatched tag: line 1, column 23")
self.assertEqual(self.obj._check_html(html, True),
"mismatched tag: line 1, column 28")
class TestRst2BloggerDelete(unittest.TestCase):
"""
Test delete method
"""
def setUp(self):
"""
Create Rst2Blogger object
"""
self.obj = Rst2Blogger()
self.obj.login = LOGIN
self.obj.password = PASS
self.obj.blogname = "test"
self.obj.vim_encoding = "utf-8"
def test_delete_without_password(self):
"""
Delete article, while password is incorrect/nonexistend
"""
self.obj.password = ""
self.assertRaises(BadAuthentication, self.obj.delete)
def test_delete(self):
"""
Delete article. Set confirmation attribute.
"""
self.obj.confirm_del = 1
Eval.value = 2 # set choice to answer "Y" for confirmation
Eval.blog = "test"
self.assertEqual(self.obj.delete(), "Article deleted")
def test_delete2(self):
"""
Delete article. Set confirmation attribute. Refuse to delete.
"""
self.obj.confirm_del = 1
Eval.value = 1 # set choice to answer "N" for confirmation
Eval.blog = "test"
self.assertEqual(self.obj.delete(), "No articles deleted")
def test_delete3(self):
"""
Delete article. Unset confirmation attribute. Delete returns something
else then None.
"""
Eval.value = 2
Eval.blog = "test"
Eval.gdata_delete = 1
self.assertEqual(self.obj.delete(), "Article deleted")
class TestRst2BloggerPost(unittest.TestCase):
"""
Test post method
"""
def setUp(self):
"""
Create Rst2Blogger object
"""
self.obj = Rst2Blogger()
self.obj.login = LOGIN
self.obj.password = PASS
self.obj.blogname = "test"
self.obj.vim_encoding = "utf-8"
self.obj.buffer_encoding = "utf-8"
# create copy of the buffer list and assign copy to the buff attribute
self._rest = MockBuffer(self.obj.buff[:])
self.obj.buff = self._rest
def test_without_password(self):
"""
Post article, while password is incorrect/nonexistend
"""
self.obj.password = ""
self.assertRaises(BadAuthentication, self.obj.post)
def test_with_wrong_data(self):
"""
Try to post not well formed html
"""
self.obj.buff.append('')
self.obj.buff.append('.. raw:: html')
self.obj.buff.append('')
self.obj.buff.append(' <p>foo<b>bar</p>baz</b>')
self.obj.buff.append('')
self.obj.post()
self.assertEqual(self.obj.post(),
'There are errors in generated document')
def test_post_create(self):
"""
Try to post well formed html, as a new article
"""
self.assertEqual(self.obj.post(),
'New article with id 1234567890 has been created')
def test_post_update(self):
"""
Try to post well formed html, as a new article
"""
self.obj.buff.append(':Id: 1234567890', 0)
self.assertEqual(self.obj.post(),
"Article 'Title \xe2\x80\x94 This is a test' "
"has been updated")
class TestRst2BloggerUpdateDocinfo(unittest.TestCase):
"""
Test _update_docinfo
"""
def setUp(self):
"""
Create Rst2Blogger object
"""
self.obj = Rst2Blogger()
self.obj.login = LOGIN
self.obj.password = PASS
self.obj.blogname = "test"
self.obj.vim_encoding = "utf-8"
self.obj.buffer_encoding = "utf-8"
# create copy of the buffer list and assign copy to the buff attribute
self._rest = MockBuffer(self.obj.buff[:])
self.obj.buff = self._rest
def test_with_empty_docinfo(self):
"""
Try to post not well formed html
"""
self.obj.buff = MockBuffer(self.obj.buff[4:])
self.obj.docinfo_len = 0
self.obj._update_docinfo('title', 'title2')
class TestRst2BloggerPreview(unittest.TestCase):
"""
Test preview
"""
def setUp(self):
"""
Create Rst2Blogger object
"""
self.obj = Rst2Blogger()
self.obj.login = LOGIN
self.obj.password = PASS
self.obj.blogname = "test"
def tearDown(self):
"""
Remove leftovers in fs
"""
try:
os.unlink(self.obj.buff.name[:-4])
except OSError:
pass
try:
os.unlink(self.obj.buff.name[:-4] + ".html")
except OSError:
pass
def test_preview_open_in_browser(self):
"""
Try to post not well formed html
"""
Eval.value = 1
print self.obj.preview()
def test_preview_save_to_file(self):
"""
Try to post not well formed html
"""
Eval.value = 0
name = self.obj.buff.name[:-4] + ".html"
self.assertEqual(self.obj.preview(),
"Generated HTML has been written to %s" % name)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,79 @@
# vim: set fileencoding=utf-8
import os
import sys
import unittest
import re
this_dir = os.path.dirname(os.path.abspath(__file__))
this_dir = os.path.abspath(os.path.join(this_dir, "../.."))
sys.path.insert(0, this_dir)
from rst2blogger.rest import blogArticleString, blogPreview
from rst2blogger.tests.shared import REST_ARTICLE
class TestBlogPreview(unittest.TestCase):
"""
Test generating HTML out of prepared reST text. It tests only for some
aspects of the entire thing, because it is not intendend to test all of
reST directives.
"""
def test_content(self):
"""
Simple case, check output
"""
html_out = blogPreview(REST_ARTICLE)
self.assertTrue(len(html_out) > 0)
self.assertTrue("<html" in html_out)
self.assertTrue("</html>" in html_out)
self.assertTrue("<?xml version=\"1.0\" encoding=\"utf-8\"" in
html_out)
self.assertTrue("\n\n<!-- more -->\n\n" in html_out)
self.assertTrue("<title>Title — This is a test</title>" in html_out)
self.assertTrue('type="text/css"' not in html_out)
self.assertTrue(re.search(r"<h1.*><a href=\"#\">Title — This is a"
" test</a></h1>", html_out))
self.assertTrue(re.search(r"<h2>Section 1</h2>", html_out))
self.assertTrue(re.search(r"<h3>Subsection 1.1</h3>", html_out))
self.assertTrue("description" not in html_out)
def test_stylesheets(self):
"""
Test output for stylesheets
"""
html_out = blogPreview(REST_ARTICLE, ["css/style1.css",
"css/blogger1.css"])
self.assertTrue('type="text/css"' in html_out)
match = re.search(r'<link rel="stylesheet" '
'href=".*" type="text/css" />', html_out)
self.assertTrue(match is not None)
self.assertEqual(len(match.span()), 2)
class TestBlogArticleString(unittest.TestCase):
"""
Test blogArticleString function, wich should return part of html and
dictionary with attributes.
"""
def test_blogArticleString(self):
html_out, attrs = blogArticleString(REST_ARTICLE)
self.assertEqual(len(attrs), 3)
self.assertTrue(len(html_out) > 0)
self.assertTrue("<html" not in html_out)
self.assertTrue("</html>" not in html_out)
self.assertTrue("<?xml version=\"1.0\" encoding=\"utf-8\"" not in
html_out)
self.assertTrue("\n\n<!-- more -->\n\n" in html_out)
self.assertTrue("<title>Title — This is a test</title>" not in
html_out)
self.assertTrue('type="text/css"' not in html_out)
self.assertTrue(re.search(r"<h4>Section 1</h4>", html_out))
self.assertTrue(re.search(r"<h5>Subsection 1.1</h5>", html_out))
self.assertTrue("description" not in html_out)
self.assertEqual(attrs['title'], u"Title — This is a test")
self.assertEqual(attrs['date'], "2010-12-12T12:36:36+01:00")
self.assertEqual(attrs['tags'], "this is a test, Blogger, rest")
if __name__ == "__main__":
unittest.main()