Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/admin/web_ui.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/admin/web_ui.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/admin/web_ui.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/admin/web_ui.py Sat Nov 15 01:14:46 2014 @@ -22,11 +22,6 @@ import pkg_resources import re import shutil -try: - from babel.core import Locale -except ImportError: - Locale = None - from genshi import HTML from genshi.builder import tag @@ -34,10 +29,10 @@ from trac.admin.api import IAdminPanelPr from trac.core import * from trac.loader import get_plugin_info, get_plugins_dir from trac.perm import PermissionSystem, IPermissionRequestor -from trac.util.datefmt import all_timezones +from trac.util.datefmt import all_timezones, pytz from trac.util.text import exception_to_unicode, \ - unicode_to_base64, unicode_from_base64 -from trac.util.translation import _, get_available_locales, ngettext + unicode_to_base64, unicode_from_base64 +from trac.util.translation import _, Locale, get_available_locales, ngettext from trac.web import HTTPNotFound, IRequestHandler from trac.web.chrome import add_notice, add_stylesheet, \ add_warning, Chrome, INavigationContributor, \ @@ -76,8 +71,8 @@ class AdminModule(Component): # admin panel is available panels, providers = self._get_panels(req) if panels: - yield 'mainnav', 'admin', tag.a(_('Admin'), href=req.href.admin(), - title=_('Administration')) + yield 'mainnav', 'admin', tag.a(_("Admin"), href=req.href.admin(), + title=_("Administration")) # IRequestHandler methods @@ -93,7 +88,7 @@ class AdminModule(Component): def process_request(self, req): panels, providers = self._get_panels(req) if not panels: - raise HTTPNotFound(_('No administration panels available')) + raise HTTPNotFound(_("No administration panels available")) def _panel_order(p1, p2): if p1[::2] == ('general', 'basics'): @@ -116,14 +111,14 @@ class AdminModule(Component): path_info = req.args.get('path_info') if not panel_id: try: - panel_id = filter( - lambda panel: panel[0] == cat_id, panels)[0][2] + panel_id = \ + filter(lambda panel: panel[0] == cat_id, panels)[0][2] except IndexError: - raise HTTPNotFound(_('Unknown administration panel')) + raise HTTPNotFound(_("Unknown administration panel")) provider = providers.get((cat_id, panel_id), None) if not provider: - raise HTTPNotFound(_('Unknown administration panel')) + raise HTTPNotFound(_("Unknown administration panel")) if hasattr(provider, 'render_admin_panel'): template, data = provider.render_admin_panel(req, cat_id, panel_id, @@ -201,14 +196,14 @@ def _save_config(config, req, log, notic try: config.save() if notices is None: - notices = [_('Your changes have been saved.')] + notices = [_("Your changes have been saved.")] for notice in notices: add_notice(req, notice) except Exception, e: - log.error('Error writing to trac.ini: %s', exception_to_unicode(e)) - add_warning(req, _('Error writing to trac.ini, make sure it is ' - 'writable by the web server. Your changes have ' - 'not been saved.')) + log.error("Error writing to trac.ini: %s", exception_to_unicode(e)) + add_warning(req, _("Error writing to trac.ini, make sure it is " + "writable by the web server. Your changes have " + "not been saved.")) class BasicsAdminPanel(Component): @@ -218,19 +213,19 @@ class BasicsAdminPanel(Component): # IAdminPanelProvider methods def get_admin_panels(self, req): - if 'TRAC_ADMIN' in req.perm: - yield ('general', _('General'), 'basics', _('Basic Settings')) + if 'TRAC_ADMIN' in req.perm('admin', 'general/basics'): + yield ('general', _("General"), 'basics', _("Basic Settings")) def render_admin_panel(self, req, cat, page, path_info): - req.perm.require('TRAC_ADMIN') - if Locale: - locales = [Locale.parse(locale) - for locale in get_available_locales()] - languages = sorted((str(locale), locale.display_name) - for locale in locales) + locale_ids = get_available_locales() + locales = [Locale.parse(locale) for locale in locale_ids] + # don't use str(locale) to prevent storing expanded locale + # identifier, see #11258 + languages = sorted((id, locale.display_name) + for id, locale in zip(locale_ids, locales)) else: - locales, languages = [], [] + locale_ids, locales, languages = [], [], [] if req.method == 'POST': for option in ('name', 'url', 'descr'): @@ -242,7 +237,7 @@ class BasicsAdminPanel(Component): self.config.set('trac', 'default_timezone', default_timezone) default_language = req.args.get('default_language') - if default_language not in locales: + if default_language not in locale_ids: default_language = '' self.config.set('trac', 'default_language', default_language) @@ -261,9 +256,11 @@ class BasicsAdminPanel(Component): data = { 'default_timezone': default_timezone, 'timezones': all_timezones, + 'has_pytz': pytz is not None, 'default_language': default_language.replace('-', '_'), 'languages': languages, 'default_date_format': default_date_format, + 'has_babel': Locale is not None, } Chrome(self.env).add_textarea_grips(req) return 'admin_basics.html', data @@ -276,7 +273,8 @@ class LoggingAdminPanel(Component): # IAdminPanelProvider methods def get_admin_panels(self, req): - if 'TRAC_ADMIN' in req.perm and not getattr(self.env, 'parent', None): + if 'TRAC_ADMIN' in req.perm('admin', 'general/logging') and \ + not getattr(self.env, 'parent', None): yield ('general', _('General'), 'logging', _('Logging')) def render_admin_panel(self, req, cat, page, path_info): @@ -288,16 +286,18 @@ class LoggingAdminPanel(Component): log_dir = os.path.join(self.env.path, 'log') log_types = [ - dict(name='none', label=_('None'), selected=log_type == 'none', disabled=False), - dict(name='stderr', label=_('Console'), + dict(name='none', label=_("None"), + selected=log_type == 'none', disabled=False), + dict(name='stderr', label=_("Console"), selected=log_type == 'stderr', disabled=False), - dict(name='file', label=_('File'), selected=log_type == 'file', - disabled=False), - dict(name='syslog', label=_('Syslog'), disabled=os.name != 'posix', - selected=log_type in ('unix', 'syslog')), - dict(name='eventlog', label=_('Windows event log'), - disabled=os.name != 'nt', - selected=log_type in ('winlog', 'eventlog', 'nteventlog')), + dict(name='file', label=_("File"), + selected=log_type == 'file', disabled=False), + dict(name='syslog', label=_("Syslog"), + selected=log_type in ('unix', 'syslog'), + disabled=os.name != 'posix'), + dict(name='eventlog', label=_("Windows event log"), + selected=log_type in ('winlog', 'eventlog', 'nteventlog'), + disabled=os.name != 'nt'), ] log_levels = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'] @@ -308,8 +308,8 @@ class LoggingAdminPanel(Component): new_type = req.args.get('log_type') if new_type not in [t['name'] for t in log_types]: raise TracError( - _('Unknown log type %(type)s', type=new_type), - _('Invalid log type') + _("Unknown log type %(type)s", type=new_type), + _("Invalid log type") ) if new_type != log_type: self.config.set('logging', 'log_type', new_type) @@ -323,8 +323,8 @@ class LoggingAdminPanel(Component): new_level = req.args.get('log_level') if new_level not in log_levels: raise TracError( - _('Unknown log level %(level)s', level=new_level), - _('Invalid log level')) + _("Unknown log level %(level)s", level=new_level), + _("Invalid log level")) if new_level != log_level: self.config.set('logging', 'log_level', new_level) changed = True @@ -337,8 +337,8 @@ class LoggingAdminPanel(Component): changed = True log_file = new_file if not log_file: - raise TracError(_('You must specify a log file'), - _('Missing field')) + raise TracError(_("You must specify a log file"), + _("Missing field")) else: self.config.remove('logging', 'log_file') changed = True @@ -366,8 +366,9 @@ class PermissionAdminPanel(Component): # IAdminPanelProvider methods def get_admin_panels(self, req): - if 'PERMISSION_GRANT' in req.perm or 'PERMISSION_REVOKE' in req.perm: - yield ('general', _('General'), 'perm', _('Permissions')) + perm = req.perm('admin', 'general/perm') + if 'PERMISSION_GRANT' in perm or 'PERMISSION_REVOKE' in perm: + yield ('general', _("General"), 'perm', _("Permissions")) def render_admin_panel(self, req, cat, page, path_info): perm = PermissionSystem(self.env) @@ -380,51 +381,57 @@ class PermissionAdminPanel(Component): group = req.args.get('group', '').strip() if subject and subject.isupper() or \ - group and group.isupper(): - raise TracError(_('All upper-cased tokens are reserved for ' - 'permission names')) + group and group.isupper(): + raise TracError(_("All upper-cased tokens are reserved for " + "permission names")) # Grant permission to subject if req.args.get('add') and subject and action: - req.perm.require('PERMISSION_GRANT') + req.perm('admin', 'general/perm').require('PERMISSION_GRANT') if action not in all_actions: - raise TracError(_('Unknown action')) + raise TracError(_("Unknown action")) req.perm.require(action) if (subject, action) not in all_permissions: perm.grant_permission(subject, action) - add_notice(req, _('The subject %(subject)s has been ' - 'granted the permission %(action)s.', + add_notice(req, _("The subject %(subject)s has been " + "granted the permission %(action)s.", subject=subject, action=action)) req.redirect(req.href.admin(cat, page)) else: - add_warning(req, _('The permission %(action)s was already ' - 'granted to %(subject)s.', + add_warning(req, _("The permission %(action)s was already " + "granted to %(subject)s.", action=action, subject=subject)) # Add subject to group elif req.args.get('add') and subject and group: - req.perm.require('PERMISSION_GRANT') + req.perm('admin', 'general/perm').require('PERMISSION_GRANT') for action in perm.get_user_permissions(group): if not action in all_actions: # plugin disabled? - self.env.log.warn("Adding %s to group %s: " \ - "Permission %s unavailable, skipping perm check." \ - % (subject, group, action)) + self.env.log.warn("Adding %s to group %s: " + "Permission %s unavailable, skipping perm check.", + subject, group, action) else: - req.perm.require(action) + req.perm.require(action, + message=_("The subject %(subject)s was not added " + "to the group %(group)s because the " + "group has %(perm)s permission and " + "users cannot grant permissions they " + "don't possess.", subject=subject, + group=group, perm=action)) if (subject, group) not in all_permissions: perm.grant_permission(subject, group) - add_notice(req, _('The subject %(subject)s has been added ' - 'to the group %(group)s.', + add_notice(req, _("The subject %(subject)s has been added " + "to the group %(group)s.", subject=subject, group=group)) req.redirect(req.href.admin(cat, page)) else: - add_warning(req, _('The subject %(subject)s was already ' - 'added to the group %(group)s.', + add_warning(req, _("The subject %(subject)s was already " + "added to the group %(group)s.", subject=subject, group=group)) # Remove permissions action elif req.args.get('remove') and req.args.get('sel'): - req.perm.require('PERMISSION_REVOKE') + req.perm('admin', 'general/perm').require('PERMISSION_REVOKE') sel = req.args.get('sel') sel = sel if isinstance(sel, list) else [sel] for key in sel: @@ -433,8 +440,8 @@ class PermissionAdminPanel(Component): action = unicode_from_base64(action) if (subject, action) in perm.get_all_permissions(): perm.revoke_permission(subject, action) - add_notice(req, _('The selected permissions have been ' - 'revoked.')) + add_notice(req, _("The selected permissions have been " + "revoked.")) req.redirect(req.href.admin(cat, page)) perms = [perm for perm in all_permissions if perm[1].isupper()] @@ -453,13 +460,14 @@ class PluginAdminPanel(Component): # IAdminPanelProvider methods def get_admin_panels(self, req): - if 'TRAC_ADMIN' in req.perm and not getattr(self.env, 'parent', None): + if 'TRAC_ADMIN' in req.perm('admin', 'general/plugin') and \ + not getattr(self.env, 'parent', None): yield ('general', _('General'), 'plugin', _('Plugins')) def render_admin_panel(self, req, cat, page, path_info): if getattr(self.env, 'parent', None): raise PermissionError() - req.perm.require('TRAC_ADMIN') + req.perm('admin', 'general/plugin').require('TRAC_ADMIN') if req.method == 'POST': if 'install' in req.args: @@ -469,7 +477,7 @@ class PluginAdminPanel(Component): else: self._do_update(req) anchor = '' - if req.args.has_key('plugin'): + if 'plugin' in req.args: anchor = '#no%d' % (int(req.args.get('plugin')) + 1) req.redirect(req.href.admin(cat, page) + anchor) @@ -479,26 +487,26 @@ class PluginAdminPanel(Component): def _do_install(self, req): """Install a plugin.""" - if not req.args.has_key('plugin_file'): - raise TracError(_('No file uploaded')) + if 'plugin_file' not in req.args: + raise TracError(_("No file uploaded")) upload = req.args['plugin_file'] if isinstance(upload, unicode) or not upload.filename: - raise TracError(_('No file uploaded')) + raise TracError(_("No file uploaded")) plugin_filename = upload.filename.replace('\\', '/').replace(':', '/') plugin_filename = os.path.basename(plugin_filename) if not plugin_filename: - raise TracError(_('No file uploaded')) + raise TracError(_("No file uploaded")) if not plugin_filename.endswith('.egg') and \ not plugin_filename.endswith('.py'): - raise TracError(_('Uploaded file is not a Python source file or ' - 'egg')) + raise TracError(_("Uploaded file is not a Python source file or " + "egg")) target_path = os.path.join(self.env.path, 'plugins', plugin_filename) if os.path.isfile(target_path): - raise TracError(_('Plugin %(name)s already installed', + raise TracError(_("Plugin %(name)s already installed", name=plugin_filename)) - self.log.info('Installing plugin %s', plugin_filename) + self.log.info("Installing plugin %s", plugin_filename) flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL try: flags += os.O_BINARY @@ -507,7 +515,7 @@ class PluginAdminPanel(Component): pass with os.fdopen(os.open(target_path, flags, 0666), 'w') as target_file: shutil.copyfileobj(upload.file, target_file) - self.log.info('Plugin %s installed to %s', plugin_filename, + self.log.info("Plugin %s installed to %s", plugin_filename, target_path) # TODO: Validate that the uploaded file is actually a valid Trac plugin @@ -522,7 +530,7 @@ class PluginAdminPanel(Component): plugin_path = os.path.join(self.env.path, 'plugins', plugin_filename) if not os.path.isfile(plugin_path): return - self.log.info('Uninstalling plugin %s', plugin_filename) + self.log.info("Uninstalling plugin %s", plugin_filename) os.remove(plugin_path) # Make the environment reset itself on the next request @@ -543,8 +551,8 @@ class PluginAdminPanel(Component): if is_enabled != must_enable: self.config.set('components', component, 'disabled' if is_enabled else 'enabled') - self.log.info('%sabling component %s', - 'Dis' if is_enabled else 'En', component) + self.log.info("%sabling component %s", + "Dis" if is_enabled else "En", component) if must_enable: added.append(component) else: @@ -562,13 +570,13 @@ class PluginAdminPanel(Component): removed.sort() notices = [] if removed: - msg = ngettext('The following component has been disabled:', - 'The following components have been disabled:', + msg = ngettext("The following component has been disabled:", + "The following components have been disabled:", len(removed)) notices.append(tag(msg, make_list(removed))) if added: - msg = ngettext('The following component has been enabled:', - 'The following components have been enabled:', + msg = ngettext("The following component has been enabled:", + "The following components have been enabled:", len(added)) notices.append(tag(msg, make_list(added))) @@ -581,7 +589,7 @@ class PluginAdminPanel(Component): try: return format_to_html(self.env, context, text) except Exception, e: - self.log.error('Unable to render component documentation: %s', + self.log.error("Unable to render component documentation: %s", exception_to_unicode(e, traceback=True)) return tag.pre(text)
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/attachment.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/attachment.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/attachment.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/attachment.py Sat Nov 15 01:14:46 2014 @@ -38,7 +38,7 @@ from trac.mimeview import * from trac.perm import PermissionError, IPermissionPolicy from trac.resource import * from trac.search import search_to_sql, shorten_result -from trac.util import content_disposition, get_reporter_id +from trac.util import content_disposition, create_zipinfo, get_reporter_id from trac.util.compat import sha1 from trac.util.datefmt import format_datetime, from_utimestamp, \ to_datetime, to_utimestamp, utc @@ -93,8 +93,9 @@ class IAttachmentManipulator(Interface): attachment. Therefore, a return value of ``[]`` means everything is OK.""" + class ILegacyAttachmentPolicyDelegate(Interface): - """Interface that can be used by plugins to seemlessly participate + """Interface that can be used by plugins to seamlessly participate to the legacy way of checking for attachment permissions. This should no longer be necessary once it becomes easier to @@ -310,6 +311,12 @@ class Attachment(object): t = to_datetime(t, utc) self.date = t + parent_resource = self.resource.parent + if not resource_exists(self.env, parent_resource): + raise ResourceNotFound( + _("%(parent)s doesn't exist, can't create attachment", + parent=get_resource_name(self.env, parent_resource))) + # Make sure the path to the attachment is inside the environment # attachments directory attachments_dir = os.path.join(os.path.normpath(self.env.path), @@ -341,7 +348,6 @@ class Attachment(object): listener.attachment_added(self) ResourceSystem(self.env).resource_created(self) - @classmethod def select(cls, env, parent_realm, parent_id, db=None): """Iterator yielding all `Attachment` instances attached to @@ -377,7 +383,8 @@ class Attachment(object): os.rmdir(attachment_dir) except OSError, e: env.log.error("Can't delete attachment directory %s: %s", - attachment_dir, exception_to_unicode(e, traceback=True)) + attachment_dir, + exception_to_unicode(e, traceback=True)) @classmethod def reparent_all(cls, env, parent_realm, parent_id, new_realm, new_id): @@ -393,7 +400,8 @@ class Attachment(object): os.rmdir(attachment_dir) except OSError, e: env.log.error("Can't delete attachment directory %s: %s", - attachment_dir, exception_to_unicode(e, traceback=True)) + attachment_dir, + exception_to_unicode(e, traceback=True)) def open(self): path = self.path @@ -436,8 +444,7 @@ class AttachmentModule(Component): CHUNK_SIZE = 4096 max_size = IntOption('attachment', 'max_size', 262144, - """Maximum allowed file size (in bytes) for ticket and wiki - attachments.""") + """Maximum allowed file size (in bytes) for attachments.""") max_zip_size = IntOption('attachment', 'max_zip_size', 2097152, """Maximum allowed total size (in bytes) for an attachment list to be @@ -499,6 +506,10 @@ class AttachmentModule(Component): parent_id, filename = path[:last_slash], path[last_slash + 1:] parent = parent_realm(id=parent_id) + if not resource_exists(self.env, parent): + raise ResourceNotFound( + _("Parent resource %(parent)s doesn't exist", + parent=get_resource_name(self.env, parent))) # Link the attachment page to parent resource parent_name = get_resource_name(self.env, parent) @@ -695,10 +706,6 @@ class AttachmentModule(Component): def _do_save(self, req, attachment): req.perm(attachment.resource).require('ATTACHMENT_CREATE') parent_resource = attachment.resource.parent - if not resource_exists(self.env, parent_resource): - raise ResourceNotFound( - _("%(parent)s doesn't exist, can't create attachment", - parent=get_resource_name(self.env, parent_resource))) if 'cancel' in req.args: req.redirect(get_resource_url(self.env, parent_resource, req.href)) @@ -764,7 +771,7 @@ class AttachmentModule(Component): try: old_attachment = Attachment(self.env, attachment.resource(id=filename)) - if not (req.authname and req.authname != 'anonymous' \ + if not (req.authname and req.authname != 'anonymous' and old_attachment.author == req.authname) \ and 'ATTACHMENT_DELETE' \ not in req.perm(attachment.resource): @@ -774,7 +781,7 @@ class AttachmentModule(Component): "attachments requires ATTACHMENT_DELETE permission.", name=filename)) if (not attachment.description.strip() and - old_attachment.description): + old_attachment.description): attachment.description = old_attachment.description old_attachment.delete() except TracError: @@ -806,7 +813,7 @@ class AttachmentModule(Component): def _render_form(self, req, attachment): req.perm(attachment.resource).require('ATTACHMENT_CREATE') return {'mode': 'new', 'author': get_reporter_id(req), - 'attachment': attachment, 'max_size': self.max_size} + 'attachment': attachment, 'max_size': self.max_size} def _download_as_zip(self, req, parent, attachments=None): if attachments is None: @@ -823,19 +830,14 @@ class AttachmentModule(Component): req.send_header('Content-Disposition', content_disposition('inline', filename)) - from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED + from zipfile import ZipFile, ZIP_DEFLATED buf = StringIO() zipfile = ZipFile(buf, 'w', ZIP_DEFLATED) for attachment in attachments: - zipinfo = ZipInfo() - zipinfo.filename = attachment.filename.encode('utf-8') - zipinfo.flag_bits |= 0x800 # filename is encoded with utf-8 - zipinfo.date_time = attachment.date.utctimetuple()[:6] - zipinfo.compress_type = ZIP_DEFLATED - if attachment.description: - zipinfo.comment = attachment.description.encode('utf-8') - zipinfo.external_attr = 0644 << 16L # needed since Python 2.5 + zipinfo = create_zipinfo(attachment.filename, + mtime=attachment.date, + comment=attachment.description) try: with attachment.open() as fd: zipfile.writestr(zipinfo, fd.read()) @@ -999,7 +1001,7 @@ class LegacyAttachmentPolicy(Component): else: for d in self.delegates: decision = d.check_attachment_permission(action, username, - resource, perm) + resource, perm) if decision is not None: return decision @@ -1113,4 +1115,3 @@ class AttachmentAdmin(Component): finally: if destination is not None: output.close() - Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/cache.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/cache.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/cache.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/cache.py Sat Nov 15 01:14:46 2014 @@ -13,6 +13,8 @@ from __future__ import with_statement +import functools + from .core import Component from .util import arity from .util.concurrency import ThreadLocal, threading @@ -34,12 +36,15 @@ def key_to_id(s): return result -class CachedPropertyBase(object): - """Base class for cached property descriptors""" +class CachedPropertyBase(property): + """Base class for cached property descriptors. + + :since 1.0.2: inherits from `property`. + """ def __init__(self, retriever): self.retriever = retriever - self.__doc__ = retriever.__doc__ + functools.update_wrapper(self, retriever) def make_key(self, cls): attr = self.retriever.__name__ Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/config.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/config.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/config.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/config.py Sat Nov 15 01:14:46 2014 @@ -18,12 +18,13 @@ from ConfigParser import ConfigParser from copy import deepcopy import os.path +from genshi.builder import tag from trac.admin import AdminCommandError, IAdminCommandProvider from trac.core import * from trac.util import AtomicFile, as_bool -from trac.util.compat import cleandoc +from trac.util.compat import cleandoc, wait_for_file_mtime_change from trac.util.text import printout, to_unicode, CRLF -from trac.util.translation import _, N_ +from trac.util.translation import _, N_, tag_ __all__ = ['Configuration', 'ConfigSection', 'Option', 'BoolOption', 'IntOption', 'FloatOption', 'ListOption', 'ChoiceOption', @@ -43,6 +44,12 @@ class ConfigurationError(TracError): """Exception raised when a value in the configuration file is not valid.""" title = N_('Configuration Error') + def __init__(self, message=None, title=None, show_traceback=False): + if message is None: + message = _("Look in the Trac log for more information.") + super(ConfigurationError, self).__init__(message, title, + show_traceback) + class Configuration(object): """Thin layer over `ConfigParser` from the Python standard library. @@ -234,10 +241,12 @@ class Configuration(object): # At this point, all the strings in `sections` are UTF-8 encoded `str` try: + wait_for_file_mtime_change(self.filename) with AtomicFile(self.filename, 'w') as fileobj: fileobj.write('# -*- coding: utf-8 -*-\n\n') - for section, options in sections: - fileobj.write('[%s]\n' % section) + for section_str, options in sections: + fileobj.write('[%s]\n' % section_str) + section = to_unicode(section_str) for key_str, val_str in options: if to_unicode(key_str) in self[section].overridden: fileobj.write('# %s = <inherited>\n' % key_str) @@ -287,7 +296,8 @@ class Configuration(object): def touch(self): if self.filename and os.path.isfile(self.filename) \ - and os.access(self.filename, os.W_OK): + and os.access(self.filename, os.W_OK): + wait_for_file_mtime_change(self.filename) os.utime(self.filename, None) def set_defaults(self, compmgr=None): @@ -296,14 +306,15 @@ class Configuration(object): Values already set in the configuration are not overridden. """ - for section, default_options in self.defaults(compmgr).items(): - for name, value in default_options.items(): - if not self.parser.has_option(_to_utf8(section), - _to_utf8(name)): - if any(parent[section].contains(name, defaults=False) - for parent in self.parents): - value = None - self.set(section, name, value) + for (section, name), option in Option.get_registry(compmgr).items(): + if not self.parser.has_option(_to_utf8(section), _to_utf8(name)): + value = option.default + if any(parent[section].contains(name, defaults=False) + for parent in self.parents): + value = None + if value is not None: + value = option.dumps(value) + self.set(section, name, value) class Section(object): @@ -325,7 +336,7 @@ class Section(object): for parent in self.config.parents: if parent[self.name].contains(key, defaults=False): return True - return defaults and Option.registry.has_key((self.name, key)) + return defaults and (self.name, key) in Option.registry __contains__ = contains @@ -608,27 +619,42 @@ class Option(object): return value def __set__(self, instance, value): - raise AttributeError, 'can\'t set attribute' + raise AttributeError(_("Setting attribute is not allowed.")) def __repr__(self): return '<%s [%s] "%s">' % (self.__class__.__name__, self.section, self.name) + def dumps(self, value): + """Return the value as a string to write to a trac.ini file""" + if value is None: + return '' + if value is True: + return 'enabled' + if value is False: + return 'disabled' + if isinstance(value, unicode): + return value + return to_unicode(value) + class BoolOption(Option): """Descriptor for boolean configuration options.""" + def accessor(self, section, name, default): return section.getbool(name, default) class IntOption(Option): """Descriptor for integer configuration options.""" + def accessor(self, section, name, default): return section.getint(name, default) class FloatOption(Option): """Descriptor for float configuration options.""" + def accessor(self, section, name, default): return section.getfloat(name, default) @@ -647,6 +673,11 @@ class ListOption(Option): def accessor(self, section, name, default): return section.getlist(name, default, self.sep, self.keep_empty) + def dumps(self, value): + if isinstance(value, (list, tuple)): + return self.sep.join(Option.dumps(self, v) or '' for v in value) + return Option.dumps(self, value) + class ChoiceOption(Option): """Descriptor for configuration options providing a choice among a list @@ -678,11 +709,15 @@ class PathOption(Option): Relative paths are resolved to absolute paths using the directory containing the configuration file as the reference. """ + def accessor(self, section, name, default): return section.getpath(name, default) class ExtensionOption(Option): + """Name of a component implementing `interface`. Raises a + `ConfigurationError` if the component cannot be found in the list of + active components implementing the interface.""" def __init__(self, section, name, interface, default=None, doc='', doc_domain='tracini'): @@ -696,11 +731,14 @@ class ExtensionOption(Option): for impl in self.xtnpt.extensions(instance): if impl.__class__.__name__ == value: return impl - raise AttributeError('Cannot find an implementation of the "%s" ' - 'interface named "%s". Please update the option ' - '%s.%s in trac.ini.' - % (self.xtnpt.interface.__name__, value, - self.section, self.name)) + raise ConfigurationError( + tag_("Cannot find an implementation of the %(interface)s " + "interface named %(implementation)s. Please check " + "that the Component is enabled or update the option " + "%(option)s in trac.ini.", + interface=tag.tt(self.xtnpt.interface.__name__), + implementation=tag.tt(value), + option=tag.tt("[%s] %s" % (self.section, self.name)))) class OrderedExtensionsOption(ListOption): @@ -722,9 +760,23 @@ class OrderedExtensionsOption(ListOption return self order = ListOption.__get__(self, instance, owner) components = [] + implementing_classes = [] for impl in self.xtnpt.extensions(instance): + implementing_classes.append(impl.__class__.__name__) if self.include_missing or impl.__class__.__name__ in order: components.append(impl) + not_found = sorted(set(order) - set(implementing_classes)) + if not_found: + raise ConfigurationError( + tag_("Cannot find implementation(s) of the %(interface)s " + "interface named %(implementation)s. Please check " + "that the Component is enabled or update the option " + "%(option)s in trac.ini.", + interface=tag.tt(self.xtnpt.interface.__name__), + implementation=tag( + (', ' if idx != 0 else None, tag.tt(impl)) + for idx, impl in enumerate(not_found)), + option=tag.tt("[%s] %s" % (self.section, self.name)))) def compare(x, y): x, y = x.__class__.__name__, y.__class__.__name__ Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/core.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/core.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/core.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/core.py Sat Nov 15 01:14:46 2014 @@ -141,6 +141,8 @@ class ComponentMeta(type): return self # The normal case where the component is not also the component manager + assert len(args) >= 1 and isinstance(args[0], ComponentManager), \ + "First argument must be a ComponentManager instance" compmgr = args[0] self = compmgr.components.get(cls) # Note that this check is racy, we intentionally don't use a @@ -204,14 +206,14 @@ class ComponentManager(object): """Activate the component instance for the given class, or return the existing instance if the component has already been activated. + + Note that `ComponentManager` components can't be activated + that way. """ if not self.is_enabled(cls): return None component = self.components.get(cls) - - # Leave other manager components out of extension point lists - # see bh:comment:5:ticket:438 and ticket:11121 - if not component and not issubclass(cls, ComponentManager) : + if not component and not issubclass(cls, ComponentManager): if cls not in ComponentMeta._components: raise TracError('Component "%s" not registered' % cls.__name__) try: Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/__init__.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/__init__.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/__init__.py Sat Nov 15 01:14:46 2014 @@ -1,2 +1,15 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + from trac.db.api import * from trac.db.schema import * Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/api.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/api.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/api.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/api.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C)2005-2009 Edgewall Software +# Copyright (C)2005-2014 Edgewall Software # Copyright (C) 2005 Christopher Lenz <[email protected]> # All rights reserved. # @@ -22,6 +22,7 @@ import urllib from trac.config import BoolOption, IntOption, Option from trac.core import * +from trac.db.schema import Table from trac.util.concurrency import ThreadLocal from trac.util.text import unicode_passwd from trac.util.translation import _ @@ -61,9 +62,9 @@ def with_transaction(env, db=None): :deprecated: This decorator is in turn deprecated in favor of context managers now that python 2.4 support has been - dropped. Use instead the new context manager, - `QueryContextManager` and - `TransactionContextManager`, which makes for much + dropped. It will be removed in Trac 1.3.1. Use instead + the new context managers, `QueryContextManager` and + `TransactionContextManager`, which make for much simpler to write code: >>> def api_method(p1, p2): @@ -250,10 +251,35 @@ class DatabaseManager(Component): args['schema'] = schema connector.init_db(**args) + def create_tables(self, schema): + """Create the specified tables. + + :param schema: an iterable of table objects. + + :since: version 1.0.2 + """ + connector = self.get_connector()[0] + with self.env.db_transaction as db: + for table in schema: + for sql in connector.to_sql(table): + db(sql) + + def drop_tables(self, schema): + """Drop the specified tables. + + :param schema: an iterable of `Table` objects or table names. + + :since: version 1.0.2 + """ + with self.env.db_transaction as db: + for table in schema: + table_name = table.name if isinstance(table, Table) else table + db.drop_table(table_name) + def get_connection(self, readonly=False): """Get a database connection from the pool. - If `readonly` is `True`, the returned connection will purposedly + If `readonly` is `True`, the returned connection will purposely lack the `rollback` and `commit` methods. """ if not self._cnx_pool: @@ -287,7 +313,7 @@ class DatabaseManager(Component): backup_dir = os.path.join(self.env.path, backup_dir) db_str = self.config.get('trac', 'database') db_name, db_path = db_str.split(":", 1) - dest_name = '%s.%i.%d.bak' % (db_name, self.env.get_version(), + dest_name = '%s.%i.%d.bak' % (db_name, self.env.database_version, int(time.time())) dest = os.path.join(backup_dir, dest_name) else: Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/mysql_backend.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/mysql_backend.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/mysql_backend.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/mysql_backend.py Sat Nov 15 01:14:46 2014 @@ -14,14 +14,19 @@ # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. -import os, re, types +import os +import re +import sys +import types from genshi.core import Markup from trac.core import * from trac.config import Option -from trac.db.api import IDatabaseConnector, _parse_db_str +from trac.db.api import DatabaseManager, IDatabaseConnector, _parse_db_str, \ + get_column_names from trac.db.util import ConnectionWrapper, IterableCursor +from trac.env import IEnvironmentSetupParticipant from trac.util import as_int, get_pkginfo from trac.util.compat import close_fds from trac.util.text import exception_to_unicode, to_unicode @@ -73,7 +78,7 @@ class MySQLConnector(Component): * `read_default_group`: Configuration group to use from the default file * `unix_socket`: Use a Unix socket at the given path to connect """ - implements(IDatabaseConnector) + implements(IDatabaseConnector, IEnvironmentSetupParticipant) mysqldump_path = Option('trac', 'mysqldump_path', 'mysqldump', """Location of mysqldump for MySQL database backups""") @@ -109,17 +114,29 @@ class MySQLConnector(Component): host=None, port=None, params={}): cnx = self.get_connection(path, log, user, password, host, port, params) + self._verify_variables(cnx) + utf8_size = self._utf8_size(cnx) cursor = cnx.cursor() - utf8_size = {'utf8': 3, 'utf8mb4': 4}.get(cnx.charset) if schema is None: from trac.db_default import schema for table in schema: for stmt in self.to_sql(table, utf8_size=utf8_size): self.log.debug(stmt) cursor.execute(stmt) + self._verify_table_status(cnx) cnx.commit() - def _collist(self, table, columns, utf8_size=3): + def _utf8_size(self, cnx): + if cnx is None: + connector, args = DatabaseManager(self.env).get_connector() + cnx = connector.get_connection(**args) + charset = cnx.charset + cnx.close() + else: + charset = cnx.charset + return 4 if charset == 'utf8mb4' else 3 + + def _collist(self, table, columns, utf8_size): """Take a list of columns and impose limits on each so that indexing works properly. @@ -148,7 +165,9 @@ class MySQLConnector(Component): cols.append(name) return ','.join(cols) - def to_sql(self, table, utf8_size=3): + def to_sql(self, table, utf8_size=None): + if utf8_size is None: + utf8_size = self._utf8_size(None) sql = ['CREATE TABLE %s (' % table.name] coldefs = [] for column in table.columns: @@ -235,6 +254,83 @@ class MySQLConnector(Component): raise TracError(_("No destination file created")) return dest_file + # IEnvironmentSetupParticipant methods + + def environment_created(self): + pass + + def environment_needs_upgrade(self, db): + if getattr(self, 'required', False): + self._verify_table_status(db) + self._verify_variables(db) + return False + + def upgrade_environment(self, db): + pass + + UNSUPPORTED_ENGINES = ('MyISAM', 'EXAMPLE', 'ARCHIVE', 'CSV', 'ISAM') + + def _verify_table_status(self, db): + from trac.db_default import schema + tables = [t.name for t in schema] + cursor = db.cursor() + cursor.execute("SHOW TABLE STATUS WHERE name IN (%s)" % + ','.join(('%s',) * len(tables)), + tables) + cols = get_column_names(cursor) + rows = [dict(zip(cols, row)) for row in cursor] + + engines = [row['Name'] for row in rows + if row['Engine'] in self.UNSUPPORTED_ENGINES] + if engines: + raise TracError(_( + "All tables must be created as InnoDB or NDB storage engine " + "to support transactions. The following tables have been " + "created as storage engine which doesn't support " + "transactions: %(tables)s", tables=', '.join(engines))) + + non_utf8bin = [row['Name'] for row in rows + if row['Collation'] not in ('utf8_bin', 'utf8mb4_bin', + None)] + if non_utf8bin: + raise TracError(_("All tables must be created with utf8_bin or " + "utf8mb4_bin as collation. The following tables " + "don't have the collations: %(tables)s", + tables=', '.join(non_utf8bin))) + + SUPPORTED_COLLATIONS = (('utf8', 'utf8_bin'), ('utf8mb4', 'utf8mb4_bin')) + + def _verify_variables(self, db): + cursor = db.cursor() + cursor.execute("SHOW VARIABLES WHERE variable_name IN (" + "'default_storage_engine','storage_engine'," + "'default_tmp_storage_engine'," + "'character_set_database','collation_database')") + vars = dict((row[0].lower(), row[1]) for row in cursor) + + engine = vars.get('default_storage_engine') or \ + vars.get('storage_engine') + if engine in self.UNSUPPORTED_ENGINES: + raise TracError(_("The current storage engine is %(engine)s. " + "It must be InnoDB or NDB storage engine to " + "support transactions.", engine=engine)) + + tmp_engine = vars.get('default_tmp_storage_engine') + if tmp_engine in self.UNSUPPORTED_ENGINES: + raise TracError(_("The current storage engine for TEMPORARY " + "tables is %(engine)s. It must be InnoDB or NDB " + "storage engine to support transactions.", + engine=tmp_engine)) + + charset = vars['character_set_database'] + collation = vars['collation_database'] + if (charset, collation) not in self.SUPPORTED_COLLATIONS: + raise TracError(_( + "The charset and collation of database are '%(charset)s' and " + "'%(collation)s'. The database must be created with one of " + "%(supported)s.", charset=charset, collation=collation, + supported=repr(self.SUPPORTED_COLLATIONS))) + class MySQLConnection(ConnectionWrapper): """Connection wrapper for MySQL.""" @@ -251,16 +347,21 @@ class MySQLConnection(ConnectionWrapper) port = 3306 opts = {} for name, value in params.iteritems(): - if name in ('init_command', 'read_default_file', - 'read_default_group', 'unix_socket'): - opts[name] = value + key = name.encode('utf-8') + if name == 'read_default_group': + opts[key] = value + elif name == 'init_command': + opts[key] = value.encode('utf-8') + elif name in ('read_default_file', 'unix_socket'): + opts[key] = value.encode(sys.getfilesystemencoding()) elif name in ('compress', 'named_pipe'): - opts[name] = as_int(value, 0) + opts[key] = as_int(value, 0) else: self.log.warning("Invalid connection string parameter '%s'", name) cnx = MySQLdb.connect(db=path, user=user, passwd=password, host=host, port=port, charset='utf8', **opts) + self.schema = path if hasattr(cnx, 'encoders'): # 'encoders' undocumented but present since 1.2.1 (r422) cnx.encoders[Markup] = cnx.encoders[types.UnicodeType] @@ -274,6 +375,24 @@ class MySQLConnection(ConnectionWrapper) ConnectionWrapper.__init__(self, cnx, log) self._is_closed = False + def cursor(self): + return IterableCursor(MySQLUnicodeCursor(self.cnx), self.log) + + def rollback(self): + self.cnx.ping() + try: + self.cnx.rollback() + except MySQLdb.ProgrammingError: + self._is_closed = True + + def close(self): + if not self._is_closed: + try: + self.cnx.close() + except MySQLdb.ProgrammingError: + pass # this error would mean it's already closed. So, ignore + self._is_closed = True + def cast(self, column, type): if type == 'int' or type == 'int64': type = 'signed' @@ -284,6 +403,27 @@ class MySQLConnection(ConnectionWrapper) def concat(self, *args): return 'concat(%s)' % ', '.join(args) + def drop_table(self, table): + cursor = MySQLdb.cursors.Cursor(self.cnx) + cursor._defer_warnings = True # ignore "Warning: Unknown table ..." + cursor.execute("DROP TABLE IF EXISTS " + self.quote(table)) + + def get_column_names(self, table): + rows = self.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_schema=%s AND table_name=%s + """, (self.schema, table)) + return [row[0] for row in rows] + + def get_last_id(self, cursor, table, column='id'): + return cursor.lastrowid + + def get_table_names(self): + rows = self.execute(""" + SELECT table_name FROM information_schema.tables + WHERE table_schema=%s""", (self.schema,)) + return [row[0] for row in rows] + def like(self): """Return a case-insensitive LIKE clause.""" return "LIKE %%s COLLATE %s_general_ci ESCAPE '/'" % self.charset @@ -291,31 +431,18 @@ class MySQLConnection(ConnectionWrapper) def like_escape(self, text): return _like_escape_re.sub(r'/\1', text) + def prefix_match(self): + """Return a case sensitive prefix-matching operator.""" + return "LIKE %s ESCAPE '/'" + + def prefix_match_value(self, prefix): + """Return a value for case sensitive prefix-matching operator.""" + return self.like_escape(prefix) + '%' + def quote(self, identifier): """Return the quoted identifier.""" return "`%s`" % identifier.replace('`', '``') - def get_last_id(self, cursor, table, column='id'): - return cursor.lastrowid - def update_sequence(self, cursor, table, column='id'): # MySQL handles sequence updates automagically pass - - def rollback(self): - self.cnx.ping() - try: - self.cnx.rollback() - except MySQLdb.ProgrammingError: - self._is_closed = True - - def close(self): - if not self._is_closed: - try: - self.cnx.close() - except MySQLdb.ProgrammingError: - pass # this error would mean it's already closed. So, ignore - self._is_closed = True - - def cursor(self): - return IterableCursor(MySQLUnicodeCursor(self.cnx), self.log) Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/pool.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/pool.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/pool.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/pool.py Sat Nov 15 01:14:46 2014 @@ -26,7 +26,7 @@ from trac.util.text import exception_to_ from trac.util.translation import _ -class TimeoutError(Exception): +class TimeoutError(TracError): """Exception raised by the connection pool when no connection has become available after a given timeout.""" @@ -93,7 +93,7 @@ class ConnectionPoolBackend(object): deferred = num == 1 and isinstance(cnx, tuple) err = None if deferred: - # Potentially lenghty operations must be done without lock held + # Potentially lengthy operations must be done without lock held op, cnx = cnx try: if op == 'ping': @@ -214,4 +214,3 @@ class ConnectionPool(object): def shutdown(self, tid=None): _backend.shutdown(tid) - Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/postgres_backend.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/postgres_backend.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/postgres_backend.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/postgres_backend.py Sat Nov 15 01:14:46 2014 @@ -22,7 +22,7 @@ from trac.core import * from trac.config import Option from trac.db.api import IDatabaseConnector, _parse_db_str from trac.db.util import ConnectionWrapper, IterableCursor -from trac.util import get_pkginfo +from trac.util import get_pkginfo, lazy from trac.util.compat import close_fds from trac.util.text import empty, exception_to_unicode, to_unicode from trac.util.translation import _ @@ -231,6 +231,9 @@ class PostgreSQLConnection(ConnectionWra cnx.rollback() ConnectionWrapper.__init__(self, cnx, log) + def cursor(self): + return IterableCursor(self.cnx.cursor(), self.log) + def cast(self, column, type): # Temporary hack needed for the union of selects in the search module return 'CAST(%s AS %s)' % (column, _type_map.get(type, type)) @@ -238,6 +241,37 @@ class PostgreSQLConnection(ConnectionWra def concat(self, *args): return '||'.join(args) + def drop_table(self, table): + if (self._version or '').startswith(('8.0.', '8.1.')): + cursor = self.cursor() + cursor.execute("""SELECT table_name FROM information_schema.tables + WHERE table_schema=current_schema() + AND table_name=%s""", (table,)) + for row in cursor: + if row[0] == table: + self.execute("DROP TABLE " + self.quote(table)) + break + else: + self.execute("DROP TABLE IF EXISTS " + self.quote(table)) + + def get_column_names(self, table): + rows = self.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_schema=%s AND table_name=%s + """, (self.schema, table)) + return [row[0] for row in rows] + + def get_last_id(self, cursor, table, column='id'): + cursor.execute("SELECT CURRVAL(%s)", + (self.quote(self._sequence_name(table, column)),)) + return cursor.fetchone()[0] + + def get_table_names(self): + rows = self.execute(""" + SELECT table_name FROM information_schema.tables + WHERE table_schema=%s""", (self.schema,)) + return [row[0] for row in rows] + def like(self): """Return a case-insensitive LIKE clause.""" return "ILIKE %s ESCAPE '/'" @@ -245,19 +279,31 @@ class PostgreSQLConnection(ConnectionWra def like_escape(self, text): return _like_escape_re.sub(r'/\1', text) + def prefix_match(self): + """Return a case sensitive prefix-matching operator.""" + return "LIKE %s ESCAPE '/'" + + def prefix_match_value(self, prefix): + """Return a value for case sensitive prefix-matching operator.""" + return self.like_escape(prefix) + '%' + def quote(self, identifier): """Return the quoted identifier.""" return '"%s"' % identifier.replace('"', '""') - def get_last_id(self, cursor, table, column='id'): - cursor.execute("""SELECT CURRVAL('"%s_%s_seq"')""" % (table, column)) - return cursor.fetchone()[0] - def update_sequence(self, cursor, table, column='id'): - cursor.execute(""" - SELECT setval('"%s_%s_seq"', (SELECT MAX(%s) FROM %s)) - """ % (table, column, column, table)) - - def cursor(self): - return IterableCursor(self.cnx.cursor(), self.log) - + cursor.execute("SELECT SETVAL(%%s, (SELECT MAX(%s) FROM %s))" + % (self.quote(column), self.quote(table)), + (self.quote(self._sequence_name(table, column)),)) + + def _sequence_name(self, table, column): + return '%s_%s_seq' % (table, column) + + @lazy + def _version(self): + cursor = self.cursor() + cursor.execute('SELECT version()') + for version, in cursor: + # retrieve "8.1.23" from "PostgreSQL 8.1.23 on ...." + if version.startswith('PostgreSQL '): + return version.split(' ', 2)[1] Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/sqlite_backend.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/sqlite_backend.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/sqlite_backend.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/sqlite_backend.py Sat Nov 15 01:14:46 2014 @@ -27,6 +27,8 @@ from trac.util.translation import _ _like_escape_re = re.compile(r'([/_%])') +_glob_escape_re = re.compile(r'[*?\[]') + try: import pysqlite2.dbapi2 as sqlite have_pysqlite = 2 @@ -255,7 +257,8 @@ class SQLiteConnection(ConnectionWrapper and sqlite.version_info >= (2, 5, 0) def __init__(self, path, log=None, params={}): - assert have_pysqlite > 0 + if have_pysqlite == 0: + raise TracError(_("Cannot load Python bindings for SQLite")) self.cnx = None if path != ':memory:': if not os.access(path, os.F_OK): @@ -312,6 +315,24 @@ class SQLiteConnection(ConnectionWrapper def concat(self, *args): return '||'.join(args) + def drop_table(self, table): + cursor = self.cursor() + cursor.execute("DROP TABLE IF EXISTS " + self.quote(table)) + + def get_column_names(self, table): + cursor = self.cnx.cursor() + rows = cursor.execute("PRAGMA table_info(%s)" + % self.quote(table)) + return [row[1] for row in rows] + + def get_last_id(self, cursor, table, column='id'): + return cursor.lastrowid + + def get_table_names(self): + rows = self.execute(""" + SELECT name FROM sqlite_master WHERE type='table'""") + return [row[0] for row in rows] + def like(self): """Return a case-insensitive LIKE clause.""" if sqlite_version >= (3, 1, 0): @@ -325,13 +346,18 @@ class SQLiteConnection(ConnectionWrapper else: return text + def prefix_match(self): + """Return a case sensitive prefix-matching operator.""" + return 'GLOB %s' + + def prefix_match_value(self, prefix): + """Return a value for case sensitive prefix-matching operator.""" + return _glob_escape_re.sub(lambda m: '[%s]' % m.group(0), prefix) + '*' + def quote(self, identifier): """Return the quoted identifier.""" return "`%s`" % identifier.replace('`', '``') - def get_last_id(self, cursor, table, column='id'): - return cursor.lastrowid - def update_sequence(self, cursor, table, column='id'): # SQLite handles sequence updates automagically # http://www.sqlite.org/autoinc.html Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/__init__.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/__init__.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/__init__.py Sat Nov 15 01:14:46 2014 @@ -1,11 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2014 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + import unittest from trac.db.tests import api, mysql_test, postgres_test, util - from trac.db.tests.functional import functionalSuite -def suite(): +def suite(): suite = unittest.TestSuite() suite.addTest(api.suite()) suite.addTest(mysql_test.suite()) @@ -15,4 +27,3 @@ def suite(): if __name__ == '__main__': unittest.main(defaultTest='suite') - Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/api.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/api.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/api.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/api.py Sat Nov 15 01:14:46 2014 @@ -1,12 +1,26 @@ # -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. from __future__ import with_statement import os import unittest +import trac.tests.compat from trac.db.api import DatabaseManager, _parse_db_str, get_column_names, \ with_transaction +from trac.db_default import schema as default_schema +from trac.db.schema import Column, Table from trac.test import EnvironmentStub, Mock from trac.util.concurrency import ThreadLocal @@ -28,7 +42,8 @@ class Error(Exception): def make_env(get_cnx): - return Mock(components={DatabaseManager: + from trac.core import ComponentManager + return Mock(ComponentManager, components={DatabaseManager: Mock(get_connection=get_cnx, _transaction_local=ThreadLocal(wdb=None, rdb=None))}) @@ -275,6 +290,9 @@ class StringsTestCase(unittest.TestCase) def setUp(self): self.env = EnvironmentStub() + def tearDown(self): + self.env.reset_db() + def test_insert_unicode(self): self.env.db_transaction( "INSERT INTO system (name,value) VALUES (%s,%s)", @@ -306,48 +324,179 @@ class StringsTestCase(unittest.TestCase) self.assertEqual(r'alpha\`\"\'\\beta``gamma""delta', get_column_names(cursor)[0]) + def test_quoted_id_with_percent(self): + db = self.env.get_read_db() + name = """%?`%s"%'%%""" + + def test(db, logging=False): + cursor = db.cursor() + if logging: + cursor.log = self.env.log + + cursor.execute('SELECT 1 AS ' + db.quote(name)) + self.assertEqual(name, get_column_names(cursor)[0]) + cursor.execute('SELECT %s AS ' + db.quote(name), (42,)) + self.assertEqual(name, get_column_names(cursor)[0]) + cursor.executemany("UPDATE system SET value=%s WHERE " + "1=(SELECT 0 AS " + db.quote(name) + ")", + []) + cursor.executemany("UPDATE system SET value=%s WHERE " + "1=(SELECT 0 AS " + db.quote(name) + ")", + [('42',), ('43',)]) + + test(db) + test(db, logging=True) + + def test_prefix_match_case_sensitive(self): + @self.env.with_transaction() + def do_insert(db): + cursor = db.cursor() + cursor.executemany("INSERT INTO system (name,value) VALUES (%s,1)", + [('blahblah',), ('BlahBlah',), ('BLAHBLAH',), + (u'BlähBlah',), (u'BlahBläh',)]) + + db = self.env.get_read_db() + cursor = db.cursor() + cursor.execute("SELECT name FROM system WHERE name %s" % + db.prefix_match(), + (db.prefix_match_value('Blah'),)) + names = sorted(name for name, in cursor) + self.assertEqual('BlahBlah', names[0]) + self.assertEqual(u'BlahBläh', names[1]) + self.assertEqual(2, len(names)) + + def test_prefix_match_metachars(self): + def do_query(prefix): + db = self.env.get_read_db() + cursor = db.cursor() + cursor.execute("SELECT name FROM system WHERE name %s " + "ORDER BY name" % db.prefix_match(), + (db.prefix_match_value(prefix),)) + return [name for name, in cursor] + + @self.env.with_transaction() + def do_insert(db): + values = ['foo*bar', 'foo*bar!', 'foo?bar', 'foo?bar!', + 'foo[bar', 'foo[bar!', 'foo]bar', 'foo]bar!', + 'foo%bar', 'foo%bar!', 'foo_bar', 'foo_bar!', + 'foo/bar', 'foo/bar!', 'fo*ob?ar[fo]ob%ar_fo/obar'] + cursor = db.cursor() + cursor.executemany("INSERT INTO system (name,value) VALUES (%s,1)", + [(value,) for value in values]) + + self.assertEqual(['foo*bar', 'foo*bar!'], do_query('foo*')) + self.assertEqual(['foo?bar', 'foo?bar!'], do_query('foo?')) + self.assertEqual(['foo[bar', 'foo[bar!'], do_query('foo[')) + self.assertEqual(['foo]bar', 'foo]bar!'], do_query('foo]')) + self.assertEqual(['foo%bar', 'foo%bar!'], do_query('foo%')) + self.assertEqual(['foo_bar', 'foo_bar!'], do_query('foo_')) + self.assertEqual(['foo/bar', 'foo/bar!'], do_query('foo/')) + self.assertEqual(['fo*ob?ar[fo]ob%ar_fo/obar'], do_query('fo*')) + self.assertEqual(['fo*ob?ar[fo]ob%ar_fo/obar'], + do_query('fo*ob?ar[fo]ob%ar_fo/obar')) + class ConnectionTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() + self.schema = [ + Table('HOURS', key='ID')[ + Column('ID', auto_increment=True), + Column('AUTHOR')], + Table('blog', key='bid')[ + Column('bid', auto_increment=True), + Column('author') + ] + ] + self.env.global_databasemanager.drop_tables(self.schema) + self.env.global_databasemanager.create_tables(self.schema) def tearDown(self): + self.env.global_databasemanager.drop_tables(self.schema) self.env.reset_db() def test_get_last_id(self): - id1 = id2 = None q = "INSERT INTO report (author) VALUES ('anonymous')" with self.env.db_transaction as db: cursor = db.cursor() cursor.execute(q) # Row ID correct before... id1 = db.get_last_id(cursor, 'report') - self.assertNotEqual(0, id1) db.commit() cursor.execute(q) # ... and after commit() db.commit() id2 = db.get_last_id(cursor, 'report') - self.assertEqual(id1 + 1, id2) - def test_update_sequence(self): - self.env.db_transaction( - "INSERT INTO report (id, author) VALUES (42, 'anonymous')") + self.assertNotEqual(0, id1) + self.assertEqual(id1 + 1, id2) + + def test_update_sequence_default_column(self): with self.env.db_transaction as db: + db("INSERT INTO report (id, author) VALUES (42, 'anonymous')") cursor = db.cursor() db.update_sequence(cursor, 'report', 'id') + self.env.db_transaction( "INSERT INTO report (author) VALUES ('next-id')") + self.assertEqual(43, self.env.db_query( "SELECT id FROM report WHERE author='next-id'")[0][0]) + def test_update_sequence_nondefault_column(self): + with self.env.db_transaction as db: + cursor = db.cursor() + cursor.execute( + "INSERT INTO blog (bid, author) VALUES (42, 'anonymous')") + db.update_sequence(cursor, 'blog', 'bid') + + self.env.db_transaction( + "INSERT INTO blog (author) VALUES ('next-id')") + + self.assertEqual(43, self.env.db_query( + "SELECT bid FROM blog WHERE author='next-id'")[0][0]) + + def test_identifiers_need_quoting(self): + """Test for regression described in comment:4:ticket:11512.""" + with self.env.db_transaction as db: + db("INSERT INTO %s (%s, %s) VALUES (42, 'anonymous')" + % (db.quote('HOURS'), db.quote('ID'), db.quote('AUTHOR'))) + cursor = db.cursor() + db.update_sequence(cursor, 'HOURS', 'ID') + + with self.env.db_transaction as db: + cursor = db.cursor() + cursor.execute( + "INSERT INTO %s (%s) VALUES ('next-id')" + % (db.quote('HOURS'), db.quote('AUTHOR'))) + last_id = db.get_last_id(cursor, 'HOURS', 'ID') + + self.assertEqual(43, last_id) + + def test_table_names(self): + schema = default_schema + self.schema + with self.env.db_query as db: + db_tables = db.get_table_names() + self.assertEqual(len(schema), len(db_tables)) + for table in schema: + self.assertIn(table.name, db_tables) + + def test_get_column_names(self): + schema = default_schema + self.schema + with self.env.db_transaction as db: + for table in schema: + db_columns = db.get_column_names(table.name) + self.assertEqual(len(table.columns), len(db_columns)) + for column in table.columns: + self.assertIn(column.name, db_columns) + def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ParseConnectionStringTestCase, 'test')) - suite.addTest(unittest.makeSuite(StringsTestCase, 'test')) - suite.addTest(unittest.makeSuite(ConnectionTestCase, 'test')) - suite.addTest(unittest.makeSuite(WithTransactionTest, 'test')) + suite.addTest(unittest.makeSuite(ParseConnectionStringTestCase)) + suite.addTest(unittest.makeSuite(StringsTestCase)) + suite.addTest(unittest.makeSuite(ConnectionTestCase)) + suite.addTest(unittest.makeSuite(WithTransactionTest)) return suite Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/functional.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/functional.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/functional.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/functional.py Sat Nov 15 01:14:46 2014 @@ -1,4 +1,16 @@ -#!/usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. import os from trac.tests.functional import * @@ -18,12 +30,11 @@ class DatabaseBackupTestCase(FunctionalT def functionalSuite(suite=None): if not suite: - import trac.tests.functional.testcases - suite = trac.tests.functional.testcases.functionalSuite() + import trac.tests.functional + suite = trac.tests.functional.functionalSuite() suite.addTest(DatabaseBackupTestCase()) return suite if __name__ == '__main__': unittest.main(defaultTest='functionalSuite') - Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/mysql_test.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/mysql_test.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/mysql_test.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/mysql_test.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009 Edgewall Software +# Copyright (C) 2010-2013 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -13,8 +13,10 @@ import unittest +import trac.tests.compat from trac.db.mysql_backend import MySQLConnector -from trac.test import EnvironmentStub +from trac.db.schema import Table, Column, Index +from trac.test import EnvironmentStub, Mock class MySQLTableAlterationSQLTest(unittest.TestCase): @@ -50,10 +52,31 @@ class MySQLTableAlterationSQLTest(unitte {'due': ('int', 'int')}) self.assertEqual([], list(sql)) + def test_utf8_size(self): + connector = MySQLConnector(self.env) + self.assertEqual(3, connector._utf8_size(Mock(charset='utf8'))) + self.assertEqual(4, connector._utf8_size(Mock(charset='utf8mb4'))) + + def test_to_sql(self): + connector = MySQLConnector(self.env) + tab = Table('blah', key=('col1', 'col2'))[Column('col1'), + Column('col2'), + Index(['col2'])] + + sql = list(connector.to_sql(tab, utf8_size=3)) + self.assertEqual(2, len(sql)) + self.assertIn(' PRIMARY KEY (`col1`(166),`col2`(166))', sql[0]) + self.assertIn(' blah_col2_idx ON blah (`col2`(255))', sql[1]) + + sql = list(connector.to_sql(tab, utf8_size=4)) + self.assertEqual(2, len(sql)) + self.assertIn(' PRIMARY KEY (`col1`(125),`col2`(125))', sql[0]) + self.assertIn(' blah_col2_idx ON blah (`col2`(191))', sql[1]) + def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(MySQLTableAlterationSQLTest, 'test')) + suite.addTest(unittest.makeSuite(MySQLTableAlterationSQLTest)) return suite Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/postgres_test.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/postgres_test.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/postgres_test.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/postgres_test.py Sat Nov 15 01:14:46 2014 @@ -1,4 +1,15 @@ # -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. import re import unittest @@ -149,8 +160,8 @@ class PostgresTableAlterationSQLTest(uni def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(PostgresTableCreationSQLTest, 'test')) - suite.addTest(unittest.makeSuite(PostgresTableAlterationSQLTest, 'test')) + suite.addTest(unittest.makeSuite(PostgresTableCreationSQLTest)) + suite.addTest(unittest.makeSuite(PostgresTableAlterationSQLTest)) return suite Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/util.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/util.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/util.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/util.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2010 Edgewall Software +# Copyright (C) 2010-2014 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -31,10 +31,41 @@ class SQLEscapeTestCase(unittest.TestCas self.assertEqual("'%% %%'", sql_escape_percent("'% %'")) self.assertEqual("'%%s %%i'", sql_escape_percent("'%s %i'")) + self.assertEqual("%", sql_escape_percent("%")) + self.assertEqual("`%%`", sql_escape_percent("`%`")) + self.assertEqual("``%``", sql_escape_percent("``%``")) + self.assertEqual("```%%```", sql_escape_percent("```%```")) + self.assertEqual("```%%`", sql_escape_percent("```%`")) + self.assertEqual("%s", sql_escape_percent("%s")) + self.assertEqual("% %", sql_escape_percent("% %")) + self.assertEqual("%s %i", sql_escape_percent("%s %i")) + self.assertEqual("`%%s`", sql_escape_percent("`%s`")) + self.assertEqual("`%% %%`", sql_escape_percent("`% %`")) + self.assertEqual("`%%s %%i`", sql_escape_percent("`%s %i`")) + + self.assertEqual('%', sql_escape_percent('%')) + self.assertEqual('"%%"', sql_escape_percent('"%"')) + self.assertEqual('""%""', sql_escape_percent('""%""')) + self.assertEqual('"""%%"""', sql_escape_percent('"""%"""')) + self.assertEqual('"""%%"', sql_escape_percent('"""%"')) + self.assertEqual('%s', sql_escape_percent('%s')) + self.assertEqual('% %', sql_escape_percent('% %')) + self.assertEqual('%s %i', sql_escape_percent('%s %i')) + self.assertEqual('"%%s"', sql_escape_percent('"%s"')) + self.assertEqual('"%% %%"', sql_escape_percent('"% %"')) + self.assertEqual('"%%s %%i"', sql_escape_percent('"%s %i"')) + + self.assertEqual("""'%%?''"%%s`%%i`%%%%"%%S'""", + sql_escape_percent("""'%?''"%s`%i`%%"%S'""")) + self.assertEqual("""`%%?``'%%s"%%i"%%%%'%%S`""", + sql_escape_percent("""`%?``'%s"%i"%%'%S`""")) + self.assertEqual('''"%%?""`%%s'%%i'%%%%`%%S"''', + sql_escape_percent('''"%?""`%s'%i'%%`%S"''')) + def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(SQLEscapeTestCase, 'test')) + suite.addTest(unittest.makeSuite(SQLEscapeTestCase)) return suite if __name__ == '__main__': Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/util.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/util.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/util.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/util.py Sat Nov 15 01:14:46 2014 @@ -15,11 +15,18 @@ # # Author: Christopher Lenz <[email protected]> +import re + +_sql_escape_percent_re = re.compile(""" + '(?:[^']+|'')*' | + `(?:[^`]+|``)*` | + "(?:[^"]+|"")*" """, re.VERBOSE) + def sql_escape_percent(sql): - import re - return re.sub("'((?:[^']|(?:''))*)'", - lambda m: m.group(0).replace('%', '%%'), sql) + def repl(match): + return match.group(0).replace('%', '%%') + return _sql_escape_percent_re.sub(repl, sql) class IterableCursor(object): @@ -118,7 +125,7 @@ class ConnectionWrapper(object): """ dql = self.check_select(query) cursor = self.cnx.cursor() - cursor.execute(query, params) + cursor.execute(query, params if params is not None else []) rows = cursor.fetchall() if dql else None cursor.close() return rows
