merge fixes from master

This commit is contained in:
Adrian Sampson 2012-09-30 13:57:01 -07:00
commit 671ac052c6
17 changed files with 622 additions and 11 deletions

View file

@ -7,3 +7,6 @@ install:
- pip install pylast flask --use-mirrors
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2 --use-mirrors; fi"
script: nosetests
branches:
only:
- master

View file

@ -23,6 +23,7 @@ import shlex
import unicodedata
import threading
import contextlib
import traceback
from collections import defaultdict
from unidecode import unidecode
from beets.mediafile import MediaFile
@ -283,7 +284,12 @@ class Item(object):
f = MediaFile(syspath(self.path))
for key in ITEM_KEYS_WRITABLE:
setattr(f, key, getattr(self, key))
f.save()
try:
f.save()
except (OSError, IOError) as exc:
raise util.FilesystemError(exc, 'write', (self.path,),
traceback.format_exc())
# The file has a new mtime.
self.mtime = self.current_mtime()
@ -1135,6 +1141,9 @@ class Library(BaseLibrary):
# Preserve extension.
_, extension = pathmod.splitext(item.path)
if fragment:
# Outputting Unicode.
extension = extension.decode('utf8', 'ignore')
subpath += extension.lower()
if fragment:

View file

@ -93,7 +93,7 @@ class FilesystemError(HumanReadableException):
clause = 'while {0} {1} to {2}'.format(
self._gerund(), repr(self.paths[0]), repr(self.paths[1])
)
elif self.verb in ('delete',):
elif self.verb in ('delete', 'write'):
clause = 'while {0} {1}'.format(
self._gerund(), repr(self.paths[0])
)

View file

@ -17,7 +17,6 @@ music player.
"""
from __future__ import print_function
import gst
import sys
import time
import gobject
@ -26,6 +25,10 @@ import os
import copy
import urllib
import pygst
pygst.require('0.10')
import gst
class GstPlayer(object):
"""A music player abstracting GStreamer's Playbin element.

104
beetsplug/fuzzy_search.py Normal file
View file

@ -0,0 +1,104 @@
# This file is part of beets.
# Copyright 2011, Philippe Mongeau.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Like beet list, but with fuzzy matching
"""
import beets
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs, print_
from beets.util.functemplate import Template
import difflib
# THRESHOLD = 0.7
def fuzzy_score(query, item):
return difflib.SequenceMatcher(a=query, b=item).quick_ratio()
def is_match(query, item, album=False, verbose=False, threshold=0.7):
query = ' '.join(query)
if album:
values = [item.albumartist, item.album]
else:
values = [item.artist, item.album, item.title]
s = max(fuzzy_score(query.lower(), i.lower()) for i in values)
if verbose:
return (s >= threshold, s)
else:
return s >= threshold
def fuzzy_list(lib, config, opts, args):
query = decargs(args)
fmt = opts.format
if opts.threshold is not None:
threshold = float(opts.threshold)
else:
threshold = float(conf['threshold'])
if fmt is None:
# If no specific template is supplied, use a default
if opts.album:
fmt = u'$albumartist - $album'
else:
fmt = u'$artist - $album - $title'
template = Template(fmt)
if opts.album:
objs = lib.albums()
else:
objs = lib.items()
items = filter(lambda i: is_match(query, i, album=opts.album,
threshold=threshold), objs)
for i in items:
if opts.path:
print_(i.item_dir() if opts.album else i.path)
elif opts.album:
print_(i.evaluate_template(template))
else:
print_(i.evaluate_template(template, lib))
if opts.verbose:
print(is_match(query, i, album=opts.album, verbose=True)[1])
fuzzy_cmd = Subcommand('fuzzy',
help='list items using fuzzy matching')
fuzzy_cmd.parser.add_option('-a', '--album', action='store_true',
help='choose an album instead of track')
fuzzy_cmd.parser.add_option('-p', '--path', action='store_true',
help='print the path of the matched item')
fuzzy_cmd.parser.add_option('-f', '--format', action='store',
help='print with custom format', default=None)
fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true',
help='output scores for matches')
fuzzy_cmd.parser.add_option('-t', '--threshold', action='store',
help='return result with a fuzzy score above threshold. \
(default is 0.7)', default=None)
fuzzy_cmd.func = fuzzy_list
conf = {}
class Fuzzy(BeetsPlugin):
def commands(self):
return [fuzzy_cmd]
def configure(self, config):
conf['threshold'] = beets.ui.config_val(config, 'fuzzy',
'threshold', 0.7)

View file

@ -21,7 +21,7 @@ import re
from beets import ui
from beets.plugins import BeetsPlugin
from beets.util import normpath
from beets.util import normpath, syspath, bytestring_path
M3U_DEFAULT_NAME = 'imported.m3u'
@ -36,9 +36,9 @@ class ImportFeedsPlugin(BeetsPlugin):
_feeds_dir = ui.config_val(config, 'importfeeds', 'feeds_dir', None)
if _feeds_dir:
_feeds_dir = os.path.expanduser(_feeds_dir)
_feeds_dir = os.path.expanduser(bytestring_path(_feeds_dir))
if not os.path.exists(_feeds_dir):
os.makedirs(_feeds_dir)
os.makedirs(syspath(_feeds_dir))
def _get_feeds_dir(lib):
"""Given a Library object, return the path to the feeds directory to be
@ -49,8 +49,8 @@ def _get_feeds_dir(lib):
dirpath = lib.directory
# Ensure directory exists.
if not os.path.exists(dirpath):
os.makedirs(dirpath)
if not os.path.exists(syspath(dirpath)):
os.makedirs(syspath(dirpath))
return dirpath
def _build_m3u_filename(basename):
@ -65,7 +65,7 @@ def _build_m3u_filename(basename):
def _write_m3u(m3u_path, items_paths):
"""Append relative paths to items into m3u file.
"""
with open(m3u_path, 'a') as f:
with open(syspath(m3u_path), 'a') as f:
for path in items_paths:
f.write(path + '\n')

