Repository navigation
Fixes: #1874 - Added entry-point for dialects #1875
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |||||||||||||
| import re | ||||||||||||||
| from typing import Any | ||||||||||||||
| from typing import Callable | ||||||||||||||
| from typing import ClassVar | ||||||||||||||
| from typing import NamedTuple | ||||||||||||||
| from typing import TYPE_CHECKING | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -21,6 +22,8 @@ | |||||||||||||
| from sqlalchemy import String | ||||||||||||||
| from sqlalchemy import Table | ||||||||||||||
| from sqlalchemy import text | ||||||||||||||
| from sqlalchemy.exc import NoSuchModuleError | ||||||||||||||
| from sqlalchemy.util import PluginLoader | ||||||||||||||
|
|
||||||||||||||
| from . import _autogen | ||||||||||||||
| from . import base | ||||||||||||||
|
|
@@ -68,23 +71,77 @@ | |||||||||||||
| log = logging.getLogger(__name__) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class ImplMeta(type): | ||||||||||||||
| def __init__( | ||||||||||||||
| cls, | ||||||||||||||
| classname: str, | ||||||||||||||
| bases: tuple[type[DefaultImpl]], | ||||||||||||||
| dict_: dict[str, Any], | ||||||||||||||
| ): | ||||||||||||||
| newtype = type.__init__(cls, classname, bases, dict_) | ||||||||||||||
| if "__dialect__" in dict_: | ||||||||||||||
| _impls[dict_["__dialect__"]] = cls # type: ignore[assignment] | ||||||||||||||
| return newtype | ||||||||||||||
| # PluginLoader only caches on hits, not misses. | ||||||||||||||
| _entrypoints_checked: set[str] = set() | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| registry = PluginLoader("alembic.dialects") | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| _impls: dict[str, type[DefaultImpl]] = {} | ||||||||||||||
| class RegisterImpl: | ||||||||||||||
| _impls: ClassVar[dict[str, type[RegisterImpl]]] = {} | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Robot 1 (ai-robot-1) wrote: The module-level Moving the registry onto
from alembic.ddl import impl
from alembic.ddl.postgresql import PostgresqlImpl
impl._impls["duckdb"] = PostgresqlImpl
print("ok")On main this prints Keeping a module-level name bound to the same dictionary avoids this at no cost, for example View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two options:
Really depends on where you land on the friendly to draconian perspective. I err on side of friendly but 2 is an option.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah i'd just keep it indefinitely for now. if we want to deprecate it later, that can be..later
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i wonder if we can turn |
||||||||||||||
| __dialect__: ClassVar[str] | ||||||||||||||
|
|
||||||||||||||
| def __init_subclass__(cls, **kwargs: Any) -> None: | ||||||||||||||
| super().__init_subclass__(**kwargs) | ||||||||||||||
| cls._register() | ||||||||||||||
|
|
||||||||||||||
| @classmethod | ||||||||||||||
| def _register(cls) -> None: | ||||||||||||||
| dialect: str | None = getattr(cls, "__dialect__", None) | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Robot 1 (ai-robot-1) wrote:
This was changed from the from sqlalchemy.dialects import postgresql
from sqlalchemy.engine import default
from alembic.ddl.impl import DefaultImpl
from alembic.ddl.postgresql import PostgresqlImpl
class SharedBase(DefaultImpl):
"""helper base for several impls; declares no __dialect__"""
transactional_ddl = False
class PGFamilyBase(PostgresqlImpl):
"""helper base for PostgreSQL compatible databases; no __dialect__"""
class CockroachLikeImpl(PGFamilyBase):
__dialect__ = "cockroachlike"
print(DefaultImpl.get_by_dialect(default.DefaultDialect()).__name__)
print(DefaultImpl.get_by_dialect(postgresql.dialect()).__name__)On main this prints The Restoring the View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure I'll add that back.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah this is just, I need to spend a lot of hours focusing on a single issue to really see everything happening. that is very difficult to do these days. im not sure if this is because "everyone is submitting bugs and PRs because they have robots so there's more work to do" or "understanding problems is less fun because the robot can do it better" or what. will take us all some years to figure these things out
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that was why i did a deep ai review on this one, anyway. changes that involve bootstrapping behavior always have lots of surprises that I've never been good at seeing |
||||||||||||||
| if dialect: | ||||||||||||||
| existing_cls = cls._impls.get(dialect) | ||||||||||||||
| if existing_cls is None: | ||||||||||||||
| cls._impls[dialect] = cls | ||||||||||||||
| return | ||||||||||||||
|
|
||||||||||||||
| # By default, the latest registrant always wins, but a warning | ||||||||||||||
| # occurs when the latest registrant is not a subclass of the | ||||||||||||||
| # existing one. Newcomer is asked first. | ||||||||||||||
| newcomer_cls = cls | ||||||||||||||
| chosen_cls = newcomer_cls.resolve_registration_conflict( | ||||||||||||||
| new=newcomer_cls, existing=existing_cls | ||||||||||||||
| ) | ||||||||||||||
| if chosen_cls is None: | ||||||||||||||
| chosen_cls = existing_cls.resolve_registration_conflict( | ||||||||||||||
| new=newcomer_cls, existing=existing_cls | ||||||||||||||
| ) | ||||||||||||||
| if chosen_cls is None: | ||||||||||||||
| msg = ( | ||||||||||||||
| "Overwriting existing registered implementation " | ||||||||||||||
| "%r for dialect %s with new implementation %r." | ||||||||||||||
| " %r will be used." | ||||||||||||||
| % (existing_cls, dialect, newcomer_cls, newcomer_cls) | ||||||||||||||
| ) | ||||||||||||||
| if issubclass(newcomer_cls, existing_cls): | ||||||||||||||
| # Assumed to be intentional if newcomer subclasses | ||||||||||||||
| # existing; log.debug() for telemetry. | ||||||||||||||
| log.debug(msg) | ||||||||||||||
| else: | ||||||||||||||
| util.warn( | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Robot 1 (ai-robot-1) wrote: Conflict handling at class creation changes behavior for existing users The maintainer's direction for this change is that conflict resolution belongs only to third party dialects that ship an impl through the new On this patchset the conflict logic runs every time a class with a import warnings
warnings.simplefilter("error")
env_py = """
from alembic.ddl.postgresql import PostgresqlImpl
class MyPostgresqlImpl(PostgresqlImpl):
__dialect__ = "postgresql"
"""
# what ScriptDirectory.run_env() does once per command
for command in (1, 2):
exec(compile(env_py, "env.py", "exec"), {"__name__": "env_py"})
print("command", command, "ok")On main this prints Without This applies to any process that runs more than one command, such as a test suite or code calling Under the direction above, View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't matter for CLI commands, it matters for calls within Python subproc
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes env.py is not treated as a module, it's treated as a python script
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i told claude to propose a test suite for this issue as well, it's somewhere down below |
||||||||||||||
| f"{msg} To make this intentional," | ||||||||||||||
| " subclass the other implementation, or remove one of" | ||||||||||||||
| " them." | ||||||||||||||
| ) | ||||||||||||||
| chosen_cls = newcomer_cls | ||||||||||||||
| cls._impls[dialect] = chosen_cls | ||||||||||||||
|
|
||||||||||||||
| @classmethod | ||||||||||||||
| def resolve_registration_conflict( | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Robot 1 (ai-robot-1) wrote: The hook should run at lookup time, on the entry point impl first and then on the user's impl The maintainer's direction for this change is that conflict resolution belongs only to third party dialects that ship an impl through the new On this patchset the hook is called at class creation, on the new class and then on the existing one. Because the hook is inherited, a user's subclass of a third party impl runs the third party hook with from sqlalchemy.engine import default
from alembic.ddl.impl import DefaultImpl
class NewDbDialect(default.DefaultDialect):
name = "newdb"
class LibraryImpl(DefaultImpl):
__dialect__ = "newdb"
@classmethod
def resolve_registration_conflict(cls, new, existing):
# "always keep the other implementation", written the same way as
# the hook in test_entry_point_hook_keeps_env_py_impl
return existing if new is cls else new
class UserImpl(LibraryImpl):
__dialect__ = "newdb"
print(DefaultImpl.get_by_dialect(NewDbDialect()).__name__)On this patchset this prints Calling the hook from
With only two fixed roles the "new" and "existing" arguments are no longer needed, and the signature could be Which impl is used while a conflict is unresolved still needs to be stated. Keeping the impl the user declared would match what the maintainer wrote in the discussion ("make a loud warning, then use that user-custom extension as given for that run"), and is the maintainer's call. View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Let me get back to this one.) |
||||||||||||||
| cls, new: type[RegisterImpl], existing: type[RegisterImpl] | ||||||||||||||
| ) -> type[RegisterImpl] | None: | ||||||||||||||
| """Decide which implementation to register when ``new`` declares a | ||||||||||||||
| ``__dialect__`` that is already registered to ``existing``. | ||||||||||||||
|
|
||||||||||||||
| Both the new and existing may enforce registration conflict behavior. | ||||||||||||||
| ``None`` essentially means 'no opinion'. The newcomer is asked for | ||||||||||||||
| its opinion first; then the existing impl. If neither has an | ||||||||||||||
| opinion, the newcomer wins. | ||||||||||||||
| """ | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class DefaultImpl(metaclass=ImplMeta): | ||||||||||||||
| class DefaultImpl(RegisterImpl): | ||||||||||||||
| """Provide the entrypoint for major migration operations, | ||||||||||||||
| including database-specific behavioral variances. | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -137,7 +194,18 @@ def __init__( | |||||||||||||
|
|
||||||||||||||
| @classmethod | ||||||||||||||
| def get_by_dialect(cls, dialect: Dialect) -> type[DefaultImpl]: | ||||||||||||||
| return _impls[dialect.name] | ||||||||||||||
| if dialect.name not in _entrypoints_checked: | ||||||||||||||
| _entrypoints_checked.add(dialect.name) | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Robot 1 (ai-robot-1) wrote: A failing entry point is reported only on the first lookup The dialect name is added to from types import SimpleNamespace
from unittest import mock
from sqlalchemy.dialects import postgresql
from sqlalchemy.engine import default
from alembic.ddl.impl import DefaultImpl
class NewDbDialect(default.DefaultDialect):
name = "newdb"
def load():
raise ImportError("No module named 'somedialect.alembic_impl'")
entry_points = [
SimpleNamespace(name="newdb", load=load),
SimpleNamespace(name="postgresql", load=load),
]
with mock.patch(
"sqlalchemy.util.compat.importlib_metadata_get",
side_effect=lambda group: (
entry_points if group == "alembic.dialects" else []
),
):
for dialect in (NewDbDialect(), postgresql.dialect()):
for attempt in (1, 2):
try:
result = DefaultImpl.get_by_dialect(dialect).__name__
except Exception as err:
result = f"{type(err).__name__}: {err}"
print(dialect.name, "attempt", attempt, "->", result)Output on this patchset: For a new dialect the second attempt hides the real error behind Adding the name to Related: the value returned by View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I did this deliberately. I can change it and I think the argument of resurfacing the same error again is sound and I think I prefer that. That said, the scenarios where it matters are quite contrived; e.g. you'd have to try:except an entrypoint in one case (so that the program doesn't terminate) and then hit it again in a second case.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right but it mentions a test suite that might be doing things multiple times. I can see a lot of ways this kind of thing can go wrong and it also produces very difficult to debug situations where the same input changes what it does on successive runs. basically the method should do the same thing every time its called, so if it raises, it raises every time. |
||||||||||||||
| try: | ||||||||||||||
| registry.load(dialect.name) | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Robot 1 (ai-robot-1) wrote: Entry points should not be consulted for dialects that Alembic implements itself On this patchset an installed package can declare an from types import SimpleNamespace
from unittest import mock
from sqlalchemy.dialects import postgresql
from alembic.ddl.impl import DefaultImpl
def load():
# stands in for the module an installed package's entry point refers to
from alembic.ddl.postgresql import PostgresqlImpl
class SomePackageImpl(PostgresqlImpl):
__dialect__ = "postgresql"
return SomePackageImpl
entry_points = [SimpleNamespace(name="postgresql", load=load)]
with mock.patch(
"sqlalchemy.util.compat.importlib_metadata_get",
side_effect=lambda group: (
entry_points if group == "alembic.dialects" else []
),
):
print(DefaultImpl.get_by_dialect(postgresql.dialect()).__name__)On this patchset, with warnings turned into errors, this prints SQLAlchemy does not allow the equivalent. Its
The maintainer has decided that Alembic should behave the same way: for the dialect names Alembic implements itself ( Nothing that works today is lost. A project that wants to customize a built-in impl still declares The test named above would be inverted to assert that the built-in impl is still used. View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was actually a little worried about this one myself. I thought about it and figured, well why not, plus I didn't consider it a security concern because anything in a Python venv is fair game, if it's there it's there. But that was the extent of my thinking. I actually really like the note about the parallel to SQLAlchemy as I think it makes a ton of intuitive sense to mimic how SQLA does this to the extent possible. I agree with the order of operations 100%, in process -> built-in -> entry-points makes sense. In Alembic, I think # 2 comes for free with # 1 if the implementations are eagerly loaded into Fixing it could be just a matter of only checking entry-points on key miss on ^ I thought I had this behavior at first? I forget what decision was made that made us check entry-points always.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes when it found this I asked it well what does SQLAlchemy do, so that's what drove my decision that the built-in impls should not have an override path. since SQLA has been that way for 20 years and nobody has complained :) |
||||||||||||||
| except NoSuchModuleError: | ||||||||||||||
| pass | ||||||||||||||
| try: | ||||||||||||||
| return cls._impls[dialect.name] # type: ignore[return-value] | ||||||||||||||
| except KeyError as ke: | ||||||||||||||
| raise util.NoSuchDialectError( | ||||||||||||||
| f"Implementation for dialect {dialect.name!r} not found" | ||||||||||||||
| ) from ke | ||||||||||||||
|
|
||||||||||||||
| def static_output(self, text: str) -> None: | ||||||||||||||
| assert self.output_buffer is not None | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,50 @@ and :ref:`sqlalchemy.ext.compiler_toplevel` systems. | |
| For programmatic usage of Alembic's migration directives, the easiest | ||
| route is to use the higher level functions given by :ref:`alembic.operations.toplevel`. | ||
|
|
||
| .. _alembic.ddl.third_party: | ||
|
|
||
| Implementations for Third Party Dialects | ||
| ======================================== | ||
|
|
||
| Alembic selects a :class:`.DefaultImpl` based on the name of the SQLAlchemy | ||
| dialect being used. If an implementation does not exist for a dialect, | ||
| Alembic will raise :class:`.NoSuchDialectError`. Third-party SQLAlchemy | ||
| libraries are therefore encouraged to implement their own | ||
| :class:`.DefaultImpl`. | ||
|
|
||
| Declaring an implementation | ||
| --------------------------- | ||
|
|
||
| Subclass :class:`.DefaultImpl` and set ``__dialect__``:: | ||
|
|
||
| from alembic.ddl.impl import DefaultImpl | ||
|
|
||
| class NewDbImpl(DefaultImpl): | ||
| __dialect__ = "newdb" | ||
|
|
||
| The ``RegisterImpl.__init_subclass__`` hook will register the implementation | ||
| to map to the ``__dialect__``. | ||
|
|
||
| Registering via entry point | ||
| --------------------------- | ||
|
|
||
| .. versionadded:: 1.21.0 | ||
|
|
||
| Third-party implementations can be registered via entry-points. | ||
| The name of the entry-point should match the name of the SQLAlchemy dialect: | ||
|
|
||
| .. code-block:: toml | ||
|
|
||
| [project.entry-points."alembic.dialects"] | ||
| newdb = "newdb_sqlalchemy.alembic:NewDbImpl" | ||
|
|
||
| When the ``context.configure()`` in ``env.py`` builds a | ||
| :class:`.MigrationContext`, Alembic will scan for entry-points matching to | ||
| the dialect required for the migration. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Robot 1 (ai-robot-1) wrote: The documentation does not cover conflicts, the hook, or eager registration This section explains how to register through an entry point but not what happens when an impl for the same dialect name already exists. That is the situation any existing third party dialect adopting the entry point will put its users in, since their The changelog entry links to
For users it would help to state the two ways to keep their own impl without a warning: subclass the dialect's impl, or define Separately, "The View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The AI has given some good feedback so far but this is the first one I'll really push back on. My 2 cents: I have an opinion that if this behavior needs to be documented that we've failed in a sense. 99.99% of Alembic users are not writing their own Impls and 90% of people writing their own Impls shouldn't need to worry about conflict resolution. This is therefore useful to 0.001% of people, and they're probably using AI anyway. This is so incredibly in the weeds and irrelevant to almost everyone that it's embarrassing to write about it. If I were to expand the docs I'd rather write more on just the entry-point stuff in general; I'm sure there is more to say, but maybe not so much about conflict resolution if it is designed right.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah this is what happens when you select "high" effort on your model.... Obviously the method itself for "resolve_registration_conflict" should have a docstring (it does). it also needs a "..versionadded:: 1.21.0" note (this will be in 1.21.0 on current schedules). it looks like you have a short example there, so that covers "that the class the entry point refers to must declare a I think mentioning the that is I think the |
||
|
|
||
| Built-In Implementation Reference | ||
| ================================= | ||
|
|
||
| .. automodule:: alembic.ddl | ||
| :members: | ||
| :undoc-members: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| .. change:: | ||
| :tags: feature, api | ||
| :tickets: 1872 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Robot 1 (ai-robot-1) wrote: Ticket number 1872 is the discussion. The issue this change fixes is 1874, so this should be View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048 |
||
|
|
||
| Added the ``alembic.dialects`` entry point group, which allows third party | ||
| dialects to register a :class:`.DefaultImpl` subclass for their dialect | ||
| without requiring it to be eagerly loaded. | ||
|
|
||
| When a :class:`.DefaultImpl` subclass declares a ``__dialect__`` that is | ||
| already registered and it is not a subclass of the existing | ||
| implementation, a warning is now presented. The new | ||
| :meth:`.RegisterImpl.resolve_registration_conflict` hook is used to resolve | ||
| conflicts; by default, the latest registered subclass wins, emulating | ||
| the existing behavior. | ||
|
|
||
| An unknown dialect name now raises :class:`.NoSuchDialectError`, which | ||
| is a subclass of :class:`.CommandError`. Registration now uses a | ||
| ``RegisterImpl`` base class, and the ``ImplMeta`` metaclass has been | ||
| removed. Pull request courtesy Daniel Reeves. | ||
|
|
||
| .. seealso:: | ||
|
|
||
| :ref:`alembic.ddl.third_party` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AI Robot 1 (ai-robot-1) wrote:
Not blocking:
importlib.metadatacould be used directly herealembic/runtime/plugins.pyreads its entry points withimportlib.metadata.entry_points(group="alembic.plugins"). Doing the same foralembic.dialectswould:sqlalchemy.util.PluginLoader, which is an internal SQLAlchemy utility_entrypoints_checkedcache, since hits and misses could be kept in one dictionarysqlalchemy.util.compat.importlib_metadata_getregistrytoalembic.ddl.impl, which is easy to confuse withsqlalchemy.dialects.registryView this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/7048
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll remove the dep on
PluginLoader. Maybe adding it in the first place was too cute and creates too much of a burden on sqlalchemy to keep it. I don't think it adds anything either way as its purely internal.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally missed that this used PluginLoader (or it just seemed fine to me) and that we didn't use it for the plugin plugin thing. Since we didnt use it for the plugins.py thing, then yes we should stay consistent here.