Merge lp:~jelmer/brz/memorytree-symlinks into lp:brz
- memorytree-symlinks
- Merge into trunk
Proposed by
Jelmer Vernooij
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~jelmer/brz/memorytree-symlinks | ||||
Merge into: | lp:brz | ||||
Diff against target: |
509 lines (+136/-108) 3 files modified
breezy/memorytree.py (+18/-4) breezy/tests/test_memorytree.py (+65/-70) breezy/transport/memory.py (+53/-34) |
||||
To merge this branch: | bzr merge lp:~jelmer/brz/memorytree-symlinks | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Breezy developers | Pending | ||
Review via email: mp+363285@code.launchpad.net |
Commit message
Description of the change
Implement MemoryTree.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'breezy/memorytree.py' |
2 | --- breezy/memorytree.py 2018-11-18 00:25:19 +0000 |
3 | +++ breezy/memorytree.py 2019-02-16 21:30:56 +0000 |
4 | @@ -22,6 +22,7 @@ |
5 | from __future__ import absolute_import |
6 | |
7 | import os |
8 | +import stat |
9 | |
10 | from . import ( |
11 | errors, |
12 | @@ -62,7 +63,15 @@ |
13 | with self.lock_tree_write(): |
14 | for f, file_id, kind in zip(files, ids, kinds): |
15 | if kind is None: |
16 | - kind = 'file' |
17 | + st_mode = self._file_transport.stat(f).st_mode |
18 | + if stat.S_ISREG(st_mode): |
19 | + kind = 'file' |
20 | + elif stat.S_ISLNK(st_mode): |
21 | + kind = 'symlink' |
22 | + elif stat.S_ISDIR(st_mode): |
23 | + kind = 'directory' |
24 | + else: |
25 | + raise AssertionError('Unknown file kind') |
26 | if file_id is None: |
27 | self._inventory.add_path(f, kind=kind) |
28 | else: |
29 | @@ -127,7 +136,7 @@ |
30 | # memory tree does not support nested trees yet. |
31 | return kind, None, None, None |
32 | elif kind == 'symlink': |
33 | - raise NotImplementedError('symlink support') |
34 | + return kind, None, None, self._inventory[id].symlink_target |
35 | else: |
36 | raise NotImplementedError('unknown kind') |
37 | |
38 | @@ -148,8 +157,7 @@ |
39 | return self._inventory.get_entry_by_path(path).executable |
40 | |
41 | def kind(self, path): |
42 | - file_id = self.path2id(path) |
43 | - return self._inventory[file_id].kind |
44 | + return self._inventory.get_entry_by_path(path).kind |
45 | |
46 | def mkdir(self, path, file_id=None): |
47 | """See MutableTree.mkdir().""" |
48 | @@ -227,6 +235,8 @@ |
49 | continue |
50 | if entry.kind == 'directory': |
51 | self._file_transport.mkdir(path) |
52 | + elif entry.kind == 'symlink': |
53 | + self._file_transport.symlink(entry.symlink_target, path) |
54 | elif entry.kind == 'file': |
55 | self._file_transport.put_file( |
56 | path, self._basis_tree.get_file(path)) |
57 | @@ -302,6 +312,10 @@ |
58 | else: |
59 | raise |
60 | |
61 | + def get_symlink_target(self, path): |
62 | + with self.lock_read(): |
63 | + return self._file_transport.readlink(path) |
64 | + |
65 | def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False): |
66 | """See MutableTree.set_parent_trees().""" |
67 | if len(parents_list) == 0: |
68 | |
69 | === modified file 'breezy/tests/test_memorytree.py' |
70 | --- breezy/tests/test_memorytree.py 2018-11-11 04:08:32 +0000 |
71 | +++ breezy/tests/test_memorytree.py 2019-02-16 21:30:56 +0000 |
72 | @@ -46,21 +46,17 @@ |
73 | rev_id = tree.commit('first post') |
74 | tree.unlock() |
75 | tree = MemoryTree.create_on_branch(branch) |
76 | - tree.lock_read() |
77 | - self.assertEqual([rev_id], tree.get_parent_ids()) |
78 | - with tree.get_file('foo') as f: |
79 | - self.assertEqual(b'contents of foo\n', f.read()) |
80 | - tree.unlock() |
81 | + with tree.lock_read(): |
82 | + self.assertEqual([rev_id], tree.get_parent_ids()) |
83 | + with tree.get_file('foo') as f: |
84 | + self.assertEqual(b'contents of foo\n', f.read()) |
85 | |
86 | def test_get_root_id(self): |
87 | branch = self.make_branch('branch') |
88 | tree = MemoryTree.create_on_branch(branch) |
89 | - tree.lock_write() |
90 | - try: |
91 | + with tree.lock_write(): |
92 | tree.add(['']) |
93 | self.assertIsNot(None, tree.get_root_id()) |
94 | - finally: |
95 | - tree.unlock() |
96 | |
97 | def test_lock_tree_write(self): |
98 | """Check we can lock_tree_write and unlock MemoryTrees.""" |
99 | @@ -73,9 +69,8 @@ |
100 | """Check that we error when trying to upgrade a read lock to write.""" |
101 | branch = self.make_branch('branch') |
102 | tree = MemoryTree.create_on_branch(branch) |
103 | - tree.lock_read() |
104 | - self.assertRaises(errors.ReadOnlyError, tree.lock_tree_write) |
105 | - tree.unlock() |
106 | + with tree.lock_read(): |
107 | + self.assertRaises(errors.ReadOnlyError, tree.lock_tree_write) |
108 | |
109 | def test_lock_write(self): |
110 | """Check we can lock_write and unlock MemoryTrees.""" |
111 | @@ -88,58 +83,63 @@ |
112 | """Check that we error when trying to upgrade a read lock to write.""" |
113 | branch = self.make_branch('branch') |
114 | tree = MemoryTree.create_on_branch(branch) |
115 | - tree.lock_read() |
116 | - self.assertRaises(errors.ReadOnlyError, tree.lock_write) |
117 | - tree.unlock() |
118 | + with tree.lock_read(): |
119 | + self.assertRaises(errors.ReadOnlyError, tree.lock_write) |
120 | |
121 | def test_add_with_kind(self): |
122 | branch = self.make_branch('branch') |
123 | tree = MemoryTree.create_on_branch(branch) |
124 | - tree.lock_write() |
125 | - tree.add(['', 'afile', 'adir'], None, |
126 | - ['directory', 'file', 'directory']) |
127 | - self.assertEqual('afile', tree.id2path(tree.path2id('afile'))) |
128 | - self.assertEqual('adir', tree.id2path(tree.path2id('adir'))) |
129 | - self.assertFalse(tree.has_filename('afile')) |
130 | - self.assertFalse(tree.has_filename('adir')) |
131 | - tree.unlock() |
132 | + with tree.lock_write(): |
133 | + tree.add(['', 'afile', 'adir'], None, |
134 | + ['directory', 'file', 'directory']) |
135 | + self.assertEqual('afile', tree.id2path(tree.path2id('afile'))) |
136 | + self.assertEqual('adir', tree.id2path(tree.path2id('adir'))) |
137 | + self.assertFalse(tree.has_filename('afile')) |
138 | + self.assertFalse(tree.has_filename('adir')) |
139 | |
140 | def test_put_new_file(self): |
141 | branch = self.make_branch('branch') |
142 | tree = MemoryTree.create_on_branch(branch) |
143 | - tree.lock_write() |
144 | - tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
145 | - kinds=['directory', 'file']) |
146 | - tree.put_file_bytes_non_atomic('foo', b'barshoom') |
147 | - self.assertEqual(b'barshoom', tree.get_file('foo').read()) |
148 | - tree.unlock() |
149 | + with tree.lock_write(): |
150 | + tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
151 | + kinds=['directory', 'file']) |
152 | + tree.put_file_bytes_non_atomic('foo', b'barshoom') |
153 | + with tree.get_file('foo') as f: |
154 | + self.assertEqual(b'barshoom', f.read()) |
155 | |
156 | def test_put_existing_file(self): |
157 | branch = self.make_branch('branch') |
158 | tree = MemoryTree.create_on_branch(branch) |
159 | - tree.lock_write() |
160 | - tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
161 | - kinds=['directory', 'file']) |
162 | - tree.put_file_bytes_non_atomic('foo', b'first-content') |
163 | - tree.put_file_bytes_non_atomic('foo', b'barshoom') |
164 | - self.assertEqual(b'barshoom', tree.get_file('foo').read()) |
165 | - tree.unlock() |
166 | + with tree.lock_write(): |
167 | + tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
168 | + kinds=['directory', 'file']) |
169 | + tree.put_file_bytes_non_atomic('foo', b'first-content') |
170 | + tree.put_file_bytes_non_atomic('foo', b'barshoom') |
171 | + self.assertEqual(b'barshoom', tree.get_file('foo').read()) |
172 | |
173 | def test_add_in_subdir(self): |
174 | branch = self.make_branch('branch') |
175 | tree = MemoryTree.create_on_branch(branch) |
176 | - tree.lock_write() |
177 | - self.addCleanup(tree.unlock) |
178 | - tree.add([''], [b'root-id'], ['directory']) |
179 | - # Unfortunately, the only way to 'mkdir' is to call 'tree.mkdir', but |
180 | - # that *always* adds the directory as well. So if you want to create a |
181 | - # file in a subdirectory, you have to split out the 'mkdir()' calls |
182 | - # from the add and put_file_bytes_non_atomic calls. :( |
183 | - tree.mkdir('adir', b'dir-id') |
184 | - tree.add(['adir/afile'], [b'file-id'], ['file']) |
185 | - self.assertEqual('adir/afile', tree.id2path(b'file-id')) |
186 | - self.assertEqual('adir', tree.id2path(b'dir-id')) |
187 | - tree.put_file_bytes_non_atomic('adir/afile', b'barshoom') |
188 | + with tree.lock_write(): |
189 | + tree.add([''], [b'root-id'], ['directory']) |
190 | + # Unfortunately, the only way to 'mkdir' is to call 'tree.mkdir', but |
191 | + # that *always* adds the directory as well. So if you want to create a |
192 | + # file in a subdirectory, you have to split out the 'mkdir()' calls |
193 | + # from the add and put_file_bytes_non_atomic calls. :( |
194 | + tree.mkdir('adir', b'dir-id') |
195 | + tree.add(['adir/afile'], [b'file-id'], ['file']) |
196 | + self.assertEqual('adir/afile', tree.id2path(b'file-id')) |
197 | + self.assertEqual('adir', tree.id2path(b'dir-id')) |
198 | + tree.put_file_bytes_non_atomic('adir/afile', b'barshoom') |
199 | + |
200 | + def test_add_symlink(self): |
201 | + branch = self.make_branch('branch') |
202 | + tree = MemoryTree.create_on_branch(branch) |
203 | + with tree.lock_write(): |
204 | + tree._file_transport.symlink('bar', 'foo') |
205 | + tree.add(['', 'foo']) |
206 | + self.assertEqual('symlink', tree.kind('foo')) |
207 | + self.assertEqual('bar', tree.get_symlink_target('foo')) |
208 | |
209 | def test_commit_trivial(self): |
210 | """Smoke test for commit on a MemoryTree. |
211 | @@ -149,40 +149,35 @@ |
212 | """ |
213 | branch = self.make_branch('branch') |
214 | tree = MemoryTree.create_on_branch(branch) |
215 | - tree.lock_write() |
216 | - tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
217 | - kinds=['directory', 'file']) |
218 | - tree.put_file_bytes_non_atomic('foo', b'barshoom') |
219 | - revision_id = tree.commit('message baby') |
220 | - # the parents list for the tree should have changed. |
221 | - self.assertEqual([revision_id], tree.get_parent_ids()) |
222 | - tree.unlock() |
223 | + with tree.lock_write(): |
224 | + tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
225 | + kinds=['directory', 'file']) |
226 | + tree.put_file_bytes_non_atomic('foo', b'barshoom') |
227 | + revision_id = tree.commit('message baby') |
228 | + # the parents list for the tree should have changed. |
229 | + self.assertEqual([revision_id], tree.get_parent_ids()) |
230 | # and we should have a revision that is accessible outside the tree lock |
231 | revtree = tree.branch.repository.revision_tree(revision_id) |
232 | - revtree.lock_read() |
233 | - self.addCleanup(revtree.unlock) |
234 | - with revtree.get_file('foo') as f: |
235 | + with revtree.lock_read(), revtree.get_file('foo') as f: |
236 | self.assertEqual(b'barshoom', f.read()) |
237 | |
238 | def test_unversion(self): |
239 | """Some test for unversion of a memory tree.""" |
240 | branch = self.make_branch('branch') |
241 | tree = MemoryTree.create_on_branch(branch) |
242 | - tree.lock_write() |
243 | - tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
244 | - kinds=['directory', 'file']) |
245 | - tree.unversion(['foo']) |
246 | - self.assertFalse(tree.is_versioned('foo')) |
247 | - self.assertFalse(tree.has_id(b'foo-id')) |
248 | - tree.unlock() |
249 | + with tree.lock_write(): |
250 | + tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
251 | + kinds=['directory', 'file']) |
252 | + tree.unversion(['foo']) |
253 | + self.assertFalse(tree.is_versioned('foo')) |
254 | + self.assertFalse(tree.has_id(b'foo-id')) |
255 | |
256 | def test_last_revision(self): |
257 | """There should be a last revision method we can call.""" |
258 | tree = self.make_branch_and_memory_tree('branch') |
259 | - tree.lock_write() |
260 | - tree.add('') |
261 | - rev_id = tree.commit('first post') |
262 | - tree.unlock() |
263 | + with tree.lock_write(): |
264 | + tree.add('') |
265 | + rev_id = tree.commit('first post') |
266 | self.assertEqual(rev_id, tree.last_revision()) |
267 | |
268 | def test_rename_file(self): |
269 | |
270 | === modified file 'breezy/transport/memory.py' |
271 | --- breezy/transport/memory.py 2018-11-17 16:53:10 +0000 |
272 | +++ breezy/transport/memory.py 2019-02-16 21:30:56 +0000 |
273 | @@ -26,9 +26,10 @@ |
274 | from io import ( |
275 | BytesIO, |
276 | ) |
277 | +import itertools |
278 | import os |
279 | import errno |
280 | -from stat import S_IFREG, S_IFDIR, S_IFLNK |
281 | +from stat import S_IFREG, S_IFDIR, S_IFLNK, S_ISDIR |
282 | |
283 | from .. import ( |
284 | transport, |
285 | @@ -50,20 +51,16 @@ |
286 | |
287 | class MemoryStat(object): |
288 | |
289 | - def __init__(self, size, kind, perms): |
290 | + def __init__(self, size, kind, perms=None): |
291 | self.st_size = size |
292 | - if kind == 'file': |
293 | + if not S_ISDIR(kind): |
294 | if perms is None: |
295 | perms = 0o644 |
296 | - self.st_mode = S_IFREG | perms |
297 | - elif kind == 'directory': |
298 | + self.st_mode = kind | perms |
299 | + else: |
300 | if perms is None: |
301 | perms = 0o755 |
302 | - self.st_mode = S_IFDIR | perms |
303 | - elif kind == 'symlink': |
304 | - self.st_mode = S_IFLNK | 0o644 |
305 | - else: |
306 | - raise AssertionError('unknown kind %r' % kind) |
307 | + self.st_mode = kind | perms |
308 | |
309 | |
310 | class MemoryTransport(transport.Transport): |
311 | @@ -80,7 +77,7 @@ |
312 | self._scheme = url[:split] |
313 | self._cwd = url[split:] |
314 | # dictionaries from absolute path to file mode |
315 | - self._dirs = {'/': None} |
316 | + self._dirs = {'/':None} |
317 | self._symlinks = {} |
318 | self._files = {} |
319 | self._locks = {} |
320 | @@ -95,6 +92,7 @@ |
321 | result._dirs = self._dirs |
322 | result._symlinks = self._symlinks |
323 | result._files = self._files |
324 | + result._symlinks = self._symlinks |
325 | result._locks = self._locks |
326 | return result |
327 | |
328 | @@ -111,7 +109,7 @@ |
329 | |
330 | def append_file(self, relpath, f, mode=None): |
331 | """See Transport.append_file().""" |
332 | - _abspath = self._abspath(relpath) |
333 | + _abspath = self._resolve_symlinks(relpath) |
334 | self._check_parent(_abspath) |
335 | orig_content, orig_mode = self._files.get(_abspath, (b"", None)) |
336 | if mode is None: |
337 | @@ -128,16 +126,20 @@ |
338 | def has(self, relpath): |
339 | """See Transport.has().""" |
340 | _abspath = self._abspath(relpath) |
341 | - return ((_abspath in self._files) |
342 | - or (_abspath in self._dirs) |
343 | - or (_abspath in self._symlinks)) |
344 | + for container in (self._files, self._dirs, self._symlinks): |
345 | + if _abspath in container.keys(): |
346 | + return True |
347 | + return False |
348 | |
349 | def delete(self, relpath): |
350 | """See Transport.delete().""" |
351 | _abspath = self._abspath(relpath) |
352 | - if _abspath not in self._files: |
353 | + if _abspath in self._files: |
354 | + del self._files[_abspath] |
355 | + elif _abspath in self._symlinks: |
356 | + del self._symlinks[_abspath] |
357 | + else: |
358 | raise NoSuchFile(relpath) |
359 | - del self._files[_abspath] |
360 | |
361 | def external_url(self): |
362 | """See breezy.transport.Transport.external_url.""" |
363 | @@ -147,8 +149,8 @@ |
364 | |
365 | def get(self, relpath): |
366 | """See Transport.get().""" |
367 | - _abspath = self._abspath(relpath) |
368 | - if _abspath not in self._files: |
369 | + _abspath = self._resolve_symlinks(relpath) |
370 | + if not _abspath in self._files: |
371 | if _abspath in self._dirs: |
372 | return LateReadError(relpath) |
373 | else: |
374 | @@ -157,15 +159,20 @@ |
375 | |
376 | def put_file(self, relpath, f, mode=None): |
377 | """See Transport.put_file().""" |
378 | - _abspath = self._abspath(relpath) |
379 | + _abspath = self._resolve_symlinks(relpath) |
380 | self._check_parent(_abspath) |
381 | raw_bytes = f.read() |
382 | self._files[_abspath] = (raw_bytes, mode) |
383 | return len(raw_bytes) |
384 | |
385 | + def symlink(self, source, target): |
386 | + _abspath = self._resolve_symlinks(target) |
387 | + self._check_parent(_abspath) |
388 | + self._symlinks[_abspath] = self._abspath(source) |
389 | + |
390 | def mkdir(self, relpath, mode=None): |
391 | """See Transport.mkdir().""" |
392 | - _abspath = self._abspath(relpath) |
393 | + _abspath = self._resolve_symlinks(relpath) |
394 | self._check_parent(_abspath) |
395 | if _abspath in self._dirs: |
396 | raise FileExists(relpath) |
397 | @@ -183,13 +190,13 @@ |
398 | return True |
399 | |
400 | def iter_files_recursive(self): |
401 | - for file in self._files: |
402 | + for file in itertools.chain(self._files, self._symlinks): |
403 | if file.startswith(self._cwd): |
404 | yield urlutils.escape(file[len(self._cwd):]) |
405 | |
406 | def list_dir(self, relpath): |
407 | """See Transport.list_dir().""" |
408 | - _abspath = self._abspath(relpath) |
409 | + _abspath = self._resolve_symlinks(relpath) |
410 | if _abspath != '/' and _abspath not in self._dirs: |
411 | raise NoSuchFile(relpath) |
412 | result = [] |
413 | @@ -197,7 +204,7 @@ |
414 | if not _abspath.endswith('/'): |
415 | _abspath += '/' |
416 | |
417 | - for path_group in self._files, self._dirs: |
418 | + for path_group in self._files, self._dirs, self._symlinks: |
419 | for path in path_group: |
420 | if path.startswith(_abspath): |
421 | trailing = path[len(_abspath):] |
422 | @@ -207,8 +214,8 @@ |
423 | |
424 | def rename(self, rel_from, rel_to): |
425 | """Rename a file or directory; fail if the destination exists""" |
426 | - abs_from = self._abspath(rel_from) |
427 | - abs_to = self._abspath(rel_to) |
428 | + abs_from = self._resolve_symlinks(rel_from) |
429 | + abs_to = self._resolve_symlinks(rel_to) |
430 | |
431 | def replace(x): |
432 | if x == abs_from: |
433 | @@ -233,21 +240,25 @@ |
434 | # fail differently depending on dict order. So work on copy, fail on |
435 | # error on only replace dicts if all goes well. |
436 | renamed_files = self._files.copy() |
437 | + renamed_symlinks = self._symlinks.copy() |
438 | renamed_dirs = self._dirs.copy() |
439 | do_renames(renamed_files) |
440 | + do_renames(renamed_symlinks) |
441 | do_renames(renamed_dirs) |
442 | # We may have been cloned so modify in place |
443 | self._files.clear() |
444 | self._files.update(renamed_files) |
445 | + self._symlinks.clear() |
446 | + self._symlinks.update(renamed_symlinks) |
447 | self._dirs.clear() |
448 | self._dirs.update(renamed_dirs) |
449 | |
450 | def rmdir(self, relpath): |
451 | """See Transport.rmdir.""" |
452 | - _abspath = self._abspath(relpath) |
453 | + _abspath = self._resolve_symlinks(relpath) |
454 | if _abspath in self._files: |
455 | self._translate_error(IOError(errno.ENOTDIR, relpath), relpath) |
456 | - for path in self._files: |
457 | + for path in itertools.chain(self._files, self._symlinks): |
458 | if path.startswith(_abspath + '/'): |
459 | self._translate_error(IOError(errno.ENOTEMPTY, relpath), |
460 | relpath) |
461 | @@ -262,13 +273,13 @@ |
462 | def stat(self, relpath): |
463 | """See Transport.stat().""" |
464 | _abspath = self._abspath(relpath) |
465 | - if _abspath in self._files: |
466 | - return MemoryStat(len(self._files[_abspath][0]), 'file', |
467 | + if _abspath in self._files.keys(): |
468 | + return MemoryStat(len(self._files[_abspath][0]), S_IFREG, |
469 | self._files[_abspath][1]) |
470 | - elif _abspath in self._dirs: |
471 | - return MemoryStat(0, 'directory', self._dirs[_abspath]) |
472 | - elif _abspath in self._symlinks: |
473 | - return MemoryStat(0, 'symlink', 0) |
474 | + elif _abspath in self._dirs.keys(): |
475 | + return MemoryStat(0, S_IFDIR, self._dirs[_abspath]) |
476 | + elif _abspath in self._symlinks.keys(): |
477 | + return MemoryStat(0, S_IFLNK) |
478 | else: |
479 | raise NoSuchFile(_abspath) |
480 | |
481 | @@ -280,6 +291,12 @@ |
482 | """See Transport.lock_write().""" |
483 | return _MemoryLock(self._abspath(relpath), self) |
484 | |
485 | + def _resolve_symlinks(self, relpath): |
486 | + path = self._abspath(relpath) |
487 | + while path in self._symlinks.keys(): |
488 | + path = self._symlinks[path] |
489 | + return path |
490 | + |
491 | def _abspath(self, relpath): |
492 | """Generate an internal absolute path.""" |
493 | relpath = urlutils.unescape(relpath) |
494 | @@ -336,6 +353,7 @@ |
495 | def start_server(self): |
496 | self._dirs = {'/': None} |
497 | self._files = {} |
498 | + self._symlinks = {} |
499 | self._locks = {} |
500 | self._scheme = "memory+%s:///" % id(self) |
501 | |
502 | @@ -344,6 +362,7 @@ |
503 | result = memory.MemoryTransport(url) |
504 | result._dirs = self._dirs |
505 | result._files = self._files |
506 | + result._symlinks = self._symlinks |
507 | result._locks = self._locks |
508 | return result |
509 | self._memory_factory = memory_factory |