View file

@ -167,16 +167,21 @@ class LastGenrePlugin(plugins.BeetsPlugin):
fallback_str = ui.config_val(config, 'lastgenre', 'fallback_str', None)
def imported(self, config, task):
tags = []
if task.is_album:
album = config.lib.get_album(task.album_id)
lastfm_obj = LASTFM.get_album(album.albumartist, album.album)
if album.genre:
tags.append(album.genre)
else:
item = task.item
lastfm_obj = LASTFM.get_track(item.artist, item.title)
if item.genre:
tags.append(item.genre)
tags = _tags_for(lastfm_obj)
tags.extend(_tags_for(lastfm_obj))
genre = _tags_to_genre(tags)
if not genre and fallback_str != None:
genre = fallback_str
log.debug(u'no last.fm genre found: fallback to %s' % genre)

131
beetsplug/the.py Normal file
View file

@ -0,0 +1,131 @@
# This file is part of beets.
# Copyright 2012, Blemjhoo Tezoulbr <baobab@heresiarch.info>.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Moves patterns in path formats (suitable for moving articles)."""
from __future__ import print_function
import sys
import re
from beets.plugins import BeetsPlugin
from beets import ui
__author__ = 'baobab@heresiarch.info'
__version__ = '1.0'
PATTERN_THE = u'^[the]{3}\s'
PATTERN_A = u'^[a][n]?\s'
FORMAT = u'{0}, {1}'
the_options = {
'debug': False,
'the': True,
'a': True,
'format': FORMAT,
'strip': False,
'silent': False,
'patterns': [PATTERN_THE, PATTERN_A],
}
class ThePlugin(BeetsPlugin):
def configure(self, config):
if not config.has_section('the'):
print('[the] plugin is not configured, using defaults',
file=sys.stderr)
return
self.in_config = True
the_options['debug'] = ui.config_val(config, 'the', 'debug', False,
bool)
the_options['the'] = ui.config_val(config, 'the', 'the', True, bool)
the_options['a'] = ui.config_val(config, 'the', 'a', True, bool)
the_options['format'] = ui.config_val(config, 'the', 'format',
FORMAT)
the_options['strip'] = ui.config_val(config, 'the', 'strip', False,
bool)
the_options['silent'] = ui.config_val(config, 'the', 'silent', False,
bool)
the_options['patterns'] = ui.config_val(config, 'the', 'patterns',
'').split()
for p in the_options['patterns']:
if p:
try:
re.compile(p)
except re.error:
print(u'[the] invalid pattern: {0}'.format(p),
file=sys.stderr)
else:
if not (p.startswith('^') or p.endswith('$')):
if not the_options['silent']:
print(u'[the] warning: pattern \"{0}\" will not '
'match string start/end'.format(p),
file=sys.stderr)
if the_options['a']:
the_options['patterns'] = [PATTERN_A] + the_options['patterns']
if the_options['the']:
the_options['patterns'] = [PATTERN_THE] + the_options['patterns']
if not the_options['patterns'] and not the_options['silent']:
print('[the] no patterns defined!')
if the_options['debug']:
print(u'[the] patterns: {0}'
.format(' '.join(the_options['patterns'])), file=sys.stderr)
def unthe(text, pattern, strip=False):
"""Moves pattern in the path format string or strips it
text -- text to handle
pattern -- regexp pattern (case ignore is already on)
strip -- if True, pattern will be removed
"""
if text:
r = re.compile(pattern, flags=re.IGNORECASE)
try:
t = r.findall(text)[0]
except IndexError:
return text
else:
r = re.sub(r, '', text).strip()
if strip:
return r
else:
return the_options['format'].format(r, t.strip()).strip()
else:
return u''
@ThePlugin.template_func('the')
def func_the(text):
"""Provides beets template function %the"""
if not the_options['patterns']:
return text
if text:
for p in the_options['patterns']:
r = unthe(text, p, the_options['strip'])
if r != text:
break
if the_options['debug']:
print(u'[the] \"{0}\" -> \"{1}\"'.format(text, r), file=sys.stderr)
return r
else:
return u''
# simple tests
if __name__ == '__main__':
print(unthe('The The', PATTERN_THE))
print(unthe('An Apple', PATTERN_A))
print(unthe('A Girl', PATTERN_A, strip=True))

