Changeset 357

Show
Ignore:
Timestamp:
01/04/07 06:52:59 (2 years ago)
Author:
athomas
Message:

pyndexter:

Refactoring branch.

  • Refactored Indexer into two classes: Indexer, which simply abstracts access to the underlying indexer, and Framework which glues Source and Indexer together along with all the housekeeping that involves.
  • Added an Indexer independent Query language. Indexers should translate the Query parse tree into their own query language.
  • Not using XapWrap anymore. Replaced with stock Xapian bindings (see r354 and #26).
  • No longer using Hype, as it's disappeared off the Internet. Switched to the stock Hyper Estraier bindings.
  • Added util.uriparse().
  • Bumped version to 0.2.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • pyndexter/branches/refactoring/pyndexter/file.py

    r354 r357  
    6464        return os.path.exists(self._uri2file(uri)) 
    6565 
    66     def _fetch_content(self, uri): 
    67         path = self._uri2file(uri) 
    68         return codecs.open(path, encoding='utf-8', errors='replace').read() 
    69  
    7066    def __hash__(self): 
    7167        return hash(self._file2uri(self.root) + '-'.join(self.exclude) + \ 
     
    7369 
    7470    # Internal methods 
     71    def _fetch_content(self, uri): 
     72        path = self._uri2file(uri) 
     73        return codecs.open(path, encoding='utf-8', errors='replace').read() 
     74 
    7575    def _file2uri(self, file): 
    7676        return urlunsplit(('file', '', file, '', '')) 
  • pyndexter/branches/refactoring/pyndexter/hyperestraier.py

    r354 r357  
    88 
    99import os 
    10 import hype 
     10import HyperEstraier 
    1111from pyndexter import * 
    1212 
    1313class HyperestraierIndexer(Indexer): 
     14    """ Pyndexter adapter for the Hyperestraier indexer. """ 
    1415    capabilities = CAP_READONLY | CAP_CONTENT | CAP_ATTRIBUTES | CAP_ORDERING |\ 
    1516                   CAP_HITCOUNT | CAP_LIST | CAP_RELEVANCE | CAP_WHOLEWORD | \ 
    1617                   CAP_ASTERISK | CAP_INTERSECTION 
    1718 
    18     def __init__(self, path, source=None, mode=READWRITE, hype_mode=None): 
    19         Indexer.__init__(self, source, mode, os.path.join(path, 'state.db')) 
    20         self.path = path 
    21         self._init_env(self.path) 
    22         self.hype_path = os.path.join(self.path, 'hyperestraier.db') 
    23         if hype_mode is None: 
    24             hype_mode = 0 
    25             if mode == READONLY: 
    26                 hype_mode |= hype.ESTDBREADER 
    27             elif mode == READWRITE: 
    28                 hype_mode |= hype.ESTDBWRITER|hype.ESTDBCREAT 
    29         self.db = hype.Database(self.hype_path, hype_mode) 
     19    def __init__(self, mode=READWRITE, hype_mode=None): 
     20        Indexer.__init__(self) 
     21        self.hype_mode = hype_mode 
     22 
     23    def bind(self, framework): 
     24        Indexer.bind(self, framework) 
     25 
     26        self.path = os.path.join(framework.path, 'hyperestraier.db') 
     27 
     28        if framework.mode == READWRITE: 
     29            if not os.path.exists(self.path): 
     30                os.makedirs(self.path) 
     31 
     32        if self.hype_mode is None: 
     33            self.hype_mode = HyperEstraier.Database.DBREADER 
     34            if self.framework.mode == READWRITE: 
     35                self.hype_mode |= HyperEstraier.Database.DBWRITER|HyperEstraier.Database.DBCREAT 
     36 
     37        self.db = HyperEstraier.Database() 
     38        self.db.open(self.path, self.hype_mode) 
    3039 
    3140    def fetch(self, uri): 
    32         if self.source: 
    33             return self.source.fetch(uri) 
    34         doc = self.db.get_doc_by_uri(uri) 
     41        id = self.db.uri_to_id(uri) 
     42        doc = self.db.get_doc(id, 0) 
    3543        if doc is None: 
    3644            raise DocumentNotFound(uri) 
    3745        attributes = self._translate_attributes(doc) 
    38         return Document(content=doc.text, source=self.source, **attributes) 
     46        return Document(content=''.join(doc.texts()), source=self.framework.source, **attributes) 
    3947 
    4048    def index(self, document): 
    41         self._assert_rw() 
    42         if isinstance(document, basestring): 
    43             document = self.fetch(document) 
    44         hdoc = hype.Document(document.uri) 
     49        hdoc = HyperEstraier.Document() 
    4550        for k, v in document.attributes.iteritems(): 
    46             if k != 'uri': 
    47                 hdoc['@' + k] = v 
    48         hdoc.add_text(document.content
    49         self.db.put_doc(hdoc
     51            hdoc.add_attr(unicode('@' + k).encode('utf-8'), 
     52                          unicode(v).encode('utf-8')) 
     53        hdoc.add_text(document.content.encode('utf-8')
     54        self.db.put_doc(hdoc, 1
    5055 
    51     def discard(self, document): 
    52         self._assert_rw() 
    53         if isinstance(document, Document): 
    54             document = document.uri 
    55         doc = self.db.get_doc_by_uri(document) 
     56    def discard(self, uri): 
     57        doc = self.db.get_doc_by_uri(uri) 
    5658        if not doc: 
    57             raise DocumentNotFound(document
    58         self.db.remove(doc
     59            raise DocumentNotFound(uri
     60        self.db.out_doc(doc, HyperEstraier.Database.ODCLEAN
    5961 
    60     def search(self, phrase, flags=0, order_by=None, 
    61                order_ascending=True, order_type=str): 
    62         phrase = ((not flags & SEARCH_UNION) and ' ' or '|').join(phrase.split()) 
    63         order = None 
    64         if order_by is not None: 
    65             if order_type is int: 
    66                 order_type = 'NUM' 
    67             else: 
    68                 order_type = 'STR' 
    69             order = u'@%s %s%s' % (order_by, order_type, 
    70                                    order_ascending and 'A' or 'D') 
    71         if not flags & SEARCH_ASTERISK: 
    72             phrase = phrase.replace('*', '\\*') 
    73         if not flags & SEARCH_QUESTION: 
    74             phrase = phrase.replace('?', '\\?') 
    75         if not flags & SEARCH_WHOLEWORD: 
    76             phrase = '*' + '* *'.join(phrase.split()) + '*' 
    77         return self.hype_search(phrase, order=order) 
     62    def search(self, query): 
     63        raise NotImplementedError 
     64#    def search(self, phrase, flags=0, order_by=None, 
     65#               order_ascending=True, order_type=str): 
     66#        phrase = ((not flags & SEARCH_UNION) and ' ' or '|').join(phrase.split()) 
     67#        order = None 
     68#        if order_by is not None: 
     69#            if order_type is int: 
     70#                order_type = 'NUM' 
     71#            else: 
     72#                order_type = 'STR' 
     73#            order = u'@%s %s%s' % (order_by, order_type, 
     74#                                   order_ascending and 'A' or 'D') 
     75#        if not flags & SEARCH_ASTERISK: 
     76#            phrase = phrase.replace('*', '\\*') 
     77#        if not flags & SEARCH_QUESTION: 
     78#            phrase = phrase.replace('?', '\\?') 
     79#        if not flags & SEARCH_WHOLEWORD: 
     80#            phrase = '*' + '* *'.join(phrase.split()) + '*' 
     81#        return self.hype_search(phrase, order=order) 
    7882 
     83    def optimise(self): 
     84        self.db.optimize() 
     85 
     86    def sync(self): 
     87        self.db.sync() 
     88 
     89    def close(self): 
     90        self.db.close() 
     91        self.db = None 
     92 
     93    # Hyperestraier-specific methods 
    7994    def hype_search(self, phrase, simple=True, order=None): 
    8095        """ Full Hyperestraier search phrase. """ 
     
    8499        return HyperestraierSearch(self, phrase, search) 
    85100 
    86     def optimize(self): 
    87         self._assert_rw() 
    88         self.db.optimize() 
    89  
    90     def sync(self): 
    91         if self.mode == READWRITE: 
    92             self.db.sync() 
    93             self._sync_source_state() 
    94  
    95     def close(self): 
    96         if self.mode == READWRITE: 
    97             self.sync() 
    98         self.db = None 
    99  
    100101    # Internal methods 
    101102    def _translate_attributes(self, hdoc): 
    102103        attributes = {} 
    103         for k in hdoc.attributes
     104        for k in hdoc.attr_names()
    104105            if k[0] == '@': 
    105                 attributes[k[1:]] = hdoc.get(k
     106                attributes[k[1:]] = hdoc.attr(k).decode('utf-8'
    106107            else: 
    107                 attributes[k] = hdoc.get(k
     108                attributes[k] = hdoc.attr(k).decode('utf-8'
    108109        return attributes 
    109110 
  • pyndexter/branches/refactoring/pyndexter/__init__.py

    r354 r357  
    77# 
    88 
     9import re 
    910import os 
    1011import pickle 
     
    2526IndexerError 
    2627SourceError 
     28InvalidQuery 
    2729 
    2830REMOVED ADDED MODIFIED 
     
    3638SEARCH_WHOLEWORD SEARCH_ASTERISK SEARCH_QUESTION SEARCH_UNION 
    3739 
    38 Document Source Indexer Search Hit 
     40Query Framework Document Source Indexer Search Hit 
    3941""".split() 
    4042 
     
    8890    """ The mode (READONLY or READWRITE) of the indexer is an 
    8991    invalid state for a particular operation. """ 
     92class InvalidQuery(Error): 
     93    """ Invalid query string. """ 
    9094 
    9195 
     
    146150    be able to determine what has changed. For FileSource this is a list of all 
    147151    files and their modification times, for a SubversionSource it would be as 
    148     simple as the changeset number. The default state() and difference() 
    149     methods use the data in self._state. 
     152    simple as the changeset number. By default, `marshal()` and 
     153    `difference()` assume that `_state` will contain a dictionary of 
     154    uri:modification-time mappings. 
    150155 
    151156    (All attributes, including document contents and URI's must be in unicode) 
     
    153158 
    154159    def __init__(self, include=None, exclude=None, predicate=None): 
    155         self.include = include or ['*'] 
    156         self.exclude = exclude or [] 
     160        if include is None: 
     161            include = ['*'] 
     162        if exclude is None: 
     163            exclude = [] 
     164        self.include = include 
     165        self.exclude = exclude 
    157166        self.predicate = predicate or self._glob_predicate 
    158167        self._state = {} 
     
    164173 
    165174    def __hash__(self): 
    166         """ The hash must uniquely identify the source. (This 
    167         method is primarily used by the MetaSource class) """ 
    168         raise NotImplementedError('The hash of a Source is required by the ' 
    169                                   'MetaSource class.') 
     175        """ The hash must uniquely identify the source. (This method is 
     176        primarily used by the MetaSource class) """ 
     177        raise NotImplementedError 
    170178 
    171179    def __iter__(self): 
     
    179187        DocumentNotFound if unable to fetch the document. """ 
    180188        raise NotImplementedError 
     189 
     190    def bind(self, framework): 
     191        """ Bind the `Source` to the given framework. """ 
    181192 
    182193    def exists(self, uri): 
     
    188199            return False 
    189200 
    190     def state(self): 
    191         """ Return a raw byte string representing the current state of this 
    192         source.  Storage and retrieval of this byte string is typically handled 
    193         by the Indexer. If this method returns false, the Indexer will assume 
    194         that state information is not available, and do nothing. """ 
    195         if not self._state: 
    196             return None 
    197         return self._marshal_state(self._state) 
    198  
    199     def difference(self, state): 
     201    def marshal(self, file): 
     202        """ Store the state of the `Source` to `file`. Used during an 
     203        `update()`. """ 
     204        state = pickle.dumps(self._state, 2) 
     205        gzip.GzipFile(filename='pyndexter source state', fileobj=file, 
     206                      mode='wb', compresslevel=1).write(state) 
     207 
     208    def difference(self, file): 
    200209        """ Return an iterable of tuples representing the differences between 
    201         the current state of the source and that in the provided state. Each 
     210        the current state of the `Source` and that in the provided state. Each 
    202211        tuple is in the form `(<transition>, uri)`, where <transition> is one 
    203         of ADDED, REMOVED or MODIFIED. """ 
     212        of `ADDED`, `REMOVED` or `MODIFIED`. """ 
    204213        current = set() 
    205         state = self._unmarshal_state(state) 
     214        try: 
     215            ungzipped = gzip.GzipFile(fileobj=file, mode='rb').read() 
     216            state = pickle.loads(ungzipped) 
     217        except Exception, e: 
     218            raise InvalidState('Invalid state provided to document source. ' 
     219                               'Exception was %s: %s' % (e.__class__.__name__, e)) 
    206220        for uri in self: 
    207221            current.add(uri) 
     
    226240        return False 
    227241 
    228     def _marshal_state(self, state): 
    229         """ Pickle and compress state. This is used by the default state() 
    230         implementation, but can be reused. """ 
    231         state = pickle.dumps(state, 2) 
    232         compressed = StringIO() 
    233         gzip.GzipFile(filename='pyndexer source state', fileobj=compressed, 
    234                       mode='wb', compresslevel=1).write(state) 
    235         return compressed.getvalue() 
    236  
    237     def _unmarshal_state(self, state): 
    238         """ Uncompress and unpickle state. Used by the default difference() 
    239         method, but can be reused. """ 
    240         state = StringIO(state) 
    241         try: 
    242             ungzipped = gzip.GzipFile(fileobj=state, mode='rb').read() 
    243             return pickle.loads(ungzipped) 
    244         except Exception, e: 
    245             raise InvalidState('Invalid state provided to document source. ' 
    246                                'Exception was %s: %s' % (e.__class__.__name__, e)) 
     242 
     243class QueryNode(object): 
     244    """ A query parse node. """ 
     245 
     246    TERM = 0 
     247    NOT = 1 
     248    AND = 2 
     249    OR = 3 
     250 
     251    __slots__ = ('type', 'value', 'left', 'right') 
     252 
     253    def __init__(self, type, value=None, left=None, right=None): 
     254        self.type = type 
     255        self.value = value 
     256        self.left = left 
     257        self.right = right 
     258 
     259    def __repr__(self): 
     260        if self.type is None: 
     261            return '' 
     262        type_map = ('term', 'not', 'and', 'or') 
     263        def show(node, depth=0): 
     264            if node.type == QueryNode.TERM: 
     265                text = '%s("%s"' % ('  ' * depth, node.value) 
     266            else: 
     267                text = "%s(%s%s" % ('  ' * depth, type_map[node.type], node.value and ' "%s"' % node.value or "") 
     268            if node.left or node.right: 
     269                text += "\n" 
     270                if node.left: 
     271                    text += show(node.left, depth + 1) 
     272                else: 
     273                    text += "%snil" % ('  ' * (depth + 1)) 
     274                text += "\n" 
     275                if node.right: 
     276                    text += show(node.right, depth + 1) 
     277                else: 
     278                    text += "%snil" % ('  ' * (depth + 1)) 
     279            text += ")" 
     280            return text 
     281        return show(self)    
     282 
     283 
     284class Query(QueryNode): 
     285    """ Query parser. Converts a simple query language into a parse tree which 
     286    Indexers can then convert into their own implementation-specific 
     287    representation. 
     288 
     289    The query language is in the following form: 
     290 
     291        <term> <term>     document must contain all of these terms 
     292        "some term"       return documents matching this exact phrase 
     293        -<term>           exclude documents containing this term 
     294        <term> or <term>  return documents matching either term 
     295     
     296    eg. 
     297 
     298    >>> Query('lettuce tomato -cheese') 
     299    (and 
     300      ("lettuce") 
     301      (and 
     302        ("tomato") 
     303        (not 
     304          ("cheese") 
     305          nil))) 
     306 
     307    >>> Query('"mint slices" -timtams') 
     308    (and 
     309      ("mint slices") 
     310      (not 
     311        ("timtams") 
     312        nil)) 
     313 
     314    >>> Query('brie cheese or camembert cheese') 
     315    (and 
     316      ("brie") 
     317      (or 
     318        ("cheese") 
     319        (and 
     320          ("camembert") 
     321          ("cheese")))) 
     322    """ 
     323 
     324    _tokenise = re.compile(r"(?P<ex>-)|(?P<or>or)|\"(?P<dq>(?:\\.|[^\"])*)\"|'(?P<sq>(?:\\.|[^'])*)'|(?P<te>(?:\S)+)", re.I) 
     325    _group_map = {'dq': QueryNode.TERM, 'sq': QueryNode.TERM, 'te': QueryNode.TERM, 
     326                  'ex': QueryNode.NOT, 'or': QueryNode.OR} 
     327 
     328    def __init__(self, query): 
     329        QueryNode.__init__(self, None) 
     330        tokens = [(self._group_map[token.lastgroup], token.group(token.lastindex)) 
     331                  for token in self._tokenise.finditer(query)] 
     332        root = self.parse(tokens) 
     333        if root: 
     334            for k in self.__slots__: 
     335                setattr(self, k, getattr(root, k)) 
     336 
     337    def parse(self, tokens): 
     338        # TODO: add support for sub-expressions eg. "(a b) or c" 
     339        left = self.parse_unary(tokens) 
     340        if tokens: 
     341            if tokens[0][0] == QueryNode.OR: 
     342                tokens.pop(0) 
     343                return QueryNode(QueryNode.OR, left=left, right=self.parse(tokens)) 
     344            else: 
     345                return QueryNode(QueryNode.AND, left=left, right=self.parse(tokens)) 
     346        return left 
     347 
     348    def parse_unary(self, tokens): 
     349        if not tokens: 
     350            return None 
     351        if tokens[0][0] == QueryNode.NOT: 
     352            tokens.pop(0) 
     353            return QueryNode(QueryNode.NOT, left=self.parse_terminal(tokens)) 
     354        return self.parse_terminal(tokens) 
     355 
     356    def parse_terminal(self, tokens): 
     357        if not tokens: 
     358            raise InvalidQuery('Unexpected end of string') 
     359        if tokens[0][0] in (QueryNode.TERM, QueryNode.OR): 
     360            token = tokens.pop(0) 
     361            return QueryNode(QueryNode.TERM, value=token[1]) 
     362        raise InvalidQuery('Expected terminal, got "%s"' % tokens[0][1]) 
     363 
    247364 
    248365class Indexer(object): 
     366    """ An Indexer performs document indexing and searching. This base object 
     367    provides a framework for indexers. """ 
     368 
    249369    capabilities = 0 
    250370 
    251     """ An Indexer performs indexing and searching on a document Source. 
     371    def __init__(self): 
     372        """ Initialise indexer. """ 
     373        self.framework = None 
     374 
     375    def close(self): 
     376        """ Close the indexer. The object is subsequently not usable. 
     377         
     378        `sync()` is automatically called by the `Framework` prior to `close()`.""" 
     379        raise NotImplementedError 
     380 
     381    def index(self, document): 
     382        """ Index a single Document object. """ 
     383        raise NotImplementedError 
     384 
     385    def discard(self, uri): 
     386        """ Discard a document. """ 
     387        raise NotImplementedError 
     388 
     389    def search(self, query): 
     390        """ Search with the given Query. """ 
     391        raise NotImplementedError 
     392 
     393    def __iter__(self): 
     394        """ Iterate over all documents in the index. """ 
     395        raise NotImplementedError 
     396 
     397    # Optional methods 
     398    def bind(self, framework): 
     399        """ Bind the `Indexer` to the given framework. """ 
     400        self.framework = framework 
     401 
     402    def optimise(self): 
     403        """ Optimise the indexer. """ 
     404 
     405    def sync(self): 
     406        """ Synchronise indexer with stored representation. """ 
    252407     
    253     `source` is the Source object, if any. 
    254     `state_path` is the location to store `source` state data. If this is 
    255     provided, update() and sync() will automatically store and retrieve source 
    256     state. """ 
    257     def __init__(self, source=None, mode=READWRITE, state_path=None): 
     408    def fetch(self, uri): 
     409        """ Fetch a Document. Note that, depending on the Indexer, the returned 
     410        content may not be identical to the originall indexed document. """ 
     411 
     412 
     413class Framework(object): 
     414    """ The glue. Ties `Indexer` and `Source` together and provides a 
     415    convenient interface. """ 
     416    def __init__(self, path, indexer, source=None, mode=READWRITE): 
     417        self.path = path 
     418        self.state_path = os.path.join(self.path, 'state.db') 
     419        self.indexer = indexer 
    258420        self.source = source 
    259421        self.mode = mode 
    260         self.state_path = state_path 
     422 
     423        if not os.path.exists(self.path): 
     424            os.makedirs(self.path) 
     425 
     426        self.indexer.bind(self) 
     427        if self.source: 
     428            self.source.bind(self) 
    261429 
    262430    def fetch(self, uri): 
    263         """ Fetch a document. Try to use the indexers data, but fall back 
    264         on the Source copy, if available. """ 
     431        """ Fetch a document. Prefer to fetch from `Source` object if 
     432        available, otherwise fall back to `Indexer`. """ 
    265433        if not self.source: 
    266             raise IndexerError("This indexer has no associated Source object " 
    267                                "and as such can not fetch() documents.") 
     434            return self.indexer.fetch(uri) 
    268435        return self.source.fetch(uri) 
    269  
    270     def __iter__(self): 
    271         """ Iterate over all URI's in the index. """ 
    272         raise NotImplementedError 
    273436 
    274437    def update(self): 
     
    281444                               "capable of automatic updates.") 
    282445        if os.path.exists(self.state_path): 
    283             try: 
    284                 state = open(self.state_path).read() 
    285             except Exception, e: 
    286                 raise IndexerError("Source state '%s' is not readable. " 
    287                                    "Exception was %s: %s" %  
    288                                    (self.state_path, e.__class__.__name__, 
    289                                     unicode(e))) 
    290  
     446            state = open(self.state_path) 
    291447            for transition, uri in self.source.difference(state): 
    292448                if transition == REMOVED: 
     
    296452        else: 
    297453            for uri in self.source: 
     454                print uri 
    298455                self.index(uri) 
    299456 
     
    301458        """ Index a single document, specified as either a Document object 
    302459        or a URI. """ 
    303         raise NotImplementedError 
     460        self._assert_rw() 
     461        if isinstance(document, basestring): 
     462            document = self.fetch(document) 
     463        return self.indexer.index(document) 
     464 
    304465 
    305466    def discard(self, document): 
    306467        """ Discard the specified document from the index, specified as either 
    307468        a Document object or a URI. """ 
    308         raise NotImplementedError 
    309  
    310     def search(self, phrase, flags=0, order_by=None, 
    311                order_ascending=True, order_type=str): 
    312         """ Search the index for documents containing the given terms. If 
    313         intersection is True, return only documents that match all terms. This 
     469        self._assert_rw() 
     470        if isinstance(document, Document): 
     471            document = document.uri 
     472        return self.indexer.discard(document) 
     473 
     474 
     475    def search(self, query): 
     476        """ Search the index for documents matching the given query.  This 
    314477        method is guaranteed to work across all indexers. 
     478 
     479        `query` is a pyndexter compatible search string. 
    315480         
    316         `order` is an optional attribute by which to order results. If prefixed 
    317         by a `<`, results will be in descending order, `>` for ascending. 
    318  
    319         `order_type` is typically either `str` or `int`. 
    320  
    321         `flags` is a bitwise or of the `SEARCH_*` flags 
    322          
    323         Returns a Search object. """ 
    324         raise NotImplementedError 
     481        Returns a `Search` object. """ 
     482        query = Query(query) 
     483        return self.indexer.search(query) 
    325484 
    326485    def close(self): 
    327486        """ Sync and close the indexer. The object is subsequently not 
    328487        usable. """ 
    329         raise NotImplementedError 
    330  
    331     # Default NOP methods 
    332     def optimize(self): 
     488        self.indexer.sync() 
     489        self.indexer.close() 
     490 
     491    def optimise(self): 
    333492        """ Optimise the indexer. """ 
     493        self.indexer.optimise() 
    334494 
    335495    def sync(self): 
    336496        """ Synchronise indexer with on-disk representation. """ 
     497        if self.mode == READWRITE: 
     498            self._sync_source_state() 
     499            self.indexer.sync() 
    337500 
    338501    # Helper methods 
     
    346509        constructor. """ 
    347510        if self.mode == READWRITE and self.source and self.state_path: 
    348             state = self.source.state() 
    349             if state: 
    350                 open(self.state_path, 'w').write(self.source.state()) 
    351  
    352     def _init_env(self, path): 
    353         """ Create a default environment with a <path> base directory. """ 
    354         if not os.path.exists(path): 
    355             if self.mode != READWRITE: 
    356                 raise IndexError("Indexer environment has not been initialised") 
    357             os.makedirs(path) 
     511            file = open(self.state_path, 'wb') 
     512            self.source.marshal(file) 
     513            file.close() 
     514 
    358515 
    359516class Search(object): 
     
    409566        return self._document 
    410567    document = property(_get_document) 
     568 
     569 
     570if __name__ == '__main__': 
     571    import doctest 
     572    doctest.testmod() 
  • pyndexter/branches/refactoring/pyndexter/metasource.py

    r354 r357  
    4747        return False 
    4848 
    49     def state(self): 
     49    def marshal(self, file): 
    5050        state = {} 
    5151        for source in self.sources: 
    52             state[hash(source)] = source.state() 
    53         return pickle.dumps(state, 2) 
     52            stream = StringIO() 
     53            source.marshal(stream) 
     54            state[hash(source)] = stream.getvalue() 
     55        file.write(pickle.dumps(state, 2)) 
    5456 
    5557    def difference(self, state): 
  • pyndexter/branches/refactoring/pyndexter/util.py

    r354 r357  
    5454                key = self.accessbytime[age] 
    5555                del self[key] 
     56 
     57 
     58def uriparse(uri): 
     59    """ Parse a URI into its component parts. The query is passed through 
     60    `cgi.parse_qs()`.  (scheme://netloc/path;parameters?query#fragment). PS. 
     61    `urlparse` is not useful. """ 
     62    from cgi import parse_qs 
     63    import re 
     64 
     65    global urisplit 
     66    if not hasattr(urisplit, '_pattern'): 
     67        urisplit._pattern = re.compile(r'(?P<scheme>[^:]+)://(?P<netloc>[^/]*)(?P<path>/[^#?]*)?(?:\?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?') 
     68 
     69    match = urisplit._pattern.match(uri) 
     70    if match is None: 
     71        raise ValueError('Invalid URI') 
     72    groups = match.groups() 
     73    return groups[0:3] + (parse_qs(groups[3] or ''),) + groups[4:] 
  • pyndexter/branches/refactoring/pyndexter/xapian.py

    r354 r357  
    1818                   CAP_INTERSECTION 
    1919 
    20     def __init__(self, path, source=None, mode=READWRITE, stemmer='english', 
    21                  words=r'\w+'): 
    22         Indexer.__init__(self, source, mode, os.path.join(path, 'state.db')) 
    23         self.path = path 
    24         self._init_env(self.path) 
    25         self.idx_path = os.path.join(path, 'xapian.db') 
    26         if mode == READWRITE: 
    27             self.db = xapian.WritableDatabase(self.idx_path, 
    28                                               xapian.DB_CREATE_OR_OPEN) 
    29         else: 
    30             self.db = xapian.Database(self.idx_path) 
     20    def __init__(self, stemmer='english', words=r'\w+'): 
     21        Indexer.__init__(self) 
    3122        self.stemmer = xapian.Stem('english') 
    3223        self.words = re.compile(words) 
    3324 
     25    def bind(self, framework): 
     26        Indexer.bind(self, framework) 
     27        self.path = os.path.join(framework.path, 'xapian.db') 
     28        if self.framework.mode == READWRITE: 
     29            if not os.path.exists(self.path): 
     30                os.makedirs(self.path) 
     31            self.db = xapian.flint_open(self.path, xapian.DB_CREATE_OR_OPEN) 
     32        else: 
     33            self.db = xapian.flint_open(self.path) 
    3434 
    3535    def index(self, document): 
    36         self._assert_rw() 
    37         if isinstance(document, basestring): 
    38             document = self.fetch(document) 
    39  
    4036        doc = xapian.Document() 
    4137 
     
    5450        self.db.replace_document('Q' + uri, doc) 
    5551 
    56     def discard(self, document): 
    57         self._assert_rw() 
    58         if isinstance(document, Document): 
    59             document = document.uri 
    60         self.db.delete_document('Q' + document.encode('utf-8')) 
     52    def discard(self, uri): 
     53        self.db.delete_document('Q' + uri.encode('utf-8')) 
    6154 
    6255    def sync(self): 
    63         if self.mode == READWRITE: 
    64             self._assert_rw() 
    65             self.db.flush() 
    66             self._sync_source_state() 
     56        self.db.flush() 
    6757 
    6858    def close(self): 
    6959        self.sync() 
     60        self.db.close() 
    7061        self.db = None 
    7162 
  • pyndexter/branches/refactoring/setup.py

    r354 r357  
    1414      author='Alec Thomas', 
    1515      author_email='alec@swapoff.org', 
    16       version='0.1', 
     16      version='0.2', 
    1717      classifiers=['Development Status :: 3 - Alpha', 
    1818                   'Environment :: Plugins', 
     
    2121                   'Operating System :: OS Independent', 
    2222                   'Topic :: Software Development :: Libraries'], 
    23       extras_require={'hype': ['hype>=0.1'], 
    24                       'Xapwrap': ['Xapwrap>=0.3']}, 
     23      extras_require={'hype': ['hype>=0.1']}, 
    2524      ext_modules=[Extension('pyndexter.pyrex', ['pyndexter/pyrex.pyx'])], 
    2625      packages=['pyndexter']) 
  • pyndexter/branches/refactoring/.todo

    r354 r357  
    2727        For storing state, perhaps there should be default store_state(store)/restore_state(store) methods. Also need a Store class, or just use a file object... 
    2828    </note> 
     29    <note priority="high" time="1159197046"> 
     30        Refactor Indexer into two classes: the Indexer itself, and a class that glues Source and the Indexer together. This would remove the duplication I'm getting in all the stock methods (update, index, fetch, etc.) 
     31    </note> 
    2932</todo>