aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorSix <unknown>2010-12-11 18:01:32 -0500
committerSix <unknown>2010-12-11 18:01:32 -0500
commit09a34696d529e1686970df5cf1555e160fd0c8f3 (patch)
tree30e3a8049e64a565505ee4a05bee9ec592a4bc89 /lib
parentbb5163b54d30bdced46e6f75ddc1676f8c5fef5d (diff)
downloaddodai-macsupport-09a34696d529e1686970df5cf1555e160fd0c8f3.tar.bz2
dodai-macsupport-09a34696d529e1686970df5cf1555e160fd0c8f3.tar.xz
dodai-macsupport-09a34696d529e1686970df5cf1555e160fd0c8f3.zip
reorg of files, added traditional setup.py and removed pavement files
Diffstat (limited to 'lib')
-rw-r--r--lib/dodai/__init__.py72
-rw-r--r--lib/dodai/config/__init__.py76
-rw-r--r--lib/dodai/config/databases/__init__.py285
-rw-r--r--lib/dodai/config/databases/sa.py90
-rw-r--r--lib/dodai/config/files.py106
-rw-r--r--lib/dodai/config/log.py150
-rw-r--r--lib/dodai/config/option.py61
-rw-r--r--lib/dodai/config/sections.py117
-rw-r--r--lib/dodai/db.py30
-rw-r--r--lib/dodai/exception.py289
-rw-r--r--lib/dodai/tools/__init__.py143
-rw-r--r--lib/dodai/tools/himo.py174
-rw-r--r--lib/dodai/tools/odict.py1399
13 files changed, 2992 insertions, 0 deletions
diff --git a/lib/dodai/__init__.py b/lib/dodai/__init__.py
new file mode 100644
index 0000000..55bb57d
--- /dev/null
+++ b/lib/dodai/__init__.py
@@ -0,0 +1,72 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18import os
19import sys
20from dodai.config import Config
21from dodai.tools import config_directories
22
23class Configure(Config):
24
25 CONFIG_FILES = ['config', 'configs', 'configure', 'connect',
26 'connections', 'connection', 'setup']
27 CONFIG_EXTENSIONS = ['cfg', 'txt', 'ini']
28
29
30 def __init__(self, project_name, config_files=None):
31 self.project_name = project_name
32 self._load_config_files()
33 self._add_files(config_files)
34 self._add_config_files_to_database_handler()
35
36 def _config_files(self):
37 # Returns a list of possible config file names
38 out = []
39 for name in self.CONFIG_FILES:
40 out.append(name)
41 for ext in self.CONFIG_EXTENSIONS:
42 name = "{0}.{1}".format(name, ext)
43 out.append(name)
44 return out
45
46 def _load_config_files(self):
47 # Adds any default config file if it exists
48 out = []
49 directories = config_directories(self.project_name)
50 for dir in directories:
51 if os.path.isdir(dir):
52 for name in self._config_files():
53 path = os.path.join(dir, name)
54 self._add_file(path)
55
56 def _add_files(self, filenames):
57 # Adds a list or tuple of filenames
58 if filenames:
59 if isinstance(filenames, list) or isinstance(filenames, tuple):
60 for filename in filenames:
61 self._add_file(filename)
62 else:
63 self._add_file(filenames)
64
65 def _add_file(self, filename):
66 # Adds the given filename to the dodai.config.files object
67 if os.path.isfile(filename):
68 self.files.add(filename)
69
70 def _add_config_files_to_database_handler(self):
71 sections = self.files.load()
72 self.databases.add(sections)
diff --git a/lib/dodai/config/__init__.py b/lib/dodai/config/__init__.py
new file mode 100644
index 0000000..e849f8e
--- /dev/null
+++ b/lib/dodai/config/__init__.py
@@ -0,0 +1,76 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19class Config(object):
20
21 def __call__(self):
22 obj = ConfigResults()
23 if hasattr(self, 'vars'):
24 for key, val in self.vars.items():
25 setattr(obj, key, val)
26 return obj
27
28 @property
29 def files(self):
30 if not hasattr(self, '_files'):
31 from dodai.config.files import ConfigFiles
32 from dodai.tools.odict import OrderedDict
33 from dodai.tools import Section
34 from dodai.tools.himo import String2Himo
35 from ConfigParser import ConfigParser
36 s2h = String2Himo()
37 self._files = ConfigFiles(OrderedDict, Section, s2h)
38 self._files.register_parser_object(ConfigParser)
39 return self._files
40
41 @property
42 def options(self):
43 if not hasattr(self, '_options'):
44 from dodai.config.option import ConfigOption
45 self._options = ConfigOption()
46 return self._options
47
48 @property
49 def logs(self):
50 if not hasattr(self, '_logs'):
51 from dodai.config.log import ConfigLog
52 self._logs = ConfigLog()
53 return self._logs
54
55 @property
56 def databases(self):
57 if not hasattr(self, '_databases'):
58 # Wire up the sqlalchemy objects to use in the sa object
59 # which will be used as the default database handler
60 from dodai.config.databases import ConfigDatabases
61 from dodai.config.databases.sa import Sa
62 from sqlalchemy.orm import sessionmaker
63 from sqlalchemy import create_engine
64 from dodai.db import Db
65 sa = Sa(create_engine, sessionmaker, Db)
66 self._databases = ConfigDatabases(sa, 'sa')
67 return self._databases
68
69 def set(self, key, val):
70 if not hasattr(self, 'vars'):
71 self.vars = {}
72 self.vars[key] = val
73
74
75class ConfigResults(object):
76 pass
diff --git a/lib/dodai/config/databases/__init__.py b/lib/dodai/config/databases/__init__.py
new file mode 100644
index 0000000..c91e77f
--- /dev/null
+++ b/lib/dodai/config/databases/__init__.py
@@ -0,0 +1,285 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19from dodai.exception import DatabaseEmptyOptionException
20from dodai.exception import DatabasePortException
21from dodai.exception import DatabaseHostnameException
22from dodai.exception import DatabaseProtocolException
23from dodai.exception import DatabaseConnectionException
24from dodai.exception import UnknownDatabaseConnectionException
25from dodai.tools import list_to_english
26
27class ConfigDatabases(object):
28 """An object that is used for creating database connection objects
29
30 """
31 def __init__(self, default_handler, default_name):
32 self.connections = {}
33 self._handlers = {}
34 DatabaseConnection.DEFAULT_HANDLER = default_name
35 self.add_handler(default_name, default_handler)
36
37 def add(self, sections):
38 """Adds a dictionary of sections to this object that could
39 be used for creating database connection objects. The dictionary
40 should be formatted as follows:
41
42 sections[name] = {option: value, option: value, ....}
43
44 The 'name' is the section name. It is an identifier that is used
45 to load the database:
46
47 The options and values can have the following:
48
49 protocol: The database engine (postgresql, mysql, sqlite,
50 oracle, mssql etc..)
51
52 hostname: The hostname or ip address of the database server
53
54 port: The port that should be used to connect to the
55 database server should be a number between 1
56 and 65535
57
58 username: The username used to connect to the database server
59
60 password: The password used to connect to the database server
61
62 database: The database to connect to once connected to the
63 server
64
65 schema: The schema that will be used. This option does
66 not do anything but will exist in the db object
67 for use
68
69 filename: The file path to the database file (sqlite)
70
71 protocol_extra: Used to tell the handler which python db engine
72 to use. For example one might want to tell
73 the sqlalchemy handler to use the psycopg2
74 python object.
75
76 handler: The handler used to build the orm object. Dodai
77 only works with sqlalchemy so the default 'sa'
78 is used.
79
80 """
81 for name, section in sections.items():
82 self.connections[name] = DatabaseConnection(section, name,
83 self._handlers)
84
85 def load(self, section_name):
86 """Returns a database connection object of the given section_name.
87 Throws Exceptions for any type of configuration errors or missing
88 configuration data
89
90 """
91 if section_name in self.connections:
92 return self.connections[section_name].load()
93 else:
94 raise UnknownDatabaseConnectionException(section_name)
95
96 def add_handler(self, name, handler):
97 """Adds the given handler and name to this objects handlers
98
99 """
100 self._handlers[name] = handler
101
102class DatabaseConnection(object):
103
104 DEFAULT_HANDLER = None
105
106 def __init__(self, section, name, handlers):
107 self.section = section
108 self.name = name
109 self.handlers = handlers
110 self.validate = DatabaseConnectionValidator.load(section, name)
111 self.obj = None
112
113 def load(self):
114 if not self.obj:
115 self.validate()
116 handler = self._get_handler()
117 self.obj = handler(self.section, self.name)
118 return self.obj
119
120 def _get_handler(self):
121 if self.section.has_key('handler'):
122 name = self.section['handler']
123 if name in self.handlers:
124 return self.handlers[name]
125 return self.handlers[self.DEFAULT_HANDLER]
126
127
128class DatabaseConnectionValidator(object):
129
130 def __init__(self, section, name, validators=[]):
131 self.section = section
132 self.name = name
133 self.validators = validators
134
135 def __call__(self):
136 return self.validate()
137
138 def validate(self):
139 """Validates the connection. Returns true if valid. Throws
140 DodaiDatabaseConnectionConfigurationError on any errors
141
142 """
143 raise NotImplementedError()
144
145 def _validate_option(self, name):
146 try:
147 value = self.section[name]
148 except KeyError:
149 value = None
150
151 if not value:
152 raise DatabaseEmptyOptionException(self.name, name)
153 return True
154
155 def _validate_protocol(self):
156
157 self._validate_option('protocol')
158
159 if self.section['protocol'] not in self.PROTOCOLS:
160 raise DatabaseProtocolException(self.name,
161 self.section['protocol'], self.DB_TYPE,
162 self.PROTOCOLS)
163 return True
164
165 @staticmethod
166 def load(section, name):
167 """Static method (factory) that loads the correct database
168 validation class.
169
170 Attributes:
171 section: Dictionary of key val connection information
172 name: String name of the section
173
174 """
175 action = None
176 validators = [DatabaseConnectionValidatorServer,
177 DatabaseConnectionValidatorFile]
178
179 for validator in validators:
180 obj = validator.load(section, name)
181 if obj:
182 return obj
183
184 return DatabaseConnectionValidatorUnknown(section, name, validators)
185
186class DatabaseConnectionValidatorServer(DatabaseConnectionValidator):
187
188 DB_TYPE = 'server'
189 REQUIRED = ['protocol', 'hostname', 'port', 'username', 'password',
190 'database']
191 PROTOCOLS = ['postgresql', 'mysql', 'mssql', 'oracle']
192
193 def _validate_port(self):
194 self._validate_option('port')
195
196 try:
197 port = int(self.section['port'])
198 except ValueError:
199 port = self.section['port']
200 else:
201 if port >=1 and port <=65535:
202 return True
203 raise DatabasePortException(self.name, port)
204
205 def validate(self):
206 """Validates the connection. Returns true if valid. Throws
207 DodaiDatabaseConnectionConfigurationError on any errors
208
209 """
210 self._validate_protocol()
211 self._validate_port()
212 self._validate_option('hostname')
213 self._validate_option('username')
214 self._validate_option('password')
215 self._validate_option('database')
216 return True
217
218 @classmethod
219 def load(cls, section, name):
220 """Return this validation class if it is possible that the
221 given connection information contains enough data to make
222 a database server connection.
223
224 Attributes:
225 section: Dictionary of key val connection information
226 name: String name of the section
227
228 """
229 if section.has_key('protocol'):
230 if section['protocol'] in cls.PROTOCOLS:
231 return cls(section, name)
232 keys = section.keys()
233 for key in keys:
234 if key in ['hostname', 'port']:
235 return cls(section, name)
236
237
238class DatabaseConnectionValidatorFile(DatabaseConnectionValidator):
239
240 DB_TYPE = 'file'
241 REQUIRED = ['protocol', 'filename']
242 PROTOCOLS = ['sqlite']
243
244 def validate(self):
245 """Validates the connection. Returns true if valid. Throws
246 DodaiDatabaseConnectionConfigurationError on any errors
247
248 """
249 self._validate_protocol()
250 self._validate_option('filename')
251 return True
252
253 @classmethod
254 def load(cls, section, name):
255 """Return this validation class if it is possible that the
256 given connection information contains enough data to make
257 a database file connection.
258
259 Attributes:
260 section: Dictionary of key val connection information
261 name: String name of the section
262
263 """
264 if section.has_key('protocol'):
265 if section['protocol'] in cls.PROTOCOLS:
266 return cls(section, name)
267 keys = section.keys()
268 for key in keys:
269 if key in ['filename']:
270 return cls(section, name)
271
272
273class DatabaseConnectionValidatorUnknown(DatabaseConnectionValidator):
274
275 DB_TYPE = 'unkonwn'
276 REQUIRED = []
277 PROTOCOLS = []
278
279 def validate(self):
280 """Validates the connection. Returns true if valid. Throws
281 DodaiDatabaseConnectionConfigurationError on any errors
282
283 """
284
285 raise DatabaseConnectionException(self.name, self.validators)
diff --git a/lib/dodai/config/databases/sa.py b/lib/dodai/config/databases/sa.py
new file mode 100644
index 0000000..eaf35ea
--- /dev/null
+++ b/lib/dodai/config/databases/sa.py
@@ -0,0 +1,90 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18class Sa(object):
19 """Callable object that will wire up sqlalchemy engine and
20 session objects
21
22 Attributes:
23 create_engine: The sqlalchemy create_engine object
24 session_maker: The sqlalchemy session_maker object
25 result_object: The object that will be populated with
26 all the connection information along with
27 the sqlalchemy objects
28
29 """
30 def __init__(self, create_engine, session_maker, result_object):
31 self._create_engine = create_engine
32 self._session_maker = session_maker
33 self._result_object = result_object
34
35 def __call__(self, section, name):
36 db = self._result_object()
37 db.name = name
38 db.protocol = section['protocol']
39
40 if section.has_key('protocol_extra') and section['protocol_extra']:
41 db.protocol_extra = section['protocol_extra']
42
43 if section.has_key('filename') and section['filename']:
44 db.filename = section['filename']
45
46 if section.has_key('port') and section['port']:
47 db.port = int(section['port'])
48
49 if section.has_key('database') and section['database']:
50 db.database = section['database']
51
52 if section.has_key('schema') and section['schema']:
53 db.schema = section['schema']
54
55 if section.has_key('username') and section['username']:
56 db.username = section['username']
57
58 if section.has_key('hostname') and section['hostname']:
59 db.hostname = section['hostname']
60
61 db.engine = self._build_engine(section)
62 db.session = self._build_session(db.engine)
63 return db
64
65 def _build_session(self, engine):
66 session = self._session_maker(bind=engine)
67 return session()
68
69 def _build_connection_string(self, section):
70 out = []
71 out.append('{section[protocol]}')
72 if section.has_key('protocol_extra') and section['protocol_extra']:
73 out.append('+{section[protocol_extra]}')
74 out.append('://')
75 if section.has_key('filename') and section['filename']:
76 out.append('/{section[filename]}')
77 else:
78 out.append('{section[username]}:{section[password]}@')
79 out.append('{section[hostname]}')
80 if section.has_key('port') and section['port']:
81 out.append(':{section[port]}')
82 out.append('/{section[database]}')
83 out = ''.join(out)
84 out = out.format(section=section)
85 return out
86
87 def _build_engine(self, section):
88 connection_string = self._build_connection_string(section)
89 db_obj = self._create_engine(connection_string)
90 return db_obj
diff --git a/lib/dodai/config/files.py b/lib/dodai/config/files.py
new file mode 100644
index 0000000..a27e4a2
--- /dev/null
+++ b/lib/dodai/config/files.py
@@ -0,0 +1,106 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18import os
19import ConfigParser
20from dodai.config.sections import ConfigSections
21from dodai.exception import InvalidConfigParser
22from dodai.exception import FileDoesNotExist
23from dodai.exception import FileIsDirectory
24
25class ConfigFiles(object):
26
27 REQUIRED_METHODS = ['read', 'sections', 'options', 'get']
28
29 def __init__(self, ordered_dict_object, section_object, string_object):
30
31 self._ordered_dict_object = ordered_dict_object
32 self._section_object = section_object
33 self._string_object = string_object
34 self.parser_objects = []
35 self.files = self._ordered_dict_object()
36 self.files_read = []
37
38 def register_parser_object(self, parser_object):
39 """Registers a config file parser with this object. Raises
40 InvalidConfigParser error if the parser does not have the 'read',
41 'sections', 'options' and 'get' methods.
42
43 """
44 if self.check_parser_object(parser_object):
45 self.parser_objects.append(parser_object)
46
47 def check_parser_object(self, parser_object):
48 """Checks the given parser object to insure it has all of the required
49 methods needed to parse files.
50
51 """
52 for name in self.REQUIRED_METHODS:
53 if not hasattr(parser_object, name):
54 raise InvalidConfigParser(self.REQUIRED_METHODS,
55 parser_object.__name__)
56 return True
57
58 def add(self, filename, encoding=None):
59 """Adds the given filename to this object to be parsed.
60
61 """
62 if os.path.exists(filename):
63 if os.path.isdir(filename):
64 raise FileIsDirectory(filename)
65 else:
66 filename = os.path.realpath(filename)
67 self.files[filename] = encoding
68 else:
69 raise FileDoesNotExist(filename)
70
71 def _read(self):
72 self.files_read = []
73 out = self._ordered_dict_object()
74 for filename, encoding in self.files.items():
75 error = False
76 for parser_obj in self.parser_objects:
77 try:
78 parser = parser_obj()
79 parser.read(filename)
80 except Exception, e:
81 error = True
82 else:
83 out[filename] = self._ordered_dict_object()
84 out[filename]['encoding'] = encoding
85 out[filename]['parser'] = parser
86 self.files_read.append(filename)
87 error = False
88 break
89 if error:
90 raise e
91 return out
92
93
94 def load(self):
95 """Returns a ConfigSections object, which acts like a dictionary,
96 that contains all parsed data.
97
98 """
99 files = self._read()
100 sections = ConfigSections(self._ordered_dict_object,
101 self._section_object, self._string_object)
102 for filename, data in files.items():
103 encoding = data['encoding']
104 parser = data['parser']
105 sections(parser, encoding)
106 return sections
diff --git a/lib/dodai/config/log.py b/lib/dodai/config/log.py
new file mode 100644
index 0000000..fdb5c93
--- /dev/null
+++ b/lib/dodai/config/log.py
@@ -0,0 +1,150 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19import logging
20import logging.handlers
21import os
22
23class ConfigLog(object):
24
25 LEVELS = {
26 logging.CRITICAL: [
27 'critical',
28 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
29 "%(message)s"],
30 logging.ERROR: [
31 'error',
32 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
33 "%(message)s"],
34 logging.WARNING: [
35 'warning',
36 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
37 "%(message)s"],
38 logging.INFO: [
39 'info',
40 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
41 "%(message)s"],
42 logging.DEBUG: [
43 'debug',
44 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
45 "%(message)s"]
46 }
47
48 MAX_BYTES = 10485760
49 BACKUP_COUNT = 5
50 FILE_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
51 STDOUT_FORMAT = "%(message)s"
52
53 def __init__(self):
54 self.log_level = logging.CRITICAL
55 self.directory = None
56 self._levels = {}
57
58 def set_log_level(self, level):
59 try:
60 level = self._fetch_log_level(level)
61 except InvalidLevelException:
62 pass
63 else:
64 self.log_level = level
65
66 def set_directory(self, directory):
67 if os.path.isdir(directory):
68 self.directory = directory
69 else:
70 raise NoDirectoryExistException(directory)
71
72 def get_file_message_format(self, level):
73 if not self._levels:
74 self._levels = self.LEVELS
75 level = self._fetch_log_level(level)
76 return self._levels[level][1]
77
78 def get_screen_message_format(self, level):
79 if not self._levels:
80 self._levels = self.LEVELS
81 level = self._fetch_log_level(level)
82 return self._levels[level][2]
83
84 def _fetch_log_level(self, level):
85 out = None
86 if isinstance(level, str):
87 level = level.strip().lower()
88 if level in self.LEVELS:
89 out = level
90 else:
91 for key, items in self.LEVELS.items():
92 if level == items[0]:
93 out = key
94 if not out:
95 raise InvalidLevelException(level)
96 else:
97 return out
98
99 def _build_filepath(self, data):
100 data = os.path.normpath(data)
101 if data.startswith(os.path.sep):
102 dir = os.path.dirname(data)
103 if not os.path.isdir(dir):
104 raise NoDirectoryExistException(dir)
105 else:
106 if not self.directory:
107 raise DirectoryNotSetException()
108 else:
109 data = os.path.join(self.directory, data)
110 return data
111
112 def load(self, name):
113 log =logging.getLogger(name)
114 log.setLevel(self.log_level)
115 return log
116
117 def attach_file_handler(self, log, filename):
118 filepath = self._build_filepath(filename)
119 handler = logging.handlers.RotatingFileHandler(
120 filepath, maxBytes = self.MAX_BYTES,
121 backupCount=self.BACKUP_COUNT)
122 file_format = self.get_file_message_format(self.log_level)
123 format_obj = logging.Formatter(file_format)
124 handler.setFormatter(format_obj)
125 handler.setLevel(self.log_level)
126 log.addHandler(handler)
127
128 def attach_screen_handler(self, log, level=None):
129 if level:
130 level = self._fetch_log_level(level)
131 else:
132 level = self.log_level
133 message_format = self.get_screen_message_format(level)
134 handler = logging.StreamHandler()
135 handler.setLevel(level)
136 format_obj = logging.Formatter(message_format)
137 handler.setFormatter(format_obj)
138 log.addHandler(handler)
139
140
141class NoDirectoryExistException(Exception):
142 pass
143
144
145class DirectoryNotSetException(Exception):
146 pass
147
148
149class InvalidLevelException(Exception):
150 pass
diff --git a/lib/dodai/config/option.py b/lib/dodai/config/option.py
new file mode 100644
index 0000000..0561881
--- /dev/null
+++ b/lib/dodai/config/option.py
@@ -0,0 +1,61 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19from optparse import OptionParser
20
21class ConfigOption(object):
22
23 def __init__(self):
24
25 self.parser = OptionParser()
26 self._options = None
27 self._args = []
28
29 def get_args(self):
30 self._parse_args()
31 return self._args
32
33 def get_options(self):
34 self._parse_args()
35 return self._options
36
37 def _parse_args(self):
38 options, args = self.parser.parse_args()
39 self._options = options
40 self._args = args
41
42 def add_quiet(self):
43
44 self.parser.add_option("-q", "--quiet", dest="verbose", default=True,
45 action="store_false",
46 help="Don't print status messages to the screen")
47
48 def add_verbose(self):
49 self.parser.add_option("-v", "--verbose", dest="verbose",
50 action="store_true",
51 default=False, help="Print status messages to the screen")
52
53 def add_log_level(self, default='critical'):
54 self.parser.add_option("-l", "--log-level", dest="log_level",
55 default=default, help="Sets the log level")
56
57 def add_setup(self):
58 self.parser.add_option('', "--setup", dest="setup",
59 action="store_true", default=False,
60 help="run the setup which builds the config "\
61 "files.")
diff --git a/lib/dodai/config/sections.py b/lib/dodai/config/sections.py
new file mode 100644
index 0000000..d789a24
--- /dev/null
+++ b/lib/dodai/config/sections.py
@@ -0,0 +1,117 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19class ConfigSections(object):
20 """
21 An iterable object that contains ConfigSection objects
22
23 """
24
25 def __init__(self, ordered_dict_object, section_object,
26 string_object = None,):
27 """
28 Iterable object that handles the conversion of a config
29 parser object to a list of section objects.
30
31
32 string_object: This is an object (non instantiated or
33 callable) that the results of the config's
34 sections, and options will be stored in.
35 This enables you to store your values as a
36 custom object. A good object to use is the
37 dodai.tools.himo Himo object. If the
38 string_object is not given the default python
39 str() object will be used. The callable of
40 this object must also allow for encoding
41 to be passed in string_object(data, 'UTF-8')
42
43
44 """
45 self._string_object = string_object or ''
46 self._ordered_dict_object = ordered_dict_object
47 self._section_object = section_object
48 self._sections = self._ordered_dict_object()
49
50 def __call__(self, parser, encoding=None):
51 """
52 Parses the given parser object into this object's sections.
53
54 parser: The actual parser object that is used to
55 get the sections. This object must have
56 the sections(), options() and get()
57 methods. A good object to use is the native
58 ConfigParse object. However, you can create
59 your own
60
61 """
62 self._build_sections(parser, encoding)
63
64 def _build_sections(self, parser, encoding):
65 # Builds ConfigSection objects from the parser object
66
67 for section_name in parser.sections():
68 section = self.get_section(section_name, encoding)
69 self._build_options(parser, section_name, section, encoding)
70
71 def _build_options(self, parser, section_name, section, encoding):
72 # Adds the options to the section object
73
74 for key in parser.options(section_name):
75 value = self._build_string_object(parser.get(section_name, key),
76 encoding)
77 key = self._build_string_object(key, encoding)
78 section[key] = value
79
80 def _build_string_object(self, data, encoding=None):
81 if self._string_object:
82 return self._string_object(data, encoding)
83 else:
84 return data
85
86 def get_section(self, section_name, encoding=None):
87 """
88 Returns a Section (aka dict) object from this object's section
89 dictionary or creates a new ConfigSection object, which is
90 stored int this object's section dictionary then is returned
91
92 """
93 section_name = self._build_string_object(section_name, encoding)
94 if section_name in self._sections:
95 return self._sections[section_name]
96 else:
97 section = self._section_object(section_name)
98 self._sections[section_name] = section
99 return section
100
101 def __getitem__(self, key):
102 return self._sections[key]
103
104 def __getattr__(self, key):
105 try:
106 out = self._sections[key]
107 except KeyError:
108 return getattr(self._sections, key)
109 else:
110 return out
111
112 def __iter__(self, *args, **kargs):
113 return self._sections.__iter__(*args, **kargs)
114
115
116 def __len__(self):
117 return len(self._sections)
diff --git a/lib/dodai/db.py b/lib/dodai/db.py
new file mode 100644
index 0000000..5cbe605
--- /dev/null
+++ b/lib/dodai/db.py
@@ -0,0 +1,30 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19class Db(object):
20 pass
21
22 def __init__(self):
23 self.name = None
24 self.protocol = None
25 self.hostname = None
26 self.port = None
27 self.database = None
28 self.filename = None
29 self.engine = None
30 self.session = None
diff --git a/lib/dodai/exception.py b/lib/dodai/exception.py
new file mode 100644
index 0000000..23302c7
--- /dev/null
+++ b/lib/dodai/exception.py
@@ -0,0 +1,289 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18from dodai.tools import list_to_english
19from dodai.tools import quote_list
20
21class DodaiException(Exception):
22
23 def _system_encoding(self):
24 # Returns the character encoding of the operating system
25
26 encoding = sys.getdefaultencoding()
27 filesystem_encoding = sys.getfilesystemencoding()
28 if filesystem_encoding:
29 encoding = filesystem_encoding
30 return encoding
31
32
33class HimoAsciiError(DodaiException):
34 """Exception raised when the Himo object can't convert a character
35 down to it's root character.
36
37 Attributes:
38 char: The character that can't be converted down to ascii
39 """
40 MESSAGE="Unable to convert the '{char}' character to ascii"
41
42 def __init__(self, char):
43 self.char = char
44 self.msg = self._build_message()
45
46 def _build_message(self):
47 encoding = self._system_encoding()
48 try:
49 char = self.char.encode(encoding)
50 except UnicodeEncodeError:
51 char = 'unichr({0})'.format(ord(self.char))
52 return self.MESSAGE.format(char=self.char)
53
54 def __str__(self):
55 return self.msg
56
57class DodaiDatabaseConnectionConfigurationError(DodaiException):
58 pass
59
60
61class DatabaseEmptyOptionException(DodaiDatabaseConnectionConfigurationError):
62 """Exception raised for an empty option in the config
63
64 Attributes:
65 section_name: The section of the config file that contains
66 the invalid protocol
67 option_name: The name of the empty option
68
69 """
70 MESSAGE = "In the '{section_name}' section, the '{option_name}' "\
71 "was not set or is missing. Please set this option."
72
73 def __init__(self, section_name, option_name):
74 self.section_name = section_name
75 self.option_name = option_name
76 self.msg = self._build_message()
77
78 def _build_message(self):
79 return self.MESSAGE.format(section_name=self.section_name,
80 option_name=self.option_name)
81
82 def __str__(self):
83 return self.msg
84
85
86class DatabasePortException(DodaiDatabaseConnectionConfigurationError):
87 """Exception raised for invalid database port connection
88
89 Attributes:
90 section_name: The section of the config file that contains
91 the invalid protocol
92 section_port: The port value that was listed in the
93 config file
94
95 """
96 MESSAGE = "In the '{section_name}' section, the port of "\
97 "'{section_port}' is invalid. The port must be a "\
98 "number between 1 and 65535"
99
100 def __init__(self, section_name, section_port):
101 self.section_name = section_name
102 self.section_port = section_port
103 self.msg = self._build_message()
104
105 def _build_message(self):
106 return self.MESSAGE.format(section_name=self.section_name,
107 section_port=self.section_port)
108
109 def __str__(self):
110 return self.msg
111
112
113class DatabaseHostnameException(DodaiDatabaseConnectionConfigurationError):
114 """Exception raised for invalid database hostname
115
116 Attributes:
117 section_name: The section of the config file that contains
118 the invalid protocol
119 section_hostname: The hostname value that was listed in the
120 config file
121
122 """
123 MESSAGE = "In the '{section_name}' section, the hostname of "\
124 "'{section_hostname}' is invalid. Please use a valid "\
125 "hostname."
126
127 MSG_NON = "In the '{section_name}' section, the hostname was "\
128 "not set. Please set the hostname."
129
130 def __init__(self, section_name, section_hostname):
131 self.section_name = section_name
132 self.section_hostname = section_hostname
133 self.msg = self._build_message()
134
135 def _build_message(self):
136 if self.section_hostname:
137 return self.MESSAGE.format(section_name=self.section_name,
138 section_hostname=self.section_hostname)
139 else:
140 return self.MSG_NON.format(section_name=self.section_name)
141
142 def __str__(self):
143 return self.msg
144
145
146class DatabaseProtocolException(DodaiDatabaseConnectionConfigurationError):
147 """Exception raised for invalid database connection protocols
148
149 Attributes:
150 section_name: The section of the config file that contains
151 the invalid protocol
152 section_protocol: The protocol value that was listed in the
153 config file
154 database_type: Usually 'server' or 'file'
155 protocols: List of valid protocols
156
157 """
158 MESSAGE = "In the '{section_name}' section, the protocol of "\
159 "'{section_protocol}' is invalid. The valid protocols "\
160 "for a '{database_type}' connection are: {protocols}"
161
162 def __init__(self, section_name, section_protocol, database_type,
163 protocols):
164 self.section_name = section_name
165 self.section_protocol = section_protocol
166 self.database_type = database_type
167 self.protocols = protocols
168 self.msg = self._build_message()
169
170 def _build_message(self):
171 protocols = list_to_english(self.protocols)
172 return self.MESSAGE.format(section_name=self.section_name,
173 section_protocol=self.section_protocol,
174 database_type=self.database_type,
175 protocols=protocols)
176
177 def __str__(self):
178 return self.msg
179
180
181class DatabaseConnectionException(DodaiDatabaseConnectionConfigurationError):
182 """Exception raised for missing database connection parameters
183
184 Attributes:
185 section_name: The section of the config file that contains
186 the invalid connection information
187 options_required: A dictionary containing the database_type
188 as the key and a list of required options
189 as the value
190
191 """
192 MESSAGE = "The '{section_name}' section does not contain all of the "\
193 "correct information needed to make a database connection."
194 MSG_TYPE = "To make a '{database_type}' connection please make sure "\
195 "To have all of the following options: {options}."
196 MSG_END = "Please remember that the option names are case sensitive."
197
198 def __init__(self, section_name, validators):
199 self.section_name = section_name
200 self.validators = validators
201 self.msg = self._build_message()
202
203 def _build_message(self):
204 out = []
205 out.append(self.MESSAGE.format(section_name=self.section_name))
206 for validator in self.validators:
207 options = list_to_english(validator.REQUIRED)
208 out.append(self.MSG_TYPE.format(database_type=validator.DB_TYPE,
209 options=options))
210 out.append(self.MSG_END)
211 return ' '.join(out)
212
213 def __str__(self):
214 return self.msg
215
216
217class UnknownDatabaseConnectionException(
218 DodaiDatabaseConnectionConfigurationError):
219 """Exception raised for missing database connection parameters
220
221 Attributes:
222 section: The requested section of the config file that can not
223 be found.
224
225 """
226 MESSAGE = "Unable to find the '{section}' section to create a "\
227 "database connection."
228
229 def __init__(self, section):
230 self.section = section
231 self.msg = self._build_message()
232
233 def _build_message(self):
234 return self.MESSAGE.format(section=self.section)
235
236 def __str__(self):
237 return self.msg
238
239
240class InvalidConfigParser(DodaiException):
241 """Exception raised when an invalid parser is registered in the
242 dodai.config.ConfigFiles object.
243
244 """
245 MESSAGE = "The parser object '{name}' that you were trying to register "\
246 "is not a valid parser object. Please make sure that this "\
247 "object contains all of the following methods: {methods}"
248
249 def __init__(self, required_methods, name):
250 self.msg = self._build_message(required_methods, name)
251
252 def _build_message(self, required_methods, name):
253 methods = quote_list(required_methods)
254 return self.MESSAGE.format(methods=methods, name=name)
255
256 def __str__(self):
257 return self.msg
258
259
260class FileDoesNotExist(DodaiException):
261 """Exception raised when a file does not exist.
262
263 """
264 MESSAGE = "The file: '{file_}' does not exist."
265
266 def __init__(self, filepath):
267 self.msg = self._build_message(filepath)
268
269 def _build_message(self, filepath):
270 return self.MESSAGE.format(file_=filepath)
271
272 def __str__(self):
273 return self.msg
274
275
276class FileIsDirectory(DodaiException):
277 """Exception raised when a file is a directory.
278
279 """
280 MESSAGE = "The file: '{file_}' is a directory."
281
282 def __init__(self, filepath):
283 self.msg = self._build_message(filepath)
284
285 def _build_message(self, filepath):
286 return self.MESSAGE.format(file_=filepath)
287
288 def __str__(self):
289 return self.msg
diff --git a/lib/dodai/tools/__init__.py b/lib/dodai/tools/__init__.py
new file mode 100644
index 0000000..bcef547
--- /dev/null
+++ b/lib/dodai/tools/__init__.py
@@ -0,0 +1,143 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18import sys
19import os
20import platform
21from dodai.tools.odict import OrderedDict
22import unicodedata
23
24def home_directory():
25 """Returns the full real path to the home directory of the user who
26 is executing this script.
27
28 """
29 out = None
30 try:
31 from win32com.shell import shellcon, shell
32 out = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0)
33 except ImportError:
34 out = os.path.expanduser("~")
35 return os.path.realpath(out)
36
37def config_directory_system(project_name=None):
38 """Returns the system config directory with the passed in project
39 appended to the path. Returns None for windows and java systems.
40
41 """
42 path = None
43 if platform.system and platform.system() not in ['Windows', 'Java']:
44 if project_name:
45 path = os.path.join('/etc', project_name.strip())
46 else:
47 path = '/etc'
48 return path
49
50def config_directory_user(project_name):
51 """Returns the config direcotry of the project which is located in
52 the user's home directory. Returns None for Java and unknown systems
53
54 """
55 path = None
56 project = '.{0}'.format(project_name.strip())
57 if platform.system and platform.system() not in ['Java']:
58 home_dir = home_directory()
59 path = os.path.join(home_dir, project)
60 return path
61
62def config_directory_project():
63 """Returns the config directory that is located in the same directory
64 of the executable that ran this script
65
66 """
67 path = os.path.dirname(os.path.abspath(sys.argv[0]))
68 return os.path.join(path, 'config')
69
70def config_directories(project_name):
71 """Returns a list of possible project directories
72
73 """
74 dirs = []
75 dir = config_directory_system(project_name)
76 if dir:
77 dirs.append(dir)
78 dir = config_directory_user(project_name)
79 if dir:
80 dirs.append(dir)
81 dir = config_directory_project()
82 if dir:
83 dirs.append(dir)
84 return dirs
85
86def list_to_english(data):
87 """Takes the input list and creates a string with each option
88 encased in single quotes and seperated by a comma with exception
89 of the last option which is prefixed with the word 'and'
90
91 """
92 if data:
93 if len(data) > 1:
94 out = []
95 last = "{0}".format(data.pop())
96 for row in data:
97 out.append("{0}".format(row))
98 out = ', '.join(out)
99 return "{0} and {1}".format(out, last)
100 else:
101 return "{0}".format(data.pop())
102
103def quote_list(data, double_quotes=False):
104 """Takes the give list (data) and adds quotes around each item. Returns
105 a list
106
107 """
108 out = []
109 if double_quotes:
110 quote = '"'
111 else:
112 quote = "'"
113 for item in data:
114 item = "{quote}{item}{quote}".format(quote=quote, item=item)
115 out.append(item)
116 return out
117
118def normalize_unicode(data):
119 """Normalizes the unicode data so that compare functions will work
120 correctly.
121
122 """
123 data = unicode(data)
124 data = unicodedata.normalize('NFC', data)
125 return data
126
127class Section(OrderedDict):
128 """An ordered dictionary object that has the added benfit of holding
129 a name or title of this grouping.
130
131 """
132 def __init__(self, title):
133 self.set_section_title(title)
134 super(Section,self).__init__()
135
136 def get_section_title(self):
137 return self.___title___
138
139 def set_section_title(self, val):
140 self.___title___ = val
141
142 def __repr__(self):
143 return "<Section('{0}')>".format(self.get_section_title())
diff --git a/lib/dodai/tools/himo.py b/lib/dodai/tools/himo.py
new file mode 100644
index 0000000..f56eaf8
--- /dev/null
+++ b/lib/dodai/tools/himo.py
@@ -0,0 +1,174 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18import chardet
19import re
20import sys
21import unicodedata
22from htmlentitydefs import name2codepoint
23from htmlentitydefs import codepoint2name
24from decimal import Decimal as D
25from dodai.exception import HimoAsciiError
26from dodai.tools import normalize_unicode
27
28class String2Himo(object):
29 """
30 This in an object that is used for converting python string objects
31 into Himo (unicode) objects.
32
33 """
34
35 def __init__(self, default_encoding=None):
36 """
37 default_encoding is the encoding value to be used as the
38 classwide default. If default encoding is not set then
39 the encoding of the system will be used.
40
41 """
42 self.default_encoding = default_encoding or self._system_encoding()
43 self._expression = re.compile(r'&(#?)(x?)(\w+);')
44
45 def __call__(self, data, encoding=None):
46 """
47 Converts the input (data) string object into a Himo object
48 using the passed in (encoding). If encoding is omitted then
49 the default_encoding will be used.
50
51 returns a Himo object
52
53 """
54 encoding = encoding or self.default_encoding
55 data = self._as_unicode(data, encoding)
56 data = self._decode_html(data)
57 data = normalize_unicode(data)
58 return Himo(data)
59
60 def _as_unicode(self, data, encoding):
61 # Returns string as a unicode string.
62
63 if not isinstance(data, unicode):
64 if not isinstance(data, str):
65 data = str(data)
66 try:
67 data = data.decode(encoding)
68 except UnicodeDecodeError:
69 info = chardet.detect(data)
70 data = data.decode(info['encoding'])
71 return unicodedata.normalize('NFC', data)
72
73 def _decode_html(self, data):
74 # Returns a unicode string. If data contains any html encoded
75 # characters, the characters will be converted to their unicode
76 # equivalent
77
78 return unicode(self._expression.subn(self._html_decode, data)[0])
79
80 def _html_decode(self, values):
81 # Returns the unicode character from the re.subn
82
83 value = values.group(3)
84 if values.group(1):
85 if values.group(2):
86 return unichr(int('0x{0}'.format(value), 16))
87 else:
88 return unichr(int(value))
89 else:
90 try:
91 char = name2codepoint[value]
92 except KeyError:
93 return values.group()
94 else:
95 return unichr(char)
96
97 def _system_encoding(self):
98 # Returns the character encoding of the operating system
99
100 encoding = sys.getdefaultencoding()
101 filesystem_encoding = sys.getfilesystemencoding()
102 if filesystem_encoding:
103 encoding = filesystem_encoding
104 return encoding
105
106
107class Himo(unicode):
108 """
109 A unicode-string object with some added features to help with
110 output formatting. Himo means rope or string in Japanese, hence
111 the string to Himo connection.
112
113 """
114
115 MAP = {169:u'(C)', 174:u'(R)', 8471:u'(P)'}
116
117 def html(self):
118 """
119 Returns a unicode string containing this object's value
120 html enetity encoded.
121
122 """
123 out = []
124 for char in self:
125 out.append(self._html_char_encode(char))
126 return unicode(''.join(out))
127
128 def _html_char_encode(self, char):
129 # Returns an html version of the char
130
131 number = ord(char)
132 try:
133 char = "&{0};".format(codepoint2name[number])
134 except KeyError:
135 if number > 127:
136 char = "&#{0};".format(number)
137 return char
138
139 def decimal(self):
140 """
141 Returns a decimal object with the value of this object
142
143 """
144
145 return D(self)
146
147 def ascii(self):
148 """
149 Returns an ascii representation of this object value.
150 Throws HimoAsciiError if this method was unable to
151 convert a unicode character down to it's root character.
152 For example if in your string you have a character
153 like the letter 'e' but it has an accent mark over it,
154 this method will convert that character to it's root
155 character. Thus 'e' with an accent mark over it will
156 replaced with the regular letter 'e'.
157
158 """
159 out = []
160 for char in self:
161 if ord(char) < 127:
162 out.append(char)
163 elif ord(char) in self.MAP:
164 out.append(self.MAP[ord(char)])
165 else:
166 num = unicodedata.decomposition(char).split(' ')[0]
167 if num:
168 out.append(unichr(int(num, 16)))
169 else:
170 raise HimoAsciiError(char)
171 return str(''.join(out))
172
173class HimoAsciiError(Exception):
174 pass
diff --git a/lib/dodai/tools/odict.py b/lib/dodai/tools/odict.py
new file mode 100644
index 0000000..2c8391d
--- /dev/null
+++ b/lib/dodai/tools/odict.py
@@ -0,0 +1,1399 @@
1# odict.py
2# An Ordered Dictionary object
3# Copyright (C) 2005 Nicola Larosa, Michael Foord
4# E-mail: nico AT tekNico DOT net, fuzzyman AT voidspace DOT org DOT uk
5
6# This software is licensed under the terms of the BSD license.
7# http://www.voidspace.org.uk/python/license.shtml
8# Basically you're free to copy, modify, distribute and relicense it,
9# So long as you keep a copy of the license with it.
10
11# Documentation at http://www.voidspace.org.uk/python/odict.html
12# For information about bugfixes, updates and support, please join the
13# Pythonutils mailing list:
14# http://groups.google.com/group/pythonutils/
15# Comments, suggestions and bug reports welcome.
16
17"""A dict that keeps keys in insertion order"""
18from __future__ import generators
19
20__author__ = ('Nicola Larosa <nico-NoSp@m-tekNico.net>,'
21 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>')
22
23__docformat__ = "restructuredtext en"
24
25__revision__ = '$Id: odict.py 129 2005-09-12 18:15:28Z teknico $'
26
27__version__ = '0.2.2'
28
29__all__ = ['OrderedDict', 'SequenceOrderedDict']
30
31import sys
32INTP_VER = sys.version_info[:2]
33if INTP_VER < (2, 2):
34 raise RuntimeError("Python v.2.2 or later required")
35
36import types, warnings
37
38class OrderedDict(dict):
39 """
40 A class of dictionary that keeps the insertion order of keys.
41
42 All appropriate methods return keys, items, or values in an ordered way.
43
44 All normal dictionary methods are available. Update and comparison is
45 restricted to other OrderedDict objects.
46
47 Various sequence methods are available, including the ability to explicitly
48 mutate the key ordering.
49
50 __contains__ tests:
51
52 >>> d = OrderedDict(((1, 3),))
53 >>> 1 in d
54 1
55 >>> 4 in d
56 0
57
58 __getitem__ tests:
59
60 >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[2]
61 1
62 >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[4]
63 Traceback (most recent call last):
64 KeyError: 4
65
66 __len__ tests:
67
68 >>> len(OrderedDict())
69 0
70 >>> len(OrderedDict(((1, 3), (3, 2), (2, 1))))
71 3
72
73 get tests:
74
75 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
76 >>> d.get(1)
77 3
78 >>> d.get(4) is None
79 1
80 >>> d.get(4, 5)
81 5
82 >>> d
83 OrderedDict([(1, 3), (3, 2), (2, 1)])
84
85 has_key tests:
86
87 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
88 >>> d.has_key(1)
89 1
90 >>> d.has_key(4)
91 0
92 """
93
94 def __init__(self, init_val=(), strict=False):
95 """
96 Create a new ordered dictionary. Cannot init from a normal dict,
97 nor from kwargs, since items order is undefined in those cases.
98
99 If the ``strict`` keyword argument is ``True`` (``False`` is the
100 default) then when doing slice assignment - the ``OrderedDict`` you are
101 assigning from *must not* contain any keys in the remaining dict.
102
103 >>> OrderedDict()
104 OrderedDict([])
105 >>> OrderedDict({1: 1})
106 Traceback (most recent call last):
107 TypeError: undefined order, cannot get items from dict
108 >>> OrderedDict({1: 1}.items())
109 OrderedDict([(1, 1)])
110 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
111 >>> d
112 OrderedDict([(1, 3), (3, 2), (2, 1)])
113 >>> OrderedDict(d)
114 OrderedDict([(1, 3), (3, 2), (2, 1)])
115 """
116 self.strict = strict
117 dict.__init__(self)
118 if isinstance(init_val, OrderedDict):
119 self._sequence = init_val.keys()
120 dict.update(self, init_val)
121 elif isinstance(init_val, dict):
122 # we lose compatibility with other ordered dict types this way
123 raise TypeError('undefined order, cannot get items from dict')
124 else:
125 self._sequence = []
126 self.update(init_val)
127
128### Special methods ###
129
130 def __delitem__(self, key):
131 """
132 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
133 >>> del d[3]
134 >>> d
135 OrderedDict([(1, 3), (2, 1)])
136 >>> del d[3]
137 Traceback (most recent call last):
138 KeyError: 3
139 >>> d[3] = 2
140 >>> d
141 OrderedDict([(1, 3), (2, 1), (3, 2)])
142 >>> del d[0:1]
143 >>> d
144 OrderedDict([(2, 1), (3, 2)])
145 """
146 if isinstance(key, types.SliceType):
147 # FIXME: efficiency?
148 keys = self._sequence[key]
149 for entry in keys:
150 dict.__delitem__(self, entry)
151 del self._sequence[key]
152 else:
153 # do the dict.__delitem__ *first* as it raises
154 # the more appropriate error
155 dict.__delitem__(self, key)
156 self._sequence.remove(key)
157
158 def __eq__(self, other):
159 """
160 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
161 >>> d == OrderedDict(d)
162 True
163 >>> d == OrderedDict(((1, 3), (2, 1), (3, 2)))
164 False
165 >>> d == OrderedDict(((1, 0), (3, 2), (2, 1)))
166 False
167 >>> d == OrderedDict(((0, 3), (3, 2), (2, 1)))
168 False
169 >>> d == dict(d)
170 False
171 >>> d == False
172 False
173 """
174 if isinstance(other, OrderedDict):
175 # FIXME: efficiency?
176 # Generate both item lists for each compare
177 return (self.items() == other.items())
178 else:
179 return False
180
181 def __lt__(self, other):
182 """
183 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
184 >>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
185 >>> c < d
186 True
187 >>> d < c
188 False
189 >>> d < dict(c)
190 Traceback (most recent call last):
191 TypeError: Can only compare with other OrderedDicts
192 """
193 if not isinstance(other, OrderedDict):
194 raise TypeError('Can only compare with other OrderedDicts')
195 # FIXME: efficiency?
196 # Generate both item lists for each compare
197 return (self.items() < other.items())
198
199 def __le__(self, other):
200 """
201 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
202 >>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
203 >>> e = OrderedDict(d)
204 >>> c <= d
205 True
206 >>> d <= c
207 False
208 >>> d <= dict(c)
209 Traceback (most recent call last):
210 TypeError: Can only compare with other OrderedDicts
211 >>> d <= e
212 True
213 """
214 if not isinstance(other, OrderedDict):
215 raise TypeError('Can only compare with other OrderedDicts')
216 # FIXME: efficiency?
217 # Generate both item lists for each compare
218 return (self.items() <= other.items())
219
220 def __ne__(self, other):
221 """
222 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
223 >>> d != OrderedDict(d)
224 False
225 >>> d != OrderedDict(((1, 3), (2, 1), (3, 2)))
226 True
227 >>> d != OrderedDict(((1, 0), (3, 2), (2, 1)))
228 True
229 >>> d == OrderedDict(((0, 3), (3, 2), (2, 1)))
230 False
231 >>> d != dict(d)
232 True
233 >>> d != False
234 True
235 """
236 if isinstance(other, OrderedDict):
237 # FIXME: efficiency?
238 # Generate both item lists for each compare
239 return not (self.items() == other.items())
240 else:
241 return True
242
243 def __gt__(self, other):
244 """
245 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
246 >>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
247 >>> d > c
248 True
249 >>> c > d
250 False
251 >>> d > dict(c)
252 Traceback (most recent call last):
253 TypeError: Can only compare with other OrderedDicts
254 """
255 if not isinstance(other, OrderedDict):
256 raise TypeError('Can only compare with other OrderedDicts')
257 # FIXME: efficiency?
258 # Generate both item lists for each compare
259 return (self.items() > other.items())
260
261 def __ge__(self, other):
262 """
263 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
264 >>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
265 >>> e = OrderedDict(d)
266 >>> c >= d
267 False
268 >>> d >= c
269 True
270 >>> d >= dict(c)
271 Traceback (most recent call last):
272 TypeError: Can only compare with other OrderedDicts
273 >>> e >= d
274 True
275 """
276 if not isinstance(other, OrderedDict):
277 raise TypeError('Can only compare with other OrderedDicts')
278 # FIXME: efficiency?
279 # Generate both item lists for each compare
280 return (self.items() >= other.items())
281
282 def __repr__(self):
283 """
284 Used for __repr__ and __str__
285
286 >>> r1 = repr(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f'))))
287 >>> r1
288 "OrderedDict([('a', 'b'), ('c', 'd'), ('e', 'f')])"
289 >>> r2 = repr(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd'))))
290 >>> r2
291 "OrderedDict([('a', 'b'), ('e', 'f'), ('c', 'd')])"
292 >>> r1 == str(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f'))))
293 True
294 >>> r2 == str(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd'))))
295 True
296 """
297 return '%s([%s])' % (self.__class__.__name__, ', '.join(
298 ['(%r, %r)' % (key, self[key]) for key in self._sequence]))
299
300 def __setitem__(self, key, val):
301 """
302 Allows slice assignment, so long as the slice is an OrderedDict
303 >>> d = OrderedDict()
304 >>> d['a'] = 'b'
305 >>> d['b'] = 'a'
306 >>> d[3] = 12
307 >>> d
308 OrderedDict([('a', 'b'), ('b', 'a'), (3, 12)])
309 >>> d[:] = OrderedDict(((1, 2), (2, 3), (3, 4)))
310 >>> d
311 OrderedDict([(1, 2), (2, 3), (3, 4)])
312 >>> d[::2] = OrderedDict(((7, 8), (9, 10)))
313 >>> d
314 OrderedDict([(7, 8), (2, 3), (9, 10)])
315 >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)))
316 >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8)))
317 >>> d
318 OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)])
319 >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)), strict=True)
320 >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8)))
321 >>> d
322 OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)])
323
324 >>> a = OrderedDict(((0, 1), (1, 2), (2, 3)), strict=True)
325 >>> a[3] = 4
326 >>> a
327 OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
328 >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
329 >>> a
330 OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
331 >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)])
332 Traceback (most recent call last):
333 ValueError: slice assignment must be from unique keys
334 >>> a = OrderedDict(((0, 1), (1, 2), (2, 3)))
335 >>> a[3] = 4
336 >>> a
337 OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
338 >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
339 >>> a
340 OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
341 >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
342 >>> a
343 OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
344 >>> a[::-1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
345 >>> a
346 OrderedDict([(3, 4), (2, 3), (1, 2), (0, 1)])
347
348 >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
349 >>> d[:1] = 3
350 Traceback (most recent call last):
351 TypeError: slice assignment requires an OrderedDict
352
353 >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
354 >>> d[:1] = OrderedDict([(9, 8)])
355 >>> d
356 OrderedDict([(9, 8), (1, 2), (2, 3), (3, 4)])
357 """
358 if isinstance(key, types.SliceType):
359 if not isinstance(val, OrderedDict):
360 # FIXME: allow a list of tuples?
361 raise TypeError('slice assignment requires an OrderedDict')
362 keys = self._sequence[key]
363 # NOTE: Could use ``range(*key.indices(len(self._sequence)))``
364 indexes = range(len(self._sequence))[key]
365 if key.step is None:
366 # NOTE: new slice may not be the same size as the one being
367 # overwritten !
368 # NOTE: What is the algorithm for an impossible slice?
369 # e.g. d[5:3]
370 pos = key.start or 0
371 del self[key]
372 newkeys = val.keys()
373 for k in newkeys:
374 if k in self:
375 if self.strict:
376 raise ValueError('slice assignment must be from '
377 'unique keys')
378 else:
379 # NOTE: This removes duplicate keys *first*
380 # so start position might have changed?
381 del self[k]
382 self._sequence = (self._sequence[:pos] + newkeys +
383 self._sequence[pos:])
384 dict.update(self, val)
385 else:
386 # extended slice - length of new slice must be the same
387 # as the one being replaced
388 if len(keys) != len(val):
389 raise ValueError('attempt to assign sequence of size %s '
390 'to extended slice of size %s' % (len(val), len(keys)))
391 # FIXME: efficiency?
392 del self[key]
393 item_list = zip(indexes, val.items())
394 # smallest indexes first - higher indexes not guaranteed to
395 # exist
396 item_list.sort()
397 for pos, (newkey, newval) in item_list:
398 if self.strict and newkey in self:
399 raise ValueError('slice assignment must be from unique'
400 ' keys')
401 self.insert(pos, newkey, newval)
402 else:
403 if key not in self:
404 self._sequence.append(key)
405 dict.__setitem__(self, key, val)
406
407 def __getitem__(self, key):
408 """
409 Allows slicing. Returns an OrderedDict if you slice.
410 >>> b = OrderedDict([(7, 0), (6, 1), (5, 2), (4, 3), (3, 4), (2, 5), (1, 6)])
411 >>> b[::-1]
412 OrderedDict([(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0)])
413 >>> b[2:5]
414 OrderedDict([(5, 2), (4, 3), (3, 4)])
415 >>> type(b[2:4])
416 <class '__main__.OrderedDict'>
417 """
418 if isinstance(key, types.SliceType):
419 # FIXME: does this raise the error we want?
420 keys = self._sequence[key]
421 # FIXME: efficiency?
422 return OrderedDict([(entry, self[entry]) for entry in keys])
423 else:
424 return dict.__getitem__(self, key)
425
426 __str__ = __repr__
427
428 def __setattr__(self, name, value):
429 """
430 Implemented so that accesses to ``sequence`` raise a warning and are
431 diverted to the new ``setkeys`` method.
432 """
433 if name == 'sequence':
434 warnings.warn('Use of the sequence attribute is deprecated.'
435 ' Use the keys method instead.', DeprecationWarning)
436 # NOTE: doesn't return anything
437 self.setkeys(value)
438 else:
439 # FIXME: do we want to allow arbitrary setting of attributes?
440 # Or do we want to manage it?
441 object.__setattr__(self, name, value)
442
443 def __getattr__(self, name):
444 """
445 Implemented so that access to ``sequence`` raises a warning.
446
447 >>> d = OrderedDict()
448 >>> d.sequence
449 []
450 """
451 if name == 'sequence':
452 warnings.warn('Use of the sequence attribute is deprecated.'
453 ' Use the keys method instead.', DeprecationWarning)
454 # NOTE: Still (currently) returns a direct reference. Need to
455 # because code that uses sequence will expect to be able to
456 # mutate it in place.
457 return self._sequence
458 else:
459 # raise the appropriate error
460 raise AttributeError("OrderedDict has no '%s' attribute" % name)
461
462 def __deepcopy__(self, memo):
463 """
464 To allow deepcopy to work with OrderedDict.
465
466 >>> from copy import deepcopy
467 >>> a = OrderedDict([(1, 1), (2, 2), (3, 3)])
468 >>> a['test'] = {}
469 >>> b = deepcopy(a)
470 >>> b == a
471 True
472 >>> b is a
473 False
474 >>> a['test'] is b['test']
475 False
476 """
477 from copy import deepcopy
478 return self.__class__(deepcopy(self.items(), memo), self.strict)
479
480
481### Read-only methods ###
482
483 def copy(self):
484 """
485 >>> OrderedDict(((1, 3), (3, 2), (2, 1))).copy()
486 OrderedDict([(1, 3), (3, 2), (2, 1)])
487 """
488 return OrderedDict(self)
489
490 def items(self):
491 """
492 ``items`` returns a list of tuples representing all the
493 ``(key, value)`` pairs in the dictionary.
494
495 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
496 >>> d.items()
497 [(1, 3), (3, 2), (2, 1)]
498 >>> d.clear()
499 >>> d.items()
500 []
501 """
502 return zip(self._sequence, self.values())
503
504 def keys(self):
505 """
506 Return a list of keys in the ``OrderedDict``.
507
508 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
509 >>> d.keys()
510 [1, 3, 2]
511 """
512 return self._sequence[:]
513
514 def values(self, values=None):
515 """
516 Return a list of all the values in the OrderedDict.
517
518 Optionally you can pass in a list of values, which will replace the
519 current list. The value list must be the same len as the OrderedDict.
520
521 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
522 >>> d.values()
523 [3, 2, 1]
524 """
525 return [self[key] for key in self._sequence]
526
527 def iteritems(self):
528 """
529 >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iteritems()
530 >>> ii.next()
531 (1, 3)
532 >>> ii.next()
533 (3, 2)
534 >>> ii.next()
535 (2, 1)
536 >>> ii.next()
537 Traceback (most recent call last):
538 StopIteration
539 """
540 def make_iter(self=self):
541 keys = self.iterkeys()
542 while True:
543 key = keys.next()
544 yield (key, self[key])
545 return make_iter()
546
547 def iterkeys(self):
548 """
549 >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iterkeys()
550 >>> ii.next()
551 1
552 >>> ii.next()
553 3
554 >>> ii.next()
555 2
556 >>> ii.next()
557 Traceback (most recent call last):
558 StopIteration
559 """
560 return iter(self._sequence)
561
562 __iter__ = iterkeys
563
564 def itervalues(self):
565 """
566 >>> iv = OrderedDict(((1, 3), (3, 2), (2, 1))).itervalues()
567 >>> iv.next()
568 3
569 >>> iv.next()
570 2
571 >>> iv.next()
572 1
573 >>> iv.next()
574 Traceback (most recent call last):
575 StopIteration
576 """
577 def make_iter(self=self):
578 keys = self.iterkeys()
579 while True:
580 yield self[keys.next()]
581 return make_iter()
582
583### Read-write methods ###
584
585 def clear(self):
586 """
587 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
588 >>> d.clear()
589 >>> d
590 OrderedDict([])
591 """
592 dict.clear(self)
593 self._sequence = []
594
595 def pop(self, key, *args):
596 """
597 No dict.pop in Python 2.2, gotta reimplement it
598
599 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
600 >>> d.pop(3)
601 2
602 >>> d
603 OrderedDict([(1, 3), (2, 1)])
604 >>> d.pop(4)
605 Traceback (most recent call last):
606 KeyError: 4
607 >>> d.pop(4, 0)
608 0
609 >>> d.pop(4, 0, 1)
610 Traceback (most recent call last):
611 TypeError: pop expected at most 2 arguments, got 3
612 """
613 if len(args) > 1:
614 raise TypeError, ('pop expected at most 2 arguments, got %s' %
615 (len(args) + 1))
616 if key in self:
617 val = self[key]
618 del self[key]
619 else:
620 try:
621 val = args[0]
622 except IndexError:
623 raise KeyError(key)
624 return val
625
626 def popitem(self, i=-1):
627 """
628 Delete and return an item specified by index, not a random one as in
629 dict. The index is -1 by default (the last item).
630
631 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
632 >>> d.popitem()
633 (2, 1)
634 >>> d
635 OrderedDict([(1, 3), (3, 2)])
636 >>> d.popitem(0)
637 (1, 3)
638 >>> OrderedDict().popitem()
639 Traceback (most recent call last):
640 KeyError: 'popitem(): dictionary is empty'
641 >>> d.popitem(2)
642 Traceback (most recent call last):
643 IndexError: popitem(): index 2 not valid
644 """
645 if not self._sequence:
646 raise KeyError('popitem(): dictionary is empty')
647 try:
648 key = self._sequence[i]
649 except IndexError:
650 raise IndexError('popitem(): index %s not valid' % i)
651 return (key, self.pop(key))
652
653 def setdefault(self, key, defval = None):
654 """
655 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
656 >>> d.setdefault(1)
657 3
658 >>> d.setdefault(4) is None
659 True
660 >>> d
661 OrderedDict([(1, 3), (3, 2), (2, 1), (4, None)])
662 >>> d.setdefault(5, 0)
663 0
664 >>> d
665 OrderedDict([(1, 3), (3, 2), (2, 1), (4, None), (5, 0)])
666 """
667 if key in self:
668 return self[key]
669 else:
670 self[key] = defval
671 return defval
672
673 def update(self, from_od):
674 """
675 Update from another OrderedDict or sequence of (key, value) pairs
676
677 >>> d = OrderedDict(((1, 0), (0, 1)))
678 >>> d.update(OrderedDict(((1, 3), (3, 2), (2, 1))))
679 >>> d
680 OrderedDict([(1, 3), (0, 1), (3, 2), (2, 1)])
681 >>> d.update({4: 4})
682 Traceback (most recent call last):
683 TypeError: undefined order, cannot get items from dict
684 >>> d.update((4, 4))
685 Traceback (most recent call last):
686 TypeError: cannot convert dictionary update sequence element "4" to a 2-item sequence
687 """
688 if isinstance(from_od, OrderedDict):
689 for key, val in from_od.items():
690 self[key] = val
691 elif isinstance(from_od, dict):
692 # we lose compatibility with other ordered dict types this way
693 raise TypeError('undefined order, cannot get items from dict')
694 else:
695 # FIXME: efficiency?
696 # sequence of 2-item sequences, or error
697 for item in from_od:
698 try:
699 key, val = item
700 except TypeError:
701 raise TypeError('cannot convert dictionary update'
702 ' sequence element "%s" to a 2-item sequence' % item)
703 self[key] = val
704
705 def rename(self, old_key, new_key):
706 """
707 Rename the key for a given value, without modifying sequence order.
708
709 For the case where new_key already exists this raise an exception,
710 since if new_key exists, it is ambiguous as to what happens to the
711 associated values, and the position of new_key in the sequence.
712
713 >>> od = OrderedDict()
714 >>> od['a'] = 1
715 >>> od['b'] = 2
716 >>> od.items()
717 [('a', 1), ('b', 2)]
718 >>> od.rename('b', 'c')
719 >>> od.items()
720 [('a', 1), ('c', 2)]
721 >>> od.rename('c', 'a')
722 Traceback (most recent call last):
723 ValueError: New key already exists: 'a'
724 >>> od.rename('d', 'b')
725 Traceback (most recent call last):
726 KeyError: 'd'
727 """
728 if new_key == old_key:
729 # no-op
730 return
731 if new_key in self:
732 raise ValueError("New key already exists: %r" % new_key)
733 # rename sequence entry
734 value = self[old_key]
735 old_idx = self._sequence.index(old_key)
736 self._sequence[old_idx] = new_key
737 # rename internal dict entry
738 dict.__delitem__(self, old_key)
739 dict.__setitem__(self, new_key, value)
740
741 def setitems(self, items):
742 """
743 This method allows you to set the items in the dict.
744
745 It takes a list of tuples - of the same sort returned by the ``items``
746 method.
747
748 >>> d = OrderedDict()
749 >>> d.setitems(((3, 1), (2, 3), (1, 2)))
750 >>> d
751 OrderedDict([(3, 1), (2, 3), (1, 2)])
752 """
753 self.clear()
754 # FIXME: this allows you to pass in an OrderedDict as well :-)
755 self.update(items)
756
757 def setkeys(self, keys):
758 """
759 ``setkeys`` all ows you to pass in a new list of keys which will
760 replace the current set. This must contain the same set of keys, but
761 need not be in the same order.
762
763 If you pass in new keys that don't match, a ``KeyError`` will be
764 raised.
765
766 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
767 >>> d.keys()
768 [1, 3, 2]
769 >>> d.setkeys((1, 2, 3))
770 >>> d
771 OrderedDict([(1, 3), (2, 1), (3, 2)])
772 >>> d.setkeys(['a', 'b', 'c'])
773 Traceback (most recent call last):
774 KeyError: 'Keylist is not the same as current keylist.'
775 """
776 # FIXME: Efficiency? (use set for Python 2.4 :-)
777 # NOTE: list(keys) rather than keys[:] because keys[:] returns
778 # a tuple, if keys is a tuple.
779 kcopy = list(keys)
780 kcopy.sort()
781 self._sequence.sort()
782 if kcopy != self._sequence:
783 raise KeyError('Keylist is not the same as current keylist.')
784 # NOTE: This makes the _sequence attribute a new object, instead
785 # of changing it in place.
786 # FIXME: efficiency?
787 self._sequence = list(keys)
788
789 def setvalues(self, values):
790 """
791 You can pass in a list of values, which will replace the
792 current list. The value list must be the same len as the OrderedDict.
793
794 (Or a ``ValueError`` is raised.)
795
796 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
797 >>> d.setvalues((1, 2, 3))
798 >>> d
799 OrderedDict([(1, 1), (3, 2), (2, 3)])
800 >>> d.setvalues([6])
801 Traceback (most recent call last):
802 ValueError: Value list is not the same length as the OrderedDict.
803 """
804 if len(values) != len(self):
805 # FIXME: correct error to raise?
806 raise ValueError('Value list is not the same length as the '
807 'OrderedDict.')
808 self.update(zip(self, values))
809
810### Sequence Methods ###
811
812 def index(self, key):
813 """
814 Return the position of the specified key in the OrderedDict.
815
816 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
817 >>> d.index(3)
818 1
819 >>> d.index(4)
820 Traceback (most recent call last):
821 ValueError: list.index(x): x not in list
822 """
823 return self._sequence.index(key)
824
825 def insert(self, index, key, value):
826 """
827 Takes ``index``, ``key``, and ``value`` as arguments.
828
829 Sets ``key`` to ``value``, so that ``key`` is at position ``index`` in
830 the OrderedDict.
831
832 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
833 >>> d.insert(0, 4, 0)
834 >>> d
835 OrderedDict([(4, 0), (1, 3), (3, 2), (2, 1)])
836 >>> d.insert(0, 2, 1)
837 >>> d
838 OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2)])
839 >>> d.insert(8, 8, 1)
840 >>> d
841 OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2), (8, 1)])
842 """
843 if key in self:
844 # FIXME: efficiency?
845 del self[key]
846 self._sequence.insert(index, key)
847 dict.__setitem__(self, key, value)
848
849 def reverse(self):
850 """
851 Reverse the order of the OrderedDict.
852
853 >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
854 >>> d.reverse()
855 >>> d
856 OrderedDict([(2, 1), (3, 2), (1, 3)])
857 """
858 self._sequence.reverse()
859
860 def sort(self, *args, **kwargs):
861 """
862 Sort the key order in the OrderedDict.
863
864 This method takes the same arguments as the ``list.sort`` method on
865 your version of Python.
866
867 >>> d = OrderedDict(((4, 1), (2, 2), (3, 3), (1, 4)))
868 >>> d.sort()
869 >>> d
870 OrderedDict([(1, 4), (2, 2), (3, 3), (4, 1)])
871 """
872 self._sequence.sort(*args, **kwargs)
873
874class Keys(object):
875 # FIXME: should this object be a subclass of list?
876 """
877 Custom object for accessing the keys of an OrderedDict.
878
879 Can be called like the normal ``OrderedDict.keys`` method, but also
880 supports indexing and sequence methods.
881 """
882
883 def __init__(self, main):
884 self._main = main
885
886 def __call__(self):
887 """Pretend to be the keys method."""
888 return self._main._keys()
889
890 def __getitem__(self, index):
891 """Fetch the key at position i."""
892 # NOTE: this automatically supports slicing :-)
893 return self._main._sequence[index]
894
895 def __setitem__(self, index, name):
896 """
897 You cannot assign to keys, but you can do slice assignment to re-order
898 them.
899
900 You can only do slice assignment if the new set of keys is a reordering
901 of the original set.
902 """
903 if isinstance(index, types.SliceType):
904 # FIXME: efficiency?
905 # check length is the same
906 indexes = range(len(self._main._sequence))[index]
907 if len(indexes) != len(name):
908 raise ValueError('attempt to assign sequence of size %s '
909 'to slice of size %s' % (len(name), len(indexes)))
910 # check they are the same keys
911 # FIXME: Use set
912 old_keys = self._main._sequence[index]
913 new_keys = list(name)
914 old_keys.sort()
915 new_keys.sort()
916 if old_keys != new_keys:
917 raise KeyError('Keylist is not the same as current keylist.')
918 orig_vals = [self._main[k] for k in name]
919 del self._main[index]
920 vals = zip(indexes, name, orig_vals)
921 vals.sort()
922 for i, k, v in vals:
923 if self._main.strict and k in self._main:
924 raise ValueError('slice assignment must be from '
925 'unique keys')
926 self._main.insert(i, k, v)
927 else:
928 raise ValueError('Cannot assign to keys')
929
930 ### following methods pinched from UserList and adapted ###
931 def __repr__(self): return repr(self._main._sequence)
932
933 # FIXME: do we need to check if we are comparing with another ``Keys``
934 # object? (like the __cast method of UserList)
935 def __lt__(self, other): return self._main._sequence < other
936 def __le__(self, other): return self._main._sequence <= other
937 def __eq__(self, other): return self._main._sequence == other
938 def __ne__(self, other): return self._main._sequence != other
939 def __gt__(self, other): return self._main._sequence > other
940 def __ge__(self, other): return self._main._sequence >= other
941 # FIXME: do we need __cmp__ as well as rich comparisons?
942 def __cmp__(self, other): return cmp(self._main._sequence, other)
943
944 def __contains__(self, item): return item in self._main._sequence
945 def __len__(self): return len(self._main._sequence)
946 def __iter__(self): return self._main.iterkeys()
947 def count(self, item): return self._main._sequence.count(item)
948 def index(self, item, *args): return self._main._sequence.index(item, *args)
949 def reverse(self): self._main._sequence.reverse()
950 def sort(self, *args, **kwds): self._main._sequence.sort(*args, **kwds)
951 def __mul__(self, n): return self._main._sequence*n
952 __rmul__ = __mul__
953 def __add__(self, other): return self._main._sequence + other
954 def __radd__(self, other): return other + self._main._sequence
955
956 ## following methods not implemented for keys ##
957 def __delitem__(self, i): raise TypeError('Can\'t delete items from keys')
958 def __iadd__(self, other): raise TypeError('Can\'t add in place to keys')
959 def __imul__(self, n): raise TypeError('Can\'t multiply keys in place')
960 def append(self, item): raise TypeError('Can\'t append items to keys')
961 def insert(self, i, item): raise TypeError('Can\'t insert items into keys')
962 def pop(self, i=-1): raise TypeError('Can\'t pop items from keys')
963 def remove(self, item): raise TypeError('Can\'t remove items from keys')
964 def extend(self, other): raise TypeError('Can\'t extend keys')
965
966class Items(object):
967 """
968 Custom object for accessing the items of an OrderedDict.
969
970 Can be called like the normal ``OrderedDict.items`` method, but also
971 supports indexing and sequence methods.
972 """
973
974 def __init__(self, main):
975 self._main = main
976
977 def __call__(self):
978 """Pretend to be the items method."""
979 return self._main._items()
980
981 def __getitem__(self, index):
982 """Fetch the item at position i."""
983 if isinstance(index, types.SliceType):
984 # fetching a slice returns an OrderedDict
985 return self._main[index].items()
986 key = self._main._sequence[index]
987 return (key, self._main[key])
988
989 def __setitem__(self, index, item):
990 """Set item at position i to item."""
991 if isinstance(index, types.SliceType):
992 # NOTE: item must be an iterable (list of tuples)
993 self._main[index] = OrderedDict(item)
994 else:
995 # FIXME: Does this raise a sensible error?
996 orig = self._main.keys[index]
997 key, value = item
998 if self._main.strict and key in self and (key != orig):
999 raise ValueError('slice assignment must be from '
1000 'unique keys')
1001 # delete the current one
1002 del self._main[self._main._sequence[index]]
1003 self._main.insert(index, key, value)
1004
1005 def __delitem__(self, i):
1006 """Delete the item at position i."""
1007 key = self._main._sequence[i]
1008 if isinstance(i, types.SliceType):
1009 for k in key:
1010 # FIXME: efficiency?
1011 del self._main[k]
1012 else:
1013 del self._main[key]
1014
1015 ### following methods pinched from UserList and adapted ###
1016 def __repr__(self): return repr(self._main.items())
1017
1018 # FIXME: do we need to check if we are comparing with another ``Items``
1019 # object? (like the __cast method of UserList)
1020 def __lt__(self, other): return self._main.items() < other
1021 def __le__(self, other): return self._main.items() <= other
1022 def __eq__(self, other): return self._main.items() == other
1023 def __ne__(self, other): return self._main.items() != other
1024 def __gt__(self, other): return self._main.items() > other
1025 def __ge__(self, other): return self._main.items() >= other
1026 def __cmp__(self, other): return cmp(self._main.items(), other)
1027
1028 def __contains__(self, item): return item in self._main.items()
1029 def __len__(self): return len(self._main._sequence) # easier :-)
1030 def __iter__(self): return self._main.iteritems()
1031 def count(self, item): return self._main.items().count(item)
1032 def index(self, item, *args): return self._main.items().index(item, *args)
1033 def reverse(self): self._main.reverse()
1034 def sort(self, *args, **kwds): self._main.sort(*args, **kwds)
1035 def __mul__(self, n): return self._main.items()*n
1036 __rmul__ = __mul__
1037 def __add__(self, other): return self._main.items() + other
1038 def __radd__(self, other): return other + self._main.items()
1039
1040 def append(self, item):
1041 """Add an item to the end."""
1042 # FIXME: this is only append if the key isn't already present
1043 key, value = item
1044 self._main[key] = value
1045
1046 def insert(self, i, item):
1047 key, value = item
1048 self._main.insert(i, key, value)
1049
1050 def pop(self, i=-1):
1051 key = self._main._sequence[i]
1052 return (key, self._main.pop(key))
1053
1054 def remove(self, item):
1055 key, value = item
1056 try:
1057 assert value == self._main[key]
1058 except (KeyError, AssertionError):
1059 raise ValueError('ValueError: list.remove(x): x not in list')
1060 else:
1061 del self._main[key]
1062
1063 def extend(self, other):
1064 # FIXME: is only a true extend if none of the keys already present
1065 for item in other:
1066 key, value = item
1067 self._main[key] = value
1068
1069 def __iadd__(self, other):
1070 self.extend(other)
1071
1072 ## following methods not implemented for items ##
1073
1074 def __imul__(self, n): raise TypeError('Can\'t multiply items in place')
1075
1076class Values(object):
1077 """
1078 Custom object for accessing the values of an OrderedDict.
1079
1080 Can be called like the normal ``OrderedDict.values`` method, but also
1081 supports indexing and sequence methods.
1082 """
1083
1084 def __init__(self, main):
1085 self._main = main
1086
1087 def __call__(self):
1088 """Pretend to be the values method."""
1089 return self._main._values()
1090
1091 def __getitem__(self, index):
1092 """Fetch the value at position i."""
1093 if isinstance(index, types.SliceType):
1094 return [self._main[key] for key in self._main._sequence[index]]
1095 else:
1096 return self._main[self._main._sequence[index]]
1097
1098 def __setitem__(self, index, value):
1099 """
1100 Set the value at position i to value.
1101
1102 You can only do slice assignment to values if you supply a sequence of
1103 equal length to the slice you are replacing.
1104 """
1105 if isinstance(index, types.SliceType):
1106 keys = self._main._sequence[index]
1107 if len(keys) != len(value):
1108 raise ValueError('attempt to assign sequence of size %s '
1109 'to slice of size %s' % (len(name), len(keys)))
1110 # FIXME: efficiency? Would be better to calculate the indexes
1111 # directly from the slice object
1112 # NOTE: the new keys can collide with existing keys (or even
1113 # contain duplicates) - these will overwrite
1114 for key, val in zip(keys, value):
1115 self._main[key] = val
1116 else:
1117 self._main[self._main._sequence[index]] = value
1118
1119 ### following methods pinched from UserList and adapted ###
1120 def __repr__(self): return repr(self._main.values())
1121
1122 # FIXME: do we need to check if we are comparing with another ``Values``
1123 # object? (like the __cast method of UserList)
1124 def __lt__(self, other): return self._main.values() < other
1125 def __le__(self, other): return self._main.values() <= other
1126 def __eq__(self, other): return self._main.values() == other
1127 def __ne__(self, other): return self._main.values() != other
1128 def __gt__(self, other): return self._main.values() > other
1129 def __ge__(self, other): return self._main.values() >= other
1130 def __cmp__(self, other): return cmp(self._main.values(), other)
1131
1132 def __contains__(self, item): return item in self._main.values()
1133 def __len__(self): return len(self._main._sequence) # easier :-)
1134 def __iter__(self): return self._main.itervalues()
1135 def count(self, item): return self._main.values().count(item)
1136 def index(self, item, *args): return self._main.values().index(item, *args)
1137
1138 def reverse(self):
1139 """Reverse the values"""
1140 vals = self._main.values()
1141 vals.reverse()
1142 # FIXME: efficiency
1143 self[:] = vals
1144
1145 def sort(self, *args, **kwds):
1146 """Sort the values."""
1147 vals = self._main.values()
1148 vals.sort(*args, **kwds)
1149 self[:] = vals
1150
1151 def __mul__(self, n): return self._main.values()*n
1152 __rmul__ = __mul__
1153 def __add__(self, other): return self._main.values() + other
1154 def __radd__(self, other): return other + self._main.values()
1155
1156 ## following methods not implemented for values ##
1157 def __delitem__(self, i): raise TypeError('Can\'t delete items from values')
1158 def __iadd__(self, other): raise TypeError('Can\'t add in place to values')
1159 def __imul__(self, n): raise TypeError('Can\'t multiply values in place')
1160 def append(self, item): raise TypeError('Can\'t append items to values')
1161 def insert(self, i, item): raise TypeError('Can\'t insert items into values')
1162 def pop(self, i=-1): raise TypeError('Can\'t pop items from values')
1163 def remove(self, item): raise TypeError('Can\'t remove items from values')
1164 def extend(self, other): raise TypeError('Can\'t extend values')
1165
1166class SequenceOrderedDict(OrderedDict):
1167 """
1168 Experimental version of OrderedDict that has a custom object for ``keys``,
1169 ``values``, and ``items``.
1170
1171 These are callable sequence objects that work as methods, or can be
1172 manipulated directly as sequences.
1173
1174 Test for ``keys``, ``items`` and ``values``.
1175
1176 >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)))
1177 >>> d
1178 SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
1179 >>> d.keys
1180 [1, 2, 3]
1181 >>> d.keys()
1182 [1, 2, 3]
1183 >>> d.setkeys((3, 2, 1))
1184 >>> d
1185 SequenceOrderedDict([(3, 4), (2, 3), (1, 2)])
1186 >>> d.setkeys((1, 2, 3))
1187 >>> d.keys[0]
1188 1
1189 >>> d.keys[:]
1190 [1, 2, 3]
1191 >>> d.keys[-1]
1192 3
1193 >>> d.keys[-2]
1194 2
1195 >>> d.keys[0:2] = [2, 1]
1196 >>> d
1197 SequenceOrderedDict([(2, 3), (1, 2), (3, 4)])
1198 >>> d.keys.reverse()
1199 >>> d.keys
1200 [3, 1, 2]
1201 >>> d.keys = [1, 2, 3]
1202 >>> d
1203 SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
1204 >>> d.keys = [3, 1, 2]
1205 >>> d
1206 SequenceOrderedDict([(3, 4), (1, 2), (2, 3)])
1207 >>> a = SequenceOrderedDict()
1208 >>> b = SequenceOrderedDict()
1209 >>> a.keys == b.keys
1210 1
1211 >>> a['a'] = 3
1212 >>> a.keys == b.keys
1213 0
1214 >>> b['a'] = 3
1215 >>> a.keys == b.keys
1216 1
1217 >>> b['b'] = 3
1218 >>> a.keys == b.keys
1219 0
1220 >>> a.keys > b.keys
1221 0
1222 >>> a.keys < b.keys
1223 1
1224 >>> 'a' in a.keys
1225 1
1226 >>> len(b.keys)
1227 2
1228 >>> 'c' in d.keys
1229 0
1230 >>> 1 in d.keys
1231 1
1232 >>> [v for v in d.keys]
1233 [3, 1, 2]
1234 >>> d.keys.sort()
1235 >>> d.keys
1236 [1, 2, 3]
1237 >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)), strict=True)
1238 >>> d.keys[::-1] = [1, 2, 3]
1239 >>> d
1240 SequenceOrderedDict([(3, 4), (2, 3), (1, 2)])
1241 >>> d.keys[:2]
1242 [3, 2]
1243 >>> d.keys[:2] = [1, 3]
1244 Traceback (most recent call last):
1245 KeyError: 'Keylist is not the same as current keylist.'
1246
1247 >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)))
1248 >>> d
1249 SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
1250 >>> d.values
1251 [2, 3, 4]
1252 >>> d.values()
1253 [2, 3, 4]
1254 >>> d.setvalues((4, 3, 2))
1255 >>> d
1256 SequenceOrderedDict([(1, 4), (2, 3), (3, 2)])
1257 >>> d.values[::-1]
1258 [2, 3, 4]
1259 >>> d.values[0]
1260 4
1261 >>> d.values[-2]
1262 3
1263 >>> del d.values[0]
1264 Traceback (most recent call last):
1265 TypeError: Can't delete items from values
1266 >>> d.values[::2] = [2, 4]
1267 >>> d
1268 SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
1269 >>> 7 in d.values
1270 0
1271 >>> len(d.values)
1272 3
1273 >>> [val for val in d.values]
1274 [2, 3, 4]
1275 >>> d.values[-1] = 2
1276 >>> d.values.count(2)
1277 2
1278 >>> d.values.index(2)
1279 0
1280 >>> d.values[-1] = 7
1281 >>> d.values
1282 [2, 3, 7]
1283 >>> d.values.reverse()
1284 >>> d.values
1285 [7, 3, 2]
1286 >>> d.values.sort()
1287 >>> d.values
1288 [2, 3, 7]
1289 >>> d.values.append('anything')
1290 Traceback (most recent call last):
1291 TypeError: Can't append items to values
1292 >>> d.values = (1, 2, 3)
1293 >>> d
1294 SequenceOrderedDict([(1, 1), (2, 2), (3, 3)])
1295
1296 >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)))
1297 >>> d
1298 SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
1299 >>> d.items()
1300 [(1, 2), (2, 3), (3, 4)]
1301 >>> d.setitems([(3, 4), (2 ,3), (1, 2)])
1302 >>> d
1303 SequenceOrderedDict([(3, 4), (2, 3), (1, 2)])
1304 >>> d.items[0]
1305 (3, 4)
1306 >>> d.items[:-1]
1307 [(3, 4), (2, 3)]
1308 >>> d.items[1] = (6, 3)
1309 >>> d.items
1310 [(3, 4), (6, 3), (1, 2)]
1311 >>> d.items[1:2] = [(9, 9)]
1312 >>> d
1313 SequenceOrderedDict([(3, 4), (9, 9), (1, 2)])
1314 >>> del d.items[1:2]
1315 >>> d
1316 SequenceOrderedDict([(3, 4), (1, 2)])
1317 >>> (3, 4) in d.items
1318 1
1319 >>> (4, 3) in d.items
1320 0
1321 >>> len(d.items)
1322 2
1323 >>> [v for v in d.items]
1324 [(3, 4), (1, 2)]
1325 >>> d.items.count((3, 4))
1326 1
1327 >>> d.items.index((1, 2))
1328 1
1329 >>> d.items.index((2, 1))
1330 Traceback (most recent call last):
1331 ValueError: list.index(x): x not in list
1332 >>> d.items.reverse()
1333 >>> d.items
1334 [(1, 2), (3, 4)]
1335 >>> d.items.reverse()
1336 >>> d.items.sort()
1337 >>> d.items
1338 [(1, 2), (3, 4)]
1339 >>> d.items.append((5, 6))
1340 >>> d.items
1341 [(1, 2), (3, 4), (5, 6)]
1342 >>> d.items.insert(0, (0, 0))
1343 >>> d.items
1344 [(0, 0), (1, 2), (3, 4), (5, 6)]
1345 >>> d.items.insert(-1, (7, 8))
1346 >>> d.items
1347 [(0, 0), (1, 2), (3, 4), (7, 8), (5, 6)]
1348 >>> d.items.pop()
1349 (5, 6)
1350 >>> d.items
1351 [(0, 0), (1, 2), (3, 4), (7, 8)]
1352 >>> d.items.remove((1, 2))
1353 >>> d.items
1354 [(0, 0), (3, 4), (7, 8)]
1355 >>> d.items.extend([(1, 2), (5, 6)])
1356 >>> d.items
1357 [(0, 0), (3, 4), (7, 8), (1, 2), (5, 6)]
1358 """
1359
1360 def __init__(self, init_val=(), strict=True):
1361 OrderedDict.__init__(self, init_val, strict=strict)
1362 self._keys = self.keys
1363 self._values = self.values
1364 self._items = self.items
1365 self.keys = Keys(self)
1366 self.values = Values(self)
1367 self.items = Items(self)
1368 self._att_dict = {
1369 'keys': self.setkeys,
1370 'items': self.setitems,
1371 'values': self.setvalues,
1372 }
1373
1374 def __setattr__(self, name, value):
1375 """Protect keys, items, and values."""
1376 if not '_att_dict' in self.__dict__:
1377 object.__setattr__(self, name, value)
1378 else:
1379 try:
1380 fun = self._att_dict[name]
1381 except KeyError:
1382 OrderedDict.__setattr__(self, name, value)
1383 else:
1384 fun(value)
1385
1386if __name__ == '__main__':
1387 if INTP_VER < (2, 3):
1388 raise RuntimeError("Tests require Python v.2.3 or later")
1389 # turn off warnings for tests
1390 warnings.filterwarnings('ignore')
1391 # run the code tests in doctest format
1392 import doctest
1393 m = sys.modules.get('__main__')
1394 globs = m.__dict__.copy()
1395 globs.update({
1396 'INTP_VER': INTP_VER,
1397 })
1398 doctest.testmod(m, globs=globs)
1399