129
beetsplug/zero.py Normal file
View file

@ -0,0 +1,129 @@
# This file is part of beets.
# Copyright 2012, Blemjhoo Tezoulbr <baobab@heresiarch.info>.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
""" Clears tag fields in media files."""
from __future__ import print_function
import sys
import re
from beets.plugins import BeetsPlugin
from beets import ui
from beets.library import ITEM_KEYS
from beets.importer import action
__author__ = 'baobab@heresiarch.info'
__version__ = '0.9'
class ZeroPlugin(BeetsPlugin):
_instance = None
debug = False
fields = []
patterns = {}
warned = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(ZeroPlugin,
cls).__new__(cls, *args, **kwargs)
return cls._instance
def __str__(self):
return ('[zero]\n debug = {0}\n fields = {1}\n patterns = {2}\n'
' warned = {3}'.format(self.debug, self.fields, self.patterns,
self.warned))
def dbg(self, *args):
"""Prints message to stderr."""
if self.debug:
print('[zero]', *args, file=sys.stderr)
def configure(self, config):
if not config.has_section('zero'):
self.dbg('plugin is not configured')
return
self.debug = ui.config_val(config, 'zero', 'debug', True, bool)
for f in ui.config_val(config, 'zero', 'fields', '').split():
if f not in ITEM_KEYS:
self.dbg(
'invalid field \"{0}\" (try \'beet fields\')'.format(f)
)
else:
self.fields.append(f)
p = ui.config_val(config, 'zero', f, '').split()
if p:
self.patterns[f] = p
else:
self.patterns[f] = ['.']
if self.debug:
print(self, file=sys.stderr)
def import_task_choice_event(self, task, config):
"""Listen for import_task_choice event."""
if self.debug:
self.dbg('listen: import_task_choice')
if task.choice_flag == action.ASIS and not self.warned:
self.dbg('cannot zero in \"as-is\" mode')
self.warned = True
# TODO request write in as-is mode
@classmethod
def match_patterns(cls, field, patterns):
"""Check if field (as string) is matching any of the patterns in
the list.
"""
for p in patterns:
if re.findall(p, unicode(field), flags=re.IGNORECASE):
return True
return False
def write_event(self, item):
"""Listen for write event."""
if self.debug:
self.dbg('listen: write')
if not self.fields:
self.dbg('no fields, nothing to do')
return
for fn in self.fields:
try:
fval = getattr(item, fn)
except AttributeError:
self.dbg('? no such field: {0}'.format(fn))
else:
if not self.match_patterns(fval, self.patterns[fn]):
self.dbg('\"{0}\" ({1}) is not match any of: {2}'
.format(fval, fn, ' '.join(self.patterns[fn])))
continue
self.dbg('\"{0}\" ({1}) match: {2}'
.format(fval, fn, ' '.join(self.patterns[fn])))
setattr(item, fn, type(fval)())
self.dbg('{0}={1}'.format(fn, getattr(item, fn)))
@ZeroPlugin.listen('import_task_choice')
def zero_choice(task, config):
ZeroPlugin().import_task_choice_event(task, config)
@ZeroPlugin.listen('write')
def zero_write(item):
ZeroPlugin().write_event(item)
# simple test
if __name__ == '__main__':
print(ZeroPlugin().match_patterns('test', ['[0-9]']))
print(ZeroPlugin().match_patterns('test', ['.']))

View file

@ -4,6 +4,13 @@ Changelog
1.0b16 (in development)
-----------------------
* New plugin: :doc:`/plugins/fuzzy_search` lets you find albums and tracks using
fuzzy string matching so you don't have to type (or even remember) their exact
names. Thanks to Philippe Mongeau.
* New plugin: :doc:`/plugins/the` adds a template function that helps format
text for nicely-sorted directory listings. Thanks to Blemjhoo Tezoulbr.
* New plugin: :doc:`/plugins/zero` filters out undesirable fields before they
are written to your tags. Thanks again to Blemjhoo Tezoulbr.
* :doc:`/plugins/scrub`: Scrubbing now removes *all* types of tags from a file
rather than just one. For example, if your FLAC file has both ordinary FLAC
tags and ID3 tags, the ID3 tags are now also removed.
@ -20,11 +27,16 @@ Changelog
`Tomahawk resolver`_).
* :doc:`/plugins/web`: Files now download with a reasonable filename rather
than just being called "file" (thanks to Zach Denton).
* :doc:`/plugins/importfeeds`: Fix error in symlink mode with non-ASCII
filenames.
* Add the track mapping dictionary to the ``album_distance`` plugin function.
* Fix an assertion failure when the MusicBrainz main database and search server
disagree.
* Fix a bug that caused the :doc:`/plugins/lastgenre` and other plugins not to
modify files' tags even when they successfully change the database.
* Fix a VFS bug leading to a crash in the :doc:`/plugins/bpd` when files had
non-ASCII extensions.
* Add a human-readable error message when writing files' tags fails.
.. _Tomahawk resolver: http://beets.radbox.org/blog/tomahawk-resolver.html

View file

@ -0,0 +1,25 @@
Fuzzy Search Plugin
===================
The ``fuzzy_search`` plugin provides a command that search your library using
fuzzy pattern matching. This can be useful if you want to find a track with complicated characters in the title.
First, enable the plugin named ``fuzzy_search`` (see :doc:`/plugins/index`).
You'll then be able to use the ``beet fuzzy`` command::
$ beet fuzzy Vareoldur
Sigur Rós - Valtari - Varðeldur
The command has several options that resemble those for the ``beet list``
command (see :doc:`/reference/cli`). To choose an album instead of a single
track, use ``-a``; to print paths to items instead of metadata, use ``-p``; and
to use a custom format for printing, use ``-f FORMAT``.
The ``-t NUMBER`` option lets you specify how precise the fuzzy match has to be
(default is 0.7). To make a fuzzier search, try ``beet fuzzy -t 0.5 Varoeldur``.
A value of ``1`` will show only perfect matches and a value of ``0`` will match everything.
The default threshold can also be set from the config file.::
[fuzzy]
threshold: 0.8

View file

@ -50,6 +50,9 @@ disabled by default, but you can turn them on as described above.
rdm
mbcollection
importfeeds
the
fuzzy_search
zero
Autotagger Extensions
''''''''''''''''''''''
@ -66,12 +69,14 @@ Metadata
* :doc:`embedart`: Embed album art images into files' metadata.
* :doc:`replaygain`: Calculate volume normalization for players that support it.
* :doc:`scrub`: Clean extraneous metadata from music files.
* :doc:`zero`: Nullify fields by pattern or unconditionally.
Path Formats
''''''''''''
* :doc:`inline`: Use Python snippets to customize path format strings.
* :doc:`rewrite`: Substitute values in path formats.
* :doc:`the`: Moves patterns in path formats (suitable for moving articles).
Interoperability
''''''''''''''''
@ -85,6 +90,7 @@ Miscellaneous
* :doc:`web`: An experimental Web-based GUI for beets.
* :doc:`rdm`: Randomly choose albums and tracks from your library.
* :doc:`fuzzy_search`: Search albums and tracks with fuzzy string matching.
* :doc:`mbcollection`: Maintain your MusicBrainz collection list.
* :doc:`bpd`: A music player for your beets library that emulates `MPD`_ and is
compatible with `MPD clients`_.

47
docs/plugins/the.rst Normal file
View file

@ -0,0 +1,47 @@
The Plugin
==========
The ``the`` plugin allows you to move patterns in path formats. It's suitable,
for example, for moving articles from string start to the end. This is useful
for quick search on filesystems and generally looks good. Plugin DOES NOT
change tags. By default plugin supports English "the, a, an", but custom
regexp patterns can be added by user. How it works::
The Something -> Something, The
A Band -> Band, A
An Orchestra -> Orchestra, An
To use plugin, enable it by including ``the`` into ``plugins`` line of
your beets config::
[beets]
plugins = the
Plugin provides template function %the, so you can use it on $albumartist or $artist::
[paths]
default: %the{$albumartist}/($year) $album/$track $title
Default options are acceptable (moves all English articles to the end), but you
can add plugin section into config file::
[the]
# handle The, default is on
the=yes
# handle A/An, default is on
a=yes
# format string, {0} - part w/o article, {1} - article
# spaces already trimmed from ends of both parts
# default is '{0}, {1}'
format={0}, {1}
# strip instead of moving to the end, default is off
strip=no
# do not print warnings, default is off
silent=no
# custom regexp patterns, separated by space
patterns=
Custom patterns are usual regular expressions. Ignore case is turned on, but ^ is not added
automatically, so be careful. Actually, you can swap arguments in format option and write
regexp to match end of the string, so things will be moved from the end of the string to
start.

29
docs/plugins/zero.rst Normal file
View file

@ -0,0 +1,29 @@
Zero Plugin
===========
The ``zero`` plugin allows you to null fields before writing tags to files.
Fields can be nulled unconditionally or by pattern match. For example, it can
be used to strip useless comments like "ripped by" etc or any other stuff you
hate. Library is not modified.
To use plugin, enable it by including ``zero`` into ``plugins`` line of
your beets config::
[beets]
plugins = zero
To configure the plugin, use a ``[zero]`` section in your configuration file.
Set ``fields`` to the (whitespace-separated) list of fields to null. You can get
the list of available fields by running ``beet fields``. To conditionally filter
a field, use ``field=regexp regexp`` to specify regular expressions.
For example::
[zero]
fields=month day genre comments
# Custom regexp patterns for each field, separated by spaces:
comments=EAC LAME from.+collection ripped\sby
genre=rnb power\smetal
If custom pattern is not defined, field will be nulled unconditionally. Note
that the plugin currently does not zero fields when importing "as-is".

View file

