Skip to content

Commit 9e26772

Browse files
authored
Merge pull request piccolo-orm#127 from piccolo-orm/migration_integration_testing
Migration integration testing
2 parents 15bd78f + e7217af commit 9e26772

File tree

20 files changed

+502
-62
lines changed

20 files changed

+502
-62
lines changed

piccolo/apps/migrations/auto/serialisation.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from piccolo.columns.defaults.base import Default
1414
from piccolo.columns.reference import LazyTableReference
1515
from piccolo.table import Table
16+
from piccolo.utils.repr import repr_class_instance
1617
from .serialisation_legacy import deserialise_legacy_params
1718

1819
###############################################################################
@@ -43,13 +44,7 @@ def __eq__(self, other):
4344
return self.__hash__() == other.__hash__()
4445

4546
def __repr__(self):
46-
args = ", ".join(
47-
[
48-
f"{key}={value.__repr__()}"
49-
for key, value in self.instance.__dict__.items()
50-
]
51-
)
52-
return f"{self.instance.__class__.__name__}({args})"
47+
return repr_class_instance(self.instance)
5348

5449

5550
@dataclass
@@ -149,7 +144,7 @@ def __eq__(self, other):
149144
return self.__hash__() == other.__hash__()
150145

151146
def __repr__(self):
152-
return f"UUID({str(self.instance)})"
147+
return f"UUID('{str(self.instance)}')"
153148

154149

155150
###############################################################################
@@ -327,6 +322,14 @@ def serialise_params(params: t.Dict[str, t.Any]) -> SerialisedParams:
327322
)
328323
continue
329324

325+
# Plain class type
326+
if inspect.isclass(value) and not issubclass(value, Enum):
327+
params[key] = SerialisedCallable(callable_=value)
328+
extra_imports.append(
329+
Import(module=value.__module__, target=value.__name__)
330+
)
331+
continue
332+
330333
# All other types can remain as is.
331334

