Index: c/sas/sasgui/perspectives/file_converter/bsl_loader.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/bsl_loader.py	(revision 4b862c4f5e5e25bf09c519c8485fe68cec2e30f2)
+++ 	(revision )
@@ -1,149 +1,0 @@
-"""
-Here we handle loading of "OTOKO" data (for more info about this format see
-the comment in load_bsl_data).  Given the paths of header and data files, we
-aim to load the data into numpy arrays for use later.
-"""
-
-import itertools
-import os
-import struct
-import numpy as np
-
-class CStyleStruct:
-    """A nice and easy way to get "C-style struct" functionality."""
-    def __init__(self, **kwds):
-        self.__dict__.update(kwds)
-
-class BSLParsingError(Exception):
-    pass
-
-class BSLData:
-    def __init__(self, q_axis, data_axis):
-        self.q_axis = q_axis
-        self.data_axis = data_axis
-
-class BSLLoader(object):
-
-    def __init__(self, qaxis_path, data_path):
-        self.qaxis_path = qaxis_path
-        self.data_path = data_path
-
-    def load_bsl_data(self):
-        """
-        Loads "OTOKO" data, which is a format that stores each axis separately.
-        An axis is represented by a "header" file, which in turn will give details
-        of one or more binary files where the actual data is stored.
-
-        Given the paths of two header files, this function will load each axis in
-        turn.  If loading is successfull then an instance of the BSLData class
-        will be returned, else an exception will be raised.
-
-        For more information on the OTOKO file format, please see:
-        http://www.diamond.ac.uk/Home/Beamlines/small-angle/SAXS-Software/CCP13/
-        XOTOKO.html
-
-        The BSL format, which is based on OTOKO, is also supported.  Find out more
-        about the BSL format at http://www.diamond.ac.uk/Home/Beamlines/small-angle
-        /SAXS-Software/CCP13/BSL.html.
-        """
-        q_axis    = self._load_bsl_axis(self.qaxis_path)
-        data_axis = self._load_bsl_axis(self.data_path)
-
-        return BSLData(q_axis, data_axis)
-
-    def _load_bsl_axis(self, header_path):
-        """
-        Loads an "OTOKO" axis, given the header file path.  Essentially, the
-        header file contains information about the data in the form of integer
-        "indicators", as well as the names of each of the binary files which are
-        assumed to be in the same directory as the header.
-        """
-        if not os.path.exists(header_path):
-            raise BSLParsingError("The header file %s does not exist." % header_path)
-
-        binary_file_info_list = []
-        total_frames = 0
-        header_dir = os.path.dirname(os.path.abspath(header_path))
-
-        with open(header_path, "r") as header_file:
-            lines = header_file.readlines()
-            if len(lines) < 4:
-                raise BSLParsingError("Expected more lines in %s." % header_path)
-
-            info = lines[0] + lines[1]
-
-            def pairwise(iterable):
-                """
-                s -> (s0,s1), (s2,s3), (s4, s5), ...
-                From http://stackoverflow.com/a/5389547/778572
-                """
-                a = iter(iterable)
-                return itertools.izip(a, a)
-
-            for indicators, filename in pairwise(lines[2:]):
-                indicators = indicators.split()
-
-                if len(indicators) != 10:
-                    raise BSLParsingError(
-                        "Expected 10 integer indicators on line 3 of %s." \
-                        % header_path)
-                if not all([i.isdigit() for i in indicators]):
-                    raise BSLParsingError(
-                        "Expected all indicators on line 3 of %s to be integers." \
-                        % header_path)
-
-                binary_file_info = CStyleStruct(
-                    # The indicators at indices 4 to 8 are always zero since they
-                    # have been reserved for future use by the format.  Also, the
-                    # "last_file" indicator seems to be there for legacy reasons,
-                    # as it doesn't appear to be something we have to bother
-                    # enforcing correct use of; we just define the last file as
-                    # being the last file in the list.
-                    file_path  = os.path.join(header_dir, filename.strip()),
-                    n_channels = int(indicators[0]),
-                    n_frames   = int(indicators[1]),
-                    dimensions = int(indicators[2]),
-                    swap_bytes = int(indicators[3]) == 0,
-                    last_file  = int(indicators[9]) == 0 # We don't use this.
-                )
-                binary_file_info_list.append(binary_file_info)
-
-                total_frames += binary_file_info.n_frames
-
-        # Check that all binary files are listed in the header as having the same
-        # number of channels, since I don't think CorFunc can handle ragged data.
-        all_n_channels = [info.n_channels for info in binary_file_info_list]
-        if not all(all_n_channels[0] == c for c in all_n_channels):
-            raise BSLParsingError(
-                "Expected all binary files listed in %s to have the same number of channels." % header_path)
-
-        data = np.zeros(shape=(total_frames, all_n_channels[0]))
-        frames_so_far = 0
-
-        for info in binary_file_info_list:
-            if not os.path.exists(info.file_path):
-                raise BSLParsingError(
-                    "The data file %s does not exist." % info.file_path)
-
-            with open(info.file_path, "rb") as binary_file:
-                # Ideally we'd like to use numpy's fromfile() to read in binary
-                # data, but we are forced to roll our own float-by-float file
-                # reader because of the rules imposed on us by the file format;
-                # namely, if the swap indicator flag has been raised then the bytes
-                # of each float occur in reverse order.
-                for frame in range(info.n_frames):
-                    for channel in range(info.n_channels):
-                        b = bytes(binary_file.read(4))
-                        if info.swap_bytes:
-                            b = b[::-1] # "Extended slice" syntax, used to reverse.
-                        value = struct.unpack('f', b)[0]
-                        data[frames_so_far + frame][channel] = value
-
-                frames_so_far += info.n_frames
-
-        return CStyleStruct(
-            header_path = header_path,
-            data = data,
-            binary_file_info_list = binary_file_info_list,
-            header_info = info
-        )
Index: src/sas/sasgui/perspectives/file_converter/converter_panel.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/converter_panel.py	(revision d6bf0640d935e9a1724ba92501e293aec9b535cc)
+++ src/sas/sasgui/perspectives/file_converter/converter_panel.py	(revision ba65aff97e4c9511e2e199a78137312a6adc403d)
@@ -23,5 +23,5 @@
 from sas.sasgui.perspectives.file_converter.cansas_writer import CansasWriter
 from sas.sascalc.dataloader.readers.red2d_reader import Reader as Red2DWriter
