[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 <tablename>_<colkey> instead
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]
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
=======================
'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
import re, urllib
from sqlalchemy import exc, util
+from sqlalchemy.engine import base
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.
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
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`.
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
_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'