Merge lp:~free.ekanayaka/storm/schema-advance into lp:storm

Proposed by Free Ekanayaka
Status: Merged
Merged at revision: 468
Proposed branch: lp:~free.ekanayaka/storm/schema-advance
Merge into: lp:storm
Diff against target: 641 lines (+336/-59)
6 files modified
storm/schema/patch.py (+108/-29)
storm/schema/schema.py (+89/-20)
storm/zope/testing.py (+2/-1)
tests/schema/patch.py (+86/-7)
tests/schema/schema.py (+49/-1)
tests/zope/testing.py (+2/-1)
To merge this branch: bzr merge lp:~free.ekanayaka/storm/schema-advance
Reviewer Review Type Date Requested Status
Björn Tillenius (community) Approve
Review via email: mp+242131@code.launchpad.net

Description of the change

Add a Schema.advance method that applies to the given store a certain patch version. This is useful for applying patches with the same number across a set of schemas.

To post a comment you must log in.
470. By Free Ekanayaka

Add PatchPackage class

471. By Free Ekanayaka

Add dummy patch module

472. By Free Ekanayaka

More backward compatibility

Revision history for this message
Björn Tillenius (bjornt) wrote :

Thanks for the changes. The new directory structure looks like a reasonable compromise. It makes it fairly easy to write patches for a single store only and it gives a good overview of which stores get patched.

It looks good in general, but I'm putting it in Needs Fixing, since some tests need to be enabled and rewritten.

review: Needs Fixing
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Thanks Bjorn, should be all good.

473. By Free Ekanayaka

Address review comments