-from sas.sasgui.perspectives.file_converter.bsl_loader import BSLLoader as OTOKOLoader
+from sas.sasgui.perspectives.file_converter.otoko_loader import OTOKOLoader
 from sas.sascalc.file_converter.bsl_loader import BSLLoader
 from sas.sascalc.dataloader.data_info import Detector
@@ -45,4 +45,7 @@
 
 class ConverterPanel(ScrolledPanel, PanelBase):
+    """
+    This class provides the File Converter GUI
+    """
 
     def __init__(self, parent, base=None, *args, **kwargs):
@@ -56,4 +59,5 @@
         self.meta_frames = []
 
+        # GUI inputs
         self.q_input = None
         self.iq_input = None
@@ -61,6 +65,8 @@
         self.radiation_input = None
         self.metadata_section = None
+
         self.data_type = "ascii"
 
+        # Metadata values
         self.title = None
         self.run = None
@@ -80,10 +86,20 @@
 
     def convert_to_cansas(self, frame_data, filename, single_file):
-        reader = CansasWriter()
+        """
+        Saves an array of Data1D objects to a single CanSAS file with multiple
+        <SasData> elements, or to multiple CanSAS files, each with one
+        <SasData> element.
+
+        :param frame_data: The Data1D object to save
+        :param filename: Where to save the CanSAS file
+        :param single_file: If true, array is saved as a single file, if false,
+        each item in the array is saved to it's own file
+        """
+        writer = CansasWriter()
         entry_attrs = None
         if self.run_name is not None:
             entry_attrs = { 'name': self.run_name }
         if single_file:
