From: Mike Bayer Date: Tue, 24 Apr 2012 17:00:30 +0000 (-0400) Subject: - [feature] Added a new system X-Git-Tag: rel_0_8_0b1~462 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=71c00115747d2fb13423b0b18e728b402f117528;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - [feature] Added a new system for registration of new dialects in-process without using an entrypoint. See the docs for "Registering New Dialects". [ticket:2462] --- diff --git a/CHANGES b/CHANGES index 23f17335db..4b01c4eebd 100644 --- a/CHANGES +++ b/CHANGES @@ -116,7 +116,13 @@ CHANGES [ticket:2454]. Courtesy Jeff Dairiki also in 0.7.7. -- sql +- engine + - [feature] Added a new system + for registration of new dialects in-process + without using an entrypoint. See the + docs for "Registering New Dialects". + [ticket:2462] + - [bug] The names of the columns on the .c. attribute of a select().apply_labels() is now based on _ instead @@ -124,6 +130,8 @@ CHANGES that have a distinctly named .key. [ticket:2397] + +- sql - [feature] The Inspector object can now be acquired using the new inspect() service, part of [ticket:2208] diff --git a/doc/build/core/connections.rst b/doc/build/core/connections.rst index 9f9a8f07d2..647accd845 100644 --- a/doc/build/core/connections.rst +++ b/doc/build/core/connections.rst @@ -367,6 +367,52 @@ Calling :meth:`~.Connection.close` on the "contextual" connection does not relea its resources until all other usages of that resource are closed as well, including that any ongoing transactions are rolled back or committed. +Registering New Dialects +======================== + +The :func:`.create_engine` function call locates the given dialect +using setuptools entrypoints. These entry points can be established +for third party dialects within the setup.py script. For example, +to create a new dialect "foodialect://", the steps are as follows: + +1. Create a package called ``foodialect``. +2. The package should have a module containing the dialect class, + which is typically a subclass of :class:`sqlalchemy.engine.default.DefaultDialect`. + In this example let's say it's called ``FooDialect`` and its module is accessed + via ``foodialect.dialect``. +3. The entry point can be established in setup.py as follows:: + + entry_points=""" + [sqlalchemy.dialects] + foodialect = foodialect.dialect:FooDialect + """ + +If the dialect is providing support for a particular DBAPI on top of +an existing SQLAlchemy-supported database, the name can be given +including a database-qualification. For example, if ``FooDialect`` +were in fact a MySQL dialect, the entry point could be established like this:: + + entry_points=""" + [sqlalchemy.dialects] + mysql.foodialect = foodialect.dialect:FooDialect + """ + +The above entrypoint would then be accessed as ``create_engine("mysql+foodialect://")``. + +Registering Dialects In-Process +------------------------------- + +SQLAlchemy also allows a dialect to be registered within the current process, bypassing +the need for separate installation. Use the ``register()`` function as follows:: + + from sqlalchemy.dialects import register + registry.register("mysql.foodialect", "myapp.dialect", "MyMySQLDialect") + +The above will respond to ``create_engine("mysql+foodialect://")`` and load the +``MyMySQLDialect`` class from the ``myapp.dialect`` module. + +The ``register()`` function is new in SQLAlchemy 0.8. + Connection / Engine API ======================= diff --git a/lib/sqlalchemy/dialects/__init__.py b/lib/sqlalchemy/dialects/__init__.py index 2d48324122..16eb32e21a 100644 --- a/lib/sqlalchemy/dialects/__init__.py +++ b/lib/sqlalchemy/dialects/__init__.py @@ -17,3 +17,31 @@ __all__ = ( 'sqlite', 'sybase', ) + +from sqlalchemy import util + +def _auto_fn(name): + """default dialect importer. + + plugs into the :class:`.PluginLoader` + as a first-hit system. + + """ + if "." in name: + dialect, driver = name.split(".") + else: + dialect = name + driver = "base" + try: + module = __import__('sqlalchemy.dialects.%s' % (dialect, )).dialects + except ImportError: + return None + + module = getattr(module, dialect) + if hasattr(module, driver): + module = getattr(module, driver) + return lambda: module.dialect + else: + return None + +registry = util.PluginLoader("sqlalchemy.dialects", auto_fn=_auto_fn) \ No newline at end of file diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 392ecda116..5bbdb9d655 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -14,6 +14,7 @@ be used directly and is also accepted directly by ``create_engine()``. import re, urllib from sqlalchemy import exc, util +from sqlalchemy.engine import base class URL(object): @@ -96,49 +97,21 @@ class URL(object): to this URL's driver name. """ - try: - if '+' in self.drivername: - dialect, driver = self.drivername.split('+') - else: - dialect, driver = self.drivername, 'base' - - module = __import__('sqlalchemy.dialects.%s' % (dialect, )).dialects - module = getattr(module, dialect) - if hasattr(module, driver): - module = getattr(module, driver) - else: - module = self._load_entry_point() - if module is None: - raise exc.ArgumentError( - "Could not determine dialect for '%s'." % - self.drivername) - - return module.dialect - except ImportError: - module = self._load_entry_point() - if module is not None: - return module - else: - raise exc.ArgumentError( - "Could not determine dialect for '%s'." % self.drivername) - - def _load_entry_point(self): - """attempt to load this url's dialect from entry points, or return None - if pkg_resources is not installed or there is no matching entry point. - - Raise ImportError if the actual load fails. - - """ - try: - import pkg_resources - except ImportError: - return None - - for res in pkg_resources.iter_entry_points('sqlalchemy.dialects'): - if res.name == self.drivername.replace("+", "."): - return res.load() + if '+' not in self.drivername: + name = self.drivername + else: + name = self.drivername.replace('+', '.') + from sqlalchemy.dialects import registry + cls = registry.load(name) + # check for legacy dialects that + # would return a module with 'dialect' as the + # actual class + if hasattr(cls, 'dialect') and \ + isinstance(cls.dialect, type) and \ + issubclass(cls.dialect, base.Dialect): + return cls.dialect else: - return None + return cls def translate_connect_args(self, names=[], **kw): """Translate url attributes into a dictionary of connection arguments. diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index 13914aa7dc..76c3c829d9 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -27,7 +27,7 @@ from langhelpers import iterate_attributes, class_hierarchy, \ duck_type_collection, assert_arg_type, symbol, dictlike_iteritems,\ classproperty, set_creation_order, warn_exception, warn, NoneType,\ constructor_copy, methods_equivalent, chop_traceback, asint,\ - generic_repr, counter + generic_repr, counter, PluginLoader from deprecations import warn_deprecated, warn_pending_deprecation, \ deprecated, pending_deprecation diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index d266c96640..9e5b0e4ad8 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -52,6 +52,45 @@ def decorator(target): return update_wrapper(decorated, fn) return update_wrapper(decorate, target) +class PluginLoader(object): + def __init__(self, group, auto_fn=None): + self.group = group + self.impls = {} + self.auto_fn = auto_fn + + def load(self, name): + if name in self.impls: + return self.impls[name]() + + if self.auto_fn: + loader = self.auto_fn(name) + if loader: + self.impls[name] = loader + return loader() + + try: + import pkg_resources + except ImportError: + pass + else: + for impl in pkg_resources.iter_entry_points( + self.group, name): + self.impls[name] = impl.load + return impl.load() + + from sqlalchemy import exc + raise exc.ArgumentError( + "Can't load plugin: %s:%s" % + (self.group, name)) + + def register(self, name, modulepath, objname): + def load(): + mod = __import__(modulepath) + for token in modulepath.split(".")[1:]: + mod = getattr(mod, token) + return getattr(mod, objname) + self.impls[name] = load + def get_cls_kwargs(cls): """Return the full set of inherited kwargs for the given `cls`. diff --git a/test/engine/test_parseconnect.py b/test/engine/test_parseconnect.py index 0326395578..dcb149be87 100644 --- a/test/engine/test_parseconnect.py +++ b/test/engine/test_parseconnect.py @@ -4,6 +4,7 @@ import StringIO import sqlalchemy.engine.url as url from sqlalchemy import create_engine, engine_from_config, exc, pool from sqlalchemy.engine import _coerce_config +from sqlalchemy.engine.default import DefaultDialect import sqlalchemy as tsa from test.lib import fixtures, testing @@ -315,6 +316,44 @@ pool_timeout=10 _initialize=False, ) +class TestRegNewDBAPI(fixtures.TestBase): + def test_register_base(self): + from sqlalchemy.dialects import registry + registry.register("mockdialect", __name__, "MockDialect") + + e = create_engine("mockdialect://") + assert isinstance(e.dialect, MockDialect) + + def test_register_dotted(self): + from sqlalchemy.dialects import registry + registry.register("mockdialect.foob", __name__, "MockDialect") + + e = create_engine("mockdialect+foob://") + assert isinstance(e.dialect, MockDialect) + + def test_register_legacy(self): + from sqlalchemy.dialects import registry + tokens = __name__.split(".") + + global dialect + dialect = MockDialect + registry.register("mockdialect.foob", ".".join(tokens[0:-1]), tokens[-1]) + + e = create_engine("mockdialect+foob://") + assert isinstance(e.dialect, MockDialect) + + def test_register_per_dbapi(self): + from sqlalchemy.dialects import registry + registry.register("mysql.my_mock_dialect", __name__, "MockDialect") + + e = create_engine("mysql+my_mock_dialect://") + assert isinstance(e.dialect, MockDialect) + +class MockDialect(DefaultDialect): + @classmethod + def dbapi(cls, **kw): + return MockDBAPI() + class MockDBAPI(object): version_info = sqlite_version_info = 99, 9, 9 sqlite_version = '99.9.9'