# 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 |
| 20 | 20 | |
| 21 | 21 | from trac.config import BoolOption, IntOption, Option |
| 22 | 22 | from trac.core import * |
| 23 | | from trac.db.pool import ConnectionPool |
| 24 | 23 | from trac.util.concurrency import ThreadLocal |
| 25 | 24 | from trac.util.text import unicode_passwd |
| 26 | 25 | from trac.util.translation import _ |
| 27 | 26 | |
| | 27 | from .pool import ConnectionPool |
| | 28 | from .util import ConnectionWrapper |
| | 29 | |
| 28 | 30 | _transaction_local = ThreadLocal(db=None) |
| 29 | 31 | |
| 30 | 32 | def with_transaction(env, db=None): |
| 31 | 33 | """Function decorator to emulate a context manager for database |
| 32 | 34 | transactions. |
| 33 | | |
| | 35 | |
| 34 | 36 | >>> def api_method(p1, p2): |
| 35 | 37 | >>> result[0] = value1 |
| 36 | 38 | >>> @with_transaction(env) |
| … |
… |
def with_transaction(env, db=None): |
| 38 | 40 | >>> # implementation |
| 39 | 41 | >>> result[0] = value2 |
| 40 | 42 | >>> 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 |
| 41 | 52 | |
| 42 | 53 | In this example, the `implementation()` function is called automatically |
| 43 | 54 | right after its definition, with a database connection as an argument. |
| … |
… |
def with_transaction(env, db=None): |
| 51 | 62 | or rollback, for mutating database accesses. Its automatic handling of |
| 52 | 63 | commit, rollback and nesting makes it much more robust. |
| 53 | 64 | |
| 54 | | This decorator will be replaced by a context manager once python 2.4 |
| 55 | | support is dropped. |
| 56 | | |
| 57 | 65 | The optional `db` argument is intended for legacy code and should not |
| 58 | 66 | 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. |
| 59 | 70 | """ |
| 60 | 71 | def transaction_wrapper(fn): |
| 61 | 72 | ldb = _transaction_local.db |
| … |
… |
def with_transaction(env, db=None): |
| 72 | 83 | elif ldb: |
| 73 | 84 | fn(ldb) |
| 74 | 85 | else: |
| 75 | | ldb = _transaction_local.db = env.get_db_cnx() |
| | 86 | ldb = _transaction_local.db = get_write_db(env) |
| 76 | 87 | try: |
| 77 | 88 | fn(ldb) |
| 78 | 89 | ldb.commit() |
| … |
… |
def with_transaction(env, db=None): |
| 87 | 98 | |
| 88 | 99 | def get_read_db(env): |
| 89 | 100 | """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 | |
| | 106 | def get_write_db(env): |
| | 107 | """Get a database connection.""" |
| 90 | 108 | return _transaction_local.db or DatabaseManager(env).get_connection() |
| 91 | 109 | |
| | 110 | class 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 | |
| | 137 | class 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 | |
| 92 | 152 | |
| 93 | 153 | class IDatabaseConnector(Interface): |
| 94 | 154 | """Extension point interface for components that support the connection to |
diff -r 37d3f5c479c7 trac/db/util.py
|
a
|
b
|
class ConnectionWrapper(object): |
| 91 | 91 | |
| 92 | 92 | :since 0.12: This wrapper no longer makes cursors produced by the |
| 93 | 93 | connection iterable using `IterableCursor`. |
| | 94 | |
| | 95 | :since 0.13: added a 'readonly' flag preventing the forwarding |
| | 96 | of `commit` and `rollback`. |
| 94 | 97 | """ |
| 95 | | __slots__ = ('cnx', 'log') |
| | 98 | __slots__ = ('cnx', 'log', 'readonly') |
| 96 | 99 | |
| 97 | | def __init__(self, cnx, log=None): |
| | 100 | def __init__(self, cnx, log=None, readonly=False): |
| 98 | 101 | self.cnx = cnx |
| 99 | 102 | self.log = log |
| | 103 | self.readonly = readonly |
| 100 | 104 | |
| 101 | 105 | def __getattr__(self, name): |
| | 106 | if self.readonly and name in ('commit', 'rollback'): |
| | 107 | raise AttributeError |
| 102 | 108 | return getattr(self.cnx, name) |
diff -r 37d3f5c479c7 trac/env.py
|
a
|
b
|
from trac.cache import CacheManager |
| 25 | 25 | from trac.config import * |
| 26 | 26 | from trac.core import Component, ComponentManager, implements, Interface, \ |
| 27 | 27 | ExtensionPoint, TracError |
| 28 | | from trac.db.api import DatabaseManager, get_read_db, with_transaction |
| | 28 | from trac.db.api import (DatabaseManager, QueryContextManager, |
| | 29 | TransactionContextManager, get_read_db, get_write_db, |
| | 30 | with_transaction) |
| 29 | 31 | from trac.util import copytree, create_file, get_pkginfo, makedirs |
| 30 | 32 | from trac.util.compat import any |
| 31 | 33 | from trac.util.concurrency import threading |
| … |
… |
class Environment(Component, ComponentMa |
| 323 | 325 | Use `with_transaction` for obtaining a writable database connection |
| 324 | 326 | and `get_read_db` for anything else. |
| 325 | 327 | """ |
| 326 | | return get_read_db(self) |
| | 328 | return get_write_db(self) |
| 327 | 329 | |
| 328 | 330 | 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)""" |
| 332 | 332 | return with_transaction(self, db) |
| 333 | 333 | |
| 334 | 334 | def get_read_db(self): |
| … |
… |
class Environment(Component, ComponentMa |
| 337 | 337 | See `trac.db.api.get_read_db` for detailed documentation.""" |
| 338 | 338 | return get_read_db(self) |
| 339 | 339 | |
| | 340 | @property |
| | 341 | def db_query(self): |
| | 342 | return QueryContextManager(self) |
| | 343 | |
| | 344 | @property |
| | 345 | def db_transaction(self): |
| | 346 | return TransactionContextManager(self) |
| | 347 | |
| 340 | 348 | def shutdown(self, tid=None): |
| 341 | 349 | """Close the environment.""" |
| 342 | 350 | RepositoryManager(self).shutdown(tid) |
| … |
… |
class Environment(Component, ComponentMa |
| 507 | 515 | @return: whether the upgrade was performed |
| 508 | 516 | """ |
| 509 | 517 | upgraders = [] |
| 510 | | db = self.get_read_db() |
| | 518 | db = self.get_write_db() |
| 511 | 519 | for participant in self.setup_participants: |
| 512 | 520 | if participant.environment_needs_upgrade(db): |
| 513 | 521 | upgraders.append(participant) |
diff -r 37d3f5c479c7 trac/test.py
|
a
|
b
|
from trac.core import Component, Compone |
| 32 | 32 | from trac.env import Environment |
| 33 | 33 | from trac.db.api import _parse_db_str, DatabaseManager |
| 34 | 34 | from trac.db.sqlite_backend import SQLiteConnection |
| | 35 | from trac.db.util import ConnectionWrapper |
| 35 | 36 | import trac.db.postgres_backend |
| 36 | 37 | import trac.db.mysql_backend |
| 37 | 38 | from trac.ticket.default_workflow import load_workflow_config_snippet |
| … |
… |
class EnvironmentStub(Environment): |
| 289 | 290 | def get_read_db(self): |
| 290 | 291 | return self.get_db_cnx() |
| 291 | 292 | |
| | 293 | def get_write_db(self): |
| | 294 | return self.get_db_cnx() |
| | 295 | |
| 292 | 296 | def get_db_cnx(self, destroying=False): |
| 293 | 297 | if self.db: |
| 294 | 298 | return self.db # in-memory SQLite |
| … |
… |
class EnvironmentStub(Environment): |
| 306 | 310 | self.reset_db() # make sure we get rid of previous garbage |
| 307 | 311 | return DatabaseManager(dbenv).get_connection() |
| 308 | 312 | |
| | 313 | # see below, functions get_read_db_overrider, get_write_db_overrider |
| | 314 | |
| 309 | 315 | def reset_db(self, default_data=None): |
| 310 | 316 | """Remove all data from Trac tables, keeping the tables themselves. |
| 311 | 317 | :param default_data: after clean-up, initialize with default data |
| … |
… |
class EnvironmentStub(Environment): |
| 387 | 393 | return self.known_users |
| 388 | 394 | |
| 389 | 395 | |
| | 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 | |
| | 400 | def 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 | |
| | 406 | def get_write_db_overrider(env): |
| | 407 | return env.get_db_cnx() |
| | 408 | |
| | 409 | from trac.db import api as dbapi |
| | 410 | dbapi.get_read_db = get_read_db_overrider |
| | 411 | dbapi.get_write_db = get_write_db_overrider |
| | 412 | |
| | 413 | |
| 390 | 414 | def locate(fn): |
| 391 | 415 | """Locates a binary on the path. |
| 392 | 416 | |