Revision history for this message
Björn Tillenius (bjornt) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'storm/schema/patch.py'
2--- storm/schema/patch.py 2010-08-18 08:16:52 +0000
3+++ storm/schema/patch.py 2014-12-18 10:58:55 +0000
4@@ -37,6 +37,7 @@
5 import sys
6 import os
7 import re
8+import types
9
10 from storm.locals import StormError, Int
11
12@@ -78,32 +79,25 @@
13 """Apply to a L{Store} the database patches from a given Python package.
14
15 @param store: The L{Store} to apply the patches to.
16- @param package: The Python package containing the patches. Each patch is
17- represented by a file inside the module, whose filename must match
18- the format 'patch_N.py', where N is an integer number.
19+ @param patch_set: The L{PatchSet} containing the patches to apply.
20 @param committer: Optionally an object implementing 'commit()' and
21 'rollback()' methods, to be used to commit or rollback the changes
22 after applying a patch. If C{None} is given, the C{store} itself is
23 used.
24 """
25
26- def __init__(self, store, package, committer=None):
27+ def __init__(self, store, patch_set, committer=None):
28 self._store = store
29- self._package = package
30+ if isinstance(patch_set, types.ModuleType):
31+ # Up to version 0.20.0 the second positional parameter used to
32+ # be a raw module containing the patches. We wrap it with PatchSet
33+ # for keeping backward-compatibility.
34+ patch_set = PatchSet(patch_set)
35+ self._patch_set = patch_set
36 if committer is None:
37 committer = store
38 self._committer = committer
39
40- def _module(self, version):
41- """Import the Python module of the patch file with the given version.
42-
43- @param: The version of the module patch to import.
44- @return: The imported module.
45- """
46- module_name = "patch_%d" % (version,)
47- return __import__(self._package.__name__ + "." + module_name,
48- None, None, [''])
49-
50 def apply(self, version):
51 """Execute the patch with the given version.
52
53@@ -116,7 +110,7 @@
54 self._store.add(patch)
55 module = None
56 try:
57- module = self._module(version)
58+ module = self._patch_set.get_patch_module(version)
59 module.apply(self._store)
60 except StormError:
61 self._committer.rollback()
62@@ -136,10 +130,8 @@
63 @raises UnknownPatchError: If the patch table has versions for which
64 no patch file actually exists.
65 """
66- unknown_patches = self.get_unknown_patch_versions()
67- if unknown_patches:
68- raise UnknownPatchError(self._store, unknown_patches)
69- for version in self._get_unapplied_versions():
70+ self.check_unknown()
71+ for version in self.get_unapplied_versions():
72 self.apply(version)
73
74 def mark_applied(self, version):
75@@ -149,12 +141,12 @@
76
77 def mark_applied_all(self):
78 """Mark all unapplied patches as applied."""
79- for version in self._get_unapplied_versions():
80+ for version in self.get_unapplied_versions():
81 self.mark_applied(version)
82
83 def has_pending_patches(self):
84 """Return C{True} if there are unapplied patches, C{False} if not."""
85- for version in self._get_unapplied_versions():
86+ for version in self.get_unapplied_versions():
87 return True
88 return False
89
90@@ -164,7 +156,7 @@
91 database, but don't appear in the schema's patches module.
92 """
93 applied = self._get_applied_patches()
94- known_patches = self._get_patch_versions()
95+ known_patches = self._patch_set.get_patch_versions()
96 unknown_patches = set()
97
98 for patch in applied:
99@@ -172,10 +164,20 @@
100 unknown_patches.add(patch)
101 return unknown_patches
102
103- def _get_unapplied_versions(self):
104+ def check_unknown(self):
105+ """Look for patches that we don't know about.
106+
107+ @raises UnknownPatchError: If the store has applied patch versions
108+ this schema doesn't know about.
109+ """
110+ unknown_patches = self.get_unknown_patch_versions()
111+ if unknown_patches:
112+ raise UnknownPatchError(self._store, unknown_patches)
113+
114+ def get_unapplied_versions(self):
115 """Return the versions of all unapplied patches."""
116 applied = self._get_applied_patches()
117- for version in self._get_patch_versions():
118+ for version in self._patch_set.get_patch_versions():
119 if version not in applied:
120 yield version
121
122@@ -186,12 +188,89 @@
123 applied.add(patch.version)
124 return applied
125
126- def _get_patch_versions(self):
127+
128+class PatchSet(object):
129+ """A collection of patch modules.
130+
131+ Each patch module lives in a regular Python module file, contained in a
132+ sub-directory named against the patch version. For example, given
133+ a directory tree like:
134+
135+ mypackage/
136+ __init__.py
137+ patch_1/
138+ __init__.py
139+ foo.py
140+
141+ the following code will return a patch module object for foo.py:
142+
143+ >>> import mypackage
144+ >>> patch_set = PackagePackage(mypackage, sub_level="foo")
145+ >>> patch_module = patch_set.get_patch_module(1)
146+ >>> print patch_module.__name__
147+ 'mypackage.patch_1.foo'
148+
149+ Different sub-levels can be used to apply different patches to different
150+ stores (see L{Sharding}).
151+
152+ Alternatively if no sub-level is provided, the structure will be flat:
153+
154+ mypackage/
155+ __init__.py
156+ patch_1.py
157+
158+ >>> import mypackage
159+ >>> patch_set = PackagePackage(mypackage)
160+ >>> patch_module = patch_set.get_patch_module(1)
161+ >>> print patch_module.__name__
162+ 'mypackage.patch_1'
163+
164+ This simpler structure can be used if you have just one store to patch
165+ or you don't care to co-ordinate the patches across your stores.
166+ """
167+
168+ def __init__(self, package, sub_level=None):
169+ self._package = package
170+ self._sub_level = sub_level
171+
172+ def get_patch_versions(self):
173 """Return the versions of all available patches."""
174- format = re.compile(r"^patch_(\d+).py$")
175+ pattern = r"^patch_(\d+)"
176+ if not self._sub_level:
177+ pattern += ".py"
178+ pattern += "$"
179+ format = re.compile(pattern)
180
181- filenames = os.listdir(os.path.dirname(self._package.__file__))
182+ patch_directory = self._get_patch_directory()
183+ filenames = os.listdir(patch_directory)
184 matches = [(format.match(fn), fn) for fn in filenames]
185 matches = sorted(filter(lambda x: x[0], matches),
186- key=lambda x: int(x[1][6:-3]))
187+ key=lambda x: int(x[0].group(1)))
188 return [int(match.group(1)) for match, filename in matches]
189+
190+ def get_patch_module(self, version):
191+ """Import the Python module of the patch file with the given version.
192+
193+ @param: The version of the module patch to import.
194+ @return: The imported module.
195+ """
196+ name = "patch_%d" % version
197+ levels = [self._package.__name__, name]
198+ if self._sub_level:
199+ directory = self._get_patch_directory()
200+ path = os.path.join(directory, name, self._sub_level + ".py")
201+ if not os.path.exists(path):
202+ return _EmptyPatchModule()
203+ levels.append(self._sub_level)
204+ return __import__(".".join(levels), None, None, [''])
205+
206+ def _get_patch_directory(self):
207+ """Get the path to the directory of the patch package."""
208+ return os.path.dirname(self._package.__file__)
209+
210+
211+class _EmptyPatchModule(object):
212+ """Fake module object with a no-op C{apply} function."""
213+
214+ def apply(self, store):
215+ pass
216
217=== modified file 'storm/schema/schema.py'
218--- storm/schema/schema.py 2013-09-11 08:00:51 +0000
219+++ storm/schema/schema.py 2014-12-18 10:58:55 +0000
220@@ -34,7 +34,8 @@
221 >>> drops = ['DROP TABLE person']
222 >>> deletes = ['DELETE FROM person']
223 >>> import patch_module
224->>> schema = Schema(creates, drops, deletes, patch_module)
225+>>> patch_set = PatchSet(patch_module)
226+>>> schema = Schema(creates, drops, deletes, patch_set)
227 >>> schema.create(store)
228
229
230@@ -42,8 +43,24 @@
231 upgrade the schema over time.
232 """
233
234+import types
235+
236 from storm.locals import StormError
237-from storm.schema.patch import PatchApplier
238+from storm.schema.patch import PatchApplier, PatchSet
239+
240+
241+class SchemaMissingError(Exception):
242+ """Raised when a L{Store} has no schema at all."""
243+
244+
245+class UnappliedPatchesError(Exception):
246+ """Raised when a L{Store} has unapplied schema patches.
247+
248+ @ivar unapplied_versions: A list containing all unapplied patch versions.
249+ """
250+
251+ def __init__(self, unapplied_versions):
252+ self.unapplied_versions = unapplied_versions
253
254
255 class Schema(object):
256@@ -52,7 +69,7 @@
257 @param creates: A list of C{CREATE TABLE} statements.
258 @param drops: A list of C{DROP TABLE} statements.
259 @param deletes: A list of C{DELETE FROM} statements.
260- @param patch_package: The Python package containing patch modules to apply.
261+ @param patch_set: The L{PatchSet} containing patch modules to apply.
262 @param committer: Optionally a committer to pass to the L{PatchApplier}.
263
264 @see: L{PatchApplier}.
265@@ -61,11 +78,16 @@
266 _drop_patch = "DROP TABLE IF EXISTS patch"
267 _autocommit = True
268
269- def __init__(self, creates, drops, deletes, patch_package, committer=None):
270+ def __init__(self, creates, drops, deletes, patch_set, committer=None):
271 self._creates = creates
272 self._drops = drops
273 self._deletes = deletes
274- self._patch_package = patch_package
275+ if isinstance(patch_set, types.ModuleType):
276+ # Up to version 0.20.0 the fourth positional parameter used to
277+ # be a raw module containing the patches. We wrap it with PatchSet
278+ # for keeping backward-compatibility.
279+ patch_set = PatchSet(patch_set)
280+ self._patch_set = patch_set
281 self._committer = committer
282
283 def _execute_statements(self, store, statements):
284@@ -90,10 +112,38 @@
285 """
286 self._autocommit = flag
287
288+ def check(self, store):
289+ """Check that the given L{Store} is compliant with this L{Schema}.
290+
291+ @param store: The L{Store} to check.
292+
293+ @raises SchemaMissingError: If there is no schema at all.
294+ @raises UnappliedPatchesError: If there are unapplied schema patches.
295+ @raises UnknownPatchError: If the store has patches the schema doesn't.
296+ """
297+ try:
298+ store.execute("SELECT * FROM patch WHERE version=0")
299+ except StormError:
300+ # No schema at all. Create it from the ground.
301+ store.rollback()
302+ raise SchemaMissingError()
303+
304+ patch_applier = self._build_patch_applier(store)
305+ patch_applier.check_unknown()
306+ unapplied_versions = list(patch_applier.get_unapplied_versions())
307+ if unapplied_versions:
308+ raise UnappliedPatchesError(unapplied_versions)
309+
310 def create(self, store):
311- """Run C{CREATE TABLE} SQL statements with C{store}."""
312+ """Run C{CREATE TABLE} SQL statements with C{store}.
313+
314+ @raises SchemaAlreadyCreatedError: If the schema for this store was
315+ already created.
316+ """
317 self._execute_statements(store, [self._create_patch])
318 self._execute_statements(store, self._creates)
319+ patch_applier = self._build_patch_applier(store)
320+ patch_applier.mark_applied_all()
321
322 def drop(self, store):
323 """Run C{DROP TABLE} SQL statements with C{store}."""
324@@ -110,20 +160,39 @@
325 If a schema isn't present a new one will be created. Unapplied
326 patches will be applied to an existing schema.
327 """
328- class NoopCommitter(object):
329- commit = lambda _: None
330- rollback = lambda _: None
331-
332- committer = self._committer if self._autocommit else NoopCommitter()
333- patch_applier = PatchApplier(store, self._patch_package, committer)
334+ patch_applier = self._build_patch_applier(store)
335 try:
336- store.execute("SELECT * FROM patch WHERE version=0")
337- except StormError:
338+ self.check(store)
339+ except SchemaMissingError:
340 # No schema at all. Create it from the ground.
341- store.rollback()
342 self.create(store)
343- patch_applier.mark_applied_all()
344- if self._autocommit:
345- store.commit()
346- else:
347- patch_applier.apply_all()
348+ except UnappliedPatchesError, error:
349+ patch_applier.check_unknown()
350+ for version in error.unapplied_versions:
351+ self.advance(store, version)
352+
353+ def advance(self, store, version):
354+ """Advance the schema of C{store} by applying the next unapplied patch.
355+
356+ @return: The version of patch that has been applied or C{None} if
357+ no patch was applied (i.e. the schema is fully upgraded).
358+ """
359+ patch_applier = self._build_patch_applier(store)
360+ patch_applier.apply(version)
361+
362+ def _build_patch_applier(self, store):
363+ """Build a L{PatchApplier} to use for the given C{store}."""
364+ committer = self._committer
365+ if not self._autocommit:
366+ committer = _NoopCommitter()
367+ return PatchApplier(store, self._patch_set, committer)
368+
369+
370+class _NoopCommitter(object):
371+ """Dummy committer that does nothing."""
372+
373+ def commit(self):
374+ pass
375+
376+ def rollback(self):
377+ pass
378
379=== modified file 'storm/zope/testing.py'
380--- storm/zope/testing.py 2012-08-27 11:57:29 +0000
381+++ storm/zope/testing.py 2014-12-18 10:58:55 +0000
382@@ -196,7 +196,8 @@
383 """
384 Return the modification time of the C{schema}'s patch directory.
385 """
386- schema_stat = os.stat(os.path.dirname(schema._patch_package.__file__))
387+ patch_directory = os.path.dirname(schema._patch_set._package.__file__)
388+ schema_stat = os.stat(patch_directory)
389 return int(schema_stat.st_mtime)
390
391 def _get_schema_stamp_mtime(self, name):
392
393=== modified file 'tests/schema/patch.py'
394--- tests/schema/patch.py 2010-08-17 23:57:51 +0000
395+++ tests/schema/patch.py 2014-12-18 10:58:55 +0000
396@@ -24,7 +24,7 @@
397
398 from storm.locals import StormError, Store, create_database
399 from storm.schema.patch import (
400- Patch, PatchApplier, UnknownPatchError, BadPatchError)
401+ Patch, PatchApplier, UnknownPatchError, BadPatchError, PatchSet)
402 from tests.mocker import MockerTestCase
403
404
405@@ -99,10 +99,10 @@
406 self.committed += 1
407
408
409-class PatchTest(MockerTestCase):
410+class PatchApplierTest(MockerTestCase):
411
412 def setUp(self):
413- super(PatchTest, self).setUp()
414+ super(PatchApplierTest, self).setUp()
415
416 self.patchdir = self.makeDir()
417 self.pkgdir = os.path.join(self.patchdir, "mypackage")
418@@ -133,6 +133,7 @@
419
420 import mypackage
421 self.mypackage = mypackage
422+ self.patch_set = PatchSet(mypackage)
423
424 # Create another connection just to keep track of the state of the
425 # whole transaction manager. See the assertion functions below.
426@@ -152,11 +153,11 @@
427 self.another_store.rollback()
428
429 self.committer = Committer()
430- self.patch_applier = PatchApplier(self.store, self.mypackage,
431+ self.patch_applier = PatchApplier(self.store, self.patch_set,
432 self.committer)
433
434 def tearDown(self):
435- super(PatchTest, self).tearDown()
436+ super(PatchApplierTest, self).tearDown()
437 self.committer.rollback()
438 sys.path.remove(self.patchdir)
439 for name in list(sys.modules):
440@@ -202,6 +203,21 @@
441
442 self.assert_transaction_committed()
443
444+ def test_apply_with_patch_directory(self):
445+ """
446+ If the given L{PatchSet} uses sub-level patches, then the
447+ L{PatchApplier.apply} method will look at the per-patch directory and
448+ apply the relevant sub-level patch.
449+ """
450+ path = os.path.join(self.pkgdir, "patch_99")
451+ self.makeDir(path=path)
452+ self.makeFile(content="", path=os.path.join(path, "__init__.py"))
453+ self.makeFile(content=patch_test_0, path=os.path.join(path, "foo.py"))
454+ self.patch_set._sub_level = "foo"
455+ self.add_module("patch_99/foo.py", patch_test_0)
456+ self.patch_applier.apply(99)
457+ self.assertTrue(self.store.get(Patch, (99)))
458+
459 def test_apply_all(self):
460 """
461 L{PatchApplier.apply_all} executes all unapplied patches.
462@@ -239,10 +255,10 @@
463 """
464 self.add_module("patch_666.py", patch_explosion)
465 self.add_module("patch_667.py", patch_after_explosion)
466- self.assertEquals(list(self.patch_applier._get_unapplied_versions()),
467+ self.assertEquals(list(self.patch_applier.get_unapplied_versions()),
468 [42, 380, 666, 667])
469 self.assertRaises(StormError, self.patch_applier.apply_all)
470- self.assertEquals(list(self.patch_applier._get_unapplied_versions()),
471+ self.assertEquals(list(self.patch_applier.get_unapplied_versions()),
472 [666, 667])
473
474 def test_mark_applied(self):
475@@ -381,3 +397,66 @@
476 self.assertTrue("# Comment" in formatted)
477 else:
478 self.fail("BadPatchError not raised")
479+
480+
481+class PatchSetTest(MockerTestCase):
482+
483+ def setUp(self):
484+ super(PatchSetTest, self).setUp()
485+ self.sys_dir = self.makeDir()
486+ self.package_dir = os.path.join(self.sys_dir, "mypackage")
487+ os.makedirs(self.package_dir)
488+
489+ self.makeFile(
490+ content="", dirname=self.package_dir, basename="__init__.py")
491+
492+ sys.path.append(self.sys_dir)
493+ import mypackage
494+ self.patch_package = PatchSet(mypackage, sub_level="foo")
495+
496+ def tearDown(self):
497+ super(PatchSetTest, self).tearDown()
498+ for name in list(sys.modules):
499+ if name == "mypackage" or name.startswith("mypackage."):
500+ del sys.modules[name]
501+
502+ def test_get_patch_versions(self):
503+ """
504+ The C{get_patch_versions} method returns the available patch versions,
505+ by looking at directories named like "patch_N".
506+ """
507+ patch_1_dir = os.path.join(self.package_dir, "patch_1")
508+ os.makedirs(patch_1_dir)
509+ self.assertEqual([1], self.patch_package.get_patch_versions())
510+
511+ def test_get_patch_versions_ignores_non_patch_directories(self):
512+ """
513+ The C{get_patch_versions} method ignores files or directories not
514+ matching the required name pattern.
515+ """
516+ random_dir = os.path.join(self.package_dir, "random")
517+ os.makedirs(random_dir)
518+ self.assertEqual([], self.patch_package.get_patch_versions())
519+
520+ def test_get_patch_module(self):
521+ """
522+ The C{get_patch_module} method returns the Python module for the patch
523+ with the given version.
524+ """
525+ patch_1_dir = os.path.join(self.package_dir, "patch_1")
526+ os.makedirs(patch_1_dir)
527+ self.makeFile(content="", dirname=patch_1_dir, basename="__init__.py")
528+ self.makeFile(content="", dirname=patch_1_dir, basename="foo.py")
529+ patch_module = self.patch_package.get_patch_module(1)
530+ self.assertEqual("mypackage.patch_1.foo", patch_module.__name__)
531+
532+ def test_get_patch_module_no_sub_level(self):
533+ """
534+ The C{get_patch_module} method returns a dummy patch module if no
535+ sub-level file exists in the patch directory for the given version.
536+ """
537+ patch_1_dir = os.path.join(self.package_dir, "patch_1")
538+ os.makedirs(patch_1_dir)
539+ patch_module = self.patch_package.get_patch_module(1)
540+ store = object()
541+ self.assertIsNone(patch_module.apply(store))
542
543=== modified file 'tests/schema/schema.py'
544--- tests/schema/schema.py 2012-09-28 12:31:28 +0000
545+++ tests/schema/schema.py 2014-12-18 10:58:55 +0000
546@@ -22,7 +22,8 @@
547 import sys
548
549 from storm.locals import StormError, Store, create_database
550-from storm.schema import Schema
551+from storm.schema.schema import (
552+ Schema, SchemaMissingError, UnappliedPatchesError)
553 from tests.mocker import MockerTestCase
554
555
556@@ -96,6 +97,29 @@
557
558 return Package(package_dir, name)
559
560+ def test_check_with_missing_schema(self):
561+ """
562+ L{Schema.check} raises an exception if the given store is
563+ completely pristine and no schema has been applied yet.
564+ """
565+ rollbacks = []
566+ self.store.rollback = lambda: rollbacks.append(True)
567+ self.assertRaises(SchemaMissingError, self.schema.check, self.store)
568+ self.assertEqual([True], rollbacks)
569+
570+ def test_check_with_unapplied_patches(self):
571+ """
572+ L{Schema.check} raises an exception if the given store has unapplied
573+ schema patches.
574+ """
575+ self.schema.create(self.store)
576+ contents = """
577+def apply(store):
578+ pass
579+"""
580+ self.package.create_module("patch_1.py", contents)
581+ self.assertRaises(UnappliedPatchesError, self.schema.check, self.store)
582+
583 def test_create(self):
584 """
585 L{Schema.create} can be used to create the tables of a L{Store}.
586@@ -104,6 +128,9 @@
587 self.store.execute, "SELECT * FROM person")
588 self.schema.create(self.store)
589 self.assertEquals(list(self.store.execute("SELECT * FROM person")), [])
590+ # By default changes are committed
591+ store2 = Store(self.database)
592+ self.assertEquals(list(store2.execute("SELECT * FROM person")), [])
593
594 def test_create_with_autocommit_off(self):
595 """
596@@ -190,3 +217,24 @@
597 "INSERT INTO person (id, name, phone) VALUES (1, 'Jane', '123')")
598 self.assertEquals(list(self.store.execute("SELECT * FROM person")),
599 [(1, u"Jane", u"123")])
600+
601+ def test_advance(self):
602+ """
603+ L{Schema.advance} executes the given patch version.
604+ """
605+ self.schema.create(self.store)
606+ contents1 = """
607+def apply(store):
608+ store.execute('ALTER TABLE person ADD COLUMN phone TEXT')
609+"""
610+ contents2 = """
611+def apply(store):
612+ store.execute('ALTER TABLE person ADD COLUMN address TEXT')
613+"""
614+ self.package.create_module("patch_1.py", contents1)
615+ self.package.create_module("patch_2.py", contents2)
616+ self.schema.advance(self.store, 1)
617+ self.store.execute(
618+ "INSERT INTO person (id, name, phone) VALUES (1, 'Jane', '123')")
619+ self.assertEquals(list(self.store.execute("SELECT * FROM person")),
620+ [(1, u"Jane", u"123")])
621
622=== modified file 'tests/zope/testing.py'
623--- tests/zope/testing.py 2012-08-27 11:57:29 +0000
624+++ tests/zope/testing.py 2014-12-18 10:58:55 +0000
625@@ -27,6 +27,7 @@
626 from storm.locals import create_database, Store, Unicode, Int
627 from storm.exceptions import IntegrityError
628 from storm.testing import CaptureTracer
629+from storm.schema.patch import PatchSet
630
631 if has_transaction and has_zope_component and has_testresources:
632 from zope.component import provideUtility, getUtility
633@@ -62,7 +63,7 @@
634 drop = ["DROP TABLE test"]
635 delete = ["DELETE FROM test"]
636 uri = "sqlite:///%s" % self.makeFile()
637- schema = ZSchema(create, drop, delete, patch_package)
638+ schema = ZSchema(create, drop, delete, PatchSet(patch_package))
639 self.databases = [{"name": "test", "uri": uri, "schema": schema}]
640 self.resource = ZStormResourceManager(self.databases)
641 self.store = Store(create_database(uri))

Subscribers

People subscribed via source and target branches

to status/vote changes: