{"action":"created","comment":{"url":"https://api.github.com/repos/nipy/nibabel/pulls/comments/42750611","id":42750611,"diff_hunk":"@@ -0,0 +1,537 @@\n+# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-\n+# vi: set ft=python sts=4 ts=4 sw=4 et:\n+### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n+#\n+#   See COPYING file distributed along with the NiBabel package for the\n+#   copyright and license terms.\n+#\n+### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n+''' Common interface for any image format--volume or surface, binary or xml.'''\n+\n+import os.path\n+import warnings\n+\n+import numpy as np\n+\n+from .externals.six import string_types\n+from .fileholders import FileHolder\n+from .filename_parser import (types_filenames, TypesFilenamesError,\n+                              splitext_addext)\n+from .openers import ImageOpener\n+\n+\n+class ImageFileError(Exception):\n+    pass\n+\n+\n+class FileBasedHeader(object):\n+    ''' Template class to implement header protocol '''\n+\n+    @classmethod\n+    def from_header(klass, header=None):\n+        if header is None:\n+            return klass()\n+        # I can't do isinstance here because it is not necessarily true\n+        # that a subclass has exactly the same interface as its parent\n+        # - for example Nifti1Images inherit from Analyze, but have\n+        # different field names\n+        if type(header) == klass:\n+            return header.copy()\n+        raise NotImplementedError(\"Header class requires a conversion\"\n+                                  \" from %s to %s\" % (klass, type(header)))\n+\n+    @classmethod\n+    def from_fileobj(klass, fileobj):\n+        raise NotImplementedError\n+\n+    def write_to(self, fileobj):\n+        raise NotImplementedError\n+\n+    def __eq__(self, other):\n+        raise NotImplementedError\n+\n+    def __ne__(self, other):\n+        return not self == other\n+\n+    def copy(self):\n+        ''' Copy object to independent representation\n+\n+        The copy should not be affected by any changes to the original\n+        object.\n+        '''\n+        raise NotImplementedError\n+\n+\n+class FileBasedImage(object):\n+    '''\n+    This abstract image class defines an interface for loading/saving images\n+    from disk. It doesn't define any image properties.\n+\n+    It has:\n+\n+    attributes:\n+\n+       * extra\n+\n+    properties:\n+\n+       * shape\n+       * header\n+\n+    methods:\n+       * .get_header() (deprecated, use header property instead)\n+       * .to_filename(fname) - writes data to filename(s) derived from\n+         ``fname``, where the derivation may differ between formats.\n+       * to_file_map() - save image to files with which the image is already\n+         associated.\n+\n+    classmethods:\n+\n+       * from_filename(fname) - make instance by loading from filename\n+       * from_file_map(fmap) - make instance from file map\n+       * instance_to_filename(img, fname) - save ``img`` instance to\n+         filename ``fname``.\n+\n+    It also has a ``header`` - some standard set of meta-data that is specific to\n+    the image format, and ``extra`` - a dictionary container for any other\n+    metadata.\n+\n+    You cannot slice an image, and trying to slice an image generates an\n+    informative TypeError.\n+\n+\n+    There are several ways of writing data.\n+    =======================================\n+\n+    There is the usual way, which is the default::\n+\n+        img.to_filename(fname)\n+\n+    and that is, to take the data encapsulated by the image and cast it to\n+    the datatype the header expects, setting any available header scaling\n+    into the header to help the data match.\n+\n+    You can load the data into an image from file with::\n+\n+       img.from_filename(fname)\n+\n+    The image stores its associated files in its ``file_map`` attribute.  In order\n+    to just save an image, for which you know there is an associated filename, or\n+    other storage, you can do::\n+\n+       img.to_file_map()\n+\n+    You can get the data out again with::\n+\n+        img.get_data()\n+\n+    Less commonly, for some image types that support it, you might want to\n+    fetch out the unscaled array via the object containing the data::\n+\n+        unscaled_data = img.dataoobj.get_unscaled()\n+\n+    Analyze-type images (including nifti) support this, but others may not\n+    (MINC, for example).\n+\n+    Sometimes you might to avoid any loss of precision by making the\n+    data type the same as the input::\n+\n+        hdr = img.header\n+        hdr.set_data_dtype(data.dtype)\n+        img.to_filename(fname)\n+\n+    Files interface\n+    ===============\n+\n+    The image has an attribute ``file_map``.  This is a mapping, that has keys\n+    corresponding to the file types that an image needs for storage.  For\n+    example, the Analyze data format needs an ``image`` and a ``header``\n+    file type for storage:\n+\n+       >>> import nibabel as nib\n+       >>> data = np.arange(24, dtype='f4').reshape((2,3,4))\n+       >>> img = nib.AnalyzeImage(data, np.eye(4))\n+       >>> sorted(img.file_map)\n+       ['header', 'image']\n+\n+    The values of ``file_map`` are not in fact files but objects with\n+    attributes ``filename``, ``fileobj`` and ``pos``.\n+\n+    The reason for this interface, is that the contents of files has to\n+    contain enough information so that an existing image instance can save\n+    itself back to the files pointed to in ``file_map``.  When a file holder\n+    holds active file-like objects, then these may be affected by the\n+    initial file read; in this case, the contains file-like objects need to\n+    carry the position at which a write (with ``to_files``) should place the\n+    data.  The ``file_map`` contents should therefore be such, that this will\n+    work:\n+    '''\n+    files_types = (('image', None),)\n+    alternate_exts = ()  # Modified by @ImageOpener.register_ext_from_image","path":"nibabel/filebasedimages.py","position":170,"original_position":170,"commit_id":"6b4077704626d7e833458baf618f17c1741e0e98","original_commit_id":"6b4077704626d7e833458baf618f17c1741e0e98","user":{"login":"effigies","id":83442,"avatar_url":"https://avatars.githubusercontent.com/u/83442?v=3","gravatar_id":"","url":"https://api.github.com/users/effigies","html_url":"https://github.com/effigies","followers_url":"https://api.github.com/users/effigies/followers","following_url":"https://api.github.com/users/effigies/following{/other_user}","gists_url":"https://api.github.com/users/effigies/gists{/gist_id}","starred_url":"https://api.github.com/users/effigies/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/effigies/subscriptions","organizations_url":"https://api.github.com/users/effigies/orgs","repos_url":"https://api.github.com/users/effigies/repos","events_url":"https://api.github.com/users/effigies/events{/privacy}","received_events_url":"https://api.github.com/users/effigies/received_events","type":"User","site_admin":false},"body":"`sizeof_hdr` is on the header, [`_meta_sniff_len`](https://github.com/nipy/nibabel/pull/364/files#diff-1053478051df4b99b0c69a5365f5f0beL326) is on the image and used in [`path_maybe_image`](https://github.com/nipy/nibabel/pull/364/files#diff-496e321b697d515cfea0150d3c97dfb5R530) without being checked to exist, first. (It does check whether the header class defines `may_contain_header`, but that's not tied to presence of `_meta_sniff_len`, AFAICT.)\r\n\r\n`files_types` must also be defined by subclasses, correct? I think its purpose here is documentary. But the point of my comment was that `alternate_exts` has been removed from `SpatialImage`.","created_at":"2015-10-22T13:58:27Z","updated_at":"2015-10-22T13:58:27Z","html_url":"https://github.com/nipy/nibabel/pull/364#discussion_r42750611","pull_request_url":"https://api.github.com/repos/nipy/nibabel/pulls/364","_links":{"self":{"href":"https://api.github.com/repos/nipy/nibabel/pulls/comments/42750611"},"html":{"href":"https://github.com/nipy/nibabel/pull/364#discussion_r42750611"},"pull_request":{"href":"https://api.github.com/repos/nipy/nibabel/pulls/364"}}},"pull_request":{"url":"https://api.github.com/repos/nipy/nibabel/pulls/364","id":48334649,"html_url":"https://github.com/nipy/nibabel/pull/364","diff_url":"https://github.com/nipy/nibabel/pull/364.diff","patch_url":"https://github.com/nipy/nibabel/pull/364.patch","issue_url":"https://api.github.com/repos/nipy/nibabel/issues/364","number":364,"state":"open","locked":false,"title":"RF: move load/save logic out of SpatialImage","user":{"login":"bcipolli","id":4072455,"avatar_url":"https://avatars.githubusercontent.com/u/4072455?v=3","gravatar_id":"","url":"https://api.github.com/users/bcipolli","html_url":"https://github.com/bcipolli","followers_url":"https://api.github.com/users/bcipolli/followers","following_url":"https://api.github.com/users/bcipolli/following{/other_user}","gists_url":"https://api.github.com/users/bcipolli/gists{/gist_id}","starred_url":"https://api.github.com/users/bcipolli/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bcipolli/subscriptions","organizations_url":"https://api.github.com/users/bcipolli/orgs","repos_url":"https://api.github.com/users/bcipolli/repos","events_url":"https://api.github.com/users/bcipolli/events{/privacy}","received_events_url":"https://api.github.com/users/bcipolli/received_events","type":"User","site_admin":false},"body":"This PR precedes #360, which builds off of this work. I'll copy some text from that PR; there's quite a bit of discussion about how this fits into forward-looking design of surface vs. spatial, binary vs. xml object models.\r\n\r\n**Issue:**\r\nThe final issue blocking streamlined GIFTI/CIFTI in nibabel is: image file I/O functions are tied directly to the `SpatialImage` class; non-volumetric images (GIFTI/CIFTI) can't extend `SpatialImage` and so can't easily get in the `load`/`save` patheway.\r\n\r\n**Proposed Solution:**\r\nThe good news is, the file I/O code actually is quite general. We can easily abstract the file I/O into a higher-level class (which I've called `FileBasedHeader`/`FileBasedImage`, as the more generic `Header` was already taken) that `SpatialImage` and `GiftiImage` inherit from.\r\n\r\n**To do:**\r\n- [X] Move file I/O from `SpatialImage` into `FileBasedHeader`/`FileBasedImage` \r\n- [X] Update tests that assume all image types are of type `SpatialImage`\r\n\r\n**Notes:**\r\n* Given the above design, I believe this is mostly done. Rather than fine-tuning the code now, I'd rather get ideas about the overall design first. @matthew-brett and @effigies , your input is most welcome when you have the time!\r\n* Whatever we decide here, I'll be using to push forward GIFTI (#360) and CIFTI (#249) support","created_at":"2015-10-21T13:58:58Z","updated_at":"2015-10-22T13:58:27Z","closed_at":null,"merged_at":null,"merge_commit_sha":"d49b9fbcb1697e5e5197698bc08b6c5562b7fcc8","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/nipy/nibabel/pulls/364/commits","review_comments_url":"https://api.github.com/repos/nipy/nibabel/pulls/364/comments","review_comment_url":"https://api.github.com/repos/nipy/nibabel/pulls/comments{/number}","comments_url":"https://api.github.com/repos/nipy/nibabel/issues/364/comments","statuses_url":"https://api.github.com/repos/nipy/nibabel/statuses/6b4077704626d7e833458baf618f17c1741e0e98","head":{"label":"bcipolli:filebasedimage","ref":"filebasedimage","sha":"6b4077704626d7e833458baf618f17c1741e0e98","user":{"login":"bcipolli","id":4072455,"avatar_url":"https://avatars.githubusercontent.com/u/4072455?v=3","gravatar_id":"","url":"https://api.github.com/users/bcipolli","html_url":"https://github.com/bcipolli","followers_url":"https://api.github.com/users/bcipolli/followers","following_url":"https://api.github.com/users/bcipolli/following{/other_user}","gists_url":"https://api.github.com/users/bcipolli/gists{/gist_id}","starred_url":"https://api.github.com/users/bcipolli/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bcipolli/subscriptions","organizations_url":"https://api.github.com/users/bcipolli/orgs","repos_url":"https://api.github.com/users/bcipolli/repos","events_url":"https://api.github.com/users/bcipolli/events{/privacy}","received_events_url":"https://api.github.com/users/bcipolli/received_events","type":"User","site_admin":false},"repo":{"id":31253147,"name":"nibabel","full_name":"bcipolli/nibabel","owner":{"login":"bcipolli","id":4072455,"avatar_url":"https://avatars.githubusercontent.com/u/4072455?v=3","gravatar_id":"","url":"https://api.github.com/users/bcipolli","html_url":"https://github.com/bcipolli","followers_url":"https://api.github.com/users/bcipolli/followers","following_url":"https://api.github.com/users/bcipolli/following{/other_user}","gists_url":"https://api.github.com/users/bcipolli/gists{/gist_id}","starred_url":"https://api.github.com/users/bcipolli/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bcipolli/subscriptions","organizations_url":"https://api.github.com/users/bcipolli/orgs","repos_url":"https://api.github.com/users/bcipolli/repos","events_url":"https://api.github.com/users/bcipolli/events{/privacy}","received_events_url":"https://api.github.com/users/bcipolli/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/bcipolli/nibabel","description":"Python package to access a cacophony of neuro-imaging file formats","fork":true,"url":"https://api.github.com/repos/bcipolli/nibabel","forks_url":"https://api.github.com/repos/bcipolli/nibabel/forks","keys_url":"https://api.github.com/repos/bcipolli/nibabel/keys{/key_id}","collaborators_url":"https://api.github.com/repos/bcipolli/nibabel/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/bcipolli/nibabel/teams","hooks_url":"https://api.github.com/repos/bcipolli/nibabel/hooks","issue_events_url":"https://api.github.com/repos/bcipolli/nibabel/issues/events{/number}","events_url":"https://api.github.com/repos/bcipolli/nibabel/events","assignees_url":"https://api.github.com/repos/bcipolli/nibabel/assignees{/user}","branches_url":"https://api.github.com/repos/bcipolli/nibabel/branches{/branch}","tags_url":"https://api.github.com/repos/bcipolli/nibabel/tags","blobs_url":"https://api.github.com/repos/bcipolli/nibabel/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/bcipolli/nibabel/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/bcipolli/nibabel/git/refs{/sha}","trees_url":"https://api.github.com/repos/bcipolli/nibabel/git/trees{/sha}","statuses_url":"https://api.github.com/repos/bcipolli/nibabel/statuses/{sha}","languages_url":"https://api.github.com/repos/bcipolli/nibabel/languages","stargazers_url":"https://api.github.com/repos/bcipolli/nibabel/stargazers","contributors_url":"https://api.github.com/repos/bcipolli/nibabel/contributors","subscribers_url":"https://api.github.com/repos/bcipolli/nibabel/subscribers","subscription_url":"https://api.github.com/repos/bcipolli/nibabel/subscription","commits_url":"https://api.github.com/repos/bcipolli/nibabel/commits{/sha}","git_commits_url":"https://api.github.com/repos/bcipolli/nibabel/git/commits{/sha}","comments_url":"https://api.github.com/repos/bcipolli/nibabel/comments{/number}","issue_comment_url":"https://api.github.com/repos/bcipolli/nibabel/issues/comments{/number}","contents_url":"https://api.github.com/repos/bcipolli/nibabel/contents/{+path}","compare_url":"https://api.github.com/repos/bcipolli/nibabel/compare/{base}...{head}","merges_url":"https://api.github.com/repos/bcipolli/nibabel/merges","archive_url":"https://api.github.com/repos/bcipolli/nibabel/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/bcipolli/nibabel/downloads","issues_url":"https://api.github.com/repos/bcipolli/nibabel/issues{/number}","pulls_url":"https://api.github.com/repos/bcipolli/nibabel/pulls{/number}","milestones_url":"https://api.github.com/repos/bcipolli/nibabel/milestones{/number}","notifications_url":"https://api.github.com/repos/bcipolli/nibabel/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/bcipolli/nibabel/labels{/name}","releases_url":"https://api.github.com/repos/bcipolli/nibabel/releases{/id}","created_at":"2015-02-24T09:35:11Z","updated_at":"2015-02-24T09:35:13Z","pushed_at":"2015-10-21T21:38:25Z","git_url":"git://github.com/bcipolli/nibabel.git","ssh_url":"git@github.com:bcipolli/nibabel.git","clone_url":"https://github.com/bcipolli/nibabel.git","svn_url":"https://github.com/bcipolli/nibabel","homepage":"http://nipy.org/nibabel/","size":8608,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"nipy:master","ref":"master","sha":"63f9ef26d4c1564fb0e193c674a81f67f8972b04","user":{"login":"nipy","id":233707,"avatar_url":"https://avatars.githubusercontent.com/u/233707?v=3","gravatar_id":"","url":"https://api.github.com/users/nipy","html_url":"https://github.com/nipy","followers_url":"https://api.github.com/users/nipy/followers","following_url":"https://api.github.com/users/nipy/following{/other_user}","gists_url":"https://api.github.com/users/nipy/gists{/gist_id}","starred_url":"https://api.github.com/users/nipy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nipy/subscriptions","organizations_url":"https://api.github.com/users/nipy/orgs","repos_url":"https://api.github.com/users/nipy/repos","events_url":"https://api.github.com/users/nipy/events{/privacy}","received_events_url":"https://api.github.com/users/nipy/received_events","type":"Organization","site_admin":false},"repo":{"id":791352,"name":"nibabel","full_name":"nipy/nibabel","owner":{"login":"nipy","id":233707,"avatar_url":"https://avatars.githubusercontent.com/u/233707?v=3","gravatar_id":"","url":"https://api.github.com/users/nipy","html_url":"https://github.com/nipy","followers_url":"https://api.github.com/users/nipy/followers","following_url":"https://api.github.com/users/nipy/following{/other_user}","gists_url":"https://api.github.com/users/nipy/gists{/gist_id}","starred_url":"https://api.github.com/users/nipy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nipy/subscriptions","organizations_url":"https://api.github.com/users/nipy/orgs","repos_url":"https://api.github.com/users/nipy/repos","events_url":"https://api.github.com/users/nipy/events{/privacy}","received_events_url":"https://api.github.com/users/nipy/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nipy/nibabel","description":"Python package to access a cacophony of neuro-imaging file formats","fork":false,"url":"https://api.github.com/repos/nipy/nibabel","forks_url":"https://api.github.com/repos/nipy/nibabel/forks","keys_url":"https://api.github.com/repos/nipy/nibabel/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nipy/nibabel/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nipy/nibabel/teams","hooks_url":"https://api.github.com/repos/nipy/nibabel/hooks","issue_events_url":"https://api.github.com/repos/nipy/nibabel/issues/events{/number}","events_url":"https://api.github.com/repos/nipy/nibabel/events","assignees_url":"https://api.github.com/repos/nipy/nibabel/assignees{/user}","branches_url":"https://api.github.com/repos/nipy/nibabel/branches{/branch}","tags_url":"https://api.github.com/repos/nipy/nibabel/tags","blobs_url":"https://api.github.com/repos/nipy/nibabel/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nipy/nibabel/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nipy/nibabel/git/refs{/sha}","trees_url":"https://api.github.com/repos/nipy/nibabel/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nipy/nibabel/statuses/{sha}","languages_url":"https://api.github.com/repos/nipy/nibabel/languages","stargazers_url":"https://api.github.com/repos/nipy/nibabel/stargazers","contributors_url":"https://api.github.com/repos/nipy/nibabel/contributors","subscribers_url":"https://api.github.com/repos/nipy/nibabel/subscribers","subscription_url":"https://api.github.com/repos/nipy/nibabel/subscription","commits_url":"https://api.github.com/repos/nipy/nibabel/commits{/sha}","git_commits_url":"https://api.github.com/repos/nipy/nibabel/git/commits{/sha}","comments_url":"https://api.github.com/repos/nipy/nibabel/comments{/number}","issue_comment_url":"https://api.github.com/repos/nipy/nibabel/issues/comments{/number}","contents_url":"https://api.github.com/repos/nipy/nibabel/contents/{+path}","compare_url":"https://api.github.com/repos/nipy/nibabel/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nipy/nibabel/merges","archive_url":"https://api.github.com/repos/nipy/nibabel/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nipy/nibabel/downloads","issues_url":"https://api.github.com/repos/nipy/nibabel/issues{/number}","pulls_url":"https://api.github.com/repos/nipy/nibabel/pulls{/number}","milestones_url":"https://api.github.com/repos/nipy/nibabel/milestones{/number}","notifications_url":"https://api.github.com/repos/nipy/nibabel/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nipy/nibabel/labels{/name}","releases_url":"https://api.github.com/repos/nipy/nibabel/releases{/id}","created_at":"2010-07-22T16:28:30Z","updated_at":"2015-10-06T06:58:39Z","pushed_at":"2015-10-21T21:38:26Z","git_url":"git://github.com/nipy/nibabel.git","ssh_url":"git@github.com:nipy/nibabel.git","clone_url":"https://github.com/nipy/nibabel.git","svn_url":"https://github.com/nipy/nibabel","homepage":"http://nipy.org/nibabel/","size":17893,"stargazers_count":96,"watchers_count":96,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":95,"mirror_url":null,"open_issues_count":58,"forks":95,"open_issues":58,"watchers":96,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/nipy/nibabel/pulls/364"},"html":{"href":"https://github.com/nipy/nibabel/pull/364"},"issue":{"href":"https://api.github.com/repos/nipy/nibabel/issues/364"},"comments":{"href":"https://api.github.com/repos/nipy/nibabel/issues/364/comments"},"review_comments":{"href":"https://api.github.com/repos/nipy/nibabel/pulls/364/comments"},"review_comment":{"href":"https://api.github.com/repos/nipy/nibabel/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/nipy/nibabel/pulls/364/commits"},"statuses":{"href":"https://api.github.com/repos/nipy/nibabel/statuses/6b4077704626d7e833458baf618f17c1741e0e98"}}}}