-            reader.write(filename, frame_data,
+            writer.write(filename, frame_data,
                 sasentry_attrs=entry_attrs)
         else:
@@ -92,9 +108,20 @@
             name = filename.replace(ext, '')
             for i in range(len(frame_data)):
-                f_name = "{}{}{}".format(name, i+1, ext)
-                reader.write(f_name, [frame_data[i]],
+                # TODO: Change i to actual frame number, not consecutive numbers
+                # (maybe use info in params from frame select dialog, ie
+                # increment. Alternatively, change frame_data to a dictionary
+                # with the frame number as key and use frame_data.iteritems()
+                # (would need to update CansasWriter to deal with this)
+                f_name = name + str(i) + ext
+                writer.write(f_name, [frame_data[i]],
                     sasentry_attrs=entry_attrs)
 
     def extract_ascii_data(self, filename):
+        """
+        Extracts data from a single-column ASCII file
+
+        :param filename: The file to load data from
+        :return: A numpy array containing the extracted data
+        """
         data = np.loadtxt(filename, dtype=str)
 
@@ -122,4 +149,10 @@
 
     def extract_otoko_data(self, filename):
+        """
+        Extracts data from a 1D OTOKO file
+
+        :param filename: The OTOKO file to load the data from
+        :return: A numpy array containing the extracted data
+        """
         loader = OTOKOLoader(self.q_input.GetPath(),
             self.iq_input.GetPath())
@@ -139,4 +172,11 @@
 
     def ask_frame_range(self, n_frames):
+        """
+        Display a dialog asking the user to input the range of frames they
+        would like to export
+
+        :param n_frames: How many frames the loaded data file has
+        :return: A dictionary containing the parameters input by the user
+        """
         valid_input = False
         is_bsl = (self.data_type == 'bsl')
@@ -179,4 +219,5 @@
 
     def on_convert(self, event):
+        """Called when the Convert button is clicked"""
         if not self.validate_inputs():
             return
@@ -191,4 +232,5 @@
                 qdata, iqdata = self.extract_otoko_data(self.q_input.GetPath())
             else: # self.data_type == 'bsl'
+                # TODO: Refactor this into an extract_bsl_data method
                 loader = BSLLoader(self.iq_input.GetPath())
                 frames = [0]
@@ -243,5 +285,6 @@
         # Standard file has 3 frames: SAS, calibration and WAS
         if n_frames > 3:
-            # File has multiple frames
+            # File has multiple frames - ask the user which ones they want to
+            # export
             params = self.ask_frame_range(n_frames)
             frames = params['frames']
@@ -254,4 +297,5 @@
         output_path = self.output.GetPath()
 
+        # Prepare the metadata for writing to a file
         if self.run is None:
             self.run = []
@@ -293,4 +337,7 @@
 
     def on_help(self, event):
+        """
+        Show the File Converter documentation
+        """
         tree_location = ("user/sasgui/perspectives/file_converter/"
             "file_converter_help.html")
@@ -314,4 +361,7 @@
 
     def show_detector_window(self, event):
+        """
+        Show the window for inputting :class:`~sas.sascalc.dataloader.data_info.Detector~` metadata
+        """
         if self.meta_frames != []:
             for frame in self.meta_frames:
@@ -325,4 +375,7 @@
 
     def show_sample_window(self, event):
+        """
+        Show the window for inputting :class:`~sas.sascalc.dataloader.data_info.Sample~` metadata
+        """
         if self.meta_frames != []:
             for frame in self.meta_frames:
@@ -336,4 +389,7 @@
 
     def show_source_window(self, event):
+        """
+        Show the window for inputting :class:`~sas.sascalc.dataloader.data_info.Source~` metadata
+        """
         if self.meta_frames != []:
             for frame in self.meta_frames:
@@ -347,4 +403,8 @@
 
     def on_collapsible_pane(self, event):
+        """
+        Resize the scrollable area to fit the metadata pane when it's
+        collapsed or expanded
+        """
         self.Freeze()
         self.SetupScrolling()
@@ -353,4 +413,8 @@
 
     def datatype_changed(self, event):
+        """
+        Update the UI and self.data_type when a data type radio button is
+        pressed
+        """
         event.Skip()
         dtype = event.GetEventObject().GetName()
@@ -527,4 +591,5 @@
 
 class ConverterWindow(widget.CHILD_FRAME):
+    """Displays ConverterPanel"""
 
     def __init__(self, parent=None, title='File Converter', base=None,
Index: src/sas/sasgui/perspectives/file_converter/converter_widgets.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/converter_widgets.py	(revision af84162d85c167ebb54e6a6f87a2a2bf3b62fd8b)
+++ src/sas/sasgui/perspectives/file_converter/converter_widgets.py	(revision ba65aff97e4c9511e2e199a78137312a6adc403d)
@@ -63,4 +63,9 @@
 
     def SetValue(self, vector):
+        """
+        Set the value of the vector input
+
+        :param vector: A Vector object
+        """
         directions = ['x', 'y']
         if self.z_enabled: directions.append('z')
Index: src/sas/sasgui/perspectives/file_converter/file_converter.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/file_converter.py	(revision 77d92cd296d0af12557c509f0c48df88ef5f59e9)
+++ src/sas/sasgui/perspectives/file_converter/file_converter.py	(revision ba65aff97e4c9511e2e199a78137312a6adc403d)
@@ -8,4 +8,8 @@
 
 class Plugin(PluginBase):
+    """
+    This class defines the interface for a Plugin class
+    for File Converter perspective
+    """
 
     def __init__(self):
Index: src/sas/sasgui/perspectives/file_converter/frame_select_dialog.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/frame_select_dialog.py	(revision 05595c494ed75f8802f3fa39ed87e6c17e4a9b78)
+++ src/sas/sasgui/perspectives/file_converter/frame_select_dialog.py	(revision ba65aff97e4c9511e2e199a78137312a6adc403d)
@@ -2,4 +2,8 @@
 
 class FrameSelectDialog(wx.Dialog):
+    """
+    This class provides a wx.Dialog subclass for selecting which frames of a
+    multi-frame file to export
+    """
 
     def __init__(self, n_frames, is_bsl=False):
Index: src/sas/sasgui/perspectives/file_converter/meta_panels.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/meta_panels.py	(revision 503cc34b502c3f05fdb6c33ae937635a66cf94ce)
+++ src/sas/sasgui/perspectives/file_converter/meta_panels.py	(revision ba65aff97e4c9511e2e199a78137312a6adc403d)
@@ -21,4 +21,8 @@
 
 class MetadataPanel(ScrolledPanel, PanelBase):
+    """
+    A common base class to be extended by panels that deal with metadata input.
+    Handles input validation and passing inputted data back to ConverterPanel.
+    """
 
     def __init__(self, parent, metadata, base=None, *args, **kwargs):
@@ -30,6 +34,6 @@
         self.base = base
         self.parent = parent
-        self._to_validate = []
-        self._vectors = []
+        self._to_validate = [] # An list of inputs that should contain floats
+        self._vectors = [] # A list of VectorInputs to be validated
         self.metadata = metadata
 
Index: src/sas/sasgui/perspectives/file_converter/otoko_loader.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/otoko_loader.py	(revision ba65aff97e4c9511e2e199a78137312a6adc403d)
+++ src/sas/sasgui/perspectives/file_converter/otoko_loader.py	(revision ba65aff97e4c9511e2e199a78137312a6adc403d)
@@ -0,0 +1,149 @@
+"""
+Here we handle loading of "OTOKO" data (for more info about this format see
+the comment in load_bsl_data).  Given the paths of header and data files, we
+aim to load the data into numpy arrays for use later.
+"""
+
+import itertools
+import os
+import struct
+import numpy as np
+
+class CStyleStruct:
+    """A nice and easy way to get "C-style struct" functionality."""
+    def __init__(self, **kwds):
+        self.__dict__.update(kwds)
+
+class OTOKOParsingError(Exception):
+    pass
+
+class OTOKOData:
+    def __init__(self, q_axis, data_axis):
+        self.q_axis = q_axis
+        self.data_axis = data_axis
+
+class OTOKOLoader(object):
+
+    def __init__(self, qaxis_path, data_path):
+        self.qaxis_path = qaxis_path
+        self.data_path = data_path
+
+    def load_bsl_data(self):
+        """
+        Loads "OTOKO" data, which is a format that stores each axis separately.
+        An axis is represented by a "header" file, which in turn will give details
+        of one or more binary files where the actual data is stored.
+
+        Given the paths of two header files, this function will load each axis in
+        turn.  If loading is successfull then an instance of the OTOKOData class
+        will be returned, else an exception will be raised.
+
+        For more information on the OTOKO file format, please see:
+        http://www.diamond.ac.uk/Home/Beamlines/small-angle/SAXS-Software/CCP13/
+        XOTOKO.html
+
+        The BSL format, which is based on OTOKO, is also supported.  Find out more
+        about the BSL format at http://www.diamond.ac.uk/Home/Beamlines/small-angle
+        /SAXS-Software/CCP13/BSL.html.
+        """
+        q_axis    = self._load_bsl_axis(self.qaxis_path)
+        data_axis = self._load_bsl_axis(self.data_path)
+
+        return OTOKOData(q_axis, data_axis)
+
+    def _load_bsl_axis(self, header_path):
+        """
+        Loads an "OTOKO" axis, given the header file path.  Essentially, the
+        header file contains information about the data in the form of integer
+        "indicators", as well as the names of each of the binary files which are
+        assumed to be in the same directory as the header.
+        """
+        if not os.path.exists(header_path):
+            raise OTOKOParsingError("The header file %s does not exist." % header_path)
+
+        binary_file_info_list = []
+        total_frames = 0
+        header_dir = os.path.dirname(os.path.abspath(header_path))
+
+        with open(header_path, "r") as header_file:
+            lines = header_file.readlines()
+            if len(lines) < 4:
+                raise OTOKOParsingError("Expected more lines in %s." % header_path)
+
+            info = lines[0] + lines[1]
+
+            def pairwise(iterable):
+                """
+                s -> (s0,s1), (s2,s3), (s4, s5), ...
+                From http://stackoverflow.com/a/5389547/778572
+                """
+                a = iter(iterable)
+                return itertools.izip(a, a)
+
+            for indicators, filename in pairwise(lines[2:]):
+                indicators = indicators.split()
+
+                if len(indicators) != 10:
+                    raise OTOKOParsingError(
+                        "Expected 10 integer indicators on line 3 of %s." \
+                        % header_path)
+                if not all([i.isdigit() for i in indicators]):
+                    raise OTOKOParsingError(
+                        "Expected all indicators on line 3 of %s to be integers." \
+                        % header_path)
+
+                binary_file_info = CStyleStruct(
+                    # The indicators at indices 4 to 8 are always zero since they
+                    # have been reserved for future use by the format.  Also, the
+                    # "last_file" indicator seems to be there for legacy reasons,
+                    # as it doesn't appear to be something we have to bother
+                    # enforcing correct use of; we just define the last file as
+                    # being the last file in the list.
+                    file_path  = os.path.join(header_dir, filename.strip()),
+                    n_channels = int(indicators[0]),
+                    n_frames   = int(indicators[1]),
+                    dimensions = int(indicators[2]),
+                    swap_bytes = int(indicators[3]) == 0,
+                    last_file  = int(indicators[9]) == 0 # We don't use this.
+                )
+                binary_file_info_list.append(binary_file_info)
+
+                total_frames += binary_file_info.n_frames
+
+        # Check that all binary files are listed in the header as having the same
+        # number of channels, since I don't think CorFunc can handle ragged data.
+        all_n_channels = [info.n_channels for info in binary_file_info_list]
+        if not all(all_n_channels[0] == c for c in all_n_channels):
+            raise OTOKOParsingError(
+                "Expected all binary files listed in %s to have the same number of channels." % header_path)
+
+        data = np.zeros(shape=(total_frames, all_n_channels[0]))
+        frames_so_far = 0
+
+        for info in binary_file_info_list:
+            if not os.path.exists(info.file_path):
+                raise OTOKOParsingError(
+                    "The data file %s does not exist." % info.file_path)
+
+            with open(info.file_path, "rb") as binary_file:
+                # Ideally we'd like to use numpy's fromfile() to read in binary
+                # data, but we are forced to roll our own float-by-float file
+                # reader because of the rules imposed on us by the file format;
+                # namely, if the swap indicator flag has been raised then the bytes
+                # of each float occur in reverse order.
+                for frame in range(info.n_frames):
+                    for channel in range(info.n_channels):
+                        b = bytes(binary_file.read(4))
+                        if info.swap_bytes:
+                            b = b[::-1] # "Extended slice" syntax, used to reverse.
+                        value = struct.unpack('f', b)[0]
+                        data[frames_so_far + frame][channel] = value
+
+                frames_so_far += info.n_frames
+
+        return CStyleStruct(
+            header_path = header_path,
+            data = data,
+            binary_file_info_list = binary_file_info_list,
+            header_info = info
+        )