332335
return SerialisedParams(

piccolo/apps/migrations/commands/base.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import typing as t
77

88
from piccolo.conf.apps import (
9+
AppConfig,
910
MigrationModule,
1011
Finder,
1112
)
@@ -73,7 +74,7 @@ def get_migration_ids(
7374

7475
async def get_migration_managers(
7576
self,
76-
app_name: str,
77+
app_config: AppConfig,
7778
max_migration_id: t.Optional[str] = None,
7879
offset: int = 0,
7980
) -> t.List[MigrationManager]:
@@ -88,8 +89,6 @@ async def get_migration_managers(
8889
"""
8990
migration_managers: t.List[MigrationManager] = []
9091

91-
app_config = self.get_app_config(app_name=app_name)
92-
9392
migrations_folder = app_config.migrations_folder_path
9493

9594
migration_modules: t.Dict[
@@ -129,8 +128,11 @@ async def get_table_from_snaphot(
129128
This will generate a SchemaSnapshot up to the given migration ID, and
130129
will return a DiffableTable class from that snapshot.
131130
"""
131+
app_config = self.get_app_config(app_name=app_name)
132132
migration_managers = await self.get_migration_managers(
133-
app_name=app_name, max_migration_id=max_migration_id, offset=offset
133+
app_config=app_config,
134+
max_migration_id=max_migration_id,
135+
offset=offset,
134136
)
135137
schema_snapshot = SchemaSnapshot(managers=migration_managers)
136138

piccolo/apps/migrations/commands/forwards.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212

1313

1414
class ForwardsMigrationManager(BaseMigrationManager):
15-
def __init__(self, app_name: str, migration_id: str, fake: bool = False):
15+
def __init__(
16+
self, app_name: str, migration_id: str = "all", fake: bool = False
17+
):
1618
self.app_name = app_name
1719
self.migration_id = migration_id
1820
self.fake = fake
1921
super().__init__()
2022

2123
async def run_migrations(self, app_config: AppConfig) -> MigrationResult:
2224
already_ran = await Migration.get_migrations_which_ran(
23-
app_name=self.app_name
25+
app_name=app_config.app_name
2426
)
2527

2628
migration_modules: t.Dict[
@@ -67,7 +69,7 @@ async def run_migrations(self, app_config: AppConfig) -> MigrationResult:
6769
print(f"-> Ran {_id}")
6870

6971
await Migration.insert().add(
70-
Migration(name=_id, app_name=self.app_name)
72+
Migration(name=_id, app_name=app_config.app_name)
7173
).run()
7274

7375
return MigrationResult(success=True, message="Ran successfully")

piccolo/apps/migrations/commands/new.py

Lines changed: 46 additions & 10 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 datetime
34
from itertools import chain
45
import os
@@ -52,11 +53,22 @@ def _create_migrations_folder(migrations_path: str) -> bool:
5253
return True
5354

5455

55-
async def _create_new_migration(app_config: AppConfig, auto=False) -> None:
56+
@dataclass
57+
class NewMigrationMeta:
58+
migration_id: str
59+
migration_filename: str
60+
migration_path: str
61+
62+
63+
def _generate_migration_meta(app_config: AppConfig) -> NewMigrationMeta:
5664
"""
57-
Creates a new migration file on disk.
65+
Generates the migration ID and filename.
5866
"""
59-
_id = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
67+
# The microseconds originally weren't part of the ID, but there was a
68+
# chance that the IDs would clash if the migrations are generated
69+
# programatically in quick succession (e.g. in a unit test), so they had
70+
# to be added. The trade off is a longer ID.
71+
_id = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S:%f")
6072

6173
# Originally we just used the _id as the filename, but colons aren't
6274
# supported in Windows, so we need to sanitize it. We don't want to
@@ -66,6 +78,23 @@ async def _create_new_migration(app_config: AppConfig, auto=False) -> None:
6678

6779
path = os.path.join(app_config.migrations_folder_path, f"{filename}.py")
6880

81+
return NewMigrationMeta(
82+
migration_id=_id, migration_filename=filename, migration_path=path
83+
)
84+
85+
86+
class NoChanges(Exception):
87+
pass
88+
89+
90+
async def _create_new_migration(
91+
app_config: AppConfig, auto=False
92+
) -> NewMigrationMeta:
93+
"""
94+
Creates a new migration file on disk.
95+
"""
96+
meta = _generate_migration_meta(app_config=app_config)
97+
6998
if auto:
7099
alter_statements = await AutoMigrationManager().get_alter_statements(
71100
app_config=app_config
@@ -83,28 +112,31 @@ async def _create_new_migration(app_config: AppConfig, auto=False) -> None:
83112
)
84113

85114
if sum([len(i.statements) for i in alter_statements]) == 0:
86-
print("No changes detected - exiting.")
87-
sys.exit(0)
115+
raise NoChanges()
88116

89117
file_contents = render_template(
90-
migration_id=_id,
118+
migration_id=meta.migration_id,
91119
auto=True,
92120
alter_statements=_alter_statements,
93121
extra_imports=extra_imports,
94122
extra_definitions=extra_definitions,
95123
app_name=app_config.app_name,
96124
)
97125
else:
98-
file_contents = render_template(migration_id=_id, auto=False)
126+
file_contents = render_template(
127+
migration_id=meta.migration_id, auto=False
128+
)
99129

100130
# Beautify the file contents a bit.
101131
file_contents = black.format_str(
102132
file_contents, mode=black.FileMode(line_length=82)
103133
)
104134

105-
with open(path, "w") as f:
135+
with open(meta.migration_path, "w") as f:
106136
f.write(file_contents)
107137

138+
return meta
139+
108140

109141
###############################################################################
110142

@@ -117,7 +149,7 @@ async def get_alter_statements(
117149
Works out which alter statements are required.
118150
"""
119151
migration_managers = await self.get_migration_managers(
120-
app_name=app_config.app_name
152+
app_config=app_config
121153
)
122154

123155
schema_snapshot = SchemaSnapshot(migration_managers)
@@ -164,4 +196,8 @@ async def new(app_name: str, auto: bool = False):
164196
app_config = Finder().get_app_config(app_name=app_name)
165197

166198
_create_migrations_folder(app_config.migrations_folder_path)
167-
await _create_new_migration(app_config=app_config, auto=auto)
199+
try:
200+
await _create_new_migration(app_config=app_config, auto=auto)
201+
except NoChanges:
202+
print("No changes detected - exiting.")
203+
sys.exit(0)

piccolo/columns/base.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from enum import Enum
88
import inspect
99
import typing as t
10+
import uuid
1011

1112
from piccolo.columns.operators.comparison import (
1213
ComparisonOperator,
@@ -27,6 +28,7 @@
2728
from piccolo.columns.combination import Where
2829
from piccolo.columns.choices import Choice
2930
from piccolo.columns.defaults.base import Default
31+
from piccolo.columns.defaults.interval import IntervalCustom
3032
from piccolo.columns.reference import LazyTableReference
3133
from piccolo.columns.indexes import IndexMethod
3234
from piccolo.querystring import QueryString
@@ -342,6 +344,7 @@ def _validate_default(
342344
self,
343345
default: t.Any,
344346
allowed_types: t.Iterable[t.Union[None, t.Type[t.Any]]],
347+
allow_recursion: bool = True,
345348
) -> bool:
346349
"""
347350
Make sure that the default value is of the allowed types.
@@ -358,18 +361,23 @@ def _validate_default(
358361
self._validated = True
359362
return True
360363
elif callable(default):
361-
self._validated = True
362-
return True
364+
# We need to prevent recursion, otherwise a function which returns
365+
# a function would be an infinite loop.
366+
if allow_recursion and self._validate_default(
367+
default(), allowed_types=allowed_types, allow_recursion=False
368+
):
369+
self._validated = True
370+
return True
363371
elif (
364372
isinstance(default, Enum) and type(default.value) in allowed_types
365373
):
366374
self._validated = True
367375
return True
368-
else:
369-
raise ValueError(
370-
f"The default {default} isn't one of the permitted types - "
371-
f"{allowed_types}"
372-
)
376+
377+
raise ValueError(
378+
f"The default {default} isn't one of the permitted types - "
379+
f"{allowed_types}"
380+
)
373381

374382
def _validate_choices(
375383
self, choices: t.Type[Enum], allowed_type: t.Type[t.Any]
@@ -533,9 +541,18 @@ def get_sql_value(self, value: t.Any) -> t.Any:
533541
elif isinstance(value, bool):
534542
output = str(value).lower()
535543
elif isinstance(value, datetime.datetime):
536-
output = f"'{value.isoformat().replace('T', '')}'"
544+
output = f"'{value.isoformat().replace('T', ' ')}'"
545+
elif isinstance(value, datetime.date):
546+
output = f"'{value.isoformat()}'"
547+
elif isinstance(value, datetime.time):
548+
output = f"'{value.isoformat()}'"
549+
elif isinstance(value, datetime.timedelta):
550+
interval = IntervalCustom.from_timedelta(value)
551+
output = getattr(interval, self._meta.engine_type)
537552
elif isinstance(value, bytes):
538553
output = f"'{value.hex()}'"
554+
elif isinstance(value, uuid.UUID):
555+
output = f"'{value}'"
539556
elif isinstance(value, list):
540557
# Convert to the array syntax.
541558
output = (

piccolo/columns/column_types.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,23 @@ class Band(Table):
303303
value_type = uuid.UUID
304304

305305
def __init__(self, default: UUIDArg = UUID4(), **kwargs) -> None:
306+
if default is UUID4:
307+
# In case the class is passed in, instead of an instance.
308+
default = UUID4()
309+
306310
self._validate_default(default, UUIDArg.__args__) # type: ignore
307311

308312
if default == uuid.uuid4:
309313
default = UUID4()
310314

315+
if isinstance(default, str):
316+
try:
317+
default = uuid.UUID(default)
318+
except ValueError:
319+
raise ValueError(
320+
"The default is a string, but not a valid uuid."
321+
)
322+
311323
self.default = default
312324
kwargs.update({"default": default})
313325
super().__init__(**kwargs)
@@ -486,7 +498,7 @@ def column_type(self):
486498

487499
class Serial(Column):
488500
"""
489-
An alias to an autoincremenring integer column in Postgres.
501+
An alias to an autoincrementing integer column in Postgres.
490502
"""
491503

492504
def __init__(self, **kwargs) -> None:
@@ -656,6 +668,9 @@ def __init__(self, default: DateArg = DateNow(), **kwargs) -> None:
656668
if isinstance(default, date):
657669
default = DateCustom.from_date(default)
658670

671+
if default == date.today:
672+
default = DateNow()
673+
659674
self.default = default
660675
kwargs.update({"default": default})
661676
super().__init__(**kwargs)

piccolo/columns/defaults/base.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from abc import abstractmethod, abstractproperty, ABC
33
import typing as t
44

5+
from piccolo.utils.repr import repr_class_instance
6+
57

68
class Default(ABC):
79
@abstractproperty
@@ -50,11 +52,11 @@ def get_sqlite_interval_string(self, attributes: t.List[str]) -> str:
5052

5153
return ", ".join(interval_components)
5254

55+
def __repr__(self):
56+
return repr_class_instance(self)
57+
5358
def __str__(self):
54-
args_str = ", ".join(
55-
[f"{key}={value}" for key, value in self.__dict__.items()]
56-
)
57-
return f"{self.__class__.__name__}({args_str})"
59+
return self.__repr__()
5860

5961
def __eq__(self, other):
6062
return self.__hash__() == other.__hash__()

piccolo/columns/defaults/date.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ def __init__(
5050

5151
@property
5252
def postgres(self):
53-
return self.date.isostring()
53+
return f"'{self.date.isoformat()}'"
5454

5555
@property
5656
def sqlite(self):
57-
return self.date.isostring()
57+
return f"'{self.date.isoformat()}'"
5858

5959
def python(self):
6060
return self.date

piccolo/columns/defaults/time.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ def __init__(self, hour: int, minute: int, second: int):
5757

5858
@property
5959
def postgres(self):
60-
return self.time.isostring()
60+
return f"'{self.time.isoformat()}'"
6161

6262
@property
6363
def sqlite(self):
64-
return self.time.isostring()
64+
return f"'{self.time.isoformat()}'"
6565

6666
def python(self):
6767
return self.time

0 commit comments

Comments
 (0)