Add operation implementation replacement#1751
Add operation implementation replacement#1751justanothercatgirl wants to merge 3 commits intosqlalchemy:mainfrom
Conversation
Summary of changes made: 1. Added 'replace' argument to util.Dispatcher.dispatch.for 2. Added same argument to ops.AbstractOperation.implementation_for 3. Added documentation for 'replace' argument 4. Added tests for: 4.1. Adding logging to CreateTableOp, replacing CreateTableOp with DropTableOp for fun 4.2. Recieving error when trying to replace implementation without propper argument to @Operations.implementation_for 4.3. Overwriting custom operation's implementation Fixes: sqlalchemy#1750
|
Additional notes: I may or may not have written more tests than were needed; just wanted to be thorough. Removing extras is always easier than adding new ones. Will happily submit additional changes if anything is needed. |
sqla-tester
left a comment
There was a problem hiding this comment.
OK, this is sqla-tester setting up my work on behalf of zzzeek to try to get revision 0d31153 of this pull request into gerrit so we can run tests and reviews and stuff
|
New Gerrit review created for change 0d31153: https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/6427 |
zzzeek
left a comment
There was a problem hiding this comment.
a bunch of small changes that I can do later on if you want to be done, or whatever, no biggie
alembic/operations/base.py
Outdated
| """Register an implementation for a given :class:`.MigrateOperation`. | ||
|
|
||
| :param replace: optional flag that allows to replace an already | ||
| registered implementation. Default: False. |
alembic/util/langhelpers.py
Outdated
| raise ValueError( | ||
| "Can not set dispatch function for object %s: " | ||
| "key already exists. To replace existing function, " | ||
| "use replace=True." % target |
There was a problem hiding this comment.
probably better to use an f-string here with !r since dont know what "target" is. if "target" were a tuple this interpolation would fail.
tests/test_op.py
Outdated
| context.assert_("CREATE SEQUENCE foob") | ||
|
|
||
| def test_replace_op(self): | ||
| from alembic.operations.toimpl import drop_table as drop_table_default |
There was a problem hiding this comment.
personal preference, imports are at the top of the test module not inside of methods. (i know some people advise against that)
tests/test_op.py
Outdated
| ) | ||
|
|
||
| # restore default implementation | ||
| Operations.implementation_for(ops.CreateTableOp, replace=True)( |
There was a problem hiding this comment.
if the assert fails then this wouldn't get called. i think it would be good to use a @testing.fixture here (that's an alias for @pytest.fixture) which can restore the original implementation to CreateTableOp
tests/test_op.py
Outdated
| def create_table(operations, operation): | ||
| pass | ||
|
|
||
| assert_raises_message( |
There was a problem hiding this comment.
this assert form is the old way we do it, a newer way that is more succinct is to use with expect_raises_message(<message text>): <implementation>, that way you don't need def do_implementation
| def test_replace_custom_op(self): | ||
| context = op_fixture() | ||
|
|
||
| @Operations.register_operation("create_user") |
There was a problem hiding this comment.
this leaves around the "create_user" operation, which is not a big deal for now, unless we had a lot of other tests adding custom ops and we had a name collision. i guess we dont have "unregister_operation" right now
There was a problem hiding this comment.
I wanted to name operation _create_user, but langhelpers.py:117 does not allow that
@classmethod
def _add_proxied_attribute(cls, methname, globals_, locals_, attr_names):
if not methname.startswith("_"): # this line
meth = getattr(cls, methname)
if callable(meth):
locals_[methname] = cls._create_method_proxy(
methname, globals_, locals_
)
else:
attr_names.add(methname)gonna fix the rest today, thanks for taking your time to point everything out :)
There was a problem hiding this comment.
I tried to make a fixture that completely restored Operations class and methods in op and it almost works really good (no formatting for now):
@testing.fixture
def restore_operations(self):
saved_attrs = {}
op_cls = None
def _save_attrs(_op_cls):
nonlocal op_cls
# i know .copy() does not perform a deep copy,
# but it'll do for saving methods
saved_attrs.update(_op_cls.__dict__.copy())
op_cls = _op_cls
yield _save_attrs
for name, val in saved_attrs.items():
if not name.startswith("__") and callable(val):
setattr(op_cls, name, val)
# this loop is pointless (explained below)
for name, val in op_cls.__dict__.copy().items():
if name not in saved_attrs:
delattr(op_cls, name)It does should restore changed implementations. It does not delete added operations, because _ModuleClsMeta lacks __delattr__ and ModuleClsProxy lacks _remove_proxied_attribute.
I don't think adding an ability to remove operations is needed (looks like "solving a non-existent problem" type of thing), but if you think it should be added, I will try. Other than that, I fixed everything you pointed out and will do a follow-up commit soon.
If you think this fixture is a bad idea whatsoever, I'll move restoring an operation before the assertion so that it definitely gets called.
There was a problem hiding this comment.
looking at register_operation it seems like you could call it over and over again for the same name anyway so yes I would not worry about it
There was a problem hiding this comment.
i would think "unregister_operation" would just be delattr(op_cls, name), but i havent worked w/ this code in many years
Detailed: - added version number for changes in docs - changed % formatting to f-string - moved imports - made fixture to restore default implementations - changed assertion to check for raised error did not implement: removing custom operations created for testing (as name collisions are unlikely anyways)
sqla-tester
left a comment
There was a problem hiding this comment.
OK, this is sqla-tester setting up my work on behalf of zzzeek to try to get revision 5eef47a of this pull request into gerrit so we can run tests and reviews and stuff
|
Patchset 5eef47a added to existing Gerrit review https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/6427 |
|
Hello. Are there any other changes I should make? Can not find anything on neither github nor gerrit |
nope, you did everything, it's on me now |
|
I added docs for this, if you want to look at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/6427/2..3 to see any errors |
Didn't notice anything wrong, and thanks for your reviews and help :) |
|
Gerrit review https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/6427 has been merged. Congratulations! :) |
|
Federico Caselli (CaselIT) wrote: how did this pass all test but we did not update the stub files? View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/6427 |
|
Federico Caselli (CaselIT) wrote: fixed in https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/6442 View this in Gerrit at https://gerrit.sqlalchemy.org/c/sqlalchemy/alembic/+/6427 |
As was discussed in #1750, I added an ability to replace implementations for default alembic operations to allow customizing execution of the migration. Basically adds "replace" argument to Operations.implementation_for and Dispatcher.dispatch_for (as was suggested by @zzzeek).
This allows logging operations both into database and files/stdout, intercepting and cancelling generated operations, any other type of task you would use hooks to do, modifying an operator to be database-specific.
Description
4.2. Recieving error when trying to replace implementation without
propper argument to @Operations.implementation_for
4.3. Overwriting custom operation's implementation
Fixes: #1750
Checklist
This pull request is:
must include a complete example of the issue. one line code fixes without an
issue and demonstration will not be accepted.
Fixes: #<issue number>in the commit messageinclude a complete example of how the feature would look.
Fixes: #<issue number>in the commit messageHave a nice day!