Commits

Anonymous committed 214cc47

factored out content and updater classes

Comments (0)

Files changed (7)

moai/content.py

-import time
-import datetime
-import re
-
-from zope.interface import implements
-from lxml import etree
-from moai.interfaces import IContentObject
-
-
-class DictBasedContentObject(object):
-    """Simple Content object that gets its
-    content from a python dictionary.
-    Implements :ref:`IContentObject`
-    """
-    implements(IContentObject)
-
-    def update(self, data, provider):
-        self.provider = provider
-        data = data.copy()
-        self.id = self._extract_id(data)
-        self.label = self._extract_label(data)
-        self.content_type = self._extract_content_type(data)
-        self.when_modified = self._extract_when_modified(data)
-        self.deleted = self._extract_deleted(data)
-        self.sets = self._extract_sets(data)
-        self.is_set = self._extract_is_set(data)
-        self._assets = self._extract_assets(data)
-        self._fields = self._extract_fields(data)
-
-    def _extract_id(self, data):
-        id = data.pop('id')
-        assert isinstance(id, unicode), 'id should be a unicode value'
-        return id
-
-    def _extract_label(self, data):
-        label = data.pop('label')
-        assert isinstance(label, unicode), 'label should be a unicode value'
-        return label
-
-    def _extract_content_type(self, data):
-        content_type = data.pop('content_type')
-        assert isinstance(content_type, unicode), 'content_type should be a unicode value'
-        return content_type
-
-    def _extract_when_modified(self, data):
-        when_modified = data.pop('when_modified')
-        assert isinstance(when_modified, datetime.datetime), 'when_modified should be a datetime object'
-        return when_modified
-
-    def _extract_deleted(self, data):        
-        deleted = data.pop('deleted')
-        assert isinstance(deleted, bool), 'deleted should be a boolean object'
-        return deleted
-
-    def _extract_sets(self, data):
-        sets = data.pop('sets')
-        assert isinstance(sets, list), 'sets should be a list object'
-        return sets
-
-    def _extract_is_set(self, data):
-        is_set = data.pop('is_set')
-        assert isinstance(is_set, bool), 'is_set should be a boolean'
-        return is_set
-
-    def _extract_assets(self, data):
-        assets = data.pop('assets', [])
-        return assets
-
-    def _extract_fields(self, data):
-        return data
-
-    def field_names(self):
-        # only return names, when there is a value
-        return [kv[0] for kv in self._fields.items() if kv[1]]
-
-    def get_values(self, field_name):
-        result = self._fields.get(field_name, [])
-        assert isinstance(result, list)
-        return result
-
-    def get_assets(self):
-         return self._assets
-
-    def _sanitize(self):
-        sanitize_content(self)
-
-class XMLContentObject(object):
-    """Content object that gets an xml string,
-    parses it, and uses xpath expressions to extract
-    the values. Implements :ref:`IContentObject`.
-    """
-    implements(IContentObject)
-
-    def xpath(self, xpath, name, pytype, required=False, multi=False):
-        values = self.root.xpath(xpath, namespaces=self.nsmap)
-        if required:
-            assert values, 'required value "%s" is missing' % name
-        
-        result = []
-
-        for value in values:
-            assert isinstance(value, basestring), (
-                'xpath result of value "%s" is of type "%s", expected string|unicode' %(
-                name, type(value).__name__))
-            value = unicode(value)
-
-            if pytype is datetime.datetime:
-                value = datetime.datetime(*time.strptime(value, '%Y-%m-%dT%H:%M:%S')[:6])
-            try:
-                if type(value) != pytype:
-                    pyval = pytype(value)
-            except:
-                raise ValueError('can not convert %s value "%s" into %s' % (name, value, pytype))
-            result.append(value)
-
-            
-        if not multi and result:
-            result = result[0]
-        
-        return result
-
-    def update(self, path, provider):
-        self.provider = provider
-        self.nsmap = {}
-        doc = etree.parse(path)
-        self.root = doc.getroot()
-        
-        self.id = self._extract_id(root)
-        self.label = self._extract_label(root)
-        self.content_type = self._extract_content_type(root)
-        self.when_modified = self._extract_when_modified(root)
-        self.deleted = self._extract_deleted(root)
-        self.sets = self._extract_sets(root)
-        self.is_set = self._extract_is_set(root)
-        self._fields = self._extract_fields(root)
-        self._assets = self._extract_assets(root)
-
-    def _extract_id(self, root):
-        id = root.xpath('id/text()')
-        assert id, 'id field is missing'
-        id = unicode(id[0])
-        assert isinstance(id, unicode), 'id should be a unicode value'
-        return id
-
-    def _extract_label(self, root):
-        label = unicode(root.xpath('label/text()'))
-        assert label, 'label field is missing'
-        label = unicode(label[0])
-        assert isinstance(label, unicode), 'label should be a unicode value'
-        return label
-
-    def _extract_content_type(self, root):
-        content_type = unicode(root.xpath('content_type/text()')[0])
-        assert isinstance(content_type, unicode), 'content_type should be a unicode value'
-        return content_type
-
-    def _extract_when_modified(self, root):
-        when_modified = unicode(root.xpath('when_modified')[0])
-        when_modified = datetime.datetime(*time.strptime(when_modified,
-                                                         '%Y-%m-%dT%H:%M:%S')[:6])
-        assert isinstance(when_modified, datetime.datetime), 'when_modified should be a datetime object'
-        return when_modified
-
-    def _extract_deleted(self, root):        
-        deleted = unicode(root.xpath('deleted/text()')[0]).lower == 'true'
-        assert isinstance(deleted, bool),  'when_modified should be a datetime object'
-        return deleted
-
-    def _extract_sets(self, root):
-        sets = [unicode(s) for s in root.xpath('set')]
-        assert isinstance(sets, list), 'sets should be a list object'
-        return sets
-
-    def _extract_is_set(self, root):
-        is_set = unicode(root.xpath('is_set/text()')[0]).lower == 'true'
-        assert isinstance(is_set, bool), 'is_set should be a boolean'
-        return is_set
-
-    def _extract_fields(self, root):
-        result = {}
-        for node in root.xpath('*[text(.)]'):
-            tagname = node.tag.split('}')[-1]
-            if tagname in ['is_set', 'set', 'deleted', 'when_modified',
-                           'content_type', 'label', 'id']:
-                continue
-            result.setdefault(tagname, []).append(node.text)
-
-    def _extract_assets(self, root):
-        return []
-
-    def field_names(self):
-        return self._fields.keys()
-
-    def get_values(self, field_name):
-        result = self._fields.get(field_name, [])
-        assert isinstance(result, list), 'value of "%s" is not a list' % field_name
-        return result
-
-    def get_assets(self):
-        return self._assets
-
-    def _sanitize(self):
-        sanitize_content(self)
-
 
 import sqlalchemy as sql
 
+from moai.utils import check_type
+
 class Database(object):
     """Sql implementation of a database backend
     This implements the :ref:`IDatabase` interface, look there for
                 deleted_records.append(oai_id)
             item['record_id'] = oai_id
             inserted_records.append(item)
-                
+
         for oai_id, item in self._cache['sets'].items():
             if oai_id in oai_ids:
                 # set allready exists
             
     def update_record(self, oai_id, modified, deleted, sets, data):
         # adds a record, call flush to actually store in db
+
+        check_type(oai_id,
+                   unicode,
+                   prefix="record %s" % oai_id,
+                   suffix='for parameter "oai_id"')
+        check_type(modified,
+                   datetime.datetime,
+                   prefix="record %s" % oai_id,
+                   suffix='for parameter "modified"')
+        check_type(deleted,
+                   bool,
+                   prefix="record %s" % oai_id,
+                   suffix='for parameter "deleted"')
+        check_type(sets,
+                   dict,
+                   unicode_keys=True,
+                   unicode_values=True,
+                   recursive=True,
+                   prefix="record %s" % oai_id,
+                   suffix='for parameter "sets"')
+        check_type(data,
+                   dict,
+                   prefix="record %s" % oai_id,
+                   suffix='for parameter "dict"')
+        
         data['sets'] = sets
         data = json.dumps(data)
         self._cache['records'][oai_id] = (dict(modified=modified,

moai/example-2345.xml

     <givenName>John</givenName>
     <familyName>Doe</familyName>
   </author>
-  <access>public</access>
-  <issued>2011-10-10T15:53:00Z</issued>
-  <modified>2011-10-12T15:56:00Z</modified>
+  <access>private</access>
+  <issued>2009-10-10T15:53:00Z</issued>
+  <modified>2009-10-12T15:56:00Z</modified>
   <asset>
     <type>application/pdf</type>
     <name>example.pdf</name>
 
 from lxml import etree
 
-from moai.content import XMLContentObject
+from moai.utils import XPath
 
-class ExampleContent(XMLContentObject):
-
-    def update(self, path, provider):
+class ExampleContent(object):
+    def __init__(self, provider):
         self.provider = provider
-
-        self.nsmap = {'ex':'http://example.org/data'}
+        self.id = None
+        self.modified = None
+        self.deleted = None
+        self.data = None
+        self.sets = None
+        
+    def update(self, path):
         doc = etree.parse(path)
+        xpath = XPath(doc, nsmap={'x':'http://example.org/data'})
+        
         self.root = doc.getroot()
 
-        id = self.xpath('ex:id/text()', 'id', unicode, required=True)
+        id = xpath.string('//x:id')
         self.id = 'oai:example-%s' % id
-        self.modified = datetime(*time.gmtime(os.path.getmtime(path))[:6])
+        self.modified = xpath.date('//x:modified')
         self.deleted = False
-        self.sets = {}
-        self.sets[u'example'] = {'name':u'example',
-                                 'description':u'An Example Set'}
         
-        self.data = {'uri': 'http://example.org/data/%s' % id}
-        for el in self.root:
-            tagname = el.tag.split('}', 1)[-1]
-            if tagname in ['author', 'asset']:
-                value = {}
-                for s_el in el:
-                    text = s_el.text.strip().decode('utf8')
-                    value[s_el.tag.split('}', 1)[-1]] = text
-            else:
-                value = el.text.strip().decode('utf8')
-            self.data.setdefault(
-                {'abstract': 'description',
-                 'issued': 'date',
-                 }.get(tagname, tagname),[]).append(value)
+        self.sets = {u'example': {u'name':u'example',
+                                  u'description':u'An Example Set'}}
 
+        access = xpath.string('//x:access')
+        if access == 'public':
+            self.sets[u'public'] = {u'name':u'public',
+                                    u'description':u'Public access'}
+        elif access == 'private':
+            self.sets[u'private'] = {u'name':u'private',
+                                     u'description':u'Private access'}
 
-        if 'public' in self.data['access']:
-            self.sets[u'public'] = {'name':u'public',
-                                    'description':u'Public access'}
-        elif 'private' in self.data['access']:
-            self.sets[u'private'] = {'name':u'private',
-                                     'description':u'Private access'}
+        self.data = {'identifier': [u'http://example.org/data/%s' % id],
+                     'title': [xpath.string('//x:title')],
+                     'subject': xpath.strings('//x:subject'),
+                     'description': [xpath.string('//x:abstract')],
+                     'date': [xpath.string('//x:issued')]}
 
 from optparse import OptionParser
 
-from moai.utils import (parse_config_file,
-                        get_duration,
+from moai.utils import (get_duration,
                         get_moai_log,
                         ProgressBar)
-from moai.update import DatabaseUpdater
 from moai.database import Database
 
 VERSION = pkg_resources.working_set.by_key['moai'].version
         
     options, args = parser.parse_args()
     if not len(args):
-        sys.stderr.write('No profile name specified, use --help for more info\n')
-        sys.exit(1)
-    profile_name = args[0]
+        profile_name = 'default'
+    else:
+        profile_name = args[0]
     if options.config:
         config_path = options.config
     else:
                 config[option] = configfile.get(section, option)
             
     if not profile_name in profiles:
-        sys.stderr.write('unknown profile: %s\n' % profile_name)
-        sys.stderr.write('known profiles are: %s\n' % ', '.join(profiles))
+        if profile_name == 'default':
+            sys.stderr.write(
+                'No profile name specified, use --help for more info\n')
+        else:
+            sys.stderr.write('unknown profile: %s\n' % profile_name)
+        sys.stderr.write('(known profiles are: %s)\n' % ', '.join(profiles))
         sys.exit(1)
 
+    if options.from_date:
+        if 'T' in options.from_date:
+            fmt = '%Y-%m-%dT%H:%M:%S'
+        else:
+            fmt = '%Y-%m-%d'
+        from_date = datetime.datetime(
+            *time.strptime(options.from_date, fmt)[:6])
+    else:
+        from_date = None
+        
     database = Database(config['database'])
     for content_point in iter_entry_points(group='moai.content',
                                            name=config['content']):
-        content_class = content_point.load()
+        ContentClass = content_point.load()
 
     provider_name = config['provider'].split(':', 1)[0]
     for provider_point in iter_entry_points(group='moai.provider',
                                            name=provider_name):
         provider = provider_point.load()(config['provider'])
 
+
     log = get_moai_log()
-    updater = DatabaseUpdater(provider,
-                              content_class,
-                              database,
-                              log,
-                              flush_threshold=-1)
     
     progress = ProgressBar()
     error_count = 0
     starttime = time.time()
 
-    from_date = None
-    if options.from_date:
-        if 'T' in options.from_date:
-            from_date = datetime.datetime(*time.strptime(options.from_date,
-                                                         '%Y-%m-%dT%H:%M:%S')[:6])
-        else:
-            from_date = datetime.datetime(*time.strptime(options.from_date,
-                                                         '%Y-%m-%d')[:3])
-
     sys.stderr.write('Updating content provider..')
     count = 0    
-    for id in updater.update_provider_iterate(from_date):
+    for id in provider.update(from_date):
         if not options.quiet and not options.verbose:
             progress.animate('Updating content provider: %s' % id)
             count += 1
                               'new/modified objects' % count)
         print >> sys.stderr
     
-    total = 0
-    updated = []
-    if options.debug:
-        supress_errors = False
-    else:
-        supress_errors = True
-    for count, total, id, error in updater.update_database_iterate(
-                                                supress_errors=supress_errors):
+    total = provider.count()
 
-        msg_count = ('%%0.%sd/%%s' % len(str(total))) % (count, total)
-        if not error is None:
+    count = 0
+    ignore_count = 0
+    error_count = 0
+    flush_threshold = 10000
+    for content_id in provider.get_content_ids():
+        count += 1
+        try:
+            raw_data = provider.get_content_by_id(content_id)
+        except Exception, err:
+            if options.debug:
+                raise
+            log.error('Error retrieving data %s from provider: %s' % (
+                content_id, str(err)))
             error_count += 1
-            log.error('%s %s' % (msg_count, error.logmessage()))
+            progress.tick(count, total)
+            continue
+
+        try:
+            content = ContentClass(provider)
+            success = content.update(raw_data)
+        except Exception, err:
             if options.debug:
-                print >> sys.stderr, '\n'
-                import traceback
-                traceback.print_tb(error.tb)
-                print error.err, error.detail
-                sys.exit(1)
+                raise
+            log.error('Error converting data %s to content: %s' % (
+                content_id, str(err)))
+            error_count += 1
+            progress.tick(count, total)
             continue
-        elif options.quiet:
-            pass
-        elif options.verbose:
-            log.info('%s Added %s'  % (msg_count, id))
-        else:
+        
+        if success is False:
+            log.warning('Ignoring %s' % content_id)
+            ignore_count += 1
             progress.tick(count, total)
-        updated.append(id)
-
-    if not options.quiet and not options.verbose:
-        print >> sys.stderr, '\n'
-
+            continue
+        
+        try:
+            database.update_record(content.id,
+                                   content.modified,
+                                   content.deleted,
+                                   content.sets,
+                                   content.data)
+        except Exception, err:
+            if options.debug:
+                raise
+            log.error('Error inserting %s into database: %s' % (
+                content.id, str(err)))
+            error_count += 1
+            progress.tick(count, total)
+            continue
+            
+        if count % flush_threshold == 0:
+            log.info('Flushing database')
+            database.flush()
+        progress.tick(count, total)
+        
+    log.info('Flushing database')
+    database.flush()
     duration = get_duration(starttime)
+    print >> sys.stderr, ''
     msg = 'Updating database with %s objects took %s' % (total, duration)
     log.info(msg)
     if not options.verbose and not options.quiet:
         print >> sys.stderr, msg
 
     if error_count:
-        multi = ''
-        if error_count > 1:
-            multi = 's'
-        msg = '%s error%s occurred during updating' % (error_count, multi)
+        msg = '%s error%s occurred during updating' % (
+            error_count,
+            {1: ''}.get(error_count, 's'))
         log.warning(msg)
         if not options.verbose and not options.quiet:
             print >> sys.stderr, msg

moai/update.py

-from moai.error import ContentError, DatabaseError
-
-class DatabaseUpdater(object):
-    """Default implementation of :ref:`IDatabaseUpdater`.
-    
-    This class can update something implementing :ref:`IDatabase`
-    given a contentprovider and content object class
-    (implementations of :ref:`IContentProvider` and :ref:`IContentObject`)
-    """
-
-    def __init__(self, content, content_class, database, log, flush_threshold=-1):
-        self.set_database(database)
-        self.set_content_provider(content)
-        self.set_content_object_class(content_class)
-        self.set_logger(log)
-        self.flush_threshold = flush_threshold
-
-    def set_database(self, database):
-        self.db = database
-
-    def set_content_provider(self, content):
-        self._provider = content
-
-    def set_content_object_class(self, content_class):
-        self._content_object_class = content_class
-
-    def set_logger(self, log):
-        self._log = log
-
-    def update_provider(self, from_date=None):
-        result = []
-        for id in self.update_provider_iterate(from_date):
-            result.append(id)
-        return result
-            
-    def update_provider_iterate(self, from_date=None):
-        msg = 'Starting the update of %s' % self._provider.__class__.__name__
-        if not from_date is None:
-            msg += 'from %s' % from_date
-        self._log.info(msg)
-        count = 0
-        for id in self._provider.update(from_date):
-            yield id
-            count += 1
-        
-        self._log.info('Updating %s returned %s new/modified objects' % (
-            self._provider.__class__.__name__,
-            count))
-            
-    def update_database(self, validate=True, supress_errors=False):
-        errors = 0
-        for count, total, content_id, error in self.update_database_iterate(
-                                                    validate, supress_errors):
-            if not error is None:
-                errors += 1
-        return errors
-    
-    def update_database_iterate(self, validate=True, supress_errors=False):    
-        total = self._provider.count()
-        self._log.info('Updating %s with %s %s objects' % (
-            self.db.__class__.__name__,
-            total,
-            self._content_object_class.__name__))
-
-        count = 0
-        errors = 0
-        for content_id in self._provider.get_content_ids():
-            # If enabled (thres > -1), flush db-cache after every X records 
-            if self.flush_threshold > -1 and count > 0 and \
-               count % self.flush_threshold == 0:
-                try:
-                    self.db.flush_update()
-                except Exception, err:
-                    if not supress_errors:
-                        raise
-            count += 1
-
-            # First try to get the content
-            try:
-                content_data = self._provider.get_content_by_id(content_id)
-                content = self._content_object_class()
-                stop = content.update(content_data, self._provider)
-                
-                if stop is False:
-                    self._log.info('Ignoring %s' % content_id)
-                    continue
-            except Exception, err:
-                if not supress_errors:
-                    raise
-
-                errors += 1
-                yield (count, total, content_id,
-                       ContentError(self._content_object_class, content_id))
-                continue
-
-            # Not a set, compose the record
-            try:
-                self.db.update_record(content.id,
-                                      content.modified,
-                                      content.deleted,
-                                      content.sets,
-                                      content.data)
-            except Exception:
-                if not supress_errors:
-                    raise
-                yield count, total, content.id, DatabaseError(id, 'set')
-                continue
-           
-            yield count, total, content.id, None
-
-        # Always flush db-cache
-        try:
-            self.db.flush()
-        except Exception, err:
-            if not supress_errors:
-                raise
 import sys
 import os
+import datetime
 import time
 import logging
 from ConfigParser import ConfigParser
         duration = '%s hour%s, %s' % (int(h), {1:''}.get(h, 's'), duration)
     return duration
 
+def check_type(object,
+               expected_type,
+               unicode_keys=False,
+               unicode_values=False,
+               recursive=False,
+               prefix='',
+               suffix=''):
+
+    object_type = type(object)
+    if not isinstance(object, expected_type):
+        
+        raise TypeError(('%s expected "%s", got "%s" %s' % (
+            prefix,
+            expected_type.__name__,
+            object.__class__.__name__,
+            suffix)).strip())
+    if unicode_keys and object_type is dict:
+        check_type(object.keys(),
+                   list,
+                   unicode_values=True,
+                   prefix=prefix,
+                   suffix=suffix)
+    if unicode_values and object_type is dict:
+        check_type(object.values(),
+                   list,
+                   unicode_keys=unicode_keys,
+                   unicode_values=True,
+                   recursive=recursive,
+                   prefix=prefix,
+                   suffix=suffix)
+    if unicode_values and object_type is list:
+        for stuff in object:
+            if isinstance(stuff, str):
+                raise TypeError(('%s contains non unicode string "%s" %s' % (
+                    prefix,
+                    stuff,
+                    suffix)).strip())
+            if recursive:
+                if isinstance(stuff, list):
+                    check_type(stuff,
+                               list,
+                               unicode_keys=unicode_keys,
+                               unicode_values=True,
+                               recursive=True,
+                               prefix=prefix,
+                               suffix=suffix)
+                if isinstance(stuff, dict):
+                    check_type(stuff,
+                               dict,
+                               unicode_keys=unicode_keys,
+                               unicode_values=True,
+                               recursive=True,
+                               prefix=prefix,
+                               suffix=suffix)
+
+class XPath(object):
+    def __init__(self, doc, nsmap={}):
+        self.doc = doc
+        self.nsmap = nsmap
+
+    def string(self, xpath):
+        return (self.strings(xpath) or [None])[0]
+
+    def strings(self, xpath):
+        result = []
+        for stuff in self.doc.xpath(xpath, namespaces=self.nsmap):
+            if isinstance(stuff, str):
+                result.append(stuff.strip().decode('utf8'))
+            elif isinstance(stuff, unicode):
+                # convert to real unicode object, not lxml proxy
+                result.append(unicode(stuff.strip()))
+            elif hasattr(stuff, 'text'):
+                if isinstance(stuff.text, str):
+                    result.append(stuff.text.strip().decode('utf8'))
+                elif isinstance(stuff.text, unicode):
+                    # convert to real unicode object, not lxml proxy
+                    result.append(unicode(stuff.text.strip()))
+        return result
+    
+    def number(self, xpath):
+        return (self.numbers(xpath) or [None])[0]
+
+    def numbers(self, xpath):
+        result = []
+        for value in self.strings(xpath):
+            try:
+                value = float(value)
+                result.append(value)
+            except:
+                try:
+                    value = int(value)
+                    result.append(value)
+                except:
+                    raise ValueError('Unknown number format: %s' % value)
+        return result
+
+    def boolean(self, xpath):
+        return (self.booleans(xpath) or [None])[0]
+
+    def booleans(self, xpath):
+        result = []
+        for value in self.strings(xpath):
+            if value in ['true', 'True', 'yes']:
+                result.append(True)
+            elif value in ['false', 'False', 'no']:
+                result.append(False)
+            else:
+                raise ValueError('Unknown boolean format: %s' % value)
+        return result
+
+    def date(self, xpath):
+        return (self.dates(xpath) or [None])[0]
+
+    def dates(self, xpath):
+        result = []
+        for value in self.strings(xpath):
+            if 'T' in value:
+                if value.endswith('Z'):
+                    value = value[:-1] + ' UTC'
+                    fmt = '%Y-%m-%dT%H:%M:%S %Z'
+                else:
+                    fmt = '%Y-%m-%dT%H:%M:%S'
+            elif value.count('-') == 2:
+                fmt = '%Y-%m-%d'
+            elif value.count('/') == 2:
+                fmt = '%Y/%m/%d'
+            else:
+                fmt = '%Y%m%d'
+            try:
+                result.append(datetime.datetime.strptime(value, fmt))
+            except ValueError:
+                raise ValueError('Unknown date format: %s' % value)
+        return result
+
+    def tag(self, xpath):
+        return (self.tags(xpath) or [None])[0]
+
+    def tags(self, xpath):
+        result = []
+        for stuff in self.doc.xpath(xpath, namespaces=self.nsmap):
+            if hasattr(stuff, 'tag'):
+                if '}' in stuff.tag:
+                    result.append(stuff.tag.split('}', 1)[1].decode('utf8'))
+                else:
+                    result.append(stuff.tag.decode('utf8'))
+
+    def __call__(self, xpath):
+        result = self.doc.xpath(xpath, namespaces=self.nsmap)
+        return result
+
+
 class ProgressBar(object):
     def __init__(self, stream=sys.stderr, width=80):
         self.out = stream