Repository navigation
Replies: 9 comments 11 replies
|
yeah so i think the "plugins" system is decidedly not for dialect registration, it's for things that extend Alembic itself, typically though not necessarily in a dialect agnostic way. So for the main question I'm sure I've thought about entrypoints for dialects, and likely felt it was a bit overkill at whatever time I thought about it and also observed that dialects like sqlalchemy-redshift (or maybe....the old DB2 dialect....i dunno, some dialect) were just putting their DialectImpl in their normal dialect and by the fact that it existed, it gets registered in the DialectImpl as an available alembic dialect.
OK. meaning the alembic dialect can be totally separate from the main part of the dialect. well first off "need to import alembic", im sure all the dialects that use this pattern check if alembic is installed at all, but then if it is, sure, you end up importing alembic and I doubt we've set up alembic to be shallow-importable. so, sure I dont see another normal way around that besides entry points.
this one is not clicking with me. but im not thinking hard right now...how does entry point invocation control the order, why is that even important, "so the env.py's dialect"...env.py doesnt have a dialect? "is guaranteed to resolve last" ... well I dont understand "env.py's dialect" so I dont understand that either overall, we just added entrypoints for the plugin system, which was this huge refactor I did so that we could allow this "CHECK constraint" plugin thing to be added, which we then included, and it failed immediately once we released and we basically had to almost completely revert it, it's now useless cruft in the codebase that nobody uses. But we have a plugin system as a result. So yes entrypoints are there now so my original "let's not make that big a deal" argument is pretty over, so, sure. |
Yeah sorry I wasn't clear about this.
Anyway, from alembic.ddl.impl import DefaultImpl
class SnowflakeImpl(DefaultImpl):
__dialect__ = "snowflake"instead of having its own SnowflakeImpl. I'm proposing to the maintainers of snowflake-sqlalchemy to add a I'm also proposing that, for What breakage? Well, if snowflake-sqlalchemy adds an eagerly-loaded If it were guaranteed that the library's Without guaranteed order, this could break code, e.g. # alembic/env.py
from alembic.ddl.impl import DefaultImpl
class SnowflakeImpl(DefaultImpl):
__dialect__ = "snowflake"
def some_custom_behavior_thats_important_to_me(self, *args, **kwargs):
...^ If this isn't guaranteed to register last, then RIP to my However, if the library's code always runs first, then the worst that happens is the user doesn't get the nice little fixes I'm adding, but they do keep their method overrides. Guaranteed registration order is the important bit, so hopefully I explained that right. Saving like 100ms of start-up time and a 50kb of RAM or whatever (guesses, didnt measure) in the application runtime by removing the eager load of Alembic is cool I guess but not worth a public API feature request. The thing I really care about is making sure I can say "this won't break existing user code to add a managed SnowflakeImpl because the user's version is guaranteed to always register last." Oh one more thing. I've been saying "look at entrypoints before running
Oof... Sorry about that. 😓 Hopefully you got some other benefits out of it though. |
|
Sadly alembic doesn't error for duplicate dialect registrations at the moment, and it's likely kinda hard to retrofit now. (Also looking at the code the registration is still using a metaclass, we could likely move to
That's kinda unfortunate. Anyway, for the issue at hand my preference would be to add a different entry point for the dialect impl, but given that the registration is by subclassing we can't really prevent the current entrypont to be used this way, so maybe we could just document it? |
|
Bouncing around responses here to form a more coherent chronology for my own message.
I agree, it's the source of the problem and if they had just done what e.g. But, I also don't think conceptually having a As you say, it's hard to retrofit error for duplicate dialect registration because many people have probably built workflows around the assumption that it is not prevented.
So an example I have in my own code that I'm not implementing upstream in A more salient example may be, a user fixed an error with
The suggested migration path I was thinking of was this: https://github.com/dwreeves/snowflake-sqlalchemy/blob/81ee0abfa1fb8d1da27a4b4d6e7d593cf75beccc/MIGRATING.md?plain=1#L203-L208 # env.py
from snowflake.sqlalchemy.alembic_impl import SnowflakeImpl
class CustomSnowflakeImpl(SnowflakeImpl):
__dialect__ = 'snowflake'
def some_custom_thing(self):
...And if they don't like my version, then just keep theirs: # env.py
from alembic.ddl.impl import DefaultImpl
class SnowflakeImpl(DefaultImpl):
__dialect__ = 'snowflake'
def some_custom_thing(self):
...And if their version has nothing special in it (90%+ of users, probably, unless they also ran into the same issues I did and more), they should delete it: # env.py
passIf duplicate registration were errored, though, then this wouldn't work.
I think all of these work well for My 2 cents: I think raising by default with The others seem low risk to me. Warning on duplicate registration is probably much better than silence. Maybe gate the warning behind a |
|
Thanks for all the thoughtful discussion about this and for taking the issue seriously, btw. Really appreciate your time. |
|
so to recap:
does that make sense? |
|
Thanks for your help and for the discussion everyone. Really appreciate it. |
|
Hello, I ran into an issue pretty much immediately. 😬 alembic/alembic/runtime/plugins.py Line 24 in b42ebe1 def _setup() -> None:
# setup third party plugins
for entrypoint in metadata.entry_points(group="alembic.plugins"):
for mod in entrypoint.load():
Plugin.setup_plugin_from_module(mod, entrypoint.name)The However, modules are not iterable. The documented behavior says to set the module, but the code says to set some sort of iterable. My suspicion is, there was a mistake in double-nesting the for loop, and nobody has raised this issue because nobody is actually using plugins. The corrected code should look like this: def _setup() -> None:
# setup third party plugins
for entrypoint in metadata.entry_points(group="alembic.plugins"):
mod = entrypoint.load()
Plugin.setup_plugin_from_module(mod, entrypoint.name)In practice, for backwards compat, it should probably look like this: def _setup() -> None:
# setup third party plugins
for entrypoint in metadata.entry_points(group="alembic.plugins"):
mod = entrypoint.load()
if isinstance(mod, Iterable):
for o in mod:
Plugin.setup_plugin_from_module(o, entrypoint.name)
else:
Plugin.setup_plugin_from_module(mod, entrypoint.name)Or you could even reuse SQLA's utils and go with I think where this leaves me is that I'll end up not using Plugins after all, because this seems a little unstable and I wouldn't want to write code to match to an un-documented behavior that will likely have to be deprecated and removed at some point. |
|
Hello, I wrote a draft, but there is no open issue, so via the rules of the repo I cannot open a PR. Disclosure: Code is hand-written, but unit-tests are AI generated. Let me know if I can open a PR for this. A note for the Getting this to work with just class SnowflakeImpl(DefaultImpl):
__dialect__ = "snowflake"
@classmethod
def resolve_registration_conflict(cls, other):
print("running SnowflakeImpl.resolve_registration_conflict")
return other
class CustomSnowflakeImpl(DefaultImpl):
__dialect__ = "snowflake"
@classmethod
def resolve_registration_conflict(cls, other):
print("running CustomSnowflakeImpl.resolve_registration_conflict")
return otherIf we call on But, if we call It seems like the only way to make sure both Impl's get to "have a say" is to have the signature be The current behavior by default mimics the existing behavior in a sense, i.e. latest registrant wins. But now both the new and existing DefaultImpl's can decide whether a new or existing can override. However, of note the order is still non-deterministic in the sense described in the original issue because the entry point is called during context creation, not prior to @classmethod
def resolve_registration_conflict(cls, new, existing):
if new is cls and not issubclass(cls, existing):
from alembic import util
util.warn("You have a hand-written SnowflakeImpl. Please subclass the official one.")
return existing
return NoneOther things of note in the commit:
Again, let me know if I can open a PR. Thanks. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Hello,
I could not find any discussion on this elsewhere.
I'm wondering if it would make sense for Alembic to support entry-points for
Defaultmplsubclasses.Right now, it looks like the pattern some third-party providers use (e.g.
sqlalchemy-redshift) is to load the impl as a side-effect of importing the SqlAlchemy dialect module entry-point.There are two advantages of an entry-point. The first would be not needing to import Alembic or register anything during the normal runtime of the application. Not usually a big deal to have Alembic in the sys modules, but could be nice-to-have for custom dialects.
The second is a little more subtle, but the entry-point invocation could control the order of registration of dialects, so that the
env.py's dialect is guaranteed to resolve last.The second is actually relevant to a PR I have open for
snowflake-sqlalchemy: right now there is no guarantee of whether the library providedSnowflakeImplwill resolve before or after a user's custom implementation; it's totally dependent on theenv.py's imports. If entry-points are respected however, then Alembic can always resolve entry-points beforeenv.pyruns, guaranteeing user-generatedDefaultImpls are resolved last. snowflakedb/snowflake-sqlalchemy#758So e.g. I have a SQLAlchemy driver for MyCoolSqlDatabase, I could structure like this:
An argument against this--
I am aware that Alembic uses entry-points for plugins.
Perhaps the pattern you might suggest is to create a plugin with an empty
setup()that registers the Alembic impl as a side-effect? E.g.If so, it may make sense to show that as an example in the documentation. If that is guaranteed to resolve before
env.py, then that could also work as a path for the aforementioned PR.I think the upside to using the existing plugin ecosystem is that it reduces the API surface on Alembic's end, but the downside is this is very cheeky and not intuitive, especially without a documented example.
Lastly, thanks for the awesome ecosystem, zzzeeek.
All reactions