TracDev/Proposals/CacheInvalidation: cache-manager-r7992.patch
| File cache-manager-r7992.patch, 28.2 KB (added by rblank, 3 years ago) |
|---|
-
trac/admin/tests/console-tests.txt
diff --git a/trac/admin/tests/console-tests.txt b/trac/admin/tests/console-tests.txt
a b 11 11 attachment export Export an attachment from a resource to a file or stdout 12 12 attachment list List attachments of a resource 13 13 attachment remove Remove an attachment from a resource 14 cache invalidate Invalidate one or more caches 15 cache list List caches 14 16 component add Add a new component 15 17 component chown Change component ownership 16 18 component list Show available components -
new file trac/cache.py
diff --git a/trac/cache.py b/trac/cache.py new file mode 100644
- + 1 # -*- coding: utf-8 -*- 2 # 3 # Copyright (C) 2009 Edgewall Software 4 # All rights reserved. 5 # 6 # This software is licensed as described in the file COPYING, which 7 # you should have received as part of this distribution. The terms 8 # are also available at http://trac.edgewall.com/license.html. 9 # 10 # This software consists of voluntary contributions made by many 11 # individuals. For the exact contribution history, see the revision 12 # history and logs, available at http://trac.edgewall.org/. 13 14 try: 15 import threading 16 except ImportError: 17 import dummy_threading as threading 18 19 from trac.admin import IAdminCommandProvider 20 from trac.core import Component, implements 21 from trac.util.compat import partial 22 from trac.util.text import print_table 23 from trac.util.translation import _ 24 25 __all__ = ["CacheManager", "cached", "cached_value"] 26 27 28 class cached_value(object): 29 """Method decorator creating a cached attribute from a data retrieval 30 method. 31 32 Accessing the cached attribute gives back the cached value. The data 33 retrieval method will be called as needed by the CacheManager. 34 Invalidating the cache for this value is done by `del`eting the attribute. 35 36 The data retrieval method is called with a single argument `db` containing 37 a reference to a database connection. All data retrieval should be done 38 through this connection. 39 40 Note that the cache validity is maintained using a table in the database. 41 Most notably, a cache invalidation will trigger a commit, so don't do this 42 while another database operation is in progress. 43 44 If more control over the transaction is needed, see the `cached` decorator. 45 46 This decorator can only be used within `Component` subclasses. See 47 CacheProxy for caching attributes of other objects. 48 """ 49 def __init__(self, retriever): 50 self.retriever = retriever 51 self.__doc__ = retriever.__doc__ 52 53 def __get__(self, instance, owner): 54 if instance is None: 55 return self 56 key = owner.__module__ + '.' + owner.__name__ \ 57 + '.' + self.retriever.__name__ 58 return CacheManager(instance.env).get(key, 59 partial(self.retriever, instance)) 60 61 def __delete__(self, instance): 62 key = instance.__class__.__module__ \ 63 + '.' + instance.__class__.__name__ \ 64 + '.' + self.retriever.__name__ 65 CacheManager(instance.env).invalidate(key) 66 67 68 class cached(cached_value): 69 """Method decorator creating a cached attribute from a data retrieval 70 method. 71 72 In contrast with cached attributes created by the `cached_value` decorator, 73 accessing a cached attribute created with `cached` will not directly give 74 back the cached value. Instead, this will return a proxy object with `get` 75 and `invalidate` methods, both accepting a `db` connection. After calling 76 `invalidate(db)`, doing a `commit` is the responsibility of the caller. 77 78 This decorator can only be used within `Component` subclasses. See 79 CacheProxy for caching attributes of other objects. 80 """ 81 def __get__(self, instance, owner): 82 if instance is None: 83 return self 84 key = owner.__module__ + '.' + owner.__name__ \ 85 + '.' + self.retriever.__name__ 86 return CacheProxy(key, partial(self.retriever, instance), 87 instance.env) 88 89 90 class CacheProxy(object): 91 """Cached attribute proxy. 92 93 This is the class of the object returned when accessing an attribute 94 cached with the `cached` decorator. 95 96 It can also be instantiated explicitly to cache attributes of 97 non-`Component` objects. In this case, the cache identifier key must be 98 provided, and the data retrieval function is a normal callable (not an 99 unbound method). 100 """ 101 __slots__ = ["key", "retriever", "env"] 102 103 def __init__(self, key, retriever, env): 104 self.key = key 105 self.retriever = retriever 106 self.env = env 107 108 def get(self, db=None): 109 return CacheManager(self.env).get(self.key, self.retriever, db) 110 111 def invalidate(self, db=None): 112 CacheManager(self.env).invalidate(self.key, db) 113 114 115 class CacheManager(Component): 116 """Cache manager component.""" 117 118 implements(IAdminCommandProvider) 119 120 def __init__(self): 121 self._cache = {} 122 self._local = threading.local() 123 self._lock = threading.RLock() 124 125 # Public interface 126 127 def reset_metadata(self): 128 """Reset per-request cache metadata.""" 129 try: 130 del self._local.meta 131 del self._local.cache 132 except AttributeError: 133 pass 134 135 def get(self, key, retriever, db=None): 136 """Get cached or fresh data for the given key.""" 137 # Get cache metadata 138 try: 139 local_meta = self._local.meta 140 local_cache = self._local.cache 141 except AttributeError: 142 # First cache usage in this request, retrieve cache metadata 143 # from the database and make a thread-local copy of the cache 144 self.log.debug("Retrieving cache metadata") 145 if db is None: 146 db = self.env.get_db_cnx() 147 cursor = db.cursor() 148 cursor.execute("SELECT key, generation FROM cache") 149 self._local.meta = local_meta = dict(cursor) 150 self._local.cache = local_cache = self._cache.copy() 151 152 db_generation = local_meta.get(key, -1) 153 154 # Try the thread-local copy first 155 try: 156 (data, generation) = local_cache[key] 157 if generation == db_generation: 158 return data 159 except KeyError: 160 pass 161 162 self._lock.acquire() 163 try: 164 # Get data from the process cache 165 try: 166 (data, generation) = local_cache[key] = self._cache[key] 167 if generation == db_generation: 168 return data 169 except KeyError: 170 generation = None # Force retrieval from the database 171 172 # Check if the process cache has the newest version, as it may 173 # have been updated after the metadata retrieval 174 if db is None: 175 db = self.env.get_db_cnx() 176 cursor = db.cursor() 177 cursor.execute("SELECT generation FROM cache WHERE key=%s", (key,)) 178 row = cursor.fetchone() 179 db_generation = row and row[0] or -1 180 if db_generation == generation: 181 return data 182 183 # Retrieve data from the database 184 self.log.debug("Retrieving data for cache '%s'", key) 185 data = retriever(db) 186 local_cache[key] = self._cache[key] = (data, db_generation) 187 local_meta[key] = db_generation 188 return data 189 finally: 190 self._lock.release() 191 192 def invalidate(self, key, db=None): 193 """Invalidate cached data for the given key.""" 194 self.log.debug("Invalidating cache '%s'", key) 195 self._lock.acquire() 196 try: 197 # Invalidate in other processes 198 handle_ta = db is None 199 if handle_ta: 200 db = self.env.get_db_cnx() 201 cursor = db.cursor() 202 203 # The row corresponding to the cache may not exist in the table 204 # yet. 205 # - If the row exists, the UPDATE increments the generation, the 206 # SELECT returns a row and we're done. 207 # - If the row doesn't exist, the UPDATE does nothing, but starts 208 # a transaction. The SELECT then returns nothing, and we can 209 # safely INSERT a new row. 210 cursor.execute("UPDATE cache SET generation=generation+1 " 211 "WHERE key=%s", (key,)) 212 cursor.execute("SELECT generation FROM cache WHERE key=%s", (key,)) 213 if not cursor.fetchone(): 214 cursor.execute("INSERT INTO cache VALUES (%s, %s)", (key, 0)) 215 if handle_ta: 216 db.commit() 217 218 # Invalidate in this process 219 self._cache.pop(key, None) 220 221 # Invalidate in this thread 222 try: 223 del self._local.cache[key] 224 except (AttributeError, KeyError): 225 pass 226 finally: 227 self._lock.release() 228 229 # IAdminCommandProvider methods 230 231 def get_admin_commands(self): 232 yield ('cache list', '', 233 'List caches', 234 None, self._do_list) 235 yield ('cache invalidate', '<key> [key] [...]', 236 'Invalidate one or more caches', 237 self._complete_invalidate, self._do_invalidate) 238 239 def get_cache_keys(self, db=None): 240 if db is None: 241 db = self.env.get_db_cnx() 242 cursor = db.cursor() 243 cursor.execute("SELECT key FROM cache") 244 return [row[0] for row in cursor] 245 246 def _complete_invalidate(self, args): 247 if len(args) == 1: 248 return self.get_cache_keys() 249 250 def _do_list(self): 251 db = self.env.get_db_cnx() 252 cursor = db.cursor() 253 cursor.execute("SELECT key, generation FROM cache ORDER BY key") 254 print_table([(row[0], int(row[1])) for row in cursor], 255 [_('Key'), _('Generation')]) 256 257 def _do_invalidate(self, *keys): 258 db = self.env.get_db_cnx() 259 all_keys = set(self.get_cache_keys(db)) 260 inval_keys = [] 261 for key in keys: 262 if key.endswith('*'): 263 inval_keys.extend(each for each in all_keys 264 if each.startswith(key[:-1])) 265 else: 266 inval_keys.append(key) 267 for key in inval_keys: 268 self.invalidate(key, db) 269 db.commit() -
trac/db_default.py
diff --git a/trac/db_default.py b/trac/db_default.py
a b 17 17 from trac.db import Table, Column, Index 18 18 19 19 # Database version identifier. Used for automatic upgrades. 20 db_version = 2 120 db_version = 22 21 21 22 22 def __mkreports(reports): 23 23 """Utility function used to create report data in same syntax as the … … 57 57 Column('authenticated', type='int'), 58 58 Column('name'), 59 59 Column('value')], 60 Table('cache', key='key')[ 61 Column('key'), 62 Column('generation')], 60 63 61 64 # Attachments 62 65 Table('attachment', key=('type', 'id', 'filename'))[ -
trac/env.py
diff --git a/trac/env.py b/trac/env.py
a b 25 25 26 26 from trac import db_default 27 27 from trac.admin import AdminCommandError, IAdminCommandProvider 28 from trac.cache import CacheManager 28 29 from trac.config import * 29 30 from trac.core import Component, ComponentManager, implements, Interface, \ 30 31 ExtensionPoint, TracError … … 583 584 env = None 584 585 if env is None: 585 586 env = env_cache.setdefault(env_path, open_environment(env_path)) 587 else: 588 CacheManager(env).reset_metadata() 586 589 finally: 587 590 env_cache_lock.release() 588 591 else: -
trac/ticket/api.py
diff --git a/trac/ticket/api.py b/trac/ticket/api.py
a b 16 16 17 17 import re 18 18 from datetime import datetime 19 try:20 import threading21 except ImportError:22 import dummy_threading as threading23 19 24 20 from genshi.builder import tag 25 21 22 from trac.cache import cached, cached_value 26 23 from trac.config import * 27 24 from trac.core import * 28 25 from trac.perm import IPermissionRequestor, PermissionCache, PermissionSystem … … 157 154 [TracTickets#Assign-toasDrop-DownList Assign-to as Drop-Down List] 158 155 (''since 0.9'').""") 159 156 160 _fields = None161 _custom_fields = None162 163 157 def __init__(self): 164 158 self.log.debug('action controllers for ticket workflow: %r' % 165 159 [c.__class__.__name__ for c in self.action_controllers]) 166 self._fields_lock = threading.RLock()167 160 168 161 # Public API 169 162 … … 192 185 193 186 def get_ticket_fields(self): 194 187 """Returns the list of fields available for tickets.""" 195 # This is now cached - as it makes quite a number of things faster, 196 # e.g. #6436 197 if self._fields is None: 198 self._fields_lock.acquire() 199 try: 200 if self._fields is None: # double-check (race after 1st check) 201 self._fields = self._get_ticket_fields() 202 finally: 203 self._fields_lock.release() 204 return [f.copy() for f in self._fields] 188 return [f.copy() for f in self.fields.get()] 205 189 206 def reset_ticket_fields(self): 207 self._fields_lock.acquire() 208 try: 209 self._fields = None 210 self.config.touch() # brute force approach for now 211 finally: 212 self._fields_lock.release() 190 def reset_ticket_fields(self, db=None): 191 """Invalidate ticket field cache.""" 192 self.fields.invalidate(db) 213 193 214 def _get_ticket_fields(self): 194 @cached 195 def fields(self, db): 196 """Return the list of fields available for tickets.""" 215 197 from trac.ticket import model 216 198 217 db = self.env.get_db_cnx()218 199 fields = [] 219 200 220 201 # Basic text fields … … 290 271 'col', 'row', 'format', 'max', 'page', 'verbose'] 291 272 292 273 def get_custom_fields(self): 293 if self._custom_fields is None: 294 self._fields_lock.acquire() 295 try: 296 if self._custom_fields is None: # double-check 297 self._custom_fields = self._get_custom_fields() 298 finally: 299 self._fields_lock.release() 300 return [f.copy() for f in self._custom_fields] 274 return [f.copy() for f in self.custom_fields] 301 275 302 def _get_custom_fields(self): 276 @cached_value 277 def custom_fields(self, db): 278 """Return the list of custom ticket fields available for tickets.""" 303 279 fields = [] 304 280 config = self.config['ticket-custom'] 305 281 for name in [option for option, value in config.options() -
trac/ticket/model.py
diff --git a/trac/ticket/model.py b/trac/ticket/model.py
a b 418 418 enum.update(db=db) 419 419 except ValueError: 420 420 pass # Ignore cast error for this non-essential operation 421 TicketSystem(self.env).reset_ticket_fields(db) 421 422 422 423 if handle_ta: 423 424 db.commit() 424 425 self.value = self._old_value = None 425 426 self.name = self._old_name = None 426 TicketSystem(self.env).reset_ticket_fields()427 427 428 428 def insert(self, db=None): 429 429 assert not self.exists, 'Cannot insert existing %s' % self.type … … 444 444 self.value = int(float(cursor.fetchone()[0])) + 1 445 445 cursor.execute("INSERT INTO enum (type,name,value) VALUES (%s,%s,%s)", 446 446 (self.type, self.name, self.value)) 447 TicketSystem(self.env).reset_ticket_fields(db) 447 448 448 449 if handle_ta: 449 450 db.commit() 450 451 self._old_name = self.name 451 452 self._old_value = self.value 452 TicketSystem(self.env).reset_ticket_fields()453 453 454 454 def update(self, db=None): 455 455 assert self.exists, 'Cannot update non-existent %s' % self.type … … 471 471 cursor.execute("UPDATE ticket SET %s=%%s WHERE %s=%%s" % 472 472 (self.ticket_col, self.ticket_col), 473 473 (self.name, self._old_name)) 474 TicketSystem(self.env).reset_ticket_fields(db) 474 475 475 476 if handle_ta: 476 477 db.commit() 477 478 self._old_name = self.name 478 479 self._old_value = self.value 479 TicketSystem(self.env).reset_ticket_fields()480 480 481 481 @classmethod 482 482 def select(cls, env, db=None): … … 561 561 cursor.execute("DELETE FROM component WHERE name=%s", (self.name,)) 562 562 563 563 self.name = self._old_name = None 564 TicketSystem(self.env).reset_ticket_fields(db) 564 565 565 566 if handle_ta: 566 567 db.commit() 567 TicketSystem(self.env).reset_ticket_fields()568 568 569 569 def insert(self, db=None): 570 570 assert not self.exists, 'Cannot insert existing component' … … 581 581 cursor.execute("INSERT INTO component (name,owner,description) " 582 582 "VALUES (%s,%s,%s)", 583 583 (self.name, self.owner, self.description)) 584 TicketSystem(self.env).reset_ticket_fields(db) 584 585 585 586 if handle_ta: 586 587 db.commit() 587 TicketSystem(self.env).reset_ticket_fields()588 588 589 589 def update(self, db=None): 590 590 assert self.exists, 'Cannot update non-existent component' … … 607 607 cursor.execute("UPDATE ticket SET component=%s WHERE component=%s", 608 608 (self.name, self._old_name)) 609 609 self._old_name = self.name 610 TicketSystem(self.env).reset_ticket_fields(db) 610 611 611 612 if handle_ta: 612 613 db.commit() 613 TicketSystem(self.env).reset_ticket_fields()614 614 615 615 @classmethod 616 616 def select(cls, env, db=None): … … 688 688 ticket['milestone'] = retarget_to 689 689 ticket.save_changes(author, 'Milestone %s deleted' % self.name, 690 690 now, db=db) 691 TicketSystem(self.env).reset_ticket_fields(db) 691 692 692 693 if handle_ta: 693 694 db.commit() 694 TicketSystem(self.env).reset_ticket_fields()695 695 696 696 def insert(self, db=None): 697 697 assert self.name, 'Cannot create milestone with no name' … … 708 708 "VALUES (%s,%s,%s,%s)", 709 709 (self.name, to_timestamp(self.due), to_timestamp(self.completed), 710 710 self.description)) 711 TicketSystem(self.env).reset_ticket_fields(db) 711 712 712 713 if handle_ta: 713 714 db.commit() 714 TicketSystem(self.env).reset_ticket_fields()715 715 716 716 def update(self, db=None): 717 717 assert self.name, 'Cannot update milestone with no name' … … 734 734 cursor.execute("UPDATE ticket SET milestone=%s WHERE milestone=%s", 735 735 (self.name, self._old_name)) 736 736 self._old_name = self.name 737 TicketSystem(self.env).reset_ticket_fields(db) 737 738 738 739 if handle_ta: 739 740 db.commit() 740 TicketSystem(self.env).reset_ticket_fields()741 741 742 742 @classmethod 743 743 def select(cls, env, include_completed=True, db=None): … … 814 814 cursor.execute("DELETE FROM version WHERE name=%s", (self.name,)) 815 815 816 816 self.name = self._old_name = None 817 TicketSystem(self.env).reset_ticket_fields(db) 817 818 818 819 if handle_ta: 819 820 db.commit() 820 TicketSystem(self.env).reset_ticket_fields()821 821 822 822 def insert(self, db=None): 823 823 assert not self.exists, 'Cannot insert existing version' … … 834 834 cursor.execute("INSERT INTO version (name,time,description) " 835 835 "VALUES (%s,%s,%s)", 836 836 (self.name, to_timestamp(self.time), self.description)) 837 TicketSystem(self.env).reset_ticket_fields(db) 837 838 838 839 if handle_ta: 839 840 db.commit() 840 TicketSystem(self.env).reset_ticket_fields()841 841 842 842 def update(self, db=None): 843 843 assert self.exists, 'Cannot update non-existent version' … … 860 860 cursor.execute("UPDATE ticket SET version=%s WHERE version=%s", 861 861 (self.name, self._old_name)) 862 862 self._old_name = self.name 863 TicketSystem(self.env).reset_ticket_fields(db) 863 864 864 865 if handle_ta: 865 866 db.commit() 866 TicketSystem(self.env).reset_ticket_fields()867 867 868 868 @classmethod 869 869 def select(cls, env, db=None): -
new file trac/upgrades/db22.py
diff --git a/trac/upgrades/db22.py b/trac/upgrades/db22.py new file mode 100644
- + 1 from trac.db import Table, Column, DatabaseManager 2 3 def do_upgrade(env, ver, cursor): 4 """Add the cache table.""" 5 table = Table('cache', key='key')[ 6 Column('key'), 7 Column('generation') 8 ] 9 db_connector, _ = DatabaseManager(env)._get_connector() 10 for stmt in db_connector.to_sql(table): 11 cursor.execute(stmt) -
trac/wiki/api.py
diff --git a/trac/wiki/api.py b/trac/wiki/api.py
a b 16 16 # Author: Jonas Borgström <jonas@edgewall.com> 17 17 # Christopher Lenz <cmlenz@gmx.de> 18 18 19 try:20 import threading21 except ImportError:22 import dummy_threading as threading23 import time24 19 import urllib 25 20 import re 26 21 from StringIO import StringIO 27 22 28 23 from genshi.builder import tag 29 24 25 from trac.cache import cached_value 30 26 from trac.config import BoolOption 31 27 from trac.core import * 32 28 from trac.resource import IResourceManager … … 164 160 macro_providers = ExtensionPoint(IWikiMacroProvider) 165 161 syntax_providers = ExtensionPoint(IWikiSyntaxProvider) 166 162 167 INDEX_UPDATE_INTERVAL = 5 # seconds168 169 163 ignore_missing_pages = BoolOption('wiki', 'ignore_missing_pages', 'false', 170 164 """Enable/disable highlighting CamelCase links to missing pages 171 165 (''since 0.9'').""") … … 182 176 For public sites where anonymous users can edit the wiki it is 183 177 recommended to leave this option disabled (which is the default).""") 184 178 185 def __init__(self): 186 self._index = None 187 self._last_index_update = 0 188 self._index_lock = threading.RLock() 189 190 def _update_index(self): 191 self._index_lock.acquire() 192 try: 193 now = time.time() 194 if now > self._last_index_update + WikiSystem.INDEX_UPDATE_INTERVAL: 195 self.log.debug('Updating wiki page index') 196 db = self.env.get_db_cnx() 197 cursor = db.cursor() 198 cursor.execute("SELECT DISTINCT name FROM wiki") 199 self._index = {} 200 for (name,) in cursor: 201 self._index[name] = True 202 self._last_index_update = now 203 finally: 204 self._index_lock.release() 179 @cached_value 180 def pages(self, db): 181 """Return the names of all existing wiki pages.""" 182 cursor = db.cursor() 183 cursor.execute("SELECT DISTINCT name FROM wiki") 184 return [name for (name,) in cursor] 205 185 206 186 # Public API 207 187 … … 211 191 If the `prefix` parameter is given, only names that start with that 212 192 prefix are included. 213 193 """ 214 self._update_index() 215 # Note: use of keys() is intentional since iterkeys() is prone to 216 # errors with concurrent modification 217 for page in self._index.keys(): 194 for page in self.pages: 218 195 if not prefix or page.startswith(prefix): 219 196 yield page 220 197 221 198 def has_page(self, pagename): 222 199 """Whether a page with the specified name exists.""" 223 self._update_index() 224 return self._index.has_key(pagename.rstrip('/')) 200 return pagename.rstrip('/') in self.pages 225 201 226 202 # IWikiChangeListener methods 227 203 228 204 def wiki_page_added(self, page): 229 205 if not self.has_page(page.name): 230 self.log.debug('Adding page %s to index' % page.name) 231 self._index[page.name] = True 206 del self.pages 232 207 233 208 def wiki_page_changed(self, page, version, t, comment, author, ipnr): 234 209 pass 235 210 236 211 def wiki_page_deleted(self, page): 237 212 if self.has_page(page.name): 238 self.log.debug('Removing page %s from index' % page.name) 239 del self._index[page.name] 213 del self.pages 240 214 241 215 def wiki_page_version_deleted(self, page): 242 216 pass -
trac/wiki/interwiki.py
diff --git a/trac/wiki/interwiki.py b/trac/wiki/interwiki.py
a b 15 15 # Author: Christian Boos <cboos@neuf.fr> 16 16 17 17 import re 18 try:19 import threading20 except ImportError:21 import dummy_threading as threading22 18 23 19 from genshi.builder import tag 24 20 21 from trac.cache import cached_value 25 22 from trac.core import * 26 23 from trac.wiki.formatter import Formatter 27 24 from trac.wiki.parser import WikiParser … … 37 34 _interwiki_re = re.compile(r"(%s)[ \t]+([^ \t]+)(?:[ \t]+#(.*))?" % 38 35 WikiParser.LINK_SCHEME, re.UNICODE) 39 36 _argspec_re = re.compile(r"\$\d") 40 _interwiki_map = None41 37 42 def __init__(self): 43 self._interwiki_lock = threading.RLock() 44 45 def reset(self): 46 self._interwiki_map = None 47 self.config.touch() 48 # This dictionary maps upper-cased namespaces 49 # to (namespace, prefix, title) values; 50 51 # The component itself behaves as a map 38 # The component itself behaves as a read-only map 52 39 53 40 def __contains__(self, ns): 54 41 return ns.upper() in self.interwiki_map … … 56 43 def __getitem__(self, ns): 57 44 return self.interwiki_map[ns.upper()] 58 45 59 def __setitem__(self, ns, value):60 self.interwiki_map[ns.upper()] = value61 62 46 def keys(self): 63 47 return self.interwiki_map.keys() 64 48 … … 102 86 103 87 def wiki_page_changed(self, page, version, t, comment, author, ipnr): 104 88 if page.name == InterWikiMap._page_name: 105 self.reset()89 del self.interwiki_map 106 90 107 91 def wiki_page_deleted(self, page): 108 92 if page.name == InterWikiMap._page_name: 109 self.reset()93 del self.interwiki_map 110 94 111 95 def wiki_page_version_deleted(self, page): 112 96 if page.name == InterWikiMap._page_name: 113 self.reset()97 del self.interwiki_map 114 98 115 def _get_interwiki_map(self): 99 @cached_value 100 def interwiki_map(self, db): 101 """Map from upper-cased namespaces to (namespace, prefix, title) 102 values. 103 """ 116 104 from trac.wiki.model import WikiPage 117 if self._interwiki_map is None:118 self._interwiki_lock.acquire()119 try:120 if self._interwiki_map is None:121 self._interwiki_map = {}122 content = WikiPage(self.env, InterWikiMap._page_name).text105 map = {} 106 content = WikiPage(self.env, InterWikiMap._page_name, db=db).text 107 in_map = False 108 for line in content.split('\n'): 109 if in_map: 110 if line.startswith('----'): 123 111 in_map = False 124 for line in content.split('\n'): 125 if in_map: 126 if line.startswith('----'): 127 in_map = False 128 else: 129 m = re.match(InterWikiMap._interwiki_re, line) 130 if m: 131 prefix, url, title = m.groups() 132 url = url.strip() 133 title = title and title.strip() or prefix 134 self[prefix] = (prefix, url, title) 135 elif line.startswith('----'): 136 in_map = True 137 finally: 138 self._interwiki_lock.release() 139 return self._interwiki_map 140 interwiki_map = property(_get_interwiki_map) 112 else: 113 m = re.match(InterWikiMap._interwiki_re, line) 114 if m: 115 prefix, url, title = m.groups() 116 url = url.strip() 117 title = title and title.strip() or prefix 118 map[prefix.upper()] = (prefix, url, title) 119 elif line.startswith('----'): 120 in_map = True 121 return map 141 122 142 123 # IWikiMacroProvider methods 143 124
