Edgewall Software

Ticket #9536: 9536-db-context-manager-r10102.3.patch

File 9536-db-context-manager-r10102.3.patch, 8.9 KB (added by cboos, 21 months ago)

only wrap once with a readonly ConnectionWrapper, and remove trac.env / trac.db.api round trips for obtaining a connection

  • trac/db/api.py

    # HG changeset patch
    # Parent 37d3f5c479c7bb49855eef3e74b2423d99862a23
    db: introduce context managers for accessing `Connection` instances.
    
    This continues the work started in #8751.
    
    Part of #9636.
    
    diff -r 37d3f5c479c7 trac/db/api.py
    a b import time 
    2020 
    2121from trac.config import BoolOption, IntOption, Option 
    2222from trac.core import * 
    23 from trac.db.pool import ConnectionPool 
    2423from trac.util.concurrency import ThreadLocal 
    2524from trac.util.text import unicode_passwd 
    2625from trac.util.translation import _ 
    2726 
     27from .pool import ConnectionPool 
     28from .util import ConnectionWrapper 
     29 
    2830_transaction_local = ThreadLocal(db=None) 
    2931 
    3032def with_transaction(env, db=None): 
    3133    """Function decorator to emulate a context manager for database 
    3234    transactions. 
    33      
     35 
    3436    >>> def api_method(p1, p2): 
    3537    >>>     result[0] = value1 
    3638    >>>     @with_transaction(env) 
    def with_transaction(env, db=None): 
    3840    >>>         # implementation 
    3941    >>>         result[0] = value2 
    4042    >>>     return result[0] 
     43 
     44    :deprecated: use instead the new context manager: 
     45 
     46    >>> def api_method(p1, p2): 
     47    >>>     result = value1 
     48    >>>     with env.db_transaction as db: 
     49    >>>         # implementation 
     50    >>>         result = value2 
     51    >>>     return result 
    4152     
    4253    In this example, the `implementation()` function is called automatically 
    4354    right after its definition, with a database connection as an argument. 
    def with_transaction(env, db=None): 
    5162    or rollback, for mutating database accesses. Its automatic handling of 
    5263    commit, rollback and nesting makes it much more robust. 
    5364     
    54     This decorator will be replaced by a context manager once python 2.4 
    55     support is dropped. 
    56  
    5765    The optional `db` argument is intended for legacy code and should not 
    5866    be used in new code. 
     67 
     68    This decorator is in turn deprecated in favor of context managers  
     69    now that python 2.4 support has been dropped. 
    5970    """ 
    6071    def transaction_wrapper(fn): 
    6172        ldb = _transaction_local.db 
    def with_transaction(env, db=None): 
    7283        elif ldb: 
    7384            fn(ldb) 
    7485        else: 
    75             ldb = _transaction_local.db = env.get_db_cnx() 
     86            ldb = _transaction_local.db = get_write_db(env) 
    7687            try: 
    7788                fn(ldb) 
    7889                ldb.commit() 
    def with_transaction(env, db=None): 
    8798 
    8899def get_read_db(env): 
    89100    """Get a database connection for reading only.""" 
     101    db = get_write_db(env) 
     102    if not db.readonly: 
     103        db = ConnectionWrapper(db, readonly=True) 
     104    return db 
     105 
     106def get_write_db(env): 
     107    """Get a database connection.""" 
    90108    return _transaction_local.db or DatabaseManager(env).get_connection() 
    91109 
     110class TransactionContextManager(object): 
     111    """Transactioned Database Context Manager 
     112 
     113    The outermost such context manager will either commit or rollback, 
     114    depending on the context being exited normally or after an exception. 
     115    """ 
     116 
     117    db = None 
     118 
     119    def __init__(self, env): 
     120        self.env = env 
     121 
     122    def __enter__(self): 
     123        db = _transaction_local.db 
     124        if not db: 
     125            _transaction_local.db = self.db = db = get_write_db(self.env) 
     126        return db 
     127 
     128    def __exit__(self, et, ev, tb):  
     129        if self.db is not None:  
     130            _transaction_local.db = None 
     131            if et is None:  
     132                self.db.commit() 
     133            else:  
     134                self.db.rollback() 
     135 
     136 
     137class QueryContextManager(object): 
     138    """Database Context Manager for retrieving a readonly ConnectionWrapper""" 
     139 
     140    def __init__(self, env): 
     141        self.env = env 
     142 
     143    def __enter__(self): 
     144        db = _transaction_local.db 
     145        if not db: 
     146            db = get_read_db(self.env) 
     147        return db 
     148 
     149    def __exit__(self, et, ev, tb):  
     150        pass 
     151 
    92152 
    93153class IDatabaseConnector(Interface): 
    94154    """Extension point interface for components that support the connection to 
  • trac/db/util.py

    diff -r 37d3f5c479c7 trac/db/util.py
    a b class ConnectionWrapper(object): 
    9191     
    9292    :since 0.12: This wrapper no longer makes cursors produced by the 
    9393    connection iterable using `IterableCursor`. 
     94 
     95    :since 0.13: added a 'readonly' flag preventing the forwarding 
     96    of `commit` and `rollback`. 
    9497    """ 
    95     __slots__ = ('cnx', 'log') 
     98    __slots__ = ('cnx', 'log', 'readonly') 
    9699 
    97     def __init__(self, cnx, log=None): 
     100    def __init__(self, cnx, log=None, readonly=False): 
    98101        self.cnx = cnx 
    99102        self.log = log 
     103        self.readonly = readonly 
    100104 
    101105    def __getattr__(self, name): 
     106        if self.readonly and name in ('commit', 'rollback'): 
     107            raise AttributeError 
    102108        return getattr(self.cnx, name) 
  • trac/env.py

    diff -r 37d3f5c479c7 trac/env.py
    a b from trac.cache import CacheManager 
    2525from trac.config import * 
    2626from trac.core import Component, ComponentManager, implements, Interface, \ 
    2727                      ExtensionPoint, TracError 
    28 from trac.db.api import DatabaseManager, get_read_db, with_transaction 
     28from trac.db.api import (DatabaseManager, QueryContextManager,  
     29                         TransactionContextManager, get_read_db, get_write_db, 
     30                         with_transaction) 
    2931from trac.util import copytree, create_file, get_pkginfo, makedirs 
    3032from trac.util.compat import any 
    3133from trac.util.concurrency import threading 
    class Environment(Component, ComponentMa 
    323325        Use `with_transaction` for obtaining a writable database connection 
    324326        and `get_read_db` for anything else. 
    325327        """ 
    326         return get_read_db(self) 
     328        return get_write_db(self) 
    327329 
    328330    def with_transaction(self, db=None): 
    329         """Decorator for transaction functions. 
    330  
    331         See `trac.db.api.with_transaction` for detailed documentation.""" 
     331        """Decorator for transaction functions (deprecated)""" 
    332332        return with_transaction(self, db) 
    333333 
    334334    def get_read_db(self): 
    class Environment(Component, ComponentMa 
    337337        See `trac.db.api.get_read_db` for detailed documentation.""" 
    338338        return get_read_db(self) 
    339339 
     340    @property 
     341    def db_query(self): 
     342        return QueryContextManager(self) 
     343 
     344    @property 
     345    def db_transaction(self): 
     346        return TransactionContextManager(self) 
     347 
    340348    def shutdown(self, tid=None): 
    341349        """Close the environment.""" 
    342350        RepositoryManager(self).shutdown(tid) 
    class Environment(Component, ComponentMa 
    507515        @return: whether the upgrade was performed 
    508516        """ 
    509517        upgraders = [] 
    510         db = self.get_read_db() 
     518        db = self.get_write_db() 
    511519        for participant in self.setup_participants: 
    512520            if participant.environment_needs_upgrade(db): 
    513521                upgraders.append(participant) 
  • trac/test.py

    diff -r 37d3f5c479c7 trac/test.py
    a b from trac.core import Component, Compone 
    3232from trac.env import Environment 
    3333from trac.db.api import _parse_db_str, DatabaseManager 
    3434from trac.db.sqlite_backend import SQLiteConnection 
     35from trac.db.util import ConnectionWrapper 
    3536import trac.db.postgres_backend 
    3637import trac.db.mysql_backend 
    3738from trac.ticket.default_workflow import load_workflow_config_snippet 
    class EnvironmentStub(Environment): 
    289290    def get_read_db(self): 
    290291        return self.get_db_cnx() 
    291292     
     293    def get_write_db(self): 
     294        return self.get_db_cnx() 
     295     
    292296    def get_db_cnx(self, destroying=False): 
    293297        if self.db: 
    294298            return self.db # in-memory SQLite 
    class EnvironmentStub(Environment): 
    306310                self.reset_db() # make sure we get rid of previous garbage 
    307311        return DatabaseManager(dbenv).get_connection() 
    308312 
     313    # see below, functions get_read_db_overrider, get_write_db_overrider 
     314 
    309315    def reset_db(self, default_data=None): 
    310316        """Remove all data from Trac tables, keeping the tables themselves. 
    311317        :param default_data: after clean-up, initialize with default data 
    class EnvironmentStub(Environment): 
    387393        return self.known_users 
    388394 
    389395 
     396# As we'll create lots of Environments during testing, we want to reuse  
     397# the same DatabaseManager instance (see EnvironmentStub.get_db_cnx above), 
     398# so we need to override the connection dispatching in trac.db.api. 
     399 
     400def get_read_db_overrider(env): 
     401    db = env.get_db_cnx() 
     402    if not db.readonly: 
     403        db = ConnectionWrapper(db, readonly=True) 
     404    return db 
     405 
     406def get_write_db_overrider(env): 
     407    return env.get_db_cnx() 
     408 
     409from trac.db import api as dbapi 
     410dbapi.get_read_db = get_read_db_overrider 
     411dbapi.get_write_db = get_write_db_overrider 
     412 
     413 
    390414def locate(fn): 
    391415    """Locates a binary on the path. 
    392416