diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2013-08-09 05:37:50 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2013-08-09 05:37:50 (GMT) |
commit | 6a59ba5d31c15fabc28b3a2fb1aeab0e1991468e (patch) | |
tree | 9c379d9e3160fe25f37f48b4ecaef9a1c583d557 | |
parent | 3917eb055920d878a1a21f8f5170baffb44c39fa (diff) |
Create context while uploading initial implementation
-rwxr-xr-x | sugar-network | 30 | ||||
-rw-r--r-- | sugar_network/client/routes.py | 7 | ||||
-rw-r--r-- | sugar_network/db/directory.py | 2 | ||||
-rw-r--r-- | sugar_network/db/resource.py | 3 | ||||
-rw-r--r-- | sugar_network/db/routes.py | 11 | ||||
-rw-r--r-- | sugar_network/node/routes.py | 40 | ||||
-rw-r--r-- | sugar_network/toolkit/http.py | 3 | ||||
-rw-r--r-- | sugar_network/toolkit/router.py | 149 | ||||
-rwxr-xr-x | tests/units/client/online_routes.py | 3 | ||||
-rwxr-xr-x | tests/units/db/routes.py | 4 | ||||
-rwxr-xr-x | tests/units/node/node.py | 76 | ||||
-rwxr-xr-x | tests/units/toolkit/router.py | 44 |
12 files changed, 248 insertions, 124 deletions
diff --git a/sugar-network b/sugar-network index a3e233a..d48c58e 100755 --- a/sugar-network +++ b/sugar-network @@ -135,18 +135,21 @@ class Application(application.Application): path = self.args.pop(0) enforce(isfile(path), 'Cannot open bundle') - props = {'tags': []} - self._parse_args(props['tags'], props) + props = {} + self._parse_args(props) if 'license' in props: value = [i for i in _LIST_RE.split(props['license'].strip()) if i] props['license'] = value - guid = self._connect().upload(['implementation'], path, cmd='release') + conn = self._connect() + # XXX Have to proceed auth before uploading data + conn.get(cmd='whoami') + guid = conn.upload(['implementation'], path, cmd='release', **props) if porcelain.value: print guid else: - print dumps(guid) + print '-- Uploaded %s implementaion' % guid @application.command( 'send raw API POST request; ' @@ -198,7 +201,7 @@ class Application(application.Application): def head(self): request = Request() self._parse_path(request) - self._parse_args([], request) + self._parse_args(request) result = self._connect().meta(request.path, request.query) self._dump(result, []) @@ -240,8 +243,7 @@ class Application(application.Application): pass self._parse_path(request) - reply = [] - self._parse_args(reply, request) + self._parse_args(request) pid_path = None server = None @@ -267,7 +269,7 @@ class Application(application.Application): return if response.content_type == 'application/json': - self._dump(result, reply) + self._dump(result, reply.get('reply') or ['guid']) elif response.content_type == 'text/event-stream': while True: chunk = toolkit.readline(result) @@ -290,16 +292,16 @@ class Application(application.Application): if self.args and self.args[0].startswith('/'): request.path = self.args.pop(0).strip('/').split('/') - def _parse_args(self, tags, props): + def _parse_args(self, props): for arg in self.args: arg = shlex.split(arg) if not arg: continue - arg = arg[0] - if '=' not in arg: - tags.append(arg) - continue - arg, value = arg.split('=', 1) + if '=' in arg: + arg, value = arg[0].split('=', 1) + else: + arg = arg[0] + value = 1 arg = arg.strip() enforce(arg, 'No argument name in %r expression', arg) if arg in props: diff --git a/sugar_network/client/routes.py b/sugar_network/client/routes.py index 38f586e..942b052 100644 --- a/sugar_network/client/routes.py +++ b/sugar_network/client/routes.py @@ -508,14 +508,13 @@ class ClientRoutes(model.Routes, journal.Routes): requires=request.get('requires')) else: pipe = injector.clone(request.guid) + event = {} for event in pipe: event['event'] = 'clone' self.broadcast(event) - for __ in clones.walk(request.guid): - break - else: - # Cloning was failed + if event.get('state') == 'failure': self._checkin_context(request.guid, {'clone': 0}) + raise RuntimeError(event['error']) def _clone_jobject(self, request, get_props): if request.content: diff --git a/sugar_network/db/directory.py b/sugar_network/db/directory.py index b8d9176..b9fc02c 100644 --- a/sugar_network/db/directory.py +++ b/sugar_network/db/directory.py @@ -148,7 +148,7 @@ class Directory(object): cached_props = self._index.get_cached(guid) record = self._storage.get(guid) enforce(cached_props or record.exists, http.NotFound, - 'Document %r does not exist in %r', + 'Resource %r does not exist in %r', guid, self.metadata.name) return self.document_class(guid, record, cached_props) diff --git a/sugar_network/db/resource.py b/sugar_network/db/resource.py index fe071dc..7209c49 100644 --- a/sugar_network/db/resource.py +++ b/sugar_network/db/resource.py @@ -122,6 +122,9 @@ class Resource(object): def modified(self, prop): return prop in self._modifies + def __contains__(self, prop): + return self.get(prop) + def __getitem__(self, prop): return self.get(prop) diff --git a/sugar_network/db/routes.py b/sugar_network/db/routes.py index e61199a..5706587 100644 --- a/sugar_network/db/routes.py +++ b/sugar_network/db/routes.py @@ -215,14 +215,13 @@ class Routes(object): else access) if value is None: value = {'blob': None} - elif isinstance(value, dict): - enforce('url' in value, - 'Key %r is not specified in %r blob property', - 'url', name) - value = {'url': value['url']} - else: + elif isinstance(value, basestring) or hasattr(value, 'read'): value = _read_blob(request, prop, value) blobs.append(value['blob']) + elif isinstance(value, dict): + enforce('url' in value or 'blob' in value, 'No bundle') + else: + raise RuntimeError('Incorrect BLOB value') else: prop.assert_access(access) if prop.localized and isinstance(value, basestring): diff --git a/sugar_network/node/routes.py b/sugar_network/node/routes.py index 3e457c8..e2781e7 100644 --- a/sugar_network/node/routes.py +++ b/sugar_network/node/routes.py @@ -139,14 +139,15 @@ class NodeRoutes(db.Routes, model.Routes): return toolkit.iter_file(path) @route('POST', ['implementation'], cmd='release', - mime_type='application/json') + arguments={'initial': False}, + mime_type='application/json', acl=ACL.AUTH | ACL.AUTHOR) def release(self, request, document): with toolkit.NamedTemporaryFile() as blob: shutil.copyfileobj(request.content_stream, blob) blob.flush() - with load_bundle(self.volume, blob.name, request) as impl: + with load_bundle(self.volume, request, blob.name) as impl: impl['data']['blob'] = blob.name - return impl['guid'] + return impl['guid'] @route('DELETE', [None, None], acl=ACL.AUTH | ACL.AUTHOR) def delete(self, request): @@ -405,12 +406,15 @@ class NodeRoutes(db.Routes, model.Routes): @contextmanager -def load_bundle(volume, bundle_path, impl=None): - if impl is None: - impl = {} +def load_bundle(volume, request, bundle_path): + impl = request.copy() + initial = False + if 'initial' in impl: + initial = impl.pop('initial') data = impl.setdefault('data', {}) data['blob'] = bundle_path - context_updates = {} + contexts = volume['context'] + context = None try: bundle = Bundle(bundle_path, mime_type='application/zip') @@ -421,14 +425,16 @@ def load_bundle(volume, bundle_path, impl=None): _logger.debug('Load Sugar Activity bundle from %r', bundle_path) context_type = 'activity' unpack_size = 0 + with bundle: for arcname in bundle.get_names(): unpack_size += bundle.getmember(arcname).size spec = bundle.get_spec() extract = bundle.rootdir - context_updates = _load_context_metadata(bundle, spec) + context = _load_context_metadata(bundle, spec) if 'requires' in impl: spec.requires.update(parse_requires(impl.pop('requires'))) + impl['context'] = spec['context'] impl['version'] = spec['version'] impl['stability'] = spec['stability'] @@ -441,11 +447,15 @@ def load_bundle(volume, bundle_path, impl=None): data['unpack_size'] = unpack_size data['mime_type'] = 'application/vnd.olpc-sugar' + if initial and not contexts.exists(impl['context']): + context['guid'] = impl['context'] + context['type'] = 'activity' + request.call(method='POST', path=['context'], content=context) + context = None + enforce('context' in impl, 'Context is not specified') enforce('version' in impl, 'Version is not specified') - enforce(volume['context'].exists(impl['context']), - http.BadRequest, 'No such activity') - enforce(context_type in volume['context'].get(spec['context'])['type'], + enforce(context_type in contexts.get(spec['context'])['type'], http.BadRequest, 'Inappropriate bundle type') if impl.get('license') in (None, EMPTY_LICENSE): existing, total = volume['implementation'].find( @@ -459,13 +469,15 @@ def load_bundle(volume, bundle_path, impl=None): existing, __ = volume['implementation'].find( context=impl['context'], version=impl['version'], not_layer='deleted') + impl['guid'] = \ + request.call(method='POST', path=['implementation'], content=impl) for i in existing: layer = i['layer'] + ['deleted'] volume['implementation'].update(i.guid, {'layer': layer}) - impl['guid'] = volume['implementation'].create(impl) - if context_updates: - volume['context'].update(impl['context'], context_updates) + if context: + request.call(method='PUT', path=['context', impl['context']], + content=context) def _load_context_metadata(bundle, spec): diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index 07505c7..215ec03 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -206,6 +206,9 @@ class Connection(object): if response.status_code != 200: if response.status_code == 401: + enforce(method not in ('PUT', 'POST') or + not hasattr(data, 'read'), + 'Cannot resend data after authentication') enforce(self._get_profile is not None, 'Operation is not available in anonymous mode') _logger.info('User is not registered on the server, ' diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py index 2331a9d..c67ec97 100644 --- a/sugar_network/toolkit/router.py +++ b/sugar_network/toolkit/router.py @@ -103,16 +103,17 @@ class Request(dict): cmd = None content = None content_type = None + content_stream = None content_length = 0 principal = None - _if_modified_since = None - _accept_language = None + subcall = lambda *args: enforce(False) def __init__(self, environ=None, method=None, path=None, cmd=None, - **kwargs): + content=None, **kwargs): dict.__init__(self) - self._pos = 0 self._dirty_query = False + self._if_modified_since = None + self._accept_language = None if environ is None: self.environ = {} @@ -120,6 +121,7 @@ class Request(dict): self.path = path self.cmd = cmd self.update(kwargs) + self.content = content return self.environ = environ @@ -145,7 +147,7 @@ class Request(dict): if query: self.url += '?' + query - content_length = self.environ.get('CONTENT_LENGTH') + content_length = environ.get('CONTENT_LENGTH') if content_length is not None: self.content_length = int(content_length) @@ -154,6 +156,10 @@ class Request(dict): if self.content_type == 'application/json': self.content = json.load(environ['wsgi.input']) + stream = environ.get('wsgi.input') + if stream is not None: + self.content_stream = _ContentStream(stream, self.content_length) + def __setitem__(self, key, value): self._dirty_query = True if key == 'cmd': @@ -181,10 +187,6 @@ class Request(dict): return self.path[2] @property - def content_stream(self): - return self.environ.get('wsgi.input') - - @property def static_prefix(self): http_host = self.environ.get('HTTP_HOST') if http_host: @@ -227,17 +229,6 @@ class Request(dict): self._dirty_query = False return self.environ.get('QUERY_STRING') - def read(self, size=None): - if self.content_stream is None: - return '' - rest = max(0, self.content_length - self._pos) - size = rest if size is None else min(rest, size) - result = self.content_stream.read(size) - if not result: - return '' - self._pos += len(result) - return result - def add(self, key, *values): existing_value = self.get(key) for value in values: @@ -248,6 +239,15 @@ class Request(dict): else: existing_value = self[key] = [existing_value, value] + def call(self, request=None, response=None, **kwargs): + if request is None: + request = Request(**kwargs) + if response is None: + response = Response() + request.principal = self.principal + request.environ = self.environ + return self.subcall(request, response) + def __repr__(self): return '<Request method=%s path=%r cmd=%s query=%r>' % \ (self.method, self.path, self.cmd, dict(self)) @@ -300,7 +300,7 @@ class Response(dict): return result def __repr__(self): - items = ['%s=%r' % i for i in self.items()] + items = ['%s=%r' % i for i in self.items() + self.meta.items()] return '<Response %s>' % ' '.join(items) def __contains__(self, key): @@ -375,47 +375,44 @@ class Router(object): cls = cls.__base__ def call(self, request, response): - result = None - try: - result = self._call(request, response) - - if isinstance(result, Blob): - if 'url' in result: - raise http.Redirect(result['url']) - - path = result['blob'] - enforce(isfile(path), 'No such file') - - mtime = result.get('mtime') or os.stat(path).st_mtime - if request.if_modified_since and mtime and \ - mtime <= request.if_modified_since: - raise http.NotModified() - response.last_modified = mtime - - response.content_type = result.get('mime_type') or \ - 'application/octet-stream' - - filename = result.get('filename') - if not filename: - filename = _filename(result.get('name') or - splitext(split(path)[-1])[0], - response.content_type) - response['Content-Disposition'] = \ - 'attachment; filename="%s"' % filename - - result = file(path, 'rb') - - if hasattr(result, 'read'): - if hasattr(result, 'fileno'): - response.content_length = os.fstat(result.fileno()).st_size - elif hasattr(result, 'seek'): - result.seek(0, 2) - response.content_length = result.tell() - result.seek(0) - result = _stream_reader(result) - finally: - _logger.trace('%s call: request=%s response=%r result=%r', - self, request.environ, response, result) + request.subcall = self.call + result = self._call(request, response) + + if isinstance(result, Blob): + if 'url' in result: + raise http.Redirect(result['url']) + + path = result['blob'] + enforce(isfile(path), 'No such file') + + mtime = result.get('mtime') or os.stat(path).st_mtime + if request.if_modified_since and mtime and \ + mtime <= request.if_modified_since: + raise http.NotModified() + response.last_modified = mtime + + response.content_type = result.get('mime_type') or \ + 'application/octet-stream' + + filename = result.get('filename') + if not filename: + filename = _filename(result.get('name') or + splitext(split(path)[-1])[0], + response.content_type) + response['Content-Disposition'] = \ + 'attachment; filename="%s"' % filename + + result = file(path, 'rb') + + if hasattr(result, 'read'): + if hasattr(result, 'fileno'): + response.content_length = os.fstat(result.fileno()).st_size + elif hasattr(result, 'seek'): + result.seek(0, 2) + response.content_length = result.tell() + result.seek(0) + result = _stream_reader(result) + return result def __repr__(self): @@ -478,11 +475,15 @@ class Router(object): for key, value in response.meta.items(): response.set('X-SN-%s' % str(key), json.dumps(value)) + if request.method == 'HEAD' and result is not None: + _logger.warning('Content from HEAD response is ignored') + result = None + + _logger.trace('%s call: request=%s response=%r result=%r', + self, request.environ, response, result) start_response(response.status, response.items()) - if request.method == 'HEAD': - enforce(result is None, 'HEAD responses should not contain body') - elif result_streamed: + if result_streamed: for i in result: yield i elif result is not None: @@ -594,6 +595,24 @@ class Router(object): return valid +class _ContentStream(object): + + def __init__(self, stream, length): + self._stream = stream + self._length = length + self._pos = 0 + + def read(self, size=None): + if self._length: + the_rest = max(0, self._length - self._pos) + size = the_rest if size is None else min(the_rest, size) + result = self._stream.read(size) + if not result: + return '' + self._pos += len(result) + return result + + def _filename(names, mime_type): if type(names) not in (list, tuple): names = [names] diff --git a/tests/units/client/online_routes.py b/tests/units/client/online_routes.py index 691de3e..891b239 100755 --- a/tests/units/client/online_routes.py +++ b/tests/units/client/online_routes.py @@ -342,12 +342,13 @@ class OnlineRoutes(tests.Test): }, }}) - ipc.put(['context', context], 2, cmd='clone', nodeps=1, requires='dep4') + self.assertRaises(RuntimeError, ipc.put, ['context', context], 2, cmd='clone', nodeps=1, requires='foo') coroutine.sleep(.1) self.assertEqual({'clone': 0}, ipc.get(['context', context], reply=['clone'])) assert not exists('Activities/TestActivity/activity/activity.info') ipc.put(['context', context], 2, cmd='clone', nodeps=1) + # XXX seems to be an ugly low level bug, removing the following sleep means not reasing HTTP response for the next request coroutine.sleep(.1) self.assertEqual({'clone': 2}, ipc.get(['context', context], reply=['clone'])) self.assertEqual('2', Spec('Activities/TestActivity/activity/activity.info')['version']) diff --git a/tests/units/db/routes.py b/tests/units/db/routes.py index f5d44db..1ea43ed 100755 --- a/tests/units/db/routes.py +++ b/tests/units/db/routes.py @@ -1605,8 +1605,8 @@ class RoutesTest(tests.Test): request.cmd = cmd request.content = content request.content_type = content_type - if request.content_stream is not None: - request.content_length = len(request.content_stream.getvalue()) + if content_stream is not None: + request.content_length = len(content_stream.getvalue()) request.update(kwargs) request.principal = principal router = Router(routes(self.volume)) diff --git a/tests/units/node/node.py b/tests/units/node/node.py index 014ecfe..b56a61f 100755 --- a/tests/units/node/node.py +++ b/tests/units/node/node.py @@ -12,7 +12,7 @@ from os.path import exists from __init__ import tests -from sugar_network import db, node, model +from sugar_network import db, node, model, client from sugar_network.client import Connection from sugar_network.toolkit import http, coroutine from sugar_network.toolkit.rrd import Rrd @@ -672,6 +672,9 @@ class NodeTest(tests.Test): self.assertEqual('developer', impl['stability']) self.assertEqual(['Public Domain'], impl['license']) self.assertEqual('developer', impl['stability']) + assert impl['ctime'] > 0 + assert impl['mtime'] > 0 + self.assertEqual({tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, impl['author']) data = impl.meta('data') self.assertEqual({ @@ -751,7 +754,7 @@ class NodeTest(tests.Test): self.assertEqual([], volume['implementation'].get(guid4)['layer']) self.assertEqual(bundle3, conn.get(['context', 'bundle_id'], cmd='clone')) - def test_release_LoadMetadata(self): + def test_release_UpdateContext(self): volume = self.start_master() conn = Connection() @@ -820,6 +823,75 @@ class NodeTest(tests.Test): self.assertEqual('http://wiki.sugarlabs.org/go/Activities/Image_Viewer', context['homepage']) self.assertEqual(['image/bmp', 'image/gif'], context['mime_types']) + def test_release_CreateContext(self): + volume = self.start_master() + conn = Connection() + + bundle = self.zips( + ('ImageViewer.activity/activity/activity.info', '\n'.join([ + '[Activity]', + 'bundle_id = org.laptop.ImageViewerActivity', + 'name = Image Viewer', + 'summary = The Image Viewer activity is a simple and fast image viewer tool', + 'description = It has features one would expect of a standard image viewer, like zoom, rotate, etc.', + 'homepage = http://wiki.sugarlabs.org/go/Activities/Image_Viewer', + 'activity_version = 22', + 'license = GPLv2+', + 'icon = activity-imageviewer', + 'exec = true', + 'mime_types = image/bmp;image/gif', + ])), + ('ImageViewer.activity/activity/activity-imageviewer.svg', ''), + ) + self.assertRaises(http.NotFound, conn.request, 'POST', ['implementation'], bundle, params={'cmd': 'release'}) + impl = json.load(conn.request('POST', ['implementation'], bundle, params={'cmd': 'release', 'initial': 1}).raw) + + context = volume['context'].get('org.laptop.ImageViewerActivity') + self.assertEqual({'en': 'Image Viewer'}, context['title']) + self.assertEqual({'en': 'The Image Viewer activity is a simple and fast image viewer tool'}, context['summary']) + self.assertEqual({'en': 'It has features one would expect of a standard image viewer, like zoom, rotate, etc.'}, context['description']) + self.assertEqual('http://wiki.sugarlabs.org/go/Activities/Image_Viewer', context['homepage']) + self.assertEqual(['image/bmp', 'image/gif'], context['mime_types']) + assert context['ctime'] > 0 + assert context['mtime'] > 0 + self.assertEqual({tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, context['author']) + + def test_release_AuthorsOnly(self): + volume = self.start_master() + bundle = self.zips( + ('ImageViewer.activity/activity/activity.info', '\n'.join([ + '[Activity]', + 'bundle_id = org.laptop.ImageViewerActivity', + 'name = Image Viewer', + 'activity_version = 1', + 'license = GPLv2+', + 'icon = activity-imageviewer', + 'exec = true', + ])), + ('ImageViewer.activity/activity/activity-imageviewer.svg', ''), + ) + + self.override(client, 'sugar_uid', lambda: tests.UID) + conn = Connection() + impl1 = json.load(conn.request('POST', ['implementation'], bundle, params={'cmd': 'release', 'initial': 1}).raw) + impl2 = json.load(conn.request('POST', ['implementation'], bundle, params={'cmd': 'release'}).raw) + self.assertEqual(['deleted'], volume['implementation'].get(impl1)['layer']) + self.assertEqual([], volume['implementation'].get(impl2)['layer']) + + self.override(client, 'sugar_uid', lambda: tests.UID2) + self.override(client, 'sugar_profile', lambda: { + 'name': 'test', + 'color': '#000000,#000000', + 'machine_sn': '', + 'machine_uuid': '', + 'pubkey': tests.PUBKEY2, + }) + conn = Connection() + conn.get(cmd='whoami') + self.assertRaises(http.Forbidden, conn.request, 'POST', ['implementation'], bundle, params={'cmd': 'release'}) + self.assertEqual(['deleted'], volume['implementation'].get(impl1)['layer']) + self.assertEqual([], volume['implementation'].get(impl2)['layer']) + def call(routes, method, document=None, guid=None, prop=None, principal=None, cmd=None, content=None, **kwargs): path = [] diff --git a/tests/units/toolkit/router.py b/tests/units/toolkit/router.py index 32e26f3..b970a90 100755 --- a/tests/units/toolkit/router.py +++ b/tests/units/toolkit/router.py @@ -594,26 +594,40 @@ class RouterTest(tests.Test): self.value = value def read(self, size): + print self.pos, size, len(self.value) assert self.pos + size <= len(self.value) result = self.value[self.pos:self.pos + size] self.pos += size return result - request = Request({'PATH_INFO': '/', 'REQUEST_METHOD': 'GET', 'wsgi.input': Stream('123')}) - request.content_length = len(request.content_stream.value) - self.assertEqual('123', request.read()) - self.assertEqual('', request.read()) - self.assertEqual('', request.read(10)) - - request = Request({'PATH_INFO': '/', 'REQUEST_METHOD': 'GET', 'wsgi.input': Stream('123')}) - request.content_length = len(request.content_stream.value) - self.assertEqual('123', request.read(10)) - - request = Request({'PATH_INFO': '/', 'REQUEST_METHOD': 'GET', 'wsgi.input': Stream('123')}) - request.content_length = len(request.content_stream.value) - self.assertEqual('1', request.read(1)) - self.assertEqual('2', request.read(1)) - self.assertEqual('3', request.read()) + request = Request({ + 'PATH_INFO': '/', + 'REQUEST_METHOD': 'GET', + 'CONTENT_LENGTH': '3', + 'wsgi.input': Stream('123'), + }) + self.assertEqual('123', request.content_stream.read()) + self.assertEqual('', request.content_stream.read()) + self.assertEqual('', request.content_stream.read(10)) + + request = Request({ + 'PATH_INFO': '/', + 'REQUEST_METHOD': 'GET', + 'CONTENT_LENGTH': '3', + 'wsgi.input': Stream('123'), + }) + self.assertEqual('123', request.content_stream.read(10)) + + request = Request({ + 'PATH_INFO': '/', + 'REQUEST_METHOD': 'GET', + 'CONTENT_LENGTH': '3', + 'wsgi.input': Stream('123'), + }) + self.assertEqual('1', request.content_stream.read(1)) + self.assertEqual('2', request.content_stream.read(1)) + self.assertEqual('3', request.content_stream.read()) + self.assertEqual('', request.content_stream.read()) def test_IntArguments(self): |