From f005d4af39cb2c870b009b8b564748a348b80b40 Mon Sep 17 00:00:00 2001 From: Six Date: Thu, 20 May 2010 19:46:54 -0400 Subject: prepared for next release. --- LICENSE | 44 +- dodai/__init__.py | 2 - dodai/config/__init__.py | 6 +- dodai/config/files.py | 114 +-- dodai/config/sections.py | 78 +-- dodai/exception.py | 53 ++ dodai/tools/__init__.py | 44 ++ dodai/tools/himo.py | 2 + dodai/tools/odict.py | 1399 +++++++++++++++++++++++++++++++++++++ test/test_config/test_files.py | 135 ++++ test/test_config/test_sections.py | 156 ++--- test/test_dodai.py | 13 +- test/test_tools/test_init.py | 97 +++ 13 files changed, 1941 insertions(+), 202 deletions(-) create mode 100644 dodai/tools/odict.py create mode 100644 test/test_config/test_files.py create mode 100644 test/test_tools/test_init.py diff --git a/LICENSE b/LICENSE index 2a315df..b46e082 100644 --- a/LICENSE +++ b/LICENSE @@ -567,4 +567,46 @@ apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. -END OF TERMS AND CONDITIONS \ No newline at end of file +END OF TERMS AND CONDITIONS + + + +END OF GNU General Public License version 3 (GPLv3) + +------------------------------------------------------------------------------- + +DODAI USES odict WHICH IS UNDER dodai/tools/odict.py. odict IS LICENSED AS: + +Copyright (c) 2003-2010, Michael Foord +All rights reserved. +E-mail : fuzzyman AT voidspace DOT org DOT uk + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Michael Foord nor the name of Voidspace + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + diff --git a/dodai/__init__.py b/dodai/__init__.py index 87155db..55bb57d 100644 --- a/dodai/__init__.py +++ b/dodai/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Dodai. If not, see . - - import os import sys from dodai.config import Config diff --git a/dodai/config/__init__.py b/dodai/config/__init__.py index 7fce975..6c3011c 100644 --- a/dodai/config/__init__.py +++ b/dodai/config/__init__.py @@ -29,7 +29,11 @@ class Config(object): if 'files' == key: from dodai.config.files import ConfigFiles - self.files = ConfigFiles() + from dodai.tools.odict import OrderedDict + from dodai.tools import Section + from dodai.tools.himo import String2Himo + s2h = String2Himo() + self.files = ConfigFiles(OrderedDict, Section, s2h) return self.files elif 'options' == key: from dodai.config.option import ConfigOption diff --git a/dodai/config/files.py b/dodai/config/files.py index 3ca9121..a27e4a2 100644 --- a/dodai/config/files.py +++ b/dodai/config/files.py @@ -17,62 +17,90 @@ import os import ConfigParser -from dodai.tools.himo import String2Himo from dodai.config.sections import ConfigSections +from dodai.exception import InvalidConfigParser +from dodai.exception import FileDoesNotExist +from dodai.exception import FileIsDirectory class ConfigFiles(object): - def __init__(self): + REQUIRED_METHODS = ['read', 'sections', 'options', 'get'] - self._default_parser = ConfigParser.ConfigParser() - self._string_object = String2Himo() - self._files = {} + def __init__(self, ordered_dict_object, section_object, string_object): - def set_default_parser(self, parser): - self.check_config_parser(parser) - self._default_parser = parser + self._ordered_dict_object = ordered_dict_object + self._section_object = section_object + self._string_object = string_object + self.parser_objects = [] + self.files = self._ordered_dict_object() + self.files_read = [] - def set_string_object(self, obj): - self._string_object = obj + def register_parser_object(self, parser_object): + """Registers a config file parser with this object. Raises + InvalidConfigParser error if the parser does not have the 'read', + 'sections', 'options' and 'get' methods. - def add(self, path, encoding=None, parser=None): - if parser: - self.check_config_parser(parser) - if os.path.exists(path): - if parser: - self._files[path] = [parser, encoding] + """ + if self.check_parser_object(parser_object): + self.parser_objects.append(parser_object) + + def check_parser_object(self, parser_object): + """Checks the given parser object to insure it has all of the required + methods needed to parse files. + + """ + for name in self.REQUIRED_METHODS: + if not hasattr(parser_object, name): + raise InvalidConfigParser(self.REQUIRED_METHODS, + parser_object.__name__) + return True + + def add(self, filename, encoding=None): + """Adds the given filename to this object to be parsed. + + """ + if os.path.exists(filename): + if os.path.isdir(filename): + raise FileIsDirectory(filename) else: - self._files[path] = [self._default_parser, encoding] + filename = os.path.realpath(filename) + self.files[filename] = encoding else: - raise FileDoesNotExist(path) + raise FileDoesNotExist(filename) - def is_valid_config_parser(self, obj): - out = True - if not hasattr(obj, 'read'): - out = False + def _read(self): + self.files_read = [] + out = self._ordered_dict_object() + for filename, encoding in self.files.items(): + error = False + for parser_obj in self.parser_objects: + try: + parser = parser_obj() + parser.read(filename) + except Exception, e: + error = True + else: + out[filename] = self._ordered_dict_object() + out[filename]['encoding'] = encoding + out[filename]['parser'] = parser + self.files_read.append(filename) + error = False + break + if error: + raise e return out - def check_config_parser(self, obj): - if self.is_valid_config_parser(obj): - return True - else: - raise InvalidConfigParser("Please make sure your object "\ - "has the following methods: "\ - "sections, options, read and get.") - def load(self, sections_object=None): - paths = [] - sections = sections_object or ConfigSections(self._string_object) - for path, data in self._files.items(): - parser = data[0] - encoding = data[1] - parser.read(path) + def load(self): + """Returns a ConfigSections object, which acts like a dictionary, + that contains all parsed data. + + """ + files = self._read() + sections = ConfigSections(self._ordered_dict_object, + self._section_object, self._string_object) + for filename, data in files.items(): + encoding = data['encoding'] + parser = data['parser'] sections(parser, encoding) return sections - - -class InvalidConfigParser(Exception): - pass - -class FileDoesNotExist(Exception): - pass diff --git a/dodai/config/sections.py b/dodai/config/sections.py index 2b08587..d789a24 100644 --- a/dodai/config/sections.py +++ b/dodai/config/sections.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Dodai. If not, see . -import unicodedata class ConfigSections(object): """ @@ -23,7 +22,8 @@ class ConfigSections(object): """ - def __init__(self, string_object = None): + def __init__(self, ordered_dict_object, section_object, + string_object = None,): """ Iterable object that handles the conversion of a config parser object to a list of section objects. @@ -42,8 +42,10 @@ class ConfigSections(object): """ - self._string_object = string_object or None - self._sections = {} + self._string_object = string_object or '' + self._ordered_dict_object = ordered_dict_object + self._section_object = section_object + self._sections = self._ordered_dict_object() def __call__(self, parser, encoding=None): """ @@ -70,10 +72,10 @@ class ConfigSections(object): # Adds the options to the section object for key in parser.options(section_name): - key = self._build_string_object(key, encoding) value = self._build_string_object(parser.get(section_name, key), encoding) - setattr(section, key, value) + key = self._build_string_object(key, encoding) + section[key] = value def _build_string_object(self, data, encoding=None): if self._string_object: @@ -83,7 +85,7 @@ class ConfigSections(object): def get_section(self, section_name, encoding=None): """ - Returns a ConfigSection object from this object's section + Returns a Section (aka dict) object from this object's section dictionary or creates a new ConfigSection object, which is stored int this object's section dictionary then is returned @@ -92,16 +94,14 @@ class ConfigSections(object): if section_name in self._sections: return self._sections[section_name] else: - section = ConfigSection(section_name) + section = self._section_object(section_name) self._sections[section_name] = section return section def __getitem__(self, key): - key = normalize_key(key) return self._sections[key] def __getattr__(self, key): - key = normalize_key(key) try: out = self._sections[key] except KeyError: @@ -115,61 +115,3 @@ class ConfigSections(object): def __len__(self): return len(self._sections) - -class ConfigSection(object): - """ - A generic object to hold keys and values primarily from a config file - - """ - def __init__(self, title): - """ - Holds keys and values primarily from a section of a config file - - title: The title of the section of the config file - - """ - self.___title___ = title - self.___options___ = {} - - - def get_title(self): - """ - Returns the title of the section - - """ - return self.___title___ - - def __setattr__(self, key, value): - if key.startswith('___') and key.endswith('___'): - object.__setattr__(self, key, value) - else: - key = normalize_key(key) - if self.___options___.has_key(key): - self.___options___[key] = value - else: - dict.__setitem__(self.___options___, key, value) - - def __getattr__(self, key): - if key.startswith('___') and key.endswith('___'): - return self.__dict__[key] - else: - key = normalize_key(key) - try: - out = self.___options___[key] - except KeyError: - return getattr(self.___options___, key) - else: - return out - - def __getitem__(self, key): - key = normalize_key(key) - return self.___options___[key] - - def __iter__(self, *args, **kargs): - return self.___options___.__iter__(*args, **kargs) - - -def normalize_key(key): - key = unicode(key) - key = unicodedata.normalize('NFC', key) - return key diff --git a/dodai/exception.py b/dodai/exception.py index 5af10b4..23302c7 100644 --- a/dodai/exception.py +++ b/dodai/exception.py @@ -16,6 +16,7 @@ # along with Dodai. If not, see . from dodai.tools import list_to_english +from dodai.tools import quote_list class DodaiException(Exception): @@ -234,3 +235,55 @@ class UnknownDatabaseConnectionException( def __str__(self): return self.msg + + +class InvalidConfigParser(DodaiException): + """Exception raised when an invalid parser is registered in the + dodai.config.ConfigFiles object. + + """ + MESSAGE = "The parser object '{name}' that you were trying to register "\ + "is not a valid parser object. Please make sure that this "\ + "object contains all of the following methods: {methods}" + + def __init__(self, required_methods, name): + self.msg = self._build_message(required_methods, name) + + def _build_message(self, required_methods, name): + methods = quote_list(required_methods) + return self.MESSAGE.format(methods=methods, name=name) + + def __str__(self): + return self.msg + + +class FileDoesNotExist(DodaiException): + """Exception raised when a file does not exist. + + """ + MESSAGE = "The file: '{file_}' does not exist." + + def __init__(self, filepath): + self.msg = self._build_message(filepath) + + def _build_message(self, filepath): + return self.MESSAGE.format(file_=filepath) + + def __str__(self): + return self.msg + + +class FileIsDirectory(DodaiException): + """Exception raised when a file is a directory. + + """ + MESSAGE = "The file: '{file_}' is a directory." + + def __init__(self, filepath): + self.msg = self._build_message(filepath) + + def _build_message(self, filepath): + return self.MESSAGE.format(file_=filepath) + + def __str__(self): + return self.msg diff --git a/dodai/tools/__init__.py b/dodai/tools/__init__.py index b6d93a1..bcef547 100644 --- a/dodai/tools/__init__.py +++ b/dodai/tools/__init__.py @@ -18,6 +18,8 @@ import sys import os import platform +from dodai.tools.odict import OrderedDict +import unicodedata def home_directory(): """Returns the full real path to the home directory of the user who @@ -97,3 +99,45 @@ def list_to_english(data): return "{0} and {1}".format(out, last) else: return "{0}".format(data.pop()) + +def quote_list(data, double_quotes=False): + """Takes the give list (data) and adds quotes around each item. Returns + a list + + """ + out = [] + if double_quotes: + quote = '"' + else: + quote = "'" + for item in data: + item = "{quote}{item}{quote}".format(quote=quote, item=item) + out.append(item) + return out + +def normalize_unicode(data): + """Normalizes the unicode data so that compare functions will work + correctly. + + """ + data = unicode(data) + data = unicodedata.normalize('NFC', data) + return data + +class Section(OrderedDict): + """An ordered dictionary object that has the added benfit of holding + a name or title of this grouping. + + """ + def __init__(self, title): + self.set_section_title(title) + super(Section,self).__init__() + + def get_section_title(self): + return self.___title___ + + def set_section_title(self, val): + self.___title___ = val + + def __repr__(self): + return "".format(self.get_section_title()) diff --git a/dodai/tools/himo.py b/dodai/tools/himo.py index aa4da6c..f56eaf8 100644 --- a/dodai/tools/himo.py +++ b/dodai/tools/himo.py @@ -23,6 +23,7 @@ from htmlentitydefs import name2codepoint from htmlentitydefs import codepoint2name from decimal import Decimal as D from dodai.exception import HimoAsciiError +from dodai.tools import normalize_unicode class String2Himo(object): """ @@ -53,6 +54,7 @@ class String2Himo(object): encoding = encoding or self.default_encoding data = self._as_unicode(data, encoding) data = self._decode_html(data) + data = normalize_unicode(data) return Himo(data) def _as_unicode(self, data, encoding): diff --git a/dodai/tools/odict.py b/dodai/tools/odict.py new file mode 100644 index 0000000..2c8391d --- /dev/null +++ b/dodai/tools/odict.py @@ -0,0 +1,1399 @@ +# odict.py +# An Ordered Dictionary object +# Copyright (C) 2005 Nicola Larosa, Michael Foord +# E-mail: nico AT tekNico DOT net, fuzzyman AT voidspace DOT org DOT uk + +# This software is licensed under the terms of the BSD license. +# http://www.voidspace.org.uk/python/license.shtml +# Basically you're free to copy, modify, distribute and relicense it, +# So long as you keep a copy of the license with it. + +# Documentation at http://www.voidspace.org.uk/python/odict.html +# For information about bugfixes, updates and support, please join the +# Pythonutils mailing list: +# http://groups.google.com/group/pythonutils/ +# Comments, suggestions and bug reports welcome. + +"""A dict that keeps keys in insertion order""" +from __future__ import generators + +__author__ = ('Nicola Larosa ,' + 'Michael Foord ') + +__docformat__ = "restructuredtext en" + +__revision__ = '$Id: odict.py 129 2005-09-12 18:15:28Z teknico $' + +__version__ = '0.2.2' + +__all__ = ['OrderedDict', 'SequenceOrderedDict'] + +import sys +INTP_VER = sys.version_info[:2] +if INTP_VER < (2, 2): + raise RuntimeError("Python v.2.2 or later required") + +import types, warnings + +class OrderedDict(dict): + """ + A class of dictionary that keeps the insertion order of keys. + + All appropriate methods return keys, items, or values in an ordered way. + + All normal dictionary methods are available. Update and comparison is + restricted to other OrderedDict objects. + + Various sequence methods are available, including the ability to explicitly + mutate the key ordering. + + __contains__ tests: + + >>> d = OrderedDict(((1, 3),)) + >>> 1 in d + 1 + >>> 4 in d + 0 + + __getitem__ tests: + + >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[2] + 1 + >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[4] + Traceback (most recent call last): + KeyError: 4 + + __len__ tests: + + >>> len(OrderedDict()) + 0 + >>> len(OrderedDict(((1, 3), (3, 2), (2, 1)))) + 3 + + get tests: + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.get(1) + 3 + >>> d.get(4) is None + 1 + >>> d.get(4, 5) + 5 + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1)]) + + has_key tests: + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.has_key(1) + 1 + >>> d.has_key(4) + 0 + """ + + def __init__(self, init_val=(), strict=False): + """ + Create a new ordered dictionary. Cannot init from a normal dict, + nor from kwargs, since items order is undefined in those cases. + + If the ``strict`` keyword argument is ``True`` (``False`` is the + default) then when doing slice assignment - the ``OrderedDict`` you are + assigning from *must not* contain any keys in the remaining dict. + + >>> OrderedDict() + OrderedDict([]) + >>> OrderedDict({1: 1}) + Traceback (most recent call last): + TypeError: undefined order, cannot get items from dict + >>> OrderedDict({1: 1}.items()) + OrderedDict([(1, 1)]) + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1)]) + >>> OrderedDict(d) + OrderedDict([(1, 3), (3, 2), (2, 1)]) + """ + self.strict = strict + dict.__init__(self) + if isinstance(init_val, OrderedDict): + self._sequence = init_val.keys() + dict.update(self, init_val) + elif isinstance(init_val, dict): + # we lose compatibility with other ordered dict types this way + raise TypeError('undefined order, cannot get items from dict') + else: + self._sequence = [] + self.update(init_val) + +### Special methods ### + + def __delitem__(self, key): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> del d[3] + >>> d + OrderedDict([(1, 3), (2, 1)]) + >>> del d[3] + Traceback (most recent call last): + KeyError: 3 + >>> d[3] = 2 + >>> d + OrderedDict([(1, 3), (2, 1), (3, 2)]) + >>> del d[0:1] + >>> d + OrderedDict([(2, 1), (3, 2)]) + """ + if isinstance(key, types.SliceType): + # FIXME: efficiency? + keys = self._sequence[key] + for entry in keys: + dict.__delitem__(self, entry) + del self._sequence[key] + else: + # do the dict.__delitem__ *first* as it raises + # the more appropriate error + dict.__delitem__(self, key) + self._sequence.remove(key) + + def __eq__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d == OrderedDict(d) + True + >>> d == OrderedDict(((1, 3), (2, 1), (3, 2))) + False + >>> d == OrderedDict(((1, 0), (3, 2), (2, 1))) + False + >>> d == OrderedDict(((0, 3), (3, 2), (2, 1))) + False + >>> d == dict(d) + False + >>> d == False + False + """ + if isinstance(other, OrderedDict): + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() == other.items()) + else: + return False + + def __lt__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> c < d + True + >>> d < c + False + >>> d < dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() < other.items()) + + def __le__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> e = OrderedDict(d) + >>> c <= d + True + >>> d <= c + False + >>> d <= dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + >>> d <= e + True + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() <= other.items()) + + def __ne__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d != OrderedDict(d) + False + >>> d != OrderedDict(((1, 3), (2, 1), (3, 2))) + True + >>> d != OrderedDict(((1, 0), (3, 2), (2, 1))) + True + >>> d == OrderedDict(((0, 3), (3, 2), (2, 1))) + False + >>> d != dict(d) + True + >>> d != False + True + """ + if isinstance(other, OrderedDict): + # FIXME: efficiency? + # Generate both item lists for each compare + return not (self.items() == other.items()) + else: + return True + + def __gt__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> d > c + True + >>> c > d + False + >>> d > dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() > other.items()) + + def __ge__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> e = OrderedDict(d) + >>> c >= d + False + >>> d >= c + True + >>> d >= dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + >>> e >= d + True + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() >= other.items()) + + def __repr__(self): + """ + Used for __repr__ and __str__ + + >>> r1 = repr(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f')))) + >>> r1 + "OrderedDict([('a', 'b'), ('c', 'd'), ('e', 'f')])" + >>> r2 = repr(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd')))) + >>> r2 + "OrderedDict([('a', 'b'), ('e', 'f'), ('c', 'd')])" + >>> r1 == str(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f')))) + True + >>> r2 == str(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd')))) + True + """ + return '%s([%s])' % (self.__class__.__name__, ', '.join( + ['(%r, %r)' % (key, self[key]) for key in self._sequence])) + + def __setitem__(self, key, val): + """ + Allows slice assignment, so long as the slice is an OrderedDict + >>> d = OrderedDict() + >>> d['a'] = 'b' + >>> d['b'] = 'a' + >>> d[3] = 12 + >>> d + OrderedDict([('a', 'b'), ('b', 'a'), (3, 12)]) + >>> d[:] = OrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + OrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d[::2] = OrderedDict(((7, 8), (9, 10))) + >>> d + OrderedDict([(7, 8), (2, 3), (9, 10)]) + >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4))) + >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8))) + >>> d + OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)]) + >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)), strict=True) + >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8))) + >>> d + OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)]) + + >>> a = OrderedDict(((0, 1), (1, 2), (2, 3)), strict=True) + >>> a[3] = 4 + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]) + Traceback (most recent call last): + ValueError: slice assignment must be from unique keys + >>> a = OrderedDict(((0, 1), (1, 2), (2, 3))) + >>> a[3] = 4 + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::-1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(3, 4), (2, 3), (1, 2), (0, 1)]) + + >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> d[:1] = 3 + Traceback (most recent call last): + TypeError: slice assignment requires an OrderedDict + + >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> d[:1] = OrderedDict([(9, 8)]) + >>> d + OrderedDict([(9, 8), (1, 2), (2, 3), (3, 4)]) + """ + if isinstance(key, types.SliceType): + if not isinstance(val, OrderedDict): + # FIXME: allow a list of tuples? + raise TypeError('slice assignment requires an OrderedDict') + keys = self._sequence[key] + # NOTE: Could use ``range(*key.indices(len(self._sequence)))`` + indexes = range(len(self._sequence))[key] + if key.step is None: + # NOTE: new slice may not be the same size as the one being + # overwritten ! + # NOTE: What is the algorithm for an impossible slice? + # e.g. d[5:3] + pos = key.start or 0 + del self[key] + newkeys = val.keys() + for k in newkeys: + if k in self: + if self.strict: + raise ValueError('slice assignment must be from ' + 'unique keys') + else: + # NOTE: This removes duplicate keys *first* + # so start position might have changed? + del self[k] + self._sequence = (self._sequence[:pos] + newkeys + + self._sequence[pos:]) + dict.update(self, val) + else: + # extended slice - length of new slice must be the same + # as the one being replaced + if len(keys) != len(val): + raise ValueError('attempt to assign sequence of size %s ' + 'to extended slice of size %s' % (len(val), len(keys))) + # FIXME: efficiency? + del self[key] + item_list = zip(indexes, val.items()) + # smallest indexes first - higher indexes not guaranteed to + # exist + item_list.sort() + for pos, (newkey, newval) in item_list: + if self.strict and newkey in self: + raise ValueError('slice assignment must be from unique' + ' keys') + self.insert(pos, newkey, newval) + else: + if key not in self: + self._sequence.append(key) + dict.__setitem__(self, key, val) + + def __getitem__(self, key): + """ + Allows slicing. Returns an OrderedDict if you slice. + >>> b = OrderedDict([(7, 0), (6, 1), (5, 2), (4, 3), (3, 4), (2, 5), (1, 6)]) + >>> b[::-1] + OrderedDict([(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0)]) + >>> b[2:5] + OrderedDict([(5, 2), (4, 3), (3, 4)]) + >>> type(b[2:4]) + + """ + if isinstance(key, types.SliceType): + # FIXME: does this raise the error we want? + keys = self._sequence[key] + # FIXME: efficiency? + return OrderedDict([(entry, self[entry]) for entry in keys]) + else: + return dict.__getitem__(self, key) + + __str__ = __repr__ + + def __setattr__(self, name, value): + """ + Implemented so that accesses to ``sequence`` raise a warning and are + diverted to the new ``setkeys`` method. + """ + if name == 'sequence': + warnings.warn('Use of the sequence attribute is deprecated.' + ' Use the keys method instead.', DeprecationWarning) + # NOTE: doesn't return anything + self.setkeys(value) + else: + # FIXME: do we want to allow arbitrary setting of attributes? + # Or do we want to manage it? + object.__setattr__(self, name, value) + + def __getattr__(self, name): + """ + Implemented so that access to ``sequence`` raises a warning. + + >>> d = OrderedDict() + >>> d.sequence + [] + """ + if name == 'sequence': + warnings.warn('Use of the sequence attribute is deprecated.' + ' Use the keys method instead.', DeprecationWarning) + # NOTE: Still (currently) returns a direct reference. Need to + # because code that uses sequence will expect to be able to + # mutate it in place. + return self._sequence + else: + # raise the appropriate error + raise AttributeError("OrderedDict has no '%s' attribute" % name) + + def __deepcopy__(self, memo): + """ + To allow deepcopy to work with OrderedDict. + + >>> from copy import deepcopy + >>> a = OrderedDict([(1, 1), (2, 2), (3, 3)]) + >>> a['test'] = {} + >>> b = deepcopy(a) + >>> b == a + True + >>> b is a + False + >>> a['test'] is b['test'] + False + """ + from copy import deepcopy + return self.__class__(deepcopy(self.items(), memo), self.strict) + + +### Read-only methods ### + + def copy(self): + """ + >>> OrderedDict(((1, 3), (3, 2), (2, 1))).copy() + OrderedDict([(1, 3), (3, 2), (2, 1)]) + """ + return OrderedDict(self) + + def items(self): + """ + ``items`` returns a list of tuples representing all the + ``(key, value)`` pairs in the dictionary. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.items() + [(1, 3), (3, 2), (2, 1)] + >>> d.clear() + >>> d.items() + [] + """ + return zip(self._sequence, self.values()) + + def keys(self): + """ + Return a list of keys in the ``OrderedDict``. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.keys() + [1, 3, 2] + """ + return self._sequence[:] + + def values(self, values=None): + """ + Return a list of all the values in the OrderedDict. + + Optionally you can pass in a list of values, which will replace the + current list. The value list must be the same len as the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.values() + [3, 2, 1] + """ + return [self[key] for key in self._sequence] + + def iteritems(self): + """ + >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iteritems() + >>> ii.next() + (1, 3) + >>> ii.next() + (3, 2) + >>> ii.next() + (2, 1) + >>> ii.next() + Traceback (most recent call last): + StopIteration + """ + def make_iter(self=self): + keys = self.iterkeys() + while True: + key = keys.next() + yield (key, self[key]) + return make_iter() + + def iterkeys(self): + """ + >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iterkeys() + >>> ii.next() + 1 + >>> ii.next() + 3 + >>> ii.next() + 2 + >>> ii.next() + Traceback (most recent call last): + StopIteration + """ + return iter(self._sequence) + + __iter__ = iterkeys + + def itervalues(self): + """ + >>> iv = OrderedDict(((1, 3), (3, 2), (2, 1))).itervalues() + >>> iv.next() + 3 + >>> iv.next() + 2 + >>> iv.next() + 1 + >>> iv.next() + Traceback (most recent call last): + StopIteration + """ + def make_iter(self=self): + keys = self.iterkeys() + while True: + yield self[keys.next()] + return make_iter() + +### Read-write methods ### + + def clear(self): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.clear() + >>> d + OrderedDict([]) + """ + dict.clear(self) + self._sequence = [] + + def pop(self, key, *args): + """ + No dict.pop in Python 2.2, gotta reimplement it + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.pop(3) + 2 + >>> d + OrderedDict([(1, 3), (2, 1)]) + >>> d.pop(4) + Traceback (most recent call last): + KeyError: 4 + >>> d.pop(4, 0) + 0 + >>> d.pop(4, 0, 1) + Traceback (most recent call last): + TypeError: pop expected at most 2 arguments, got 3 + """ + if len(args) > 1: + raise TypeError, ('pop expected at most 2 arguments, got %s' % + (len(args) + 1)) + if key in self: + val = self[key] + del self[key] + else: + try: + val = args[0] + except IndexError: + raise KeyError(key) + return val + + def popitem(self, i=-1): + """ + Delete and return an item specified by index, not a random one as in + dict. The index is -1 by default (the last item). + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.popitem() + (2, 1) + >>> d + OrderedDict([(1, 3), (3, 2)]) + >>> d.popitem(0) + (1, 3) + >>> OrderedDict().popitem() + Traceback (most recent call last): + KeyError: 'popitem(): dictionary is empty' + >>> d.popitem(2) + Traceback (most recent call last): + IndexError: popitem(): index 2 not valid + """ + if not self._sequence: + raise KeyError('popitem(): dictionary is empty') + try: + key = self._sequence[i] + except IndexError: + raise IndexError('popitem(): index %s not valid' % i) + return (key, self.pop(key)) + + def setdefault(self, key, defval = None): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.setdefault(1) + 3 + >>> d.setdefault(4) is None + True + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1), (4, None)]) + >>> d.setdefault(5, 0) + 0 + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1), (4, None), (5, 0)]) + """ + if key in self: + return self[key] + else: + self[key] = defval + return defval + + def update(self, from_od): + """ + Update from another OrderedDict or sequence of (key, value) pairs + + >>> d = OrderedDict(((1, 0), (0, 1))) + >>> d.update(OrderedDict(((1, 3), (3, 2), (2, 1)))) + >>> d + OrderedDict([(1, 3), (0, 1), (3, 2), (2, 1)]) + >>> d.update({4: 4}) + Traceback (most recent call last): + TypeError: undefined order, cannot get items from dict + >>> d.update((4, 4)) + Traceback (most recent call last): + TypeError: cannot convert dictionary update sequence element "4" to a 2-item sequence + """ + if isinstance(from_od, OrderedDict): + for key, val in from_od.items(): + self[key] = val + elif isinstance(from_od, dict): + # we lose compatibility with other ordered dict types this way + raise TypeError('undefined order, cannot get items from dict') + else: + # FIXME: efficiency? + # sequence of 2-item sequences, or error + for item in from_od: + try: + key, val = item + except TypeError: + raise TypeError('cannot convert dictionary update' + ' sequence element "%s" to a 2-item sequence' % item) + self[key] = val + + def rename(self, old_key, new_key): + """ + Rename the key for a given value, without modifying sequence order. + + For the case where new_key already exists this raise an exception, + since if new_key exists, it is ambiguous as to what happens to the + associated values, and the position of new_key in the sequence. + + >>> od = OrderedDict() + >>> od['a'] = 1 + >>> od['b'] = 2 + >>> od.items() + [('a', 1), ('b', 2)] + >>> od.rename('b', 'c') + >>> od.items() + [('a', 1), ('c', 2)] + >>> od.rename('c', 'a') + Traceback (most recent call last): + ValueError: New key already exists: 'a' + >>> od.rename('d', 'b') + Traceback (most recent call last): + KeyError: 'd' + """ + if new_key == old_key: + # no-op + return + if new_key in self: + raise ValueError("New key already exists: %r" % new_key) + # rename sequence entry + value = self[old_key] + old_idx = self._sequence.index(old_key) + self._sequence[old_idx] = new_key + # rename internal dict entry + dict.__delitem__(self, old_key) + dict.__setitem__(self, new_key, value) + + def setitems(self, items): + """ + This method allows you to set the items in the dict. + + It takes a list of tuples - of the same sort returned by the ``items`` + method. + + >>> d = OrderedDict() + >>> d.setitems(((3, 1), (2, 3), (1, 2))) + >>> d + OrderedDict([(3, 1), (2, 3), (1, 2)]) + """ + self.clear() + # FIXME: this allows you to pass in an OrderedDict as well :-) + self.update(items) + + def setkeys(self, keys): + """ + ``setkeys`` all ows you to pass in a new list of keys which will + replace the current set. This must contain the same set of keys, but + need not be in the same order. + + If you pass in new keys that don't match, a ``KeyError`` will be + raised. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.keys() + [1, 3, 2] + >>> d.setkeys((1, 2, 3)) + >>> d + OrderedDict([(1, 3), (2, 1), (3, 2)]) + >>> d.setkeys(['a', 'b', 'c']) + Traceback (most recent call last): + KeyError: 'Keylist is not the same as current keylist.' + """ + # FIXME: Efficiency? (use set for Python 2.4 :-) + # NOTE: list(keys) rather than keys[:] because keys[:] returns + # a tuple, if keys is a tuple. + kcopy = list(keys) + kcopy.sort() + self._sequence.sort() + if kcopy != self._sequence: + raise KeyError('Keylist is not the same as current keylist.') + # NOTE: This makes the _sequence attribute a new object, instead + # of changing it in place. + # FIXME: efficiency? + self._sequence = list(keys) + + def setvalues(self, values): + """ + You can pass in a list of values, which will replace the + current list. The value list must be the same len as the OrderedDict. + + (Or a ``ValueError`` is raised.) + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.setvalues((1, 2, 3)) + >>> d + OrderedDict([(1, 1), (3, 2), (2, 3)]) + >>> d.setvalues([6]) + Traceback (most recent call last): + ValueError: Value list is not the same length as the OrderedDict. + """ + if len(values) != len(self): + # FIXME: correct error to raise? + raise ValueError('Value list is not the same length as the ' + 'OrderedDict.') + self.update(zip(self, values)) + +### Sequence Methods ### + + def index(self, key): + """ + Return the position of the specified key in the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.index(3) + 1 + >>> d.index(4) + Traceback (most recent call last): + ValueError: list.index(x): x not in list + """ + return self._sequence.index(key) + + def insert(self, index, key, value): + """ + Takes ``index``, ``key``, and ``value`` as arguments. + + Sets ``key`` to ``value``, so that ``key`` is at position ``index`` in + the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.insert(0, 4, 0) + >>> d + OrderedDict([(4, 0), (1, 3), (3, 2), (2, 1)]) + >>> d.insert(0, 2, 1) + >>> d + OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2)]) + >>> d.insert(8, 8, 1) + >>> d + OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2), (8, 1)]) + """ + if key in self: + # FIXME: efficiency? + del self[key] + self._sequence.insert(index, key) + dict.__setitem__(self, key, value) + + def reverse(self): + """ + Reverse the order of the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.reverse() + >>> d + OrderedDict([(2, 1), (3, 2), (1, 3)]) + """ + self._sequence.reverse() + + def sort(self, *args, **kwargs): + """ + Sort the key order in the OrderedDict. + + This method takes the same arguments as the ``list.sort`` method on + your version of Python. + + >>> d = OrderedDict(((4, 1), (2, 2), (3, 3), (1, 4))) + >>> d.sort() + >>> d + OrderedDict([(1, 4), (2, 2), (3, 3), (4, 1)]) + """ + self._sequence.sort(*args, **kwargs) + +class Keys(object): + # FIXME: should this object be a subclass of list? + """ + Custom object for accessing the keys of an OrderedDict. + + Can be called like the normal ``OrderedDict.keys`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the keys method.""" + return self._main._keys() + + def __getitem__(self, index): + """Fetch the key at position i.""" + # NOTE: this automatically supports slicing :-) + return self._main._sequence[index] + + def __setitem__(self, index, name): + """ + You cannot assign to keys, but you can do slice assignment to re-order + them. + + You can only do slice assignment if the new set of keys is a reordering + of the original set. + """ + if isinstance(index, types.SliceType): + # FIXME: efficiency? + # check length is the same + indexes = range(len(self._main._sequence))[index] + if len(indexes) != len(name): + raise ValueError('attempt to assign sequence of size %s ' + 'to slice of size %s' % (len(name), len(indexes))) + # check they are the same keys + # FIXME: Use set + old_keys = self._main._sequence[index] + new_keys = list(name) + old_keys.sort() + new_keys.sort() + if old_keys != new_keys: + raise KeyError('Keylist is not the same as current keylist.') + orig_vals = [self._main[k] for k in name] + del self._main[index] + vals = zip(indexes, name, orig_vals) + vals.sort() + for i, k, v in vals: + if self._main.strict and k in self._main: + raise ValueError('slice assignment must be from ' + 'unique keys') + self._main.insert(i, k, v) + else: + raise ValueError('Cannot assign to keys') + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main._sequence) + + # FIXME: do we need to check if we are comparing with another ``Keys`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main._sequence < other + def __le__(self, other): return self._main._sequence <= other + def __eq__(self, other): return self._main._sequence == other + def __ne__(self, other): return self._main._sequence != other + def __gt__(self, other): return self._main._sequence > other + def __ge__(self, other): return self._main._sequence >= other + # FIXME: do we need __cmp__ as well as rich comparisons? + def __cmp__(self, other): return cmp(self._main._sequence, other) + + def __contains__(self, item): return item in self._main._sequence + def __len__(self): return len(self._main._sequence) + def __iter__(self): return self._main.iterkeys() + def count(self, item): return self._main._sequence.count(item) + def index(self, item, *args): return self._main._sequence.index(item, *args) + def reverse(self): self._main._sequence.reverse() + def sort(self, *args, **kwds): self._main._sequence.sort(*args, **kwds) + def __mul__(self, n): return self._main._sequence*n + __rmul__ = __mul__ + def __add__(self, other): return self._main._sequence + other + def __radd__(self, other): return other + self._main._sequence + + ## following methods not implemented for keys ## + def __delitem__(self, i): raise TypeError('Can\'t delete items from keys') + def __iadd__(self, other): raise TypeError('Can\'t add in place to keys') + def __imul__(self, n): raise TypeError('Can\'t multiply keys in place') + def append(self, item): raise TypeError('Can\'t append items to keys') + def insert(self, i, item): raise TypeError('Can\'t insert items into keys') + def pop(self, i=-1): raise TypeError('Can\'t pop items from keys') + def remove(self, item): raise TypeError('Can\'t remove items from keys') + def extend(self, other): raise TypeError('Can\'t extend keys') + +class Items(object): + """ + Custom object for accessing the items of an OrderedDict. + + Can be called like the normal ``OrderedDict.items`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the items method.""" + return self._main._items() + + def __getitem__(self, index): + """Fetch the item at position i.""" + if isinstance(index, types.SliceType): + # fetching a slice returns an OrderedDict + return self._main[index].items() + key = self._main._sequence[index] + return (key, self._main[key]) + + def __setitem__(self, index, item): + """Set item at position i to item.""" + if isinstance(index, types.SliceType): + # NOTE: item must be an iterable (list of tuples) + self._main[index] = OrderedDict(item) + else: + # FIXME: Does this raise a sensible error? + orig = self._main.keys[index] + key, value = item + if self._main.strict and key in self and (key != orig): + raise ValueError('slice assignment must be from ' + 'unique keys') + # delete the current one + del self._main[self._main._sequence[index]] + self._main.insert(index, key, value) + + def __delitem__(self, i): + """Delete the item at position i.""" + key = self._main._sequence[i] + if isinstance(i, types.SliceType): + for k in key: + # FIXME: efficiency? + del self._main[k] + else: + del self._main[key] + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main.items()) + + # FIXME: do we need to check if we are comparing with another ``Items`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main.items() < other + def __le__(self, other): return self._main.items() <= other + def __eq__(self, other): return self._main.items() == other + def __ne__(self, other): return self._main.items() != other + def __gt__(self, other): return self._main.items() > other + def __ge__(self, other): return self._main.items() >= other + def __cmp__(self, other): return cmp(self._main.items(), other) + + def __contains__(self, item): return item in self._main.items() + def __len__(self): return len(self._main._sequence) # easier :-) + def __iter__(self): return self._main.iteritems() + def count(self, item): return self._main.items().count(item) + def index(self, item, *args): return self._main.items().index(item, *args) + def reverse(self): self._main.reverse() + def sort(self, *args, **kwds): self._main.sort(*args, **kwds) + def __mul__(self, n): return self._main.items()*n + __rmul__ = __mul__ + def __add__(self, other): return self._main.items() + other + def __radd__(self, other): return other + self._main.items() + + def append(self, item): + """Add an item to the end.""" + # FIXME: this is only append if the key isn't already present + key, value = item + self._main[key] = value + + def insert(self, i, item): + key, value = item + self._main.insert(i, key, value) + + def pop(self, i=-1): + key = self._main._sequence[i] + return (key, self._main.pop(key)) + + def remove(self, item): + key, value = item + try: + assert value == self._main[key] + except (KeyError, AssertionError): + raise ValueError('ValueError: list.remove(x): x not in list') + else: + del self._main[key] + + def extend(self, other): + # FIXME: is only a true extend if none of the keys already present + for item in other: + key, value = item + self._main[key] = value + + def __iadd__(self, other): + self.extend(other) + + ## following methods not implemented for items ## + + def __imul__(self, n): raise TypeError('Can\'t multiply items in place') + +class Values(object): + """ + Custom object for accessing the values of an OrderedDict. + + Can be called like the normal ``OrderedDict.values`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the values method.""" + return self._main._values() + + def __getitem__(self, index): + """Fetch the value at position i.""" + if isinstance(index, types.SliceType): + return [self._main[key] for key in self._main._sequence[index]] + else: + return self._main[self._main._sequence[index]] + + def __setitem__(self, index, value): + """ + Set the value at position i to value. + + You can only do slice assignment to values if you supply a sequence of + equal length to the slice you are replacing. + """ + if isinstance(index, types.SliceType): + keys = self._main._sequence[index] + if len(keys) != len(value): + raise ValueError('attempt to assign sequence of size %s ' + 'to slice of size %s' % (len(name), len(keys))) + # FIXME: efficiency? Would be better to calculate the indexes + # directly from the slice object + # NOTE: the new keys can collide with existing keys (or even + # contain duplicates) - these will overwrite + for key, val in zip(keys, value): + self._main[key] = val + else: + self._main[self._main._sequence[index]] = value + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main.values()) + + # FIXME: do we need to check if we are comparing with another ``Values`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main.values() < other + def __le__(self, other): return self._main.values() <= other + def __eq__(self, other): return self._main.values() == other + def __ne__(self, other): return self._main.values() != other + def __gt__(self, other): return self._main.values() > other + def __ge__(self, other): return self._main.values() >= other + def __cmp__(self, other): return cmp(self._main.values(), other) + + def __contains__(self, item): return item in self._main.values() + def __len__(self): return len(self._main._sequence) # easier :-) + def __iter__(self): return self._main.itervalues() + def count(self, item): return self._main.values().count(item) + def index(self, item, *args): return self._main.values().index(item, *args) + + def reverse(self): + """Reverse the values""" + vals = self._main.values() + vals.reverse() + # FIXME: efficiency + self[:] = vals + + def sort(self, *args, **kwds): + """Sort the values.""" + vals = self._main.values() + vals.sort(*args, **kwds) + self[:] = vals + + def __mul__(self, n): return self._main.values()*n + __rmul__ = __mul__ + def __add__(self, other): return self._main.values() + other + def __radd__(self, other): return other + self._main.values() + + ## following methods not implemented for values ## + def __delitem__(self, i): raise TypeError('Can\'t delete items from values') + def __iadd__(self, other): raise TypeError('Can\'t add in place to values') + def __imul__(self, n): raise TypeError('Can\'t multiply values in place') + def append(self, item): raise TypeError('Can\'t append items to values') + def insert(self, i, item): raise TypeError('Can\'t insert items into values') + def pop(self, i=-1): raise TypeError('Can\'t pop items from values') + def remove(self, item): raise TypeError('Can\'t remove items from values') + def extend(self, other): raise TypeError('Can\'t extend values') + +class SequenceOrderedDict(OrderedDict): + """ + Experimental version of OrderedDict that has a custom object for ``keys``, + ``values``, and ``items``. + + These are callable sequence objects that work as methods, or can be + manipulated directly as sequences. + + Test for ``keys``, ``items`` and ``values``. + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.keys + [1, 2, 3] + >>> d.keys() + [1, 2, 3] + >>> d.setkeys((3, 2, 1)) + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.setkeys((1, 2, 3)) + >>> d.keys[0] + 1 + >>> d.keys[:] + [1, 2, 3] + >>> d.keys[-1] + 3 + >>> d.keys[-2] + 2 + >>> d.keys[0:2] = [2, 1] + >>> d + SequenceOrderedDict([(2, 3), (1, 2), (3, 4)]) + >>> d.keys.reverse() + >>> d.keys + [3, 1, 2] + >>> d.keys = [1, 2, 3] + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.keys = [3, 1, 2] + >>> d + SequenceOrderedDict([(3, 4), (1, 2), (2, 3)]) + >>> a = SequenceOrderedDict() + >>> b = SequenceOrderedDict() + >>> a.keys == b.keys + 1 + >>> a['a'] = 3 + >>> a.keys == b.keys + 0 + >>> b['a'] = 3 + >>> a.keys == b.keys + 1 + >>> b['b'] = 3 + >>> a.keys == b.keys + 0 + >>> a.keys > b.keys + 0 + >>> a.keys < b.keys + 1 + >>> 'a' in a.keys + 1 + >>> len(b.keys) + 2 + >>> 'c' in d.keys + 0 + >>> 1 in d.keys + 1 + >>> [v for v in d.keys] + [3, 1, 2] + >>> d.keys.sort() + >>> d.keys + [1, 2, 3] + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)), strict=True) + >>> d.keys[::-1] = [1, 2, 3] + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.keys[:2] + [3, 2] + >>> d.keys[:2] = [1, 3] + Traceback (most recent call last): + KeyError: 'Keylist is not the same as current keylist.' + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.values + [2, 3, 4] + >>> d.values() + [2, 3, 4] + >>> d.setvalues((4, 3, 2)) + >>> d + SequenceOrderedDict([(1, 4), (2, 3), (3, 2)]) + >>> d.values[::-1] + [2, 3, 4] + >>> d.values[0] + 4 + >>> d.values[-2] + 3 + >>> del d.values[0] + Traceback (most recent call last): + TypeError: Can't delete items from values + >>> d.values[::2] = [2, 4] + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> 7 in d.values + 0 + >>> len(d.values) + 3 + >>> [val for val in d.values] + [2, 3, 4] + >>> d.values[-1] = 2 + >>> d.values.count(2) + 2 + >>> d.values.index(2) + 0 + >>> d.values[-1] = 7 + >>> d.values + [2, 3, 7] + >>> d.values.reverse() + >>> d.values + [7, 3, 2] + >>> d.values.sort() + >>> d.values + [2, 3, 7] + >>> d.values.append('anything') + Traceback (most recent call last): + TypeError: Can't append items to values + >>> d.values = (1, 2, 3) + >>> d + SequenceOrderedDict([(1, 1), (2, 2), (3, 3)]) + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.items() + [(1, 2), (2, 3), (3, 4)] + >>> d.setitems([(3, 4), (2 ,3), (1, 2)]) + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.items[0] + (3, 4) + >>> d.items[:-1] + [(3, 4), (2, 3)] + >>> d.items[1] = (6, 3) + >>> d.items + [(3, 4), (6, 3), (1, 2)] + >>> d.items[1:2] = [(9, 9)] + >>> d + SequenceOrderedDict([(3, 4), (9, 9), (1, 2)]) + >>> del d.items[1:2] + >>> d + SequenceOrderedDict([(3, 4), (1, 2)]) + >>> (3, 4) in d.items + 1 + >>> (4, 3) in d.items + 0 + >>> len(d.items) + 2 + >>> [v for v in d.items] + [(3, 4), (1, 2)] + >>> d.items.count((3, 4)) + 1 + >>> d.items.index((1, 2)) + 1 + >>> d.items.index((2, 1)) + Traceback (most recent call last): + ValueError: list.index(x): x not in list + >>> d.items.reverse() + >>> d.items + [(1, 2), (3, 4)] + >>> d.items.reverse() + >>> d.items.sort() + >>> d.items + [(1, 2), (3, 4)] + >>> d.items.append((5, 6)) + >>> d.items + [(1, 2), (3, 4), (5, 6)] + >>> d.items.insert(0, (0, 0)) + >>> d.items + [(0, 0), (1, 2), (3, 4), (5, 6)] + >>> d.items.insert(-1, (7, 8)) + >>> d.items + [(0, 0), (1, 2), (3, 4), (7, 8), (5, 6)] + >>> d.items.pop() + (5, 6) + >>> d.items + [(0, 0), (1, 2), (3, 4), (7, 8)] + >>> d.items.remove((1, 2)) + >>> d.items + [(0, 0), (3, 4), (7, 8)] + >>> d.items.extend([(1, 2), (5, 6)]) + >>> d.items + [(0, 0), (3, 4), (7, 8), (1, 2), (5, 6)] + """ + + def __init__(self, init_val=(), strict=True): + OrderedDict.__init__(self, init_val, strict=strict) + self._keys = self.keys + self._values = self.values + self._items = self.items + self.keys = Keys(self) + self.values = Values(self) + self.items = Items(self) + self._att_dict = { + 'keys': self.setkeys, + 'items': self.setitems, + 'values': self.setvalues, + } + + def __setattr__(self, name, value): + """Protect keys, items, and values.""" + if not '_att_dict' in self.__dict__: + object.__setattr__(self, name, value) + else: + try: + fun = self._att_dict[name] + except KeyError: + OrderedDict.__setattr__(self, name, value) + else: + fun(value) + +if __name__ == '__main__': + if INTP_VER < (2, 3): + raise RuntimeError("Tests require Python v.2.3 or later") + # turn off warnings for tests + warnings.filterwarnings('ignore') + # run the code tests in doctest format + import doctest + m = sys.modules.get('__main__') + globs = m.__dict__.copy() + globs.update({ + 'INTP_VER': INTP_VER, + }) + doctest.testmod(m, globs=globs) + diff --git a/test/test_config/test_files.py b/test/test_config/test_files.py new file mode 100644 index 0000000..7d32a07 --- /dev/null +++ b/test/test_config/test_files.py @@ -0,0 +1,135 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Dodai. If not, see . + +import sys +import os +import unittest +path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(path) +from ConfigParser import ConfigParser +from ConfigParser import MissingSectionHeaderError +from dodai.config.files import ConfigFiles +from dodai.tools.odict import OrderedDict +from dodai.tools.himo import String2Himo +from dodai.tools import Section +from dodai.exception import InvalidConfigParser +from dodai.exception import FileDoesNotExist +from dodai.exception import FileIsDirectory + + +class GoodParser(object): + + DATA = OrderedDict() + DATA['sec_a'] = OrderedDict() + DATA['sec_a']['opt_a'] = 'foo' + DATA['sec_a']['opt_b'] = 'bar' + DATA['sec_a']['opt_c'] = '1' + DATA['sec_a']['opt_d'] = '22' + + DATA['sec_b'] = OrderedDict() + DATA['sec_b']['opt_a'] = 'lt' + DATA['sec_b']['opt_b'] = 'el' + DATA['sec_b']['opt_c'] = 'mg' + DATA['sec_b']['opt_d'] = 'ds' + + def sections(self): + return self.DATA.keys() + + def options(self, section): + return self.DATA[section].keys() + + def get(self, section, option): + return self.DATA[section][option] + + def read(self, filename): + return [filename] + +class BadParser(object): + + DATA = OrderedDict() + DATA['sec_a'] = OrderedDict() + DATA['sec_a']['opt_a'] = 'foo' + DATA['sec_a']['opt_b'] = 'bar' + DATA['sec_a']['opt_c'] = '1' + DATA['sec_a']['opt_d'] = '22' + + DATA['sec_b'] = OrderedDict() + DATA['sec_b']['opt_a'] = 'lt' + DATA['sec_b']['opt_b'] = 'el' + DATA['sec_b']['opt_c'] = 'mg' + DATA['sec_b']['opt_d'] = 'ds' + + def sections(self): + return self.DATA.keys() + + def get(self, section, option): + return self.DATA[section][option] + + def read(self, filename): + return [filename] + +class TestFiles(unittest.TestCase): + + def setUp(self): + s2h = String2Himo() + self.obj = ConfigFiles(OrderedDict, Section, s2h) + + def test_good_parser(self): + self.obj.register_parser_object(GoodParser) + + def test_bad_parser(self): + self.failUnlessRaises(InvalidConfigParser, + self.obj.register_parser_object, + BadParser) + + def test_add(self): + filename = os.path.realpath(__file__) + self.obj.add(filename) + self.assertTrue(filename in self.obj.files) + + def test_add_bad_file(self): + filename = os.path.dirname(os.path.realpath(__file__)) + filename = os.path.join(filename, 'no_way_jose.234dasf2345') + self.failUnlessRaises(FileDoesNotExist, + self.obj.add, + filename) + + def test_add_directory(self): + filename = os.path.dirname(os.path.realpath(__file__)) + self.failUnlessRaises(FileIsDirectory, + self.obj.add, + filename) + + def file_test_read(self): + filename = os.path.realpath(__file__) + self.obj.register_parser_object(GoodParser) + self.obj.add(filename) + sec = self.obj.load() + + def test_bad_read(self): + self.obj.register_parser_object(ConfigParser) + filename = os.path.realpath(__file__) + self.obj.add(filename) + self.failUnlessRaises(MissingSectionHeaderError, self.obj.load) + + def test_good_read_with_multiple_parsers(self): + self.obj.register_parser_object(ConfigParser) + self.obj.register_parser_object(GoodParser) + filename = os.path.realpath(__file__) + self.obj.add(filename) + sec = self.obj.load() + diff --git a/test/test_config/test_sections.py b/test/test_config/test_sections.py index 39b41a7..2ca7e4f 100644 --- a/test/test_config/test_sections.py +++ b/test/test_config/test_sections.py @@ -17,101 +17,91 @@ import sys import os -import ConfigParser import unittest path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..')) sys.path.append(path) +from dodai.config.sections import ConfigSections from dodai.tools.himo import Himo from dodai.tools.himo import String2Himo -from dodai.config.sections import ConfigSections +from dodai.tools.odict import OrderedDict +from dodai.tools import Section + +class Parser(object): + + DATA = OrderedDict() + DATA['sec_a'] = OrderedDict() + DATA['sec_a']['opt_a'] = 'foo' + DATA['sec_a']['opt_b'] = 'bar' + DATA['sec_a']['opt_c'] = '1' + DATA['sec_a']['opt_d'] = '22' + + DATA['sec_b'] = OrderedDict() + DATA['sec_b']['opt_a'] = 'lt' + DATA['sec_b']['opt_b'] = 'el' + DATA['sec_b']['opt_c'] = 'mg' + DATA['sec_b']['opt_d'] = 'ds' + + def sections(self): + return self.DATA.keys() + + def options(self, section): + return self.DATA[section].keys() + + def get(self, section, option): + return self.DATA[section][option] class TestSections(unittest.TestCase): def setUp(self): - path = os.path.dirname(__file__) - filepath = os.path.join(path, 'config.cfg') - self.parser = ConfigParser.ConfigParser() - self.parser.readfp(open(filepath)) - self.sections = ConfigSections(String2Himo()) + s2h = String2Himo() + self.obj = ConfigSections(OrderedDict, Section, s2h) + self.parser = Parser() def test_call(self): - self.sections(self.parser) - count = len(self.parser.sections()) - self.assertEqual(count, len(self.sections)) - - def test_iterator(self): - result = True - for section in self.sections: - if not section.get_title() in self.parser.sections(): - result = False - self.assertTrue(result==True) - - def test_get_section(self): - self.sections(self.parser) - self.sections(self.parser) - check = self.sections.get_section('test_db') - self.assertEqual(check.get_title(), 'test_db') - - def test_build_string_object(self): - sections = ConfigSections() - sections(self.parser) - count = len(self.parser.sections()) - self.assertEqual(count, len(sections)) - - def test_getitem(self): - self.sections(self.parser) - title = self.sections['test_db'].get_title() - self.assertEqual(title, 'test_db') - - def test_getattr(self): - self.sections(self.parser) - title = self.sections.test_db.get_title() - self.assertEqual(title, 'test_db') - - def test_get_keys(self): - self.sections(self.parser) - keys = self.sections.keys() - self.assertTrue(len(keys)) - - def test_section_object_one(self): - self.sections(self.parser) - keys = self.sections.test_db.keys() - self.assertTrue(len(keys)) - - def test_section_object_two(self): - self.sections(self.parser) - keys = self.sections.test_db.___options___.keys() - self.assertTrue(len(keys)) - - def test_section_object_three(self): - self.sections(self.parser) - self.sections.test_db.___blah___ = 'test' - val = self.sections.test_db.__getattr__('___blah___') - self.assertTrue(val == 'test') - - def test_section_object_four(self): - self.sections(self.parser) - self.sections.test_db.foo = 'bar' - val = self.sections.test_db.__getattr__('foo') - self.assertTrue(val == 'bar') - - def test_section_object_five(self): - self.sections(self.parser) - keys = [] - for key in self.sections.test_db: - keys.append(key) - self.assertTrue(keys) - - def test_section_object_six(self): - self.sections(self.parser) - self.sections.test_db.foo = 'bar' - val = self.sections.test_db['foo'] - self.assertTrue(val == 'bar') - - def test_section_object_seven(self): - self.sections(self.parser, 'unicode_escape') - self.assertEqual(self.sections.extra.name, u'\u8c61') + self.obj(self.parser) + + def test_two(self): + self.obj(self.parser) + key = String2Himo('sec_a') + self.obj.__getitem__('sec_a') + self.obj.sec_a + + def test_results_one(self): + self.obj(self.parser) + for section in self.obj.keys(): + self.assertTrue(section in self.parser.sections()) + + def test_results_two(self): + self.obj(self.parser) + for section, data in self.obj.items(): + for key in data.keys(): + self.assertTrue(key in self.parser.options(section)) + + def test_results_three(self): + self.obj(self.parser) + for section, data in self.obj.items(): + for option, val in data.items(): + self.assertTrue(val, self.parser.get(section, option)) + + def test_results_four(self): + self.obj(self.parser) + for section in self.parser.sections(): + self.assertTrue(section in self.obj) + + def test_results_five(self): + self.obj(self.parser) + for section in self.parser.sections(): + for option in self.parser.options(section): + self.assertTrue(option in self.obj[section]) + + def test_results_six(self): + self.obj(self.parser) + for section in self.parser.sections(): + for option in self.parser.options(section): + val = self.parser.get(section, option) + self.assertEquals(val, self.obj[section][option]) + if __name__ == '__main__': diff --git a/test/test_dodai.py b/test/test_dodai.py index b95f71f..fbd4667 100644 --- a/test/test_dodai.py +++ b/test/test_dodai.py @@ -15,8 +15,11 @@ # You should have received a copy of the GNU General Public License # along with Dodai. If not, see . -import unittest +import sys import os +import unittest +path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.append(path) from dodai import Configure class TestConfigure(unittest.TestCase): @@ -29,11 +32,13 @@ class TestConfigure(unittest.TestCase): self.obj = Configure('test472j02ju-sfgj', config_files=[filename]) def test_results(self): - files = self.obj.files._files - print files + files = self.obj.files.files self.assertTrue(len(files) == 1) def test_add_files(self): self.obj._add_files(self.filename) - files = self.obj.files._files + files = self.obj.files.files self.assertTrue(len(files) == 1) + + + diff --git a/test/test_tools/test_init.py b/test/test_tools/test_init.py new file mode 100644 index 0000000..16d7dda --- /dev/null +++ b/test/test_tools/test_init.py @@ -0,0 +1,97 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Dodai. If not, see . + +import sys +import os +import unittest +path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(path) +from dodai.tools import home_directory +from dodai.tools import config_directory_system +from dodai.tools import config_directory_user +from dodai.tools import config_directory_project +from dodai.tools import config_directories +from dodai.tools import list_to_english +from dodai.tools import Section +from dodai.tools import normalize_unicode +from dodai.tools import quote_list + +class TestTools(unittest.TestCase): + + def setUp(self): + self.project_name = 'foo' + + def test_home_directory(self): + try: + from win32com.shell import shellcon, shell + except ImportError: + dir = os.path.expanduser("~") + else: + dir = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0) + self.assertEquals(dir, home_directory()) + + def test_config_directory_system(self): + config_directory_system() + + def test_config_directory_user(self): + project_dir = '.{0}'.format(self.project_name) + project_dir = os.path.join(home_directory(), project_dir) + data = config_directory_user(self.project_name) + self.assertEquals(data, project_dir) + + def test_config_directory_project(self): + config_directory_project() + + def test_config_directories(self): + config_directories(self.project_name) + + def test_list_to_english_one(self): + data = [1, 2, 3, 4, 5] + result = "1, 2, 3, 4 and 5" + self.assertEquals(result, list_to_english(data)) + + def test_list_to_english_two(self): + data = [1, 2] + result = "1 and 2" + self.assertEquals(result, list_to_english(data)) + + def test_section_one(self): + title = 'bar' + sec = Section(title) + self.assertEquals(title, sec.get_section_title()) + + def test_section_two(self): + title = 'bar' + sec = Section(title) + sec['el'] = 'hey' + self.assertEquals('hey', sec['el']) + + def test_normalize_unicode(self): + a = u'\u212b' + b = u'\xc5' + a = normalize_unicode(a) + self.assertEquals(a, b) + + def test_quote_list_one(self): + a = ['a', 'b', 'c'] + b = ['"a"', '"b"', '"c"'] + self.assertEquals(b, quote_list(a, True)) + + def test_quote_list_two(self): + a = ['a', 'b', 'c'] + b = ["'a'", "'b'", "'c'"] + self.assertEquals(b, quote_list(a)) -- cgit v1.2.3