@ -442,6 +442,12 @@ class DestinationTest(unittest.TestCase):
finally:
sys.getfilesystemencoding = oldfunc
def test_unicode_extension_in_fragment(self):
self.lib.path_formats = [('default', u'foo')]
self.i.path = util.bytestring_path(u'bar.caf\xe9')
dest = self.lib.destination(self.i, platform='linux2', fragment=True)
self.assertEqual(dest, u'foo.caf\xe9')
class PathFormattingMixin(object):
"""Utilities for testing path formatting."""
def _setf(self, fmt):

52
test/test_the.py Normal file
View file

@ -0,0 +1,52 @@
"""Tests for the 'the' plugin"""
from _common import unittest
from beetsplug import the
class ThePluginTest(unittest.TestCase):
def test_unthe_with_default_patterns(self):
self.assertEqual(the.unthe('', the.PATTERN_THE), '')
self.assertEqual(the.unthe('The Something', the.PATTERN_THE),
'Something, The')
self.assertEqual(the.unthe('The The', the.PATTERN_THE), 'The, The')
self.assertEqual(the.unthe('The The', the.PATTERN_THE), 'The, The')
self.assertEqual(the.unthe('The The X', the.PATTERN_THE),
u'The X, The')
self.assertEqual(the.unthe('the The', the.PATTERN_THE), 'The, the')
self.assertEqual(the.unthe('Protected The', the.PATTERN_THE),
'Protected The')
self.assertEqual(the.unthe('A Boy', the.PATTERN_A), 'Boy, A')
self.assertEqual(the.unthe('a girl', the.PATTERN_A), 'girl, a')
self.assertEqual(the.unthe('An Apple', the.PATTERN_A), 'Apple, An')
self.assertEqual(the.unthe('An A Thing', the.PATTERN_A), 'A Thing, An')
self.assertEqual(the.unthe('the An Arse', the.PATTERN_A),
'the An Arse')
self.assertEqual(the.unthe('The Something', the.PATTERN_THE,
strip=True), 'Something')
self.assertEqual(the.unthe('An A', the.PATTERN_A, strip=True), 'A')
def test_template_function_with_defaults(self):
the.the_options['patterns'] = [the.PATTERN_THE, the.PATTERN_A]
the.the_options['format'] = the.FORMAT
self.assertEqual(the.func_the('The The'), 'The, The')
self.assertEqual(the.func_the('An A'), 'A, An')
def test_custom_pattern(self):
the.the_options['patterns'] = [ u'^test\s']
the.the_options['format'] = the.FORMAT
self.assertEqual(the.func_the('test passed'), 'passed, test')
def test_custom_format(self):
the.the_options['patterns'] = [the.PATTERN_THE, the.PATTERN_A]
the.the_options['format'] = '{1} ({0})'
self.assertEqual(the.func_the('The A'), 'The (A)')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')

50
test/test_zero.py Normal file
View file

@ -0,0 +1,50 @@
"""Tests for the 'zero' plugin"""
from _common import unittest
from beets.library import Item
from beetsplug.zero import ZeroPlugin
class ZeroPluginTest(unittest.TestCase):
def test_singleton(self):
z1 = ZeroPlugin()
z2 = ZeroPlugin()
self.assertTrue(z1 is z2)
def test_no_patterns(self):
v = {'comments' : 'test comment',
'day' : 13,
'month' : 3,
'year' : 2012}
i=Item(v)
z = ZeroPlugin()
z.debug = False
z.fields = ['comments', 'month', 'day']
z.patterns = {'comments': ['.'],
'month': ['.'],
'day': ['.']}
z.write_event(i)
self.assertEqual(i.comments, '')
self.assertEqual(i.day, 0)
self.assertEqual(i.month, 0)
self.assertEqual(i.year, 2012)
def test_patterns(self):
v = {'comments' : 'from lame collection, ripped by eac',
'year' : 2012}
i=Item(v)
z = ZeroPlugin()
z.debug = False
z.fields = ['comments', 'year']
z.patterns = {'comments': 'eac lame'.split(),
'year': '2098 2099'.split()}
z.write_event(i)
self.assertEqual(i.comments, '')
self.assertEqual(i.year, 2012)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')