Skip to content

Commit 0511b9a

Browse files
authored
Merge pull request piccolo-orm#90 from piccolo-orm/programmatic_migrations
Programmatic migrations
2 parents de78c07 + 1937d7a commit 0511b9a

File tree

5 files changed

+137
-83
lines changed

5 files changed

+137
-83
lines changed

piccolo/apps/migrations/commands/backwards.py

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import sys
44

55
from piccolo.apps.migrations.auto import MigrationManager
6-
from piccolo.apps.migrations.commands.base import BaseMigrationManager
6+
from piccolo.apps.migrations.commands.base import (
7+
BaseMigrationManager,
8+
MigrationResult,
9+
)
710
from piccolo.apps.migrations.tables import Migration
811

912

@@ -21,12 +24,12 @@ def __init__(
2124
self.clean = clean
2225
super().__init__()
2326

24-
async def run(self):
27+
async def run(self) -> MigrationResult:
2528
await self.create_migration_table()
2629

2730
app_modules = self.get_app_modules()
2831

29-
migration_modules = []
32+
migration_modules = {}
3033

3134
for app_module in app_modules:
3235
app_config = getattr(app_module, "APP_CONFIG")
@@ -40,10 +43,11 @@ async def run(self):
4043
app_name=self.app_name
4144
)
4245
if len(ran_migration_ids) == 0:
43-
# Make sure a status of 0 is returned, as we don't want this
46+
# Make sure a success is returned, as we don't want this
4447
# to appear as an error in automated scripts.
45-
print("No migrations to reverse!")
46-
sys.exit(0)
48+
message = "No migrations to reverse!"
49+
print(message)
50+
return MigrationResult(success=True, message=message)
4751

4852
#######################################################################
4953

@@ -55,10 +59,12 @@ async def run(self):
5559
earliest_migration_id = self.migration_id
5660

5761
if earliest_migration_id not in ran_migration_ids:
58-
sys.exit(
62+
message = (
5963
"Unrecognized migration name - must be one of "
6064
f"{ran_migration_ids}"
6165
)
66+
print(message, file=sys.stderr)
67+
return MigrationResult(success=False, message=message)
6268

6369
#######################################################################
6470

@@ -99,33 +105,20 @@ async def run(self):
99105
if self.clean:
100106
os.unlink(migration_module.__file__)
101107

108+
return MigrationResult(success=True)
109+
102110
else: # pragma: no cover
103-
sys.exit("Not proceeding.")
111+
message = "Not proceeding."
112+
print(message, file=sys.stderr)
113+
return MigrationResult(success=False, message=message)
104114

105115

106-
async def backwards(
116+
async def run_backwards(
107117
app_name: str,
108118
migration_id: str = "1",
109119
auto_agree: bool = False,
110120
clean: bool = False,
111-
):
112-
"""
113-
Undo migrations up to a specific migration.
114-
115-
:param app_name:
116-
The app to reverse migrations for. Specify a value of 'all' to reverse
117-
migrations for all apps.
118-
:param migration_id:
119-
Migrations will be reversed up to and including this migration_id.
120-
Specify a value of 'all' to undo all of the migrations. Specify a
121-
value of '1' to undo the most recent migration.
122-
:param auto_agree:
123-
If true, automatically agree to any input prompts.
124-
:param clean:
125-
If true, the migration files which have been run backwards are deleted
126-
from the disk after completing.
127-
128-
"""
121+
) -> MigrationResult:
129122
if app_name == "all":
130123
sorted_app_names = BaseMigrationManager().get_sorted_app_names()
131124
sorted_app_names.reverse()
@@ -149,11 +142,48 @@ async def backwards(
149142
auto_agree=auto_agree,
150143
)
151144
await manager.run()
145+
return MigrationResult(success=True)
146+
else:
147+
return MigrationResult(success=False, message="User cancelled")
152148
else:
153149
manager = BackwardsMigrationManager(
154150
app_name=app_name,
155151
migration_id=migration_id,
156152
auto_agree=auto_agree,
157153
clean=clean,
158154
)
159-
await manager.run()
155+
return await manager.run()
156+
157+
158+
async def backwards(
159+
app_name: str,
160+
migration_id: str = "1",
161+
auto_agree: bool = False,
162+
clean: bool = False,
163+
):
164+
"""
165+
Undo migrations up to a specific migration.
166+
167+
:param app_name:
168+
The app to reverse migrations for. Specify a value of 'all' to reverse
169+
migrations for all apps.
170+
:param migration_id:
171+
Migrations will be reversed up to and including this migration_id.
172+
Specify a value of 'all' to undo all of the migrations. Specify a
173+
value of '1' to undo the most recent migration.
174+
:param auto_agree:
175+
If true, automatically agree to any input prompts.
176+
:param clean:
177+
If true, the migration files which have been run backwards are deleted
178+
from the disk after completing.
179+
180+
"""
181+
response = await run_backwards(
182+
app_name=app_name,
183+
migration_id=migration_id,
184+
auto_agree=auto_agree,
185+
clean=clean,
186+
)
187+
188+
if not response.success:
189+
sys.exit(1)

piccolo/apps/migrations/commands/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
from dataclasses import dataclass
23
import importlib
34
import os
45
import sys
@@ -14,6 +15,12 @@
1415
from piccolo.apps.migrations.tables import Migration
1516

1617

18+
@dataclass
19+
class MigrationResult:
20+
success: bool
21+
message: t.Optional[str] = None
22+
23+
1724
class BaseMigrationManager(Finder):
1825
async def create_migration_table(self) -> bool:
1926
"""

piccolo/apps/migrations/commands/forwards.py

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,22 @@
33
import typing as t
44

55
from piccolo.apps.migrations.auto import MigrationManager
6-
from piccolo.apps.migrations.commands.base import BaseMigrationManager
6+
from piccolo.apps.migrations.commands.base import (
7+
BaseMigrationManager,
8+
MigrationResult,
9+
)
710
from piccolo.apps.migrations.tables import Migration
811
from piccolo.conf.apps import AppConfig, MigrationModule
912

1013

1114
class ForwardsMigrationManager(BaseMigrationManager):
12-
def __init__(
13-
self,
14-
app_name: str,
15-
migration_id: str,
16-
fake: bool = False,
17-
*args,
18-
**kwargs,
19-
):
15+
def __init__(self, app_name: str, migration_id: str, fake: bool = False):
2016
self.app_name = app_name
2117
self.migration_id = migration_id
2218
self.fake = fake
2319
super().__init__()
2420

25-
async def run_migrations(self, app_config: AppConfig) -> None:
21+
async def run_migrations(self, app_config: AppConfig) -> MigrationResult:
2622
already_ran = await Migration.get_migrations_which_ran(
2723
app_name=self.app_name
2824
)
@@ -38,10 +34,11 @@ async def run_migrations(self, app_config: AppConfig) -> None:
3834
print(f"Haven't run = {havent_run}")
3935

4036
if len(havent_run) == 0:
41-
# Make sure a status of 0 is returned, as we don't want this
37+
# Make sure this still appears successful, as we don't want this
4238
# to appear as an error in automated scripts.
43-
print("No migrations left to run!")
44-
sys.exit(0)
39+
message = "No migrations left to run!"
40+
print(message)
41+
return MigrationResult(success=True, message=message)
4542

4643
if self.migration_id == "all":
4744
subset = havent_run
@@ -51,7 +48,9 @@ async def run_migrations(self, app_config: AppConfig) -> None:
5148
try:
5249
index = havent_run.index(self.migration_id)
5350
except ValueError:
54-
sys.exit(f"{self.migration_id} is unrecognised")
51+
message = f"{self.migration_id} is unrecognised"
52+
print(message, file=sys.stderr)
53+
return MigrationResult(success=False, message=message)
5554
else:
5655
subset = havent_run[: index + 1]
5756

@@ -71,13 +70,43 @@ async def run_migrations(self, app_config: AppConfig) -> None:
7170
Migration(name=_id, app_name=self.app_name)
7271
).run()
7372

74-
async def run(self):
73+
return MigrationResult(success=True, message="Ran successfully")
74+
75+
async def run(self) -> MigrationResult:
7576
print("Running migrations ...")
7677
await self.create_migration_table()
7778

7879
app_config = self.get_app_config(app_name=self.app_name)
7980

80-
await self.run_migrations(app_config)
81+
return await self.run_migrations(app_config)
82+
83+
84+
async def run_forwards(
85+
app_name: str, migration_id: str = "all", fake: bool = False
86+
) -> MigrationResult:
87+
"""
88+
Run the migrations. This function can be used to programatically run
89+
migrations - for example, in a unit test.
90+
"""
91+
if app_name == "all":
92+
sorted_app_names = BaseMigrationManager().get_sorted_app_names()
93+
for _app_name in sorted_app_names:
94+
print(f"\nMigrating {_app_name}")
95+
print("------------------------------------------------")
96+
manager = ForwardsMigrationManager(
97+
app_name=_app_name, migration_id="all", fake=fake
98+
)
99+
response = await manager.run()
100+
if not response.success:
101+
return response
102+
103+
return MigrationResult(success=True)
104+
105+
else:
106+
manager = ForwardsMigrationManager(
107+
app_name=app_name, migration_id=migration_id, fake=fake
108+
)
109+
return await manager.run()
81110

82111

83112
async def forwards(
@@ -97,17 +126,9 @@ async def forwards(
97126
If set, will record the migrations as being run without actually
98127
running them.
99128
"""
100-
if app_name == "all":
101-
sorted_app_names = BaseMigrationManager().get_sorted_app_names()
102-
for _app_name in sorted_app_names:
103-
print(f"\nMigrating {_app_name}")
104-
print("------------------------------------------------")
105-
manager = ForwardsMigrationManager(
106-
app_name=_app_name, migration_id="all", fake=fake
107-
)
108-
await manager.run()
109-
else:
110-
manager = ForwardsMigrationManager(
111-
app_name=app_name, migration_id=migration_id, fake=fake
112-
)
113-
await manager.run()
129+
response = await run_forwards(
130+
app_name=app_name, migration_id=migration_id, fake=fake
131+
)
132+
133+
if not response.success:
134+
sys.exit(1)

tests/apps/app/commands/test_show_all.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from unittest import TestCase
2-
from unittest.mock import call, patch
2+
from unittest.mock import call, patch, MagicMock
33

44
from piccolo.apps.app.commands.show_all import show_all
55

66

77
class TestShowAll(TestCase):
88
@patch("piccolo.apps.app.commands.show_all.print")
9-
def test_show_all(self, print_):
9+
def test_show_all(self, print_: MagicMock):
1010
show_all()
1111

1212
self.assertEqual(

0 commit comments

Comments
 (0)