Repository: Electrostatics/mmcif_pdbx

Issue:
Clean up sub-module structure

Discussion:

Patch:
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..fb68c9c
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,2 @@
+[MESSAGES CONTROL]
+disable = no-else-return
\ No newline at end of file
diff --git a/README.md b/README.md
index b04e032..05abdf7 100644
--- a/README.md
+++ b/README.md
@@ -15,3 +15,8 @@ All of the code in this repository is based on http://mmcif.wwpdb.org/.
 Specifically, this code is directly derived from http://mmcif.wwpdb.org/docs/sw-examples/python/src/pdbx.tar.gz linked from http://mmcif.wwpdb.org/docs/sw-examples/python/html/.
 
 See http://mmcif.wwpdb.org/docs/sw-examples/python/html/ for more information about this package, including examples.
+
+## Versions
+
+Versions 0.* maintain API compatibility with the original code.
+Subsequent version break that compatibility, primarily by renaming methods in compliance with PEP8.
diff --git a/pdbx/containers.py b/pdbx/containers.py
new file mode 100644
index 0000000..8664573
--- /dev/null
+++ b/pdbx/containers.py
@@ -0,0 +1,825 @@
+##
+#
+# File: PdbxContainers.py
+# Original: 02-Feb-2009 jdw
+#
+# Update:
+# 23-Mar-2011 jdw Added method to rename attributes in category containers.
+# 05-Apr-2011 jdw Change cif writer to select double quoting as preferred
+# quoting style where possible.
+# 16-Jan-2012 jdw Create base class for DataCategory class
+# 22-Mar-2012 jdw when append attributes to existing categories update
+# existing rows with placeholder null values.
+# 2-Sep-2012 jdw add option to avoid embedded quoting that might
+# confuse simple parsers.
+# 28-Jun-2013 jdw export remove method
+# 29-Jun-2013 jdw export remove row method
+##
+"""A collection of container classes supporting the PDBx/mmCIF storage model.
+
+A base container class is defined which supports common features of
+data and definition containers. PDBx data files are organized in
+sections called data blocks which are mapped to data containers.
+PDBx dictionaries contain definition sections and data sections
+which are mapped to definition and data containers respectively.
+
+Data in both PDBx data files and dictionaries are organized in
+data categories. In the PDBx syntax individual items or data
+identified by labels of the form '_categoryName.attribute_name'.
+The terms category and attribute in PDBx jargon are analogous
+table and column in relational data model, or class and attribute
+in an object oriented data model.
+
+The DataCategory class provides base storage container for instance
+data and definition meta data.
+
+"""
+import re
+from sys import stdout
+import traceback
+
+
+__docformat__ = "restructuredtext en"
+__author__ = "John Westbrook"
+__email__ = "jwest@rcsb.rutgers.edu"
+
+
+class CifName:
+    """Class of utilities for CIF-style data names."""
+    @staticmethod
+    def category_part(name):
+        """Get the category part of the name."""
+        tname = ""
+        if name.startswith("_"):
+            tname = name[1:]
+        else:
+            tname = name
+
+        i = tname.find(".")
+        if i == -1:
+            return tname
+        else:
+            return tname[:i]
+
+    @staticmethod
+    def attribute_part(name):
+        """Get the attribute part of the name."""
+        i = name.find(".")
+        if i == -1:
+            return None
+        else:
+            return name[i + 1:]
+
+
+class ContainerBase:
+    """Container base class for data and definition objects."""
+    def __init__(self, name):
+        # The enclosing scope of the data container (e.g. data_/save_)
+        self.__name = name
+        # List of category names within this container -
+        self.__object_name_list = []
+        # dictionary of DataCategory objects keyed by category name.
+        self.__object_catalog = {}
+        self.__type = None
+
+    def get_type(self):
+        """Get container type."""
+        return self.__type
+
+    def set_type(self, type_):
+        """Set container type."""
+        self.__type = type_
+
+    @property
+    def name(self):
+        """Get container name."""
+        return self.__name
+
+    def set_name(self, name):
+        """Set container name."""
+        self.__name = name
+
+    def exists(self, name):
+        """Determine if object name exists in object catalog."""
+        return name in self.__object_catalog
+
+    def get_object(self, name):
+        """Get object from object catalog."""
+        if name in self.__object_catalog:
+            return self.__object_catalog[name]
+        else:
+            return None
+
+    def get_object_name_list(self):
+        """Get list of object names."""
+        return self.__object_name_list
+
+    def append(self, obj):
+        """Add the input object to the current object catalog.
+        An existing object of the same name will be overwritten."""
+        if obj.name is not None:
+            if obj.name not in self.__object_catalog:
+                self.__object_name_list.append(obj.name)
+            self.__object_catalog[obj.name] = obj
+
+    def replace(self, obj):
+        """Replace an existing object with the input object."""
+        if (obj.name is not None) and (
+                obj.name in self.__object_catalog):
+            self.__object_catalog[obj.name] = obj
+
+    def print_it(self, file_=stdout, type_="brief"):
+        """Dump information about container to specified file object."""
+        file_.write(
+            "+ %s container: %30s contains %4d categories\n" % (
+                self.get_type(), self.name,
+                len(self.__object_name_list)))
+        for name in self.__object_name_list:
+            file_.write("--------------------------------------------\n")
+            file_.write("Data category: %s\n" % name)
+            if type_ == 'brief':
+                self.__object_catalog[name].print_it(file_)
+            else:
+                self.__object_catalog[name].dump_it(file_)
+
+    def rename(self, current_name, new_name):
+        """Change the name of an object in place."""
+        try:
+            i = self.__object_name_list.index(current_name)
+            self.__object_name_list[i] = new_name
+            self.__object_catalog[new_name] = self.__object_catalog[
+                current_name]
+            self.__object_catalog[new_name].set_name(new_name)
+            return True
+        except KeyError:
+            return False
+
+    def remove(self, current_name):
+        """Revmove object by name.
+        Return True on success or False otherwise."""
+        try:
+            if current_name in self.__object_catalog:
+                del self.__object_catalog[current_name]
+                i = self.__object_name_list.index(current_name)
+                del self.__object_name_list[i]
+                return True
+            else:
+                return False
+        except KeyError:
+            pass
+        return False
+
+
+class DefinitionContainer(ContainerBase):
+    """Container for definitions."""
+
+    def __init__(self, name):
+        super(DefinitionContainer, self).__init__(name)
+        self.set_type('definition')
+
+    def is_category(self):
+        """Determine if container contains category objects."""
+        if self.exists('category'):
+            return True
+        return False
+
+    def is_attribute(self):
+        """Determine if container contains item objects."""
+        if self.exists('item'):
+            return True
+        return False
+
+    def print_it(self, fh=stdout, type_="brief"):
+        """Print information about container to file object."""
+        fh.write("Definition container: %30s contains %4d categories\n" %
+                 (self.name, len(self.get_object_name_list())))
+        if self.is_category():
+            fh.write("Definition type: category\n")
+        elif self.is_attribute():
+            fh.write("Definition type: item\n")
+        else:
+            fh.write("Definition type: undefined\n")
+
+        for name in self.get_object_name_list():
+            fh.write("--------------------------------------------\n")
+            fh.write("Definition category: %s\n" % name)
+            if type_ == 'brief':
+                self.get_object(name).print_it(fh)
+            else:
+                self.get_object(name).dumpId(fh)
+
+
+class DataContainer(ContainerBase):
+    """Container class for DataCategory objects."""
+
+    def __init__(self, name):
+        super(DataContainer, self).__init__(name)
+        self.set_type('data')
+        self.__global_flag = False
+        self.__current_row = None
+
+    def invoke_data_block_method(self, method):
+        """Invoke a method for the given data block."""
+        self.__current_row = 1
+        # TODO - remove exec() commands!
+        exec(method.get_inline())
+
+    def set_global(self):
+        """Set global flag to True."""
+        self.__global_flag = True
+
+    def get_global(self):
+        """Return global flag."""
+        return self.__global_flag
+
+
+class DataCategoryBase:
+    """Base object definition for a data category."""
+
+    def __init__(self, name, attribute_name_list=None, row_list=None):
+        self._name = name
+        if row_list is not None:
+            self._row_list = row_list
+        else:
+            self._row_list = []
+
+        if attribute_name_list is not None:
+            self._attribute_name_list = attribute_name_list
+        else:
+            self._attribute_name_list = []
+        self._catalog = {}
+        self._num_attributes = 0
+        self.__setup()
+
+    def __setup(self):
+        self._num_attributes = len(self._attribute_name_list)
+        self._catalog = {}
+        for attribute_name in self._attribute_name_list:
+            attribute_name_lower = attribute_name.lower()
+            self._catalog[attribute_name_lower] = attribute_name
+
+    def set_row_list(self, row_list):
+        """Set row list."""
+        self._row_list = row_list
+
+    def set_attribute_name_list(self, attribute_name_list):
+        """Set attribute name list."""
+        self._attribute_name_list = attribute_name_list
+        self.__setup()
+
+    def set_name(self, name):
+        """Set name."""
+        self._name = name
+
+    def get(self):
+        """Set name, attribute name list, and row list."""
+        return (self._name, self._attribute_name_list, self._row_list)
+
+
+class DataCategory(DataCategoryBase):
+    """Methods for creating, accessing, formatting PDBx cif data categories."""
+
+    def __init__(self, name, attribute_name_list=None, row_list=None):
+        super().__init__(name, attribute_name_list, row_list)
+        self.__lfh = stdout
+        self.__current_row_index = 0
+        self.__current_attribute = None
+        self.__avoid_embedded_quoting = False
+        self.__whitespace_re = re.compile(r"\s")
+        self.__whitespace_quotes_re = re.compile(r"[\s'\"]")
+        self.__newline_re = re.compile(r"[\n\r]")
+        self.__single_quote_re = re.compile(r"[']")
+        self.__whitespace_single_quote_re = re.compile(r"('\s)|(\s')")
+        self.__double_quote_re = re.compile(r'["]')
+        self.__whitespace_double_quote_re = re.compile(r'("\s)|(\s")')
+        self.__integer_re = re.compile(r'^[0-9]+$')
+        self.__float_re = re.compile(
+            r'^-?(([0-9]+)[.]?|([0-9]*[.][0-9]+))([(][0-9]+[)])?([eE][+-]?[0-9]+)?$')
+        self.__data_type_list = [
+            'DT_NULL_VALUE', 'DT_INTEGER', 'DT_FLOAT', 'DT_UNQUOTED_STRING',
+            'DT_ITEM_NAME', 'DT_DOUBLE_QUOTED_STRING',
+            'DT_SINGLE_QUOTED_STRING', 'DT_MULTI_LINE_STRING',
+        ]
+        self.__format_type_list = [
+            'FT_NULL_VALUE', 'FT_NUMBER', 'FT_NUMBER', 'FT_UNQUOTED_STRING',
+            'FT_QUOTED_STRING', 'FT_QUOTED_STRING', 'FT_QUOTED_STRING',
+            'FT_MULTI_LINE_STRING'
+        ]
+
+    def __getitem__(self, item):
+        """Implements list-type functionality.
+
+        Implements op[x] for some special cases:
+            item = integer - returns the row in category (normal list behavior)
+            item = string - returns the value of attribute 'x' in first row
+        """
+        if isinstance(item, int):
+            return self._row_list[item]
+
+        elif isinstance(item, str):
+            try:
+                self.get_attribute_index(item)
+                return self._row_list[0][item]
+            except (IndexError, KeyError):
+                raise KeyError
+        raise TypeError(item)
+
+    @property
+    def current_attribute(self):
+        """Get current attribute."""
+        return self.__current_attribute
+
+    @property
+    def current_row_index(self):
+        """Get current row index."""
+        return self.__current_row_index
+
+    @property
+    def row_list(self):
+        """Get list of rows."""
+        return self._row_list
+
+    @property
+    def row_count(self):
+        """Get number of rows."""
+        return len(self._row_list)
+
+    def get_row(self, index):
+        """Get specified row.
+        Returns empty array if row not found."""
+        try:
+            return self._row_list[index]
+        except IndexError:
+            return []
+
+    def remove_row(self, index):
+        """Remove specified row."""
+        try:
+            #if ((index >= 0) and (index < len(self._row_list))):
+            if (index >= 0 and self._row_list):
+                del self._row_list[index]
+                if self.__current_row_index >= len(self._row_list):
+                    self.__current_row_index = len(self._row_list) - 1
+                return True
+            else:
+                pass
+        except IndexError:
+            pass
+        return False
+
+    def get_full_row(self, index):
+        """Return a full row based on the length of the the attribute list."""
+        try:
+            if len(self._row_list[index]) < self._num_attributes:
+                self._row_list[index] += ['?'] * (
+                    self._num_attributes - len(self._row_list[index]))
+            return self._row_list[index]
+        except IndexError:
+            return ['?'] * len(self._num_attributes)
+
+    @property
+    def name(self):
+        """Get container name."""
+        return self._name
+
+    @property
+    def attribute_list(self):
+        """Get list of attributes."""
+        return self._attribute_name_list
+
+    @property
+    def attribute_count(self):
+        """Get number of attributes."""
+        return len(self._attribute_name_list)
+
+    @property
+    def attribute_list_with_order(self):
+        """Get list of attributes in order."""
+        ordered_list = []
+        for i, att in enumerate(self._attribute_name_list):
+            ordered_list.append((att, i))
+        return ordered_list
+
+    def get_attribute_index(self, attribute_name):
+        """Get index of given attribute.
+        Raises IndexError."""
+        return self._attribute_name_list.index(attribute_name)
+
+    def has_attribute(self, attribute_name):
+        """Indicate whether container has attribute."""
+        return attribute_name in self._attribute_name_list
+
+    @property
+    def item_name_list(self):
+        """List of attribute names as fully qualified item names."""
+        item_name_list_ = []
+        for att in self._attribute_name_list:
+            item_name_list_.append("_" + self._name + "." + att)
+        return item_name_list_
+
+    def append(self, row):
+        """Add row to container."""
+        self._row_list.append(row)
+
+    def append_attribute(self, attribute_name):
+        """Add attribute to container."""
+        attribute_name_lower = attribute_name.lower()
+        if attribute_name_lower in self._catalog:
+            index = self._attribute_name_list.index(
+                self._catalog[attribute_name_lower])
+            self._attribute_name_list[index] = attribute_name
+            self._catalog[attribute_name_lower] = attribute_name
+        else:
+            self._attribute_name_list.append(attribute_name)
+            self._catalog[attribute_name_lower] = attribute_name
+        self._num_attributes = len(self._attribute_name_list)
+
+    def append_attribute_extend_rows(self, attribute_name):
+        """Append attribute and extend rows."""
+        attribute_name_lower = attribute_name.lower()
+        if attribute_name_lower in self._catalog:
+            index = self._attribute_name_list.index(
+                self._catalog[attribute_name_lower])
+            self._attribute_name_list[index] = attribute_name
+            self._catalog[attribute_name_lower] = attribute_name
+            self.__lfh.write(
+                "Appending existing attribute %s\n" % attribute_name)
+        else:
+            self._attribute_name_list.append(attribute_name)
+            self._catalog[attribute_name_lower] = attribute_name
+            # add a placeholder to any existing rows for the new attribute.
+            #if len(self._row_list) > 0:
+            for row in self._row_list:
+                row.append("?")
+        self._num_attributes = len(self._attribute_name_list)
+
+    def get_value(self, attribute_name=None, row_index=None):
+        """Get value for specified attribute and row."""
+        if attribute_name is None:
+            attribute = self.__current_attribute
+        else:
+            attribute = attribute_name
+        if row_index is None:
+            index = self.__current_row_index
+        else:
+            index = row_index
+        if isinstance(attribute, str) and isinstance(index, int):
+            return self._row_list[index][
+                self._attribute_name_list.index(attribute)]
+        raise IndexError(attribute)
+
+    def set_value(self, value, attribute_name=None, row_index=None):
+        """Set value of attribute."""
+        if attribute_name is None:
+            attribute = self.__current_attribute
+        else:
+            attribute = attribute_name
+        if row_index is None:
+            index = self.__current_row_index
+        else:
+            index = row_index
+        if isinstance(attribute, str) and isinstance(index, int):
+            try:
+                # if row index is out of range - add the rows
+                for _ in range(index + 1 - len(self._row_list)):
+                    self._row_list.append(self.__empty_row)
+                row_len = len(self._row_list[index])
+                ind = self._attribute_name_list.index(attribute)
+                # extend the list if needed
+                if ind >= row_len:
+                    self._row_list[index].extend(
+                        [None for _ in range(2 * ind - row_len)])
+                self._row_list[index][ind] = value
+            except IndexError:
+                self.__lfh.write(
+                    "DataCategory(setvalue) index error category"
+                    " %s attribute %s index %d value %r\n" %
+                    (self._name, attribute, index, value))
+                traceback.print_exc(file=self.__lfh)
+            except ValueError:
+                self.__lfh.write(
+                    "DataCategory(setvalue) value error category"
+                    " %s attribute %s index %d value %r\n" %
+                    (self._name, attribute, index, value))
+                traceback.print_exc(file=self.__lfh)
+
+    @property
+    def __empty_row(self):
+        """Return an empty row."""
+        return [None for _ in range(len(self._attribute_name_list))]
+
+    def replace_value(self, old_value, new_value, attribute_name):
+        """Replace the value of the specified attribute."""
+        num_replace = 0
+        if attribute_name not in self._attribute_name_list:
+            return num_replace
+        ind = self._attribute_name_list.index(attribute_name)
+        for row in self._row_list:
+            if row[ind] == old_value:
+                row[ind] = new_value
+                num_replace += 1
+        return num_replace
+
+    def replace_substring(self, old_value, new_value, attribute_name):
+        """Replace substring of value of given attribute.
+        Return Boolean flag indicating success."""
+        replace_ok = False
+        if attribute_name not in self._attribute_name_list:
+            return replace_ok
+        ind = self._attribute_name_list.index(attribute_name)
+        for row in self._row_list:
+            val = row[ind]
+            row[ind] = val.replace(old_value, new_value)
+            if val != row[ind]:
+                replace_ok = True
+        return replace_ok
+
+    def invoke_attribute_method(self, attribute_name, method):
+        """Invoke method of current attribute."""
+        self.__current_row_index = 0
+        self.__current_attribute = attribute_name
+        self.append_attribute(attribute_name)
+        ind = self._attribute_name_list.index(attribute_name)
+        if not self._row_list:
+            row = [None] * len(self._attribute_name_list) * 2
+            row[ind] = None
+            self._row_list.append(row)
+        for row in self._row_list:
+            row_len = len(row)
+            if ind >= row_len:
+                row.extend([None] * (2 * ind - row_len))
+                row[ind] = None
+            # TODO - just say "no" to exec()
+            exec(method.get_inline())
+            self.__current_row_index += 1
+
+    def invoke_category_method(self, method):
+        """Invoke method of current category."""
+        self.__current_row_index = 0
+        # TODO - remove exec()
+        exec(method.get_inline())
+
+    @property
+    def max_attribute_list_length(self):
+        """Get maximum attribute list length."""
+        max_list = [0] * len(self._attribute_name_list)
+        for row in self._row_list:
+            for index, value in enumerate(row):
+                max_list[index] = max(max_list[index], len(value))
+        return max_list
+
+    def rename_attribute(self, current_attribute_name, new_attribute_name):
+        """Change the name of an attribute in place."""
+        try:
+            i = self._attribute_name_list.index(current_attribute_name)
+            self._attribute_name_list[i] = new_attribute_name
+            del self._catalog[current_attribute_name.lower()]
+            self._catalog[new_attribute_name.lower()] = new_attribute_name
+            return True
+        except KeyError:
+            return False
+
+    def print_it(self, file_=stdout):
+        """Print container information."""
+        file_.write("--------------------------------------------\n")
+        file_.write(
+            " Category: %s attribute list length: %d\n" %
+            (self._name, len(self._attribute_name_list)))
+        for attr in self._attribute_name_list:
+            file_.write(" Category: %s attribute: %s\n" % (self._name, attr))
+
+        file_.write(" Row value list length: %d\n" % len(self._row_list))
+        for row in self._row_list[:2]:
+            if len(row) == len(self._attribute_name_list):
+                for index, value in enumerate(row):
+                    file_.write(" %30s: %s ...\n" % (
+                        self._attribute_name_list[index], str(value)[:30]))
+            else:
+                file_.write(
+                    "+WARNING - %s data length %d attribute name length %s "
+                    "mismatched\n" % (
+                        self._name, len(row),
+                        len(self._attribute_name_list)))
+
+    def dump_it(self, file_=stdout):
+        """Dump contents of container."""
+        file_.write("--------------------------------------------\n")
+        file_.write(
+            " Category: %s attribute list length: %d\n" %
+            (self._name, len(self._attribute_name_list)))
+        for attr in self._attribute_name_list:
+            file_.write(" Category: %s attribute: %s\n" % (self._name, attr))
+
+        file_.write(" Value list length: %d\n" % len(self._row_list))
+        for row in self._row_list:
+            for index, value in enumerate(row):
+                file_.write(" %30s: %s\n" % (
+                    self._attribute_name_list[index], value))
+
+    def __format_pdbx(self, inp):
+        """Format input data following PDBx quoting rules."""
+        try:
+            if inp is None:
+                return ("?", 'DT_NULL_VALUE')
+            # pure numerical values are returned as unquoted strings
+            if (isinstance(inp, int) or self.__integer_re.search(str(inp))):
+                return ([str(inp)], 'DT_INTEGER')
+            if (isinstance(inp, float) or self.__float_re.search(str(inp))):
+                return ([str(inp)], 'DT_FLOAT')
+            # null value handling
+            if inp in (".", "?"):
+                return ([inp], 'DT_NULL_VALUE')
+            if inp == "":
+                return (["."], 'DT_NULL_VALUE')
+            # Contains white space or quotes ?
+            if not self.__whitespace_quotes_re.search(inp):
+                if inp.startswith("_"):
+                    return (self.__double_quoted_list(inp), 'DT_ITEM_NAME')
+                else:
+                    return ([str(inp)], 'DT_UNQUOTED_STRING')
+            else:
+                if self.__newline_re.search(inp):
+                    return (
+                        self.__semicolon_quoted_list(inp),
+                        'DT_MULTI_LINE_STRING')
+                else:
+                    if self.__avoid_embedded_quoting:
+                        # change priority to choose double quoting where possible.
+                        if (
+                                not self.__double_quote_re.search(inp) and not
+                                self.__whitespace_single_quote_re.search(inp)):
+                            return (
+                                self.__double_quoted_list(inp),
+                                'DT_DOUBLE_QUOTED_STRING')
+                        elif (
+                                not self.__single_quote_re.search(inp) and not
+                                self.__whitespace_double_quote_re.search(inp)):
+                            return (
+                                self.__single_quoted_list(inp),
+                                'DT_SINGLE_QUOTED_STRING')
+                        else:
+                            return (
+                                self.__semicolon_quoted_list(inp),
+                                'DT_MULTI_LINE_STRING')
+                    else:
+                        # change priority to choose double quoting where possible.
+                        if not self.__double_quote_re.search(inp):
+                            return (
+                                self.__double_quoted_list(inp),
+                                'DT_DOUBLE_QUOTED_STRING')
+                        elif not self.__single_quote_re.search(inp):
+                            return (
+                                self.__single_quoted_list(inp),
+                                'DT_SINGLE_QUOTED_STRING')
+                        else:
+                            return (
+                                self.__semicolon_quoted_list(inp),
+                                'DT_MULTI_LINE_STRING')
+        except ValueError:
+            traceback.print_exc(file=self.__lfh)
+
+    def __data_type_pdbx(self, inp):
+        """Detect the PDBx data type."""
+        if inp is None:
+            return 'DT_NULL_VALUE'
+        # pure numerical values are returned as unquoted strings
+        if isinstance(inp, int) or self.__integer_re.search(str(inp)):
+            return 'DT_INTEGER'
+        if isinstance(inp, float) or self.__float_re.search(str(inp)):
+            return 'DT_FLOAT'
+        # null value handling
+        if inp in (".", "?"):
+            return 'DT_NULL_VALUE'
+        if inp == "":
+            return 'DT_NULL_VALUE'
+        # Contains white space or quotes ?
+        if not self.__whitespace_quotes_re.search(inp):
+            if inp.startswith("_"):
+                return 'DT_ITEM_NAME'
+            else:
+                return 'DT_UNQUOTED_STRING'
+        else:
+            if self.__newline_re.search(inp):
+                return 'DT_MULTI_LINE_STRING'
+            else:
+                if self.__avoid_embedded_quoting:
+                    if (
+                            not self.__single_quote_re.search(inp) and not
+                            self.__whitespace_double_quote_re.search(inp)):
+                        return 'DT_DOUBLE_QUOTED_STRING'
+                    elif (
+                            not self.__double_quote_re.search(inp) and not
+                            self.__whitespace_single_quote_re.search(inp)):
+                        return 'DT_SINGLE_QUOTED_STRING'
+                    else:
+                        return 'DT_MULTI_LINE_STRING'
+                else:
+                    if not self.__single_quote_re.search(inp):
+                        return 'DT_DOUBLE_QUOTED_STRING'
+                    elif not self.__double_quote_re.search(inp):
+                        return 'DT_SINGLE_QUOTED_STRING'
+                    else:
+                        return 'DT_MULTI_LINE_STRING'
+
+    @staticmethod
+    def __single_quoted_list(inp):
+        """Generate a single-quoted list from the input."""
+        return ["'"] + [inp] + ["'"]
+
+    @staticmethod
+    def __double_quoted_list(inp):
+        """Generate a double-quoted list from the input."""
+        return ['"'] + [inp] + ['"']
+
+    @staticmethod
+    def __semicolon_quoted_list(inp):
+        """Generate a semicolon-delimited quoted list from the input."""
+        if inp[-1] == '\n':
+            return ["\n", ";"] + [inp] + [";", "\n"]
+        else:
+            return ["\n", ";"] + [inp] + ["\n", ";", "\n"]
+
+    def get_value_formatted(self, attribute_name=None, row_index=None):
+        """Get formatted version of value."""
+        if attribute_name is None:
+            attribute = self.__current_attribute
+        else:
+            attribute = attribute_name
+        if row_index is None:
+            index = self.__current_row_index
+        else:
+            index = row_index
+        if isinstance(attribute, str) and isinstance(index, int):
+            try:
+                list_, _ = self.__format_pdbx(
+                    self._row_list[index][
+                        self._attribute_name_list.index(attribute)])
+                return "".join(list_)
+            except IndexError:
+                self.__lfh.write(
+                    "attribute_name %s index %r rowdata %r\n" %
+                    (attribute_name, index, self._row_list[index]))
+                raise IndexError
+        raise TypeError(attribute)
+
+    def get_value_formatted_by_index(self, attribute_index, row_index):
+        """Get value formatted by index."""
+        list_, _ = self.__format_pdbx(
+            self._row_list[row_index][attribute_index])
+        return "".join(list_)
+
+    def get_max_attribute_list_length(self, steps=1):
+        """Get maximum length of attribute value list."""
+        max_list = [0] * len(self._attribute_name_list)
+        for _ in self._row_list[::steps]:
+            for index, value in enumerate(self._attribute_name_list):
+                max_list[index] = max(max_list[index], len(str(value)))
+        return max_list
+
+    def get_format_type_list(self, steps=1):
+        """Get a formatted type list."""
+        try:
+            current_data_type_list = ['DT_NULL_VALUE'] * len(
+                self._attribute_name_list)
+            for row in self._row_list[::steps]:
+                for index, value in enumerate(self._attribute_name_list):
+                    data_type = self.__data_type_pdbx(value)
+                    data_index = self.__data_type_list.index(data_type)
+                    current_type = current_data_type_list[index]
+                    current_index = self.__data_type_list.index(
+                        current_type)
+                    current_index = max(current_index, data_index)
+                    current_data_type_list[index] = self.__data_type_list[
+                        current_index]
+            # Map the format types to the data types
+            current_format_type_list = []
+            for data_type in current_data_type_list:
+                index = self.__data_type_list.index(data_type)
+                current_format_type_list.append(self.__format_type_list[index])
+        except IndexError:
+            self.__lfh.write(
+                "PdbxDataCategory(get_format_type_list) ++Index error at "
+                "index %d in row %r\n" %
+                (index, row))
+        return current_format_type_list, current_data_type_list
+
+    @property
+    def get_format_type_list_x(self):
+        """Alternate version of format type list."""
+        current_data_type_list = ['DT_NULL_VALUE'] * len(
+            self._attribute_name_list)
+        for _ in self._row_list:
+            for index, value in enumerate(self._attribute_name_list):
+                data_type = self.__data_type_pdbx(value)
+                data_index = self.__data_type_list.index(data_type)
+                current_type = current_data_type_list[index]
+                current_index = self.__data_type_list.index(current_type)
+                current_index = max(current_index, data_index)
+                current_data_type_list[index] = self.__data_type_list[
+                    current_index]
+        # Map the format types to the data types
+        current_format_type_list = []
+        for data_type in current_data_type_list:
+            index = self.__data_type_list.index(data_type)
+            current_format_type_list.append(self.__format_type_list[index])
+        return current_format_type_list, current_data_type_list
diff --git a/pdbx/errors.py b/pdbx/errors.py
new file mode 100644
index 0000000..4785e28
--- /dev/null
+++ b/pdbx/errors.py
@@ -0,0 +1,17 @@
+"""Error classes for PDBx/mmCIF."""
+
+
+class PdbxError(Exception):
+    """Class for catching general errors."""
+
+
+class PdbxSyntaxError(Exception):
+    """Class for catching syntax errors."""
+
+    def __init__(self, line_number, text):
+        super().__init__(self)
+        self.line_number = line_number
+        self.text = text
+
+    def __str__(self):
+        return "%%ERROR - [at line: %d] %s" % (self.line_number, self.text)
diff --git a/pdbx/reader.py b/pdbx/reader.py
new file mode 100644
index 0000000..2a91215
--- /dev/null
+++ b/pdbx/reader.py
@@ -0,0 +1,397 @@
+##
+# File: PdbxReader.py
+# Date: 2012-01-09 Jdw Adapted from PdbxParser
+#
+# Updates:
+#
+# 2012-01-09 - (jdw) Separate reader and writer classes.
+#
+# 2012-09-02 - (jdw) Revise tokenizer to better handle embedded quoting.
+#
+##
+"""PDBx/mmCIF dictionary and data file parser.
+
+Acknowledgements:
+
+The tokenizer used in this module is modeled after the clever parser design
+used in the PyMMLIB package.
+
+PyMMLib Development Group
+Authors: Ethan Merritt: merritt@u.washington.ed & Jay Painter: jay.painter@gmail.com
+See: http://pymmlib.sourceforge.net/
+"""
+import re
+from .containers import DataCategory, DefinitionContainer, DataContainer
+from .errors import PdbxSyntaxError, PdbxError
+
+
+class PdbxReader:
+    """PDBx reader for data files and dictionaries."""
+
+    def __init__(self, input_file):
+        """Initialize with input file handle; e.g. as returned by open()."""
+        self.__current_line_number = 0
+        self.__input_file = input_file
+        self.__state_dict = {
+            "data": "ST_DATA_CONTAINER",
+            "loop": "ST_TABLE",
+            "global": "ST_GLOBAL_CONTAINER",
+            "save": "ST_DEFINITION",
+            "stop": "ST_STOP"
+        }
+
+    def read(self, container_list):
+        """Appends to the input list of definition and data containers."""
+        self.__current_line_number = 0
+        try:
+            self.__parser(self.__tokenizer(self.__input_file), container_list)
+        except StopIteration:
+            pass
+        else:
+            raise PdbxError()
+
+    def __syntax_error(self, error_text):
+        """Raise a PdbxSyntaxError."""
+        raise PdbxSyntaxError(self.__current_line_number, error_text)
+
+    @staticmethod
+    def __get_container_name(in_word):
+        """Returns the name of the data_ or save_ container."""
+        return str(in_word[5:]).strip()
+
+    def __get_state(self, in_word):
+        """Identifies reserved syntax elements and assigns an associated state.
+
+        Returns: (reserved word, state) where:
+            reserved word - is one of CIF syntax elements:
+                            data_, loop_, global_, save_, stop_
+            state - the parser state required to process this next section.
+        """
+        i = in_word.find("_")
+        if i == -1:
+            return None, "ST_UNKNOWN"
+        try:
+            reserved_word = in_word[:i].lower()
+            return reserved_word, self.__state_dict[reserved_word]
+        except KeyError:
+            return None, "ST_UNKNOWN"
+
+    def __parser(self, tokenizer, container_list):
+        """ Parser for PDBx data files and dictionaries.
+
+            Args:
+                tokenizer: reentrant method recognizing data item names
+                    (_category.attribute), quoted strings (single, double and
+                    multi-line semi-colon delimited), and unquoted strings.
+                container_list: list-type container for data and definition
+                    objects parsed from from the input file.
+            Returns:
+                container_list - is appended with data and definition objects
+        """
+        # Working container - data or definition
+        current_container = None
+        # Working category container
+        category_index = {}
+        current_category = None
+        current_row = None
+        state = None
+
+        # Find the first reserved word and begin capturing data.
+        while True:
+            current_category_name, current_attribute_name, \
+                current_quoted_string, current_word = next(tokenizer)
+            if current_word is None:
+                continue
+            reserved_word, state = self.__get_state(current_word)
+            if reserved_word is not None:
+                break
+
+        while True:
+            # Set the current state: at this point in the processing cycle we
+            # are expecting a token containing # either a '_category.attribute'
+            # or a reserved word.
+            if current_category_name is not None:
+                state = "ST_KEY_VALUE_PAIR"
+            elif current_word is not None:
+                reserved_word, state = self.__get_state(current_word)
+            else:
+                self.__syntax_error("Miscellaneous syntax error")
+                return
+
+            # Process _category.attribute value assignments
+            if state == "ST_KEY_VALUE_PAIR":
+                try:
+                    current_category = category_index[current_category_name]
+                except KeyError:
+                    # A new category is encountered - create a container and
+                    # add a row
+                    category_index[current_category_name] = DataCategory(
+                        current_category_name)
+                    current_category = category_index[current_category_name]
+                    try:
+                        current_container.append(current_category)
+                    except AttributeError:
+                        self.__syntax_error(
+                            "Category cannot be added to data_ block")
+                        return
+                    current_row = []
+                    current_category.append(current_row)
+                else:
+                    # Recover the existing row from the category
+                    try:
+                        current_row = current_category[0]
+                    except IndexError:
+                        self.__syntax_error(
+                            "Internal index error accessing category data")
+                        return
+                # Check for duplicate attributes and add attribute to table.
+                if current_attribute_name in current_category.attribute_list:
+                    self.__syntax_error(
+                        "Duplicate attribute encountered in category")
+                    return
+                else:
+                    current_category.append_attribute(current_attribute_name)
+                # Get the data for this attribute from the next token
+                tok_category, _, current_quoted_string, current_word = next(
+                    tokenizer)
+                if (tok_category is not None or (
+                        current_quoted_string is None
+                        and current_word is None)):
+                    self.__syntax_error(
+                        "Missing data for item _%s.%s" % (
+                            current_category_name, current_attribute_name))
+                if current_word is not None:
+                    # Validation check token for misplaced reserved words
+                    reserved_word, state = self.__get_state(current_word)
+                    if reserved_word is not None:
+                        self.__syntax_error(
+                            "Unexpected reserved word: %s" % (reserved_word))
+                    current_row.append(current_word)
+                elif current_quoted_string is not None:
+                    current_row.append(current_quoted_string)
+                else:
+                    self.__syntax_error("Missing value in item-value pair")
+                try:
+                    current_category_name, current_attribute_name, \
+                        current_quoted_string, current_word = next(tokenizer)
+                except RuntimeError as err:
+                    raise StopIteration(err)
+                continue
+
+            # Process a loop_ declaration and associated data
+            if state == "ST_TABLE":
+                # The category name in the next current_category_name,
+                # current_attribute_name pair defines the name of the category
+                # container.
+                current_category_name, current_attribute_name, \
+                    current_quoted_string, current_word = next(tokenizer)
+                if (current_category_name is None) or (
+                        current_attribute_name is None):
+                    self.__syntax_error(
+                        "Unexpected token in loop_ declaration")
+                    return
+                # Check for a previous category declaration.
+                if current_category_name in category_index:
+                    self.__syntax_error(
+                        "Duplicate category declaration in loop_")
+                    return
+                current_category = DataCategory(current_category_name)
+                try:
+                    current_container.append(current_category)
+                except AttributeError:
+                    self.__syntax_error(
+                        "loop_ declaration outside of data_ block or save_ "
+                        "frame")
+                    return
+                current_category.append_attribute(current_attribute_name)
+                # Read the rest of the loop_ declaration
+                while True:
+                    current_category_name, current_attribute_name, \
+                        current_quoted_string, current_word = next(tokenizer)
+                    if current_category_name is None:
+                        break
+                    if current_category_name != current_category.name:
+                        self.__syntax_error(
+                            "Changed category name in loop_ declaration")
+                        return
+                    current_category.append_attribute(current_attribute_name)
+                # If the next token is a 'word', check it for any reserved
+                # words
+                if current_word is not None:
+                    reserved_word, state = self.__get_state(current_word)
+                    if reserved_word is not None:
+                        if reserved_word == "stop":
+                            return
+                        else:
+                            self.__syntax_error(
+                                "Unexpected reserved word after loop "
+                                "declaration: %s" % (reserved_word))
+                # Read the table of data for this loop_
+                while True:
+                    current_row = []
+                    current_category.append(current_row)
+                    for _ in current_category.attribute_list:
+                        if current_word is not None:
+                            current_row.append(current_word)
+                        elif current_quoted_string is not None:
+                            current_row.append(current_quoted_string)
+                        try:
+                            current_category_name, current_attribute_name, \
+                                current_quoted_string, current_word = next(
+                                    tokenizer)
+                        except RuntimeError as err:
+                            raise StopIteration(err)
+                    # loop_ data processing ends if a new _category.attribute
+                    # is encountered
+                    if current_category_name is not None:
+                        break
+                    # A reserved word is encountered
+                    if current_word is not None:
+                        reserved_word, state = self.__get_state(current_word)
+                        if reserved_word is not None:
+                            break
+                continue
+
+            if state == "ST_DEFINITION":
+                # Ignore trailing unnamed saveframe delimiters e.g. 'save_'
+                state_name = self.__get_container_name(current_word)
+                if state_name:
+                    current_container = DefinitionContainer(state_name)
+                    container_list.append(current_container)
+                    category_index = {}
+                    current_category = None
+                current_category_name, current_attribute_name, \
+                    current_quoted_string, current_word = next(tokenizer)
+            elif state == "ST_DATA_CONTAINER":
+                data_name = self.__get_container_name(current_word)
+                if data_name:
+                    data_name = "unidentified"
+                current_container = DataContainer(data_name)
+                container_list.append(current_container)
+                category_index = {}
+                current_category = None
+                current_category_name, current_attribute_name, \
+                    current_quoted_string, current_word = next(tokenizer)
+            elif state == "ST_STOP":
+                return
+
+            if state == "ST_GLOBAL":
+                current_container = DataContainer("blank-global")
+                current_container.set_global()
+                container_list.append(current_container)
+                category_index = {}
+                current_category = None
+                current_category_name, current_attribute_name, \
+                    current_quoted_string, current_word = next(tokenizer)
+            elif state == "ST_UNKNOWN":
+                self.__syntax_error(
+                    "Unrecognized syntax element: " + str(current_word))
+                return
+
+    def __tokenizer(self, input_file):
+        """Tokenizer method for the mmCIF syntax file.
+
+        Each return/yield from this method returns information about the next
+        token in the form of a tuple with the following structure:
+            (category name, attribute name, quoted strings, words w/o quotes
+            or white space)
+        Differentiated the regular expression to the better handle embedded
+        quotes.
+        """
+        # Regex definition for mmCIF syntax - semi-colon delimited strings are
+        # handled outside of this regex.
+        mmcif_re = re.compile(
+            r"(?:"
+            r"(?:_(.+?)[.](\S+))" "|"  # _category.attribute
+            r"(?:['](.*?)(?:[']\s|[']$))" "|"  # single quoted strings
+            r"(?:[\"](.*?)(?:[\"]\s|[\"]$))" "|"  # double quoted strings
+            r"(?:\s*#.*$)" "|"  # comments (dumped)
+            r"(\S+)" # unquoted words
+            r")"
+        )
+        file_iterator = iter(input_file)
+        # Tokenizer loop begins here
+        while True:
+            line = next(file_iterator)
+            self.__current_line_number += 1
+            # Dump comments
+            if line.startswith("#"):
+                continue
+            # Gobble up the entire semi-colon/multi-line-delimited string and
+            # and stuff this into the string slot in the return tuple
+            if line.startswith(";"):
+                multiline_string = [line[1:]]
+                while True:
+                    line = next(file_iterator)
+                    self.__current_line_number += 1
+                    if line.startswith(";"):
+                        break
+                    multiline_string.append(line)
+                # remove trailing new-line that is part of the \n; delimiter
+                multiline_string[-1] = multiline_string[-1].rstrip()
+                yield (None, None, "".join(multiline_string), None)
+                # Need to process the remainder of the current line -
+                line = line[1:]
+
+            # Apply regex to the current line consolidate the single/double
+            # quoted within the quoted string category
+            for match in mmcif_re.finditer(line):
+                match_groups = match.groups()
+                if match_groups != (None, None, None, None, None):
+                    if match_groups[2] is not None:
+                        quoted_string = match_groups[2]
+                    elif match_groups[3] is not None:
+                        quoted_string = match_groups[3]
+                    else:
+                        quoted_string = None
+                    groups = (
+                        match_groups[0], match_groups[1], quoted_string,
+                        match_groups[4])
+                    yield groups
+
+    def __tokenizer_org(self, input_file):
+        """Tokenizer method for the mmCIF syntax file.
+
+        Each return/yield from this method returns information about the next
+        token in the form of a tuple with the following structure:
+            (category name, attribute name, quoted strings, words w/o quotes
+            or white space)
+        """
+        # Regex definition for mmCIF syntax - semi-colon delimited strings are
+        # handled outside of this regex.
+        mmcif_re = re.compile(
+            r"(?:"
+            r"(?:_(.+?)[.](\S+))" "|" # _category.attribute
+            r"(?:['\"](.*?)(?:['\"]\s|['\"]$))" "|" # quoted strings
+            r"(?:\s*#.*$)" "|"  # comments (dumped)
+            r"(\S+)"  # unquoted words
+            r")"
+        )
+        file_iterator = iter(input_file)
+        # Tokenizer loop begins here
+        while True:
+            line = next(file_iterator)
+            self.__current_line_number += 1
+            # Dump comments
+            if line.startswith("#"):
+                continue
+            # Gobble up the entire semi-colon/multi-line delimited string and
+            # and stuff this into the string slot in the return tuple
+            if line.startswith(";"):
+                multiline_string = [line[1:]]
+                while True:
+                    line = next(file_iterator)
+                    self.__current_line_number += 1
+                    if line.startswith(";"):
+                        break
+                    multiline_string.append(line)
+                # remove trailing new-line that is part of the \n; delimiter
+                multiline_string[-1] = multiline_string[-1].rstrip()
+                yield (None, None, "".join(multiline_string), None)
+                # Need to process the remainder of the current line
+                line = line[1:]
+            # Apply regex to the current line
+            for match in mmcif_re.finditer(line):
+                groups = match.groups()
+                if groups != (None, None, None, None):
+                    yield groups
diff --git a/pdbx/reader/__init__.py b/pdbx/reader/__init__.py
deleted file mode 100644
index e1128c6..0000000
--- a/pdbx/reader/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""Python PDBx/mmCIF reader resources.
-
-All of the code in this repository is based on http://mmcif.wwpdb.org/.
-Specifically, this code is directly derived from
-http://mmcif.wwpdb.org/docs/sw-examples/python/src/pdbx.tar.gz linked from
-http://mmcif.wwpdb.org/docs/sw-examples/python/html/.
-
-See http://mmcif.wwpdb.org/docs/sw-examples/python/html/ for more information
-about this package, including examples.
-"""
-__all__ = ["containers", "reader"]
diff --git a/pdbx/reader/containers.py b/pdbx/reader/containers.py
deleted file mode 100644
index 733f3b4..0000000
--- a/pdbx/reader/containers.py
+++ /dev/null
@@ -1,814 +0,0 @@
-##
-#
-# File: PdbxContainers.py
-# Original: 02-Feb-2009 jdw
-#
-# Update:
-# 23-Mar-2011 jdw Added method to rename attributes in category containers.
-# 05-Apr-2011 jdw Change cif writer to select double quoting as preferred
-# quoting style where possible.
-# 16-Jan-2012 jdw Create base class for DataCategory class
-# 22-Mar-2012 jdw when append attributes to existing categories update
-# existing rows with placeholder null values.
-# 2-Sep-2012 jdw add option to avoid embedded quoting that might
-# confuse simple parsers.
-# 28-Jun-2013 jdw export remove method
-# 29-Jun-2013 jdw export remove row method
-##
-"""
-
-A collection of container classes supporting the PDBx/mmCIF storage model.
-
-A base container class is defined which supports common features of
-data and definition containers. PDBx data files are organized in
-sections called data blocks which are mapped to data containers.
-PDBx dictionaries contain definition sections and data sections
-which are mapped to definition and data containes respectively.
-
-Data in both PDBx data files and dictionaries are organized in
-data categories. In the PDBx syntax individual items or data
-identified by labels of the form '_categoryName.attributeName'.
-The terms category and attribute in PDBx jargon are analogous
-table and column in relational data model, or class and attribute
-in an object oriented data model.
-
-The DataCategory class provides base storage container for instance
-data and definition meta data.
-
-"""
-import re
-import sys
-import traceback
-
-__docformat__ = "restructuredtext en"
-__author__ = "John Westbrook"
-__email__ = "jwest@rcsb.rutgers.edu"
-__license__ = "Creative Commons Attribution 3.0 Unported"
-__version__ = "V0.01"
-
-
-class CifName(object):
-    ''' Class of utilities for CIF-style data names -
-    '''
-    def __init__(self):
-        pass
-
-    @staticmethod
-    def categoryPart(name):
-        tname = ""
-        if name.startswith("_"):
-            tname = name[1:]
-        else:
-            tname = name
-
-        i = tname.find(".")
-        if i == -1:
-            return tname
-        else:
-            return tname[:i]
-
-    @staticmethod
-    def attributePart(name):
-        i = name.find(".")
-        if i == -1:
-            return None
-        else:
-            return name[i + 1:]
-
-
-class ContainerBase(object):
-    ''' Container base class for data and definition objects.
-    '''
-    def __init__(self, name):
-        # The enclosing scope of the data container (e.g. data_/save_)
-        self.__name = name
-        # List of category names within this container -
-        self.__objNameList = []
-        # dictionary of DataCategory objects keyed by category name.
-        self.__objCatalog = {}
-        self.__type = None
-
-    def getType(self):
-        return self.__type
-
-    def setType(self, type):
-        self.__type = type
-
-    def getName(self):
-        return self.__name
-
-    def setName(self, name):
-        self.__name = name
-
-    def exists(self, name):
-        if name in self.__objCatalog:
-            return True
-        else:
-            return False
-
-    def getObj(self, name):
-        if name in self.__objCatalog:
-            return self.__objCatalog[name]
-        else:
-            return None
-
-    def getObjNameList(self):
-        return self.__objNameList
-
-    def append(self, obj):
-        """ Add the input object to the current object catalog. An existing object
-            of the same name will be overwritten.
-        """
-        if obj.getName() is not None:
-            if obj.getName() not in self.__objCatalog:
-                # self.__objNameList is keeping track of object order here --
-                self.__objNameList.append(obj.getName())
-            self.__objCatalog[obj.getName()] = obj
-
-    def replace(self, obj):
-        """ Replace an existing object with the input object
-        """
-        if ((obj.getName() is not None) and (obj.getName() in self.__objCatalog)):
-            self.__objCatalog[obj.getName()] = obj
-
-    def printIt(self, fh=sys.stdout, type="brief"):
-        fh.write("+ %s container: %30s contains %4d categories\n" %
-                 (self.getType(), self.getName(), len(self.__objNameList)))
-        for nm in self.__objNameList:
-            fh.write("--------------------------------------------\n")
-            fh.write("Data category: %s\n" % nm)
-            if type == 'brief':
-                self.__objCatalog[nm].printIt(fh)
-            else:
-                self.__objCatalog[nm].dumpIt(fh)
-
-    def rename(self, curName, newName):
-        """ Change the name of an object in place -
-        """
-        try:
-            i = self.__objNameList.index(curName)
-            self.__objNameList[i] = newName
-            self.__objCatalog[newName] = self.__objCatalog[curName]
-            self.__objCatalog[newName].setName(newName)
-            return True
-        except:
-            return False
-
-    def remove(self, curName):
-        """ Revmove object by name. Return True on success or False otherwise.
-        """
-        try:
-            if curName in self.__objCatalog:
-                del self.__objCatalog[curName]
-                i = self.__objNameList.index(curName)
-                del self.__objNameList[i]
-                return True
-            else:
-                return False
-        except:
-            pass
-
-        return False
-
-
-class DefinitionContainer(ContainerBase):
-    def __init__(self, name):
-        super(DefinitionContainer, self).__init__(name)
-        self.setType('definition')
-
-    def isCategory(self):
-        if self.exists('category'):
-            return True
-        return False
-
-    def isAttribute(self):
-        if self.exists('item'):
-            return True
-        return False
-
-    def printIt(self, fh=sys.stdout, type="brief"):
-        fh.write("Definition container: %30s contains %4d categories\n" %
-                 (self.getName(), len(self.getObjNameList())))
-        if self.isCategory():
-            fh.write("Definition type: category\n")
-        elif self.isAttribute():
-            fh.write("Definition type: item\n")
-        else:
-            fh.write("Definition type: undefined\n")
-
-        for nm in self.getObjNameList():
-            fh.write("--------------------------------------------\n")
-            fh.write("Definition category: %s\n" % nm)
-            if type == 'brief':
-                self.getObj(nm).printIt(fh)
-            else:
-                self.getObj(nm).dumpId(fh)
-
-
-class DataContainer(ContainerBase):
-    ''' Container class for DataCategory objects.
-    '''
-    def __init__(self, name):
-        super(DataContainer, self).__init__(name)
-        self.setType('data')
-        self.__globalFlag = False
-
-    def invokeDataBlockMethod(self, type, method, db):
-        self.__currentRow = 1
-        exec(method.getInline())
-
-    def setGlobal(self):
-        self.__globalFlag = True
-
-    def getGlobal(self):
-        return self.__globalFlag
-
-
-class DataCategoryBase(object):
-    """ Base object definition for a data category -
-    """
-    def __init__(self, name, attributeNameList=None, rowList=None):
-        self._name = name
-        #
-        if rowList is not None:
-            self._rowList = rowList
-        else:
-            self._rowList = []
-
-        if attributeNameList is not None:
-            self._attributeNameList = attributeNameList
-        else:
-            self._attributeNameList = []
-        #
-        # Derived class data -
-        #
-        self._catalog = {}
-        self._numAttributes = 0
-        #
-        self.__setup()
-
-    def __setup(self):
-        self._numAttributes = len(self._attributeNameList)
-        self._catalog = {}
-        for attributeName in self._attributeNameList:
-            attributeNameLC = attributeName.lower()
-            self._catalog[attributeNameLC] = attributeName
-
-    def setRowList(self, rowList):
-        self._rowList = rowList
-
-    def setAttributeNameList(self, attributeNameList):
-        self._attributeNameList = attributeNameList
-        self.__setup()
-
-    def setName(self, name):
-        self._name = name
-
-    def get(self):
-        return (self._name, self._attributeNameList, self._rowList)
-
-
-class DataCategory(DataCategoryBase):
-    """ Methods for creating, accessing, and formatting PDBx cif data categories.
-    """
-    def __init__(self, name, attributeNameList=None, rowList=None):
-        super(DataCategory, self).__init__(name, attributeNameList, rowList)
-        #
-        self.__lfh = sys.stdout
-
-        self.__currentRowIndex = 0
-        self.__currentAttribute = None
-        #
-        self.__avoidEmbeddedQuoting = False
-        #
-        # --------------------------------------------------------------------
-        # any whitespace
-        self.__wsRe = re.compile(r"\s")
-        self.__wsAndQuotesRe = re.compile(r"[\s'\"]")
-        # any newline or carriage control
-        self.__nlRe = re.compile(r"[\n\r]")
-        #
-        # single quote
-        self.__sqRe = re.compile(r"[']")
-        #
-        self.__sqWsRe = re.compile(r"('\s)|(\s')")
-
-        # double quote
-        self.__dqRe = re.compile(r'["]')
-        self.__dqWsRe = re.compile(r'("\s)|(\s")')
-        #
-        self.__intRe = re.compile(r'^[0-9]+$')
-        self.__floatRe = re.compile(
-            r'^-?(([0-9]+)[.]?|([0-9]*[.][0-9]+))([(][0-9]+[)])?([eE][+-]?[0-9]+)?$')
-        #
-        self.__dataTypeList = [
-            'DT_NULL_VALUE', 'DT_INTEGER', 'DT_FLOAT', 'DT_UNQUOTED_STRING', 'DT_ITEM_NAME',
-            'DT_DOUBLE_QUOTED_STRING', 'DT_SINGLE_QUOTED_STRING', 'DT_MULTI_LINE_STRING',
-        ]
-        self.__formatTypeList = [
-            'FT_NULL_VALUE', 'FT_NUMBER', 'FT_NUMBER', 'FT_UNQUOTED_STRING',
-            'FT_QUOTED_STRING', 'FT_QUOTED_STRING', 'FT_QUOTED_STRING', 'FT_MULTI_LINE_STRING'
-        ]
-
-    def __getitem__(self, x):
-        """ Implements list-type functionality -
-             Implements op[x] for some special cases -
-                x = integer - returns the row in category (normal list behavior)
-                x = string - returns the value of attribute 'x' in first row.
-        """
-        if isinstance(x, int):
-            # return self._rowList.__getitem__(x)
-            return self._rowList[x]
-
-        elif isinstance(x, str):
-            try:
-                # return self._rowList[0][x]
-                ii = self.getAttributeIndex(x)
-                return self._rowList[0][ii]
-            except (IndexError, KeyError):
-                raise KeyError
-        raise TypeError(x)
-
-    def getCurrentAttribute(self):
-        return self.__currentAttribute
-
-    def getRowIndex(self):
-        return self.__currentRowIndex
-
-    def getRowList(self):
-        return self._rowList
-
-    def getRowCount(self):
-        return (len(self._rowList))
-
-    def getRow(self, index):
-        try:
-            return self._rowList[index]
-        except:
-            return []
-
-    def removeRow(self, index):
-        try:
-            if ((index >= 0) and (index < len(self._rowList))):
-                del self._rowList[index]
-                if self.__currentRowIndex >= len(self._rowList):
-                    self.__currentRowIndex = len(self._rowList) - 1
-                return True
-            else:
-                pass
-        except:
-            pass
-
-        return False
-
-    def getFullRow(self, index):
-        """ Return a full row based on the length of the the attribute list.
-        """
-        try:
-            if (len(self._rowList[index]) < self._numAttributes):
-                for ii in range(self._numAttributes - len(self._rowList[index])):
-                    self._rowList[index].append('?')
-            return self._rowList[index]
-        except:
-            return ['?' for ii in range(self._numAttributes)]
-
-    def getName(self):
-        return self._name
-
-    def getAttributeList(self):
-        return self._attributeNameList
-
-    def getAttributeCount(self):
-        return len(self._attributeNameList)
-
-    def getAttributeListWithOrder(self):
-        oL = []
-        for ii, att in enumerate(self._attributeNameList):
-            oL.append((att, ii))
-        return oL
-
-    def getAttributeIndex(self, attributeName):
-        try:
-            return self._attributeNameList.index(attributeName)
-        except:
-            return -1
-
-    def hasAttribute(self, attributeName):
-        return attributeName in self._attributeNameList
-
-    def getIndex(self, attributeName):
-        try:
-            return self._attributeNameList.index(attributeName)
-        except:
-            return -1
-
-    def getItemNameList(self):
-        itemNameList = []
-        for att in self._attributeNameList:
-            itemNameList.append("_" + self._name + "." + att)
-        return itemNameList
-
-    def append(self, row):
-        # self.__lfh.write("PdbxContainer(append) category %s row %r\n" % (self._name, row))
-        self._rowList.append(row)
-
-    def appendAttribute(self, attributeName):
-        attributeNameLC = attributeName.lower()
-        if attributeNameLC in self._catalog:
-            i = self._attributeNameList.index(self._catalog[attributeNameLC])
-            self._attributeNameList[i] = attributeName
-            self._catalog[attributeNameLC] = attributeName
-            # self.__lfh.write("Appending existing attribute %s\n" % attributeName)
-        else:
-            # self.__lfh.write("Appending existing attribute %s\n" % attributeName)
-            self._attributeNameList.append(attributeName)
-            self._catalog[attributeNameLC] = attributeName
-            #
-        self._numAttributes = len(self._attributeNameList)
-
-    def appendAttributeExtendRows(self, attributeName):
-        attributeNameLC = attributeName.lower()
-        if attributeNameLC in self._catalog:
-            i = self._attributeNameList.index(self._catalog[attributeNameLC])
-            self._attributeNameList[i] = attributeName
-            self._catalog[attributeNameLC] = attributeName
-            self.__lfh.write("Appending existing attribute %s\n" % attributeName)
-        else:
-            self._attributeNameList.append(attributeName)
-            self._catalog[attributeNameLC] = attributeName
-            # add a placeholder to any existing rows for the new attribute.
-            if (len(self._rowList) > 0):
-                for row in self._rowList:
-                    row.append("?")
-            #
-        self._numAttributes = len(self._attributeNameList)
-
-    def getValue(self, attributeName=None, rowIndex=None):
-        if attributeName is None:
-            attribute = self.__currentAttribute
-        else:
-            attribute = attributeName
-        if rowIndex is None:
-            rowI = self.__currentRowIndex
-        else:
-            rowI = rowIndex
-
-        if isinstance(attribute, str) and isinstance(rowI, int):
-            try:
-                return self._rowList[rowI][self._attributeNameList.index(attribute)]
-            except (IndexError):
-                raise IndexError
-        raise IndexError(attribute)
-
-    def setValue(self, value, attributeName=None, rowIndex=None):
-        if attributeName is None:
-            attribute = self.__currentAttribute
-        else:
-            attribute = attributeName
-
-        if rowIndex is None:
-            rowI = self.__currentRowIndex
-        else:
-            rowI = rowIndex
-
-        if isinstance(attribute, str) and isinstance(rowI, int):
-            try:
-                # if row index is out of range - add the rows -
-                for ii in range(rowI + 1 - len(self._rowList)):
-                    self._rowList.append(self.__emptyRow())
-                # self._rowList[rowI][attribute] = value
-                ll = len(self._rowList[rowI])
-                ind = self._attributeNameList.index(attribute)
-
-                # extend the list if needed -
-                if (ind >= ll):
-                    self._rowList[rowI].extend([None for ii in range(2 * ind - ll)])
-                self._rowList[rowI][ind] = value
-            except (IndexError):
-                self.__lfh.write(
-                    "DataCategory(setvalue) index error category"
-                    " %s attribute %s index %d value %r\n" %
-                    (self._name, attribute, rowI, value))
-                traceback.print_exc(file=self.__lfh)
-                # raise IndexError
-            except (ValueError):
-                self.__lfh.write(
-                    "DataCategory(setvalue) value error category"
-                    " %s attribute %s index %d value %r\n" %
-                    (self._name, attribute, rowI, value))
-                traceback.print_exc(file=self.__lfh)
-                # raise ValueError
-
-    def __emptyRow(self):
-        return [None for ii in range(len(self._attributeNameList))]
-
-    def replaceValue(self, oldValue, newValue, attributeName):
-        numReplace = 0
-        if attributeName not in self._attributeNameList:
-            return numReplace
-        ind = self._attributeNameList.index(attributeName)
-        for row in self._rowList:
-            if row[ind] == oldValue:
-                row[ind] = newValue
-                numReplace += 1
-        return numReplace
-
-    def replaceSubstring(self, oldValue, newValue, attributeName):
-        ok = False
-        if attributeName not in self._attributeNameList:
-            return ok
-        ind = self._attributeNameList.index(attributeName)
-        for row in self._rowList:
-            val = row[ind]
-            row[ind] = val.replace(oldValue, newValue)
-            if val != row[ind]:
-                ok = True
-        return ok
-
-    def invokeAttributeMethod(self, attributeName, type, method, db):
-        self.__currentRowIndex = 0
-        self.__currentAttribute = attributeName
-        self.appendAttribute(attributeName)
-        # currentRowIndex = self.__currentRowIndex # assigned by never used
-        #
-        ind = self._attributeNameList.index(attributeName)
-        if len(self._rowList) == 0:
-            row = [None for ii in range(len(self._attributeNameList) * 2)]
-            row[ind] = None
-            self._rowList.append(row)
-
-        for row in self._rowList:
-            ll = len(row)
-            if (ind >= ll):
-                row.extend([None for ii in range(2 * ind - ll)])
-                row[ind] = None
-            exec(method.getInline())
-            self.__currentRowIndex += 1
-            # currentRowIndex = self.__currentRowIndex # assigned by never used
-
-    def invokeCategoryMethod(self, type, method, db):
-        self.__currentRowIndex = 0
-        exec(method.getInline())
-
-    def getAttributeLengthMaximumList(self):
-        mList = [0 for i in len(self._attributeNameList)]
-        for row in self._rowList:
-            for indx, val in enumerate(row):
-                mList[indx] = max(mList[indx], len(val))
-        return mList
-
-    def renameAttribute(self, curAttributeName, newAttributeName):
-        """ Change the name of an attribute in place -
-        """
-        try:
-            i = self._attributeNameList.index(curAttributeName)
-            self._attributeNameList[i] = newAttributeName
-            del self._catalog[curAttributeName.lower()]
-            self._catalog[newAttributeName.lower()] = newAttributeName
-            return True
-        except:
-            return False
-
-    def printIt(self, fh=sys.stdout):
-        fh.write("--------------------------------------------\n")
-        fh.write(" Category: %s attribute list length: %d\n" %
-                 (self._name, len(self._attributeNameList)))
-        for at in self._attributeNameList:
-            fh.write(" Category: %s attribute: %s\n" % (self._name, at))
-
-        fh.write(" Row value list length: %d\n" % len(self._rowList))
-        #
-        for row in self._rowList[:2]:
-            #
-            if len(row) == len(self._attributeNameList):
-                for ii, v in enumerate(row):
-                    fh.write(" %30s: %s ...\n" % (self._attributeNameList[ii], str(v)[:30]))
-            else:
-                fh.write("+WARNING - %s data length %d attribute name length %s mismatched\n" %
-                         (self._name, len(row), len(self._attributeNameList)))
-
-    def dumpIt(self, fh=sys.stdout):
-        fh.write("--------------------------------------------\n")
-        fh.write(" Category: %s attribute list length: %d\n" %
-                 (self._name, len(self._attributeNameList)))
-        for at in self._attributeNameList:
-            fh.write(" Category: %s attribute: %s\n" % (self._name, at))
-
-        fh.write(" Value list length: %d\n" % len(self._rowList))
-        for row in self._rowList:
-            for ii, v in enumerate(row):
-                fh.write(" %30s: %s\n" % (self._attributeNameList[ii], v))
-
-    def __formatPdbx(self, inp):
-        """ Format input data following PDBx quoting rules -
-        """
-        try:
-            if (inp is None):
-                return ("?", 'DT_NULL_VALUE')
-
-            # pure numerical values are returned as unquoted strings
-            if (isinstance(inp, int) or self.__intRe.search(str(inp))):
-                return ([str(inp)], 'DT_INTEGER')
-
-            if (isinstance(inp, float) or self.__floatRe.search(str(inp))):
-                return ([str(inp)], 'DT_FLOAT')
-
-            # null value handling -
-
-            if (inp == "." or inp == "?"):
-                return ([inp], 'DT_NULL_VALUE')
-
-            if (inp == ""):
-                return (["."], 'DT_NULL_VALUE')
-
-            # Contains white space or quotes ?
-            if not self.__wsAndQuotesRe.search(inp):
-                if inp.startswith("_"):
-                    return (self.__doubleQuotedList(inp), 'DT_ITEM_NAME')
-                else:
-                    return ([str(inp)], 'DT_UNQUOTED_STRING')
-            else:
-                if self.__nlRe.search(inp):
-                    return (self.__semiColonQuotedList(inp), 'DT_MULTI_LINE_STRING')
-                else:
-                    if (self.__avoidEmbeddedQuoting):
-                        # change priority to choose double quoting where possible.
-                        if not self.__dqRe.search(inp) and not self.__sqWsRe.search(inp):
-                            return (self.__doubleQuotedList(inp), 'DT_DOUBLE_QUOTED_STRING')
-                        elif not self.__sqRe.search(inp) and not self.__dqWsRe.search(inp):
-                            return (self.__singleQuotedList(inp), 'DT_SINGLE_QUOTED_STRING')
-                        else:
-                            return (self.__semiColonQuotedList(inp), 'DT_MULTI_LINE_STRING')
-                    else:
-                        # change priority to choose double quoting where possible.
-                        if not self.__dqRe.search(inp):
-                            return (self.__doubleQuotedList(inp), 'DT_DOUBLE_QUOTED_STRING')
-                        elif not self.__sqRe.search(inp):
-                            return (self.__singleQuotedList(inp), 'DT_SINGLE_QUOTED_STRING')
-                        else:
-                            return (self.__semiColonQuotedList(inp), 'DT_MULTI_LINE_STRING')
-        except:
-            traceback.print_exc(file=self.__lfh)
-
-    def __dataTypePdbx(self, inp):
-        """ Detect the PDBx data type -
-        """
-        if (inp is None):
-            return ('DT_NULL_VALUE')
-
-        # pure numerical values are returned as unquoted strings
-        if isinstance(inp, int) or self.__intRe.search(str(inp)):
-            return ('DT_INTEGER')
-
-        if isinstance(inp, float) or self.__floatRe.search(str(inp)):
-            return ('DT_FLOAT')
-
-        # null value handling -
-
-        if (inp == "." or inp == "?"):
-            return ('DT_NULL_VALUE')
-
-        if (inp == ""):
-            return ('DT_NULL_VALUE')
-
-        # Contains white space or quotes ?
-        if not self.__wsAndQuotesRe.search(inp):
-            if inp.startswith("_"):
-                return ('DT_ITEM_NAME')
-            else:
-                return ('DT_UNQUOTED_STRING')
-        else:
-            if self.__nlRe.search(inp):
-                return ('DT_MULTI_LINE_STRING')
-            else:
-                if (self.__avoidEmbeddedQuoting):
-                    if not self.__sqRe.search(inp) and not self.__dqWsRe.search(inp):
-                        return ('DT_DOUBLE_QUOTED_STRING')
-                    elif not self.__dqRe.search(inp) and not self.__sqWsRe.search(inp):
-                        return ('DT_SINGLE_QUOTED_STRING')
-                    else:
-                        return ('DT_MULTI_LINE_STRING')
-                else:
-                    if not self.__sqRe.search(inp):
-                        return ('DT_DOUBLE_QUOTED_STRING')
-                    elif not self.__dqRe.search(inp):
-                        return ('DT_SINGLE_QUOTED_STRING')
-                    else:
-                        return ('DT_MULTI_LINE_STRING')
-
-    def __singleQuotedList(self, inp):
-        l = []
-        l.append("'")
-        l.append(inp)
-        l.append("'")
-        return(l)
-
-    def __doubleQuotedList(self, inp):
-        l = []
-        l.append('"')
-        l.append(inp)
-        l.append('"')
-        return(l)
-
-    def __semiColonQuotedList(self, inp):
-        l = []
-        l.append("\n")
-        if inp[-1] == '\n':
-            l.append(";")
-            l.append(inp)
-            l.append(";")
-            l.append("\n")
-        else:
-            l.append(";")
-            l.append(inp)
-            l.append("\n")
-            l.append(";")
-            l.append("\n")
-        return(l)
-
-    def getValueFormatted(self, attributeName=None, rowIndex=None):
-        if attributeName is None:
-            attribute = self.__currentAttribute
-        else:
-            attribute = attributeName
-
-        if rowIndex is None:
-            rowI = self.__currentRowIndex
-        else:
-            rowI = rowIndex
-
-        if isinstance(attribute, str) and isinstance(rowI, int):
-            try:
-                list, type = self.__formatPdbx(
-                    self._rowList[rowI][self._attributeNameList.index(attribute)])
-                return "".join(list)
-            except (IndexError):
-                self.__lfh.write(
-                    "attributeName %s rowI %r rowdata %r\n" %
-                    (attributeName, rowI, self._rowList[rowI]))
-                raise IndexError
-        raise TypeError(attribute)
-
-    def getValueFormattedByIndex(self, attributeIndex, rowIndex):
-        try:
-            list, type = self.__formatPdbx(self._rowList[rowIndex][attributeIndex])
-            return "".join(list)
-        except (IndexError):
-            raise IndexError
-
-    def getAttributeValueMaxLengthList(self, steps=1):
-        mList = [0 for i in range(len(self._attributeNameList))]
-        for row in self._rowList[::steps]:
-            for indx in range(len(self._attributeNameList)):
-                val = row[indx]
-                mList[indx] = max(mList[indx], len(str(val)))
-        return mList
-
-    def getFormatTypeList(self, steps=1):
-        try:
-            curDataTypeList = ['DT_NULL_VALUE' for i in range(len(self._attributeNameList))]
-            for row in self._rowList[::steps]:
-                for indx in range(len(self._attributeNameList)):
-                    val = row[indx]
-                    # print "index ", indx, " val ", val
-                    dType = self.__dataTypePdbx(val)
-                    dIndx = self.__dataTypeList.index(dType)
-                    # print "d type", dType, " d type index ", dIndx
-                    cType = curDataTypeList[indx]
-                    cIndx = self.__dataTypeList.index(cType)
-                    cIndx = max(cIndx, dIndx)
-                    curDataTypeList[indx] = self.__dataTypeList[cIndx]
-
-            # Map the format types to the data types
-            curFormatTypeList = []
-            for dt in curDataTypeList:
-                ii = self.__dataTypeList.index(dt)
-                curFormatTypeList.append(self.__formatTypeList[ii])
-        except:
-            self.__lfh.write(
-                "PdbxDataCategory(getFormatTypeList) ++Index error at index %d in row %r\n" %
-                (indx, row))
-
-        return curFormatTypeList, curDataTypeList
-
-    def getFormatTypeListX(self):
-        curDataTypeList = ['DT_NULL_VALUE' for i in range(len(self._attributeNameList))]
-        for row in self._rowList:
-            for indx in range(len(self._attributeNameList)):
-                val = row[indx]
-                # print "index ", indx, " val ", val
-                dType = self.__dataTypePdbx(val)
-                dIndx = self.__dataTypeList.index(dType)
-                # print "d type", dType, " d type index ", dIndx
-
-                cType = curDataTypeList[indx]
-                cIndx = self.__dataTypeList.index(cType)
-                cIndx = max(cIndx, dIndx)
-                curDataTypeList[indx] = self.__dataTypeList[cIndx]
-
-        # Map the format types to the data types
-        curFormatTypeList = []
-        for dt in curDataTypeList:
-            ii = self.__dataTypeList.index(dt)
-            curFormatTypeList.append(self.__formatTypeList[ii])
-        return curFormatTypeList, curDataTypeList
diff --git a/pdbx/reader/reader.py b/pdbx/reader/reader.py
deleted file mode 100644
index 4aad948..0000000
--- a/pdbx/reader/reader.py
+++ /dev/null
@@ -1,470 +0,0 @@
-##
-# File: PdbxReader.py
-# Date: 2012-01-09 Jdw Adapted from PdbxParser
-#
-# Updates:
-#
-# 2012-01-09 - (jdw) Separate reader and writer classes.
-#
-# 2012-09-02 - (jdw) Revise tokenizer to better handle embedded quoting.
-#
-##
-"""
-PDBx/mmCIF dictionary and data file parser.
-
-Acknowledgements:
-
- The tokenizer used in this module is modeled after the clever parser design
- used in the PyMMLIB package.
-
- PyMMLib Development Group
- Authors: Ethan Merritt: merritt@u.washington.ed & Jay Painter: jay.painter@gmail.com
- See: http://pymmlib.sourceforge.net/
-
-"""
-import re
-from .containers import DataCategory, DefinitionContainer, DataContainer
-
-
-class PdbxError(Exception):
-    """ Class for catch general errors
-    """
-    pass
-
-
-class SyntaxError(Exception):
-    """ Class for catching syntax errors
-    """
-    def __init__(self, lineNumber, text):
-        Exception.__init__(self)
-        self.lineNumber = lineNumber
-        self.text = text
-
-    def __str__(self):
-        return "%%ERROR - [at line: %d] %s" % (self.lineNumber, self.text)
-
-
-class PdbxReader(object):
-    """ PDBx reader for data files and dictionaries.
-
-    """
-    def __init__(self, ifh):
-        """ ifh - input file handle returned by open()
-        """
-        #
-        self.__curLineNumber = 0
-        self.__ifh = ifh
-        self.__stateDict = {
-            "data": "ST_DATA_CONTAINER",
-            "loop": "ST_TABLE",
-            "global": "ST_GLOBAL_CONTAINER",
-            "save": "ST_DEFINITION",
-            "stop": "ST_STOP"
-        }
-
-    def read(self, containerList):
-        """
-        Appends to the input list of definition and data containers.
-
-        """
-        self.__curLineNumber = 0
-        try:
-            self.__parser(self.__tokenizer(self.__ifh), containerList)
-        except StopIteration:
-            pass
-        else:
-            raise PdbxError()
-
-    def __syntaxError(self, errText):
-        raise SyntaxError(self.__curLineNumber, errText)
-
-    def __getContainerName(self, inWord):
-        """ Returns the name of the data_ or save_ container
-        """
-        return str(inWord[5:]).strip()
-
-    def __getState(self, inWord):
-        """Identifies reserved syntax elements and assigns an associated state.
-
-           Returns: (reserved word, state)
-           where -
-              reserved word - is one of CIF syntax elements:
-                               data_, loop_, global_, save_, stop_
-              state - the parser state required to process this next section.
-        """
-        i = inWord.find("_")
-        if i == -1:
-            return None, "ST_UNKNOWN"
-
-        try:
-            rWord = inWord[:i].lower()
-            return rWord, self.__stateDict[rWord]
-        except:
-            return None, "ST_UNKNOWN"
-
-    def __parser(self, tokenizer, containerList):
-        """ Parser for PDBx data files and dictionaries.
-
-            Input - tokenizer() reentrant method recognizing
-                    data item names (_category.attribute)
-                    quoted strings (single, double and multi-line semi-colon delimited),
-                    and unquoted strings.
-
-                    containerList - list-type container for data and definition objects parsed
-                                     from from the input file.
-
-            Return:
-                    containerList - is appended with data and definition objects -
-        """
-        # Working container - data or definition
-        curContainer = None
-        #
-        # Working category container
-        categoryIndex = {}
-        curCategory = None
-        #
-        curRow = None
-        state = None
-
-        # Find the first reserved word and begin capturing data.
-        #
-        while True:
-            curCatName, curAttName, curQuotedString, curWord = next(tokenizer)
-            if curWord is None:
-                continue
-            reservedWord, state = self.__getState(curWord)
-            if reservedWord is not None:
-                break
-
-        while True:
-            #
-            # Set the current state -
-            #
-            # At this point in the processing cycle we are expecting a token containing
-            # either a '_category.attribute' or a reserved word.
-            #
-            if curCatName is not None:
-                state = "ST_KEY_VALUE_PAIR"
-            elif curWord is not None:
-                reservedWord, state = self.__getState(curWord)
-            else:
-                self.__syntaxError("Miscellaneous syntax error")
-                return
-
-            #
-            # Process _category.attribute value assignments
-            #
-            if state == "ST_KEY_VALUE_PAIR":
-                try:
-                    curCategory = categoryIndex[curCatName]
-                except KeyError:
-                    # A new category is encountered - create a container and add a row
-                    curCategory = categoryIndex[curCatName] = DataCategory(curCatName)
-
-                    try:
-                        curContainer.append(curCategory)
-                    except AttributeError:
-                        self.__syntaxError("Category cannot be added to data_ block")
-                        return
-
-                    curRow = []
-                    curCategory.append(curRow)
-                else:
-                    # Recover the existing row from the category
-                    try:
-                        curRow = curCategory[0]
-                    except IndexError:
-                        self.__syntaxError("Internal index error accessing category data")
-                        return
-
-                # Check for duplicate attributes and add attribute to table.
-                if curAttName in curCategory.getAttributeList():
-                    self.__syntaxError("Duplicate attribute encountered in category")
-                    return
-                else:
-                    curCategory.appendAttribute(curAttName)
-
-                # Get the data for this attribute from the next token
-                tCat, tAtt, curQuotedString, curWord = next(tokenizer)
-
-                if tCat is not None or (curQuotedString is None and curWord is None):
-                    self.__syntaxError("Missing data for item _%s.%s" % (curCatName, curAttName))
-
-                if curWord is not None:
-                    #
-                    # Validation check token for misplaced reserved words -
-                    #
-                    reservedWord, state = self.__getState(curWord)
-                    if reservedWord is not None:
-                        self.__syntaxError("Unexpected reserved word: %s" % (reservedWord))
-
-                    curRow.append(curWord)
-
-                elif curQuotedString is not None:
-                    curRow.append(curQuotedString)
-
-                else:
-                    self.__syntaxError("Missing value in item-value pair")
-
-                try:
-                    curCatName, curAttName, curQuotedString, curWord = next(tokenizer)
-                except RuntimeError as err:
-                    raise StopIteration(err)
-                continue
-
-            #
-            # Process a loop_ declaration and associated data -
-            #
-            elif state == "ST_TABLE":
-
-                # The category name in the next curCatName, curAttName pair
-                # defines the name of the category container.
-                curCatName, curAttName, curQuotedString, curWord = next(tokenizer)
-
-                if curCatName is None or curAttName is None:
-                    self.__syntaxError("Unexpected token in loop_ declaration")
-                    return
-
-                # Check for a previous category declaration.
-                if curCatName in categoryIndex:
-                    self.__syntaxError("Duplicate category declaration in loop_")
-                    return
-
-                curCategory = DataCategory(curCatName)
-
-                try:
-                    curContainer.append(curCategory)
-                except AttributeError:
-                    self.__syntaxError("loop_ declaration outside of data_ block or save_ frame")
-                    return
-
-                curCategory.appendAttribute(curAttName)
-
-                # Read the rest of the loop_ declaration
-                while True:
-                    curCatName, curAttName, curQuotedString, curWord = next(tokenizer)
-
-                    if curCatName is None:
-                        break
-
-                    if curCatName != curCategory.getName():
-                        self.__syntaxError("Changed category name in loop_ declaration")
-                        return
-
-                    curCategory.appendAttribute(curAttName)
-
-                # If the next token is a 'word', check it for any reserved words -
-                if curWord is not None:
-                    reservedWord, state = self.__getState(curWord)
-                    if reservedWord is not None:
-                        if reservedWord == "stop":
-                            return
-                        else:
-                            self.__syntaxError(
-                                "Unexpected reserved word after loop declaration: %s" %
-                                (reservedWord))
-
-                # Read the table of data for this loop_ -
-                while True:
-                    curRow = []
-                    curCategory.append(curRow)
-
-                    for tAtt in curCategory.getAttributeList():
-                        if curWord is not None:
-                            curRow.append(curWord)
-                        elif curQuotedString is not None:
-                            curRow.append(curQuotedString)
-
-                        try:
-                            curCatName, curAttName, curQuotedString, curWord = next(tokenizer)
-                        except RuntimeError as err:
-                            raise StopIteration(err)
-
-                    # loop_ data processing ends if -
-
-                    # A new _category.attribute is encountered
-                    if curCatName is not None:
-                        break
-
-                    # A reserved word is encountered
-                    if curWord is not None:
-                        reservedWord, state = self.__getState(curWord)
-                        if reservedWord is not None:
-                            break
-
-                continue
-
-            elif state == "ST_DEFINITION":
-                # Ignore trailing unnamed saveframe delimiters e.g. 'save_'
-                sName = self.__getContainerName(curWord)
-                if (len(sName) > 0):
-                    curContainer = DefinitionContainer(sName)
-                    containerList.append(curContainer)
-                    categoryIndex = {}
-                    curCategory = None
-
-                curCatName, curAttName, curQuotedString, curWord = next(tokenizer)
-
-            elif state == "ST_DATA_CONTAINER":
-                #
-                dName = self.__getContainerName(curWord)
-                if len(dName) == 0:
-                    dName = "unidentified"
-                curContainer = DataContainer(dName)
-                containerList.append(curContainer)
-                categoryIndex = {}
-                curCategory = None
-                curCatName, curAttName, curQuotedString, curWord = next(tokenizer)
-
-            elif state == "ST_STOP":
-                return
-            elif state == "ST_GLOBAL":
-                curContainer = DataContainer("blank-global")
-                curContainer.setGlobal()
-                containerList.append(curContainer)
-                categoryIndex = {}
-                curCategory = None
-                curCatName, curAttName, curQuotedString, curWord = next(tokenizer)
-
-            elif state == "ST_UNKNOWN":
-                self.__syntaxError("Unrecogized syntax element: " + str(curWord))
-                return
-
-    def __tokenizer(self, ifh):
-        """ Tokenizer method for the mmCIF syntax file -
-
-            Each return/yield from this method returns information about
-            the next token in the form of a tuple with the following structure.
-
-            (category name, attribute name, quoted strings, words w/o quotes or white space)
-
-            Differentiated the reqular expression to the better handle embedded quotes.
-
-        """
-        #
-        # Regex definition for mmCIF syntax - semi-colon delimited strings are handled
-        # outside of this regex.
-        mmcifRe = re.compile(
-            r"(?:"
-
-            "(?:_(.+?)[.](\S+))" "|"  # _category.attribute
-
-            "(?:['](.*?)(?:[']\s|[']$))" "|"  # single quoted strings
-            "(?:[\"](.*?)(?:[\"]\s|[\"]$))" "|"  # double quoted strings
-
-            "(?:\s*#.*$)" "|"  # comments (dumped)
-
-            "(\S+)"  # unquoted words
-
-            ")"
-        )
-
-        fileIter = iter(ifh)
-
-        # Tokenizer loop begins here ---
-        while True:
-            line = next(fileIter)
-            self.__curLineNumber += 1
-
-            # Dump comments
-            if line.startswith("#"):
-                continue
-
-            # Gobble up the entire semi-colon/multi-line delimited string and
-            # and stuff this into the string slot in the return tuple
-            #
-            if line.startswith(";"):
-                mlString = [line[1:]]
-                while True:
-                    line = next(fileIter)
-                    self.__curLineNumber += 1
-                    if line.startswith(";"):
-                        break
-                    mlString.append(line)
-
-                # remove trailing new-line that is part of the \n; delimiter
-                mlString[-1] = mlString[-1].rstrip()
-                #
-                yield (None, None, "".join(mlString), None)
-                #
-                # Need to process the remainder of the current line -
-                line = line[1:]
-                # continue
-
-            # Apply regex to the current line consolidate the single/double
-            # quoted within the quoted string category
-            for it in mmcifRe.finditer(line):
-                tgroups = it.groups()
-                if tgroups != (None, None, None, None, None):
-                    if tgroups[2] is not None:
-                        qs = tgroups[2]
-                    elif tgroups[3] is not None:
-                        qs = tgroups[3]
-                    else:
-                        qs = None
-                    groups = (tgroups[0], tgroups[1], qs, tgroups[4])
-                    yield groups
-
-    def __tokenizerOrg(self, ifh):
-        """ Tokenizer method for the mmCIF syntax file -
-
-            Each return/yield from this method returns information about
-            the next token in the form of a tuple with the following structure.
-
-            (category name, attribute name, quoted strings, words w/o quotes or white space)
-
-        """
-        #
-        # Regex definition for mmCIF syntax - semi-colon delimited strings are handled
-        # outside of this regex.
-        mmcifRe = re.compile(
-            r"(?:"
-
-            "(?:_(.+?)[.](\S+))" "|"  # _category.attribute
-
-            "(?:['\"](.*?)(?:['\"]\s|['\"]$))" "|"  # quoted strings
-
-            "(?:\s*#.*$)" "|"  # comments (dumped)
-
-            "(\S+)"  # unquoted words
-
-            ")"
-        )
-
-        fileIter = iter(ifh)
-
-        # Tokenizer loop begins here ---
-        while True:
-            line = next(fileIter)
-            self.__curLineNumber += 1
-
-            # Dump comments
-            if line.startswith("#"):
-                continue
-
-            # Gobble up the entire semi-colon/multi-line delimited string and
-            # and stuff this into the string slot in the return tuple
-            #
-            if line.startswith(";"):
-                mlString = [line[1:]]
-                while True:
-                    line = next(fileIter)
-                    self.__curLineNumber += 1
-                    if line.startswith(";"):
-                        break
-                    mlString.append(line)
-
-                # remove trailing new-line that is part of the \n; delimiter
-                mlString[-1] = mlString[-1].rstrip()
-                #
-                yield (None, None, "".join(mlString), None)
-                #
-                # Need to process the remainder of the current line -
-                line = line[1:]
-                # continue
-
-            # Apply regex to the current line
-            for it in mmcifRe.finditer(line):
-                groups = it.groups()
-                if groups != (None, None, None, None):
-                    yield groups
diff --git a/pdbx/writer.py b/pdbx/writer.py
new file mode 100644
index 0000000..89fde51
--- /dev/null
+++ b/pdbx/writer.py
@@ -0,0 +1,164 @@
+##
+# File: PdbxWriter.py
+# Date: 2011-10-09 Jdw Adapted from PdbxParser.py
+#
+# Updates:
+# 5-Apr-2011 jdw Using the double quote format preference
+# 23-Oct-2012 jdw update path details and reorganize.
+#
+###
+"""Classes for writing data and dictionary containers in PDBx/mmCIF format."""
+from sys import stdout
+from .containers import DefinitionContainer, DataContainer
+from .errors import PdbxError
+
+__docformat__ = "restructuredtext en"
+__author__ = "John Westbrook"
+__email__ = "jwest@rcsb.rutgers.edu"
+
+
+MAXIMUM_LINE_LENGTH = 2048
+SPACING = 2
+INDENT_DEFINITION = 3
+DO_DEFINITION_INDENT = False
+
+
+class PdbxWriter:
+    """Write PDBx data files or dictionaries.
+    Use the input container or container list.
+    """
+    def __init__(self, output_file=stdout):
+        self.__output_file = output_file
+        self.__container_list = []
+        self.__maximum_line_length = MAXIMUM_LINE_LENGTH
+        self.__spacing = SPACING
+        self.__indent_definition = INDENT_DEFINITION
+        self.__indent_space = " " * self.__indent_definition
+        self._do_definition_indent = DO_DEFINITION_INDENT
+        # Maximum number of rows checked for value length and format
+        self.__row_partition = None
+
+    def set_row_partition(self, num_rows):
+        """Maximum number of rows checked for value length and format."""
+        self.__row_partition = num_rows
+
+    def write(self, container_list):
+        """Write out a list of containers."""
+        self.__container_list = container_list
+        for container in self.__container_list:
+            self.write_container(container)
+
+    def write_container(self, container):
+        """Write out information for an individual container."""
+        indent_string = " " * self.__indent_definition
+        if isinstance(container, DefinitionContainer):
+            self.__write("save_%s\n" % container.name)
+            self._do_definition_indent = True
+            self.__write(indent_string + "#\n")
+        elif isinstance(container, DataContainer):
+            if container.get_global():
+                self.__write("global_\n")
+                self._do_definition_indent = False
+                self.__write("\n")
+            else:
+                self.__write("data_%s\n" % container.name)
+                self._do_definition_indent = False
+                self.__write("#\n")
+        for name in container.get_object_name_list():
+            obj = container.get_object(name)
+            object_list = obj.row_list
+            # Skip empty objects
+            if not object_list:
+                continue
+            # Item - value formattting
+            if len(object_list) == 1:
+                self.__write_item_value_format(obj)
+            # Table formatting
+            elif len(object_list) > 1 and obj.attribute_list:
+                self.__write_table_format(obj)
+            else:
+                raise PdbxError(
+                    "len(object_list) = %d and len(obj.attribute_list) = %d" %
+                    (len(object_list), len(obj.attribute_list)))
+            if self._do_definition_indent:
+                self.__write(indent_string + "#")
+            else:
+                self.__write("#")
+        # Add a trailing saveframe reserved word
+        if isinstance(container, DefinitionContainer):
+            self.__write("\nsave_\n")
+        self.__write("#\n")
+
+    def __write(self, string_):
+        """Write a string."""
+        self.__output_file.write(string_)
+
+    def __write_item_value_format(self, category):
+        """Write items and values for the given category."""
+        # Compute the maximum item name length within this category -
+        attribute_name_max_length = 0
+        for attribute_name in category.attribute_list:
+            attribute_name_max_length = max(
+                attribute_name_max_length, len(attribute_name))
+        item_name_max_length = (
+            self.__spacing + len(category.name)
+            + attribute_name_max_length + 2)
+        line_list = []
+        line_list.append("#\n")
+        for attribute_name, _ in category.attribute_list_with_order:
+            if self._do_definition_indent:
+                # - add indent --
+                line_list.append(self.__indent_space)
+            item_name = "_%s.%s" % (category.name, attribute_name)
+            line_list.append(item_name.ljust(item_name_max_length))
+            line_list.append(category.get_value_formatted(attribute_name, 0))
+            line_list.append("\n")
+        self.__write("".join(line_list))
+
+    def __write_table_format(self, category):
+        """Write table format data."""
+        # Write the declaration of the loop_
+        line_list = []
+        line_list.append('#\n')
+        if self._do_definition_indent:
+            line_list.append(self.__indent_space)
+        line_list.append("loop_")
+        for attribute_name in category.attribute_list:
+            line_list.append('\n')
+            if self._do_definition_indent:
+                line_list.append(self.__indent_space)
+            item_name = "_%s.%s" % (category.name, attribute_name)
+            line_list.append(item_name)
+        self.__write("".join(line_list))
+        # Write the data in tabular format
+        # For speed make the following evaluation on a portion of the table
+        if self.__row_partition is not None:
+            num_steps = max(1, category.row_count / self.__row_partition)
+        else:
+            num_steps = 1
+        format_type_list, _ = category.get_format_type_list(steps=num_steps)
+        max_length_list = category.get_max_attribute_list_length(steps=num_steps)
+        spacing = " " * self.__spacing
+        for irow in range(category.row_count):
+            line_list = []
+            line_list.append('\n')
+            if self._do_definition_indent:
+                line_list.append(self.__indent_space + " ")
+            for iattr in range(category.attribute_count):
+                format_type = format_type_list[iattr]
+                max_length = max_length_list[iattr]
+                if format_type in ('FT_UNQUOTED_STRING', 'FT_NULL_VALUE'):
+                    val = category.get_value_formatted_by_index(iattr, irow)
+                    line_list.append(val.ljust(max_length))
+                elif format_type == 'FT_NUMBER':
+                    val = category.get_value_formatted_by_index(iattr, irow)
+                    line_list.append(val.rjust(max_length))
+                elif format_type == 'FT_QUOTED_STRING':
+                    val = category.get_value_formatted_by_index(iattr, irow)
+                    line_list.append(val.ljust(max_length + 2))
+                elif format_type == "FT_MULTI_LINE_STRING":
+                    val = category.get_value_formatted_by_index(iattr, irow)
+                    line_list.append(val)
+                line_list.append(spacing)
+            self.__write("".join(line_list))
+        self.__write("\n")
diff --git a/pdbx/writer/__init__.py b/pdbx/writer/__init__.py
deleted file mode 100644
index eea5d1a..0000000
--- a/pdbx/writer/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""Python PDBx/mmCIF writer resources.
-
-All of the code in this repository is based on http://mmcif.wwpdb.org/.
-Specifically, this code is directly derived from
-http://mmcif.wwpdb.org/docs/sw-examples/python/src/pdbx.tar.gz linked from
-http://mmcif.wwpdb.org/docs/sw-examples/python/html/.
-
-See http://mmcif.wwpdb.org/docs/sw-examples/python/html/ for more information
-about this package, including examples.
-"""
-__all__ = ["writer"]
diff --git a/pdbx/writer/writer.py b/pdbx/writer/writer.py
deleted file mode 100644
index f4db730..0000000
--- a/pdbx/writer/writer.py
+++ /dev/null
@@ -1,193 +0,0 @@
-##
-# File: PdbxWriter.py
-# Date: 2011-10-09 Jdw Adapted from PdbxParser.py
-#
-# Updates:
-# 5-Apr-2011 jdw Using the double quote format preference
-# 23-Oct-2012 jdw update path details and reorganize.
-#
-###
-"""
-Classes for writing data and dictionary containers in PDBx/mmCIF format.
-
-"""
-import sys
-from pdbx.reader.containers import DefinitionContainer, DataContainer
-
-__docformat__ = "restructuredtext en"
-__author__ = "John Westbrook"
-__email__ = "jwest@rcsb.rutgers.edu"
-__license__ = "Creative Commons Attribution 3.0 Unported"
-__version__ = "V0.01"
-
-
-class PdbxError(Exception):
-    """ Class for catch general errors
-    """
-    pass
-
-
-class PdbxWriter(object):
-    """Write PDBx data files or dictionaries using the input container
-       or container list.
-    """
-    def __init__(self, ofh=sys.stdout):
-        self.__ofh = ofh
-        self.__containerList = []
-        self.__MAXIMUM_LINE_LENGTH = 2048
-        self.__SPACING = 2
-        self.__INDENT_DEFINITION = 3
-        self.__indentSpace = " " * self.__INDENT_DEFINITION
-        self.__doDefinitionIndent = False
-        # Maximum number of rows checked for value length and format
-        self.__rowPartition = None
-
-    def setRowPartition(self, numRows):
-        ''' Maximum number of rows checked for value length and format
-        '''
-        self.__rowPartition = numRows
-
-    def write(self, containerList):
-        self.__containerList = containerList
-        for container in self.__containerList:
-            self.writeContainer(container)
-
-    def writeContainer(self, container):
-        indS = " " * self.__INDENT_DEFINITION
-        if isinstance(container, DefinitionContainer):
-            self.__write("save_%s\n" % container.getName())
-            self.__doDefinitionIndent = True
-            self.__write(indS + "#\n")
-        elif isinstance(container, DataContainer):
-            if (container.getGlobal()):
-                self.__write("global_\n")
-                self.__doDefinitionIndent = False
-                self.__write("\n")
-            else:
-                self.__write("data_%s\n" % container.getName())
-                self.__doDefinitionIndent = False
-                self.__write("#\n")
-
-        for nm in container.getObjNameList():
-            obj = container.getObj(nm)
-            objL = obj.getRowList()
-
-            # Skip empty objects
-            if len(objL) == 0:
-                continue
-
-            # Item - value formattting
-            elif len(objL) == 1:
-                self.__writeItemValueFormat(obj)
-
-            # Table formatting -
-            elif len(objL) > 1 and len(obj.getAttributeList()) > 0:
-                self.__writeTableFormat(obj)
-            else:
-                raise PdbxError()
-
-            if self.__doDefinitionIndent:
-                self.__write(indS + "#")
-            else:
-                self.__write("#")
-
-        # Add a trailing saveframe reserved word
-        if isinstance(container, DefinitionContainer):
-            self.__write("\nsave_\n")
-        self.__write("#\n")
-
-    def __write(self, st):
-        self.__ofh.write(st)
-
-    def __writeItemValueFormat(self, myCategory):
-
-        # Compute the maximum item name length within this category -
-        attributeNameLengthMax = 0
-        for attributeName in myCategory.getAttributeList():
-            attributeNameLengthMax = max(attributeNameLengthMax, len(attributeName))
-        itemNameLengthMax = self.__SPACING + len(myCategory.getName()) + attributeNameLengthMax + 2
-        #
-        lineList = []
-        lineList.append("#\n")
-        for attributeName, iPos in myCategory.getAttributeListWithOrder():
-            if self.__doDefinitionIndent:
-                # - add indent --
-                lineList.append(self.__indentSpace)
-
-            itemName = "_%s.%s" % (myCategory.getName(), attributeName)
-            lineList.append(itemName.ljust(itemNameLengthMax))
-
-            lineList.append(myCategory.getValueFormatted(attributeName, 0))
-            lineList.append("\n")
-
-        self.__write("".join(lineList))
-
-    def __writeTableFormat(self, myCategory):
-
-        # Write the declaration of the loop_
-        #
-        lineList = []
-        lineList.append('#\n')
-        if self.__doDefinitionIndent:
-            lineList.append(self.__indentSpace)
-        lineList.append("loop_")
-        for attributeName in myCategory.getAttributeList():
-            lineList.append('\n')
-            if self.__doDefinitionIndent:
-                lineList.append(self.__indentSpace)
-            itemName = "_%s.%s" % (myCategory.getName(), attributeName)
-            lineList.append(itemName)
-        self.__write("".join(lineList))
-
-        #
-        # Write the data in tabular format -
-        #
-        # print myCategory.getName()
-        # print myCategory.getAttributeList()
-
-        # For speed make the following evaluation on a portion of the table
-        if self.__rowPartition is not None:
-            numSteps = max(1, myCategory.getRowCount() / self.__rowPartition)
-        else:
-            numSteps = 1
-
-        formatTypeList, dataTypeList = myCategory.getFormatTypeList(steps=numSteps)
-        maxLengthList = myCategory.getAttributeValueMaxLengthList(steps=numSteps)
-        spacing = " " * self.__SPACING
-        #
-
-        # print formatTypeList
-        # print dataTypeList
-        # print maxLengthList
-        #
-        for iRow in range(myCategory.getRowCount()):
-            lineList = []
-            lineList.append('\n')
-            if self.__doDefinitionIndent:
-                lineList.append(self.__indentSpace + " ")
-
-            for iAt in range(myCategory.getAttributeCount()):
-                formatType = formatTypeList[iAt]
-                maxLength = maxLengthList[iAt]
-
-                if (formatType == 'FT_UNQUOTED_STRING' or formatType == 'FT_NULL_VALUE'):
-                    val = myCategory.getValueFormattedByIndex(iAt, iRow)
-                    lineList.append(val.ljust(maxLength))
-
-                elif formatType == 'FT_NUMBER':
-                    val = myCategory.getValueFormattedByIndex(iAt, iRow)
-                    lineList.append(val.rjust(maxLength))
-
-                elif formatType == 'FT_QUOTED_STRING':
-                    val = myCategory.getValueFormattedByIndex(iAt, iRow)
-
-                    lineList.append(val.ljust(maxLength + 2))
-
-                elif formatType == "FT_MULTI_LINE_STRING":
-                    val = myCategory.getValueFormattedByIndex(iAt, iRow)
-                    lineList.append(val)
-
-                lineList.append(spacing)
-
-            self.__write("".join(lineList))
-        self.__write("\n")
diff --git a/setup.py b/setup.py
index b89552e..f8228a1 100644
--- a/setup.py
+++ b/setup.py
@@ -3,18 +3,18 @@ import setuptools
 
 
 with open("README.md", "r") as fh:
-    long_description = fh.read()
+    LONG_DESCRIPTION = fh.read()
 
 
 setuptools.setup(
     name="mmcif_pdbx",
-    version="0.0.1",
+    version="1.0.0",
     author="John Westbrook",
     author_email="jwest@rcsb.rutgers.edu",
     maintainer="Nathan Baker",
     maintainer_email="nathanandrewbaker@gmail.com",
     description="Python utilities for PDBx/mmCIF storage model",
-    long_description=long_description,
+    long_description=LONG_DESCRIPTION,
     long_description_content_type="text/markdown",
     url="https://github.com/Electrostatics/mmcif_pdbx",
     packages=setuptools.find_packages(),


Test patch:
diff --git a/tests/reader_test.py b/tests/reader_test.py
index fa0ca50..4307ad9 100644
--- a/tests/reader_test.py
+++ b/tests/reader_test.py
@@ -5,122 +5,38 @@
 # Version: 0.001
 #
 # Update:
-#  27-Sep-2012  jdw add test case for reading PDBx structure factor file 
+#  27-Sep-2012  jdw add test case for reading PDBx structure factor file
 #
 ##
-"""
-Test cases for reading PDBx/mmCIF data files reader class -
-
-"""
-import sys, unittest, traceback
-import sys, time, os, os.path, shutil
-
-from pdbx.reader.reader import PdbxReader
-from pdbx.reader.containers import *
-
-class readerTests(unittest.TestCase):
-    def setUp(self):
-        self.lfh=sys.stderr
-        self.verbose=False
-        self.pathPdbxDataFile     ="tests/data/1kip.cif"
-        self.pathBigPdbxDataFile  ="tests/data/1ffk.cif"
-        self.pathSFDataFile       ="tests/data/1kip-sf.cif"
-
-    def tearDown(self):
-        pass
-
-    def testReadSmallDataFile(self): 
-        """Test case -  read data file 
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            #
-            myDataList=[]
-            ifh = open(self.pathPdbxDataFile, "r")
-            pRd=PdbxReader(ifh)
-            pRd.read(myDataList)
-            ifh.close()            
-        except:
-            traceback.print_exc(file=sys.stderr)
-            self.fail()
-
-    def testReadBigDataFile(self): 
-        """Test case -  read data file 
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            #
-            myDataList=[]
-            ifh = open(self.pathBigPdbxDataFile, "r")
-            pRd=PdbxReader(ifh)
-            pRd.read(myDataList)
-            ifh.close()            
-        except:
-            traceback.print_exc(file=sys.stderr)
-            self.fail()
-
-    def testReadSFDataFile(self): 
-        """Test case -  read PDB structure factor data  file and compute statistics on f/sig(f).
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            #
-            myContainerList=[]
-            ifh = open(self.pathSFDataFile, "r")
-            pRd=PdbxReader(ifh)
-            pRd.read(myContainerList)
-            c0=myContainerList[0]
-            #
-            catObj=c0.getObj("refln")
-            if catObj is None:
-                return false
-        
-            nRows=catObj.getRowCount()
-            #
-            # Get column name index.
-            #
-            itDict={}
-            itNameList=catObj.getItemNameList()
-            for idxIt,itName in enumerate(itNameList):
-                itDict[str(itName).lower()]=idxIt
-                #
-            idf=itDict['_refln.f_meas_au']
-            idsigf=itDict['_refln.f_meas_sigma_au']
-            minR=100
-            maxR=-1
-            sumR=0
-            icount=0
-            for row in  catObj.getRowList():
-                try:
-                    f=float(row[idf])
-                    sigf=float(row[idsigf])
-                    ratio=sigf/f
-                    #self.lfh.write(" %f %f %f\n" % (f,sigf,ratio))
-                    maxR=max(maxR,ratio)
-                    minR=min(minR,ratio)
-                    sumR+=ratio
-                    icount+=1
-                except:
-                    continue
-            
-            ifh.close()
-            self.lfh.write("f/sig(f) min %f max %f avg %f count %d\n" % (minR, maxR, sumR/icount,icount))
-        except:
-            traceback.print_exc(file=sys.stderr)
-            self.fail()
-
-
-def simpleSuite():
-    suiteSelect = unittest.TestSuite()
-    suiteSelect.addTest(readerTests("testReadBigDataFile"))
-    suiteSelect.addTest(readerTests("testReadSmallDataFile"))    
-    suiteSelect.addTest(readerTests("testReadSFDataFile"))
-    return suiteSelect
-
-if __name__ == '__main__':
-    mySuite=simpleSuite()    
-    unittest.TextTestRunner(verbosity=2).run(mySuite)
-    #
+"""Test cases for reading PDBx/mmCIF data files reader class."""
+import logging
+from pathlib import Path
+import pytest
+from pdbx.reader import PdbxReader
+
+
+DATA_DIR = Path("tests/data")
+LOGGER = logging.getLogger()
+
+
+@pytest.mark.parametrize("input_cif", ["1kip.cif", "1ffk.cif"], ids=str)
+def test_data_file(input_cif):
+    """Test data file input."""
+    input_path = DATA_DIR / Path(input_cif)
+    with open(input_path, "rt") as input_file:
+        reader = PdbxReader(input_file)
+        data_list = []
+        reader.read(data_list)
+
+
+@pytest.mark.parametrize("input_cif", ["1kip-sf.cif"], ids=str)
+def test_structure_factor_file(input_cif):
+    """Test structure factor input."""
+    input_path = DATA_DIR / Path(input_cif)
+    with open(input_path, "rt") as input_file:
+        reader = PdbxReader(input_file)
+        container_list = []
+        reader.read(container_list)
+    container = container_list[0]
+    refln_object = container.get_object("refln")
+    assert refln_object is not None
diff --git a/tests/readwrite_test.py b/tests/readwrite_test.py
index 3443242..2cb085a 100644
--- a/tests/readwrite_test.py
+++ b/tests/readwrite_test.py
@@ -8,229 +8,103 @@
 #         24-Oct-2012 jdw update path details and reorganize.
 #
 ##
-"""  Various tests caess for PDBx/mmCIF data file and dictionary reader and writer. 
-"""
-
-__docformat__ = "restructuredtext en"
-__author__    = "John Westbrook"
-__email__     = "jwest@rcsb.rutgers.edu"
-__license__   = "Creative Commons Attribution 3.0 Unported"
-__version__   = "V0.01"
-
-import sys, unittest, traceback
-import sys, time, os, os.path, shutil
-
-from pdbx.reader.reader import PdbxReader
-from pdbx.writer.writer import PdbxWriter
-from pdbx.reader.containers import *
-
-
-class PdbxReadWriteTests(unittest.TestCase):
-    def setUp(self):
-        self.lfh=sys.stdout
-        self.verbose=False
-        self.pathPdbxDataFile     = "tests/data/1kip.cif"
-        self.pathOutputFile       = "testOutputDataFile.cif"
-
-    def tearDown(self):
-        pass
-
-
-    def testSimpleInitialization(self):
-        """Test case -  Simple initialization of a data category and data block
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            #
-            fn="test-simple.cif"
-            attributeNameList=['aOne','aTwo','aThree','aFour','aFive','aSix','aSeven','aEight','aNine','aTen']
-            rowList=[[1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10],
-                     [1,2,3,4,5,6,7,8,9,10] 
-                     ]
-            nameCat='myCategory'
-            #
-            #
-            curContainer=DataContainer("myblock")
-            aCat=DataCategory(nameCat,attributeNameList,rowList)
-            aCat.printIt()
-            curContainer.append(aCat)
-            curContainer.printIt()
-            #
-            myContainerList=[]
-            myContainerList.append(curContainer)
-            ofh = open(fn, "w")        
-            pdbxW=PdbxWriter(ofh)
-            pdbxW.write(myContainerList)
-            ofh.close()
-
-            myContainerList=[]            
-            ifh = open(fn, "r")
-            pRd=PdbxReader(ifh)
-            pRd.read(myContainerList)
-            ifh.close()
-            for container in myContainerList:
-                for objName in container.getObjNameList():
-                    name,aList,rList=container.getObj(objName).get()
-                    self.lfh.write("Recovered data category  %s\n" % name)
-                    self.lfh.write("Attribute list           %r\n" % repr(aList))
-                    self.lfh.write("Row list                 %r\n" % repr(rList))                                        
-        except:
-            traceback.print_exc(file=self.lfh)
-            self.fail()
-            
-        
-    def testWriteDataFile(self): 
-        """Test case -  write data file 
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            #
-            myDataList=[]
-            ofh = open("test-output.cif", "w")
-            curContainer=DataContainer("myblock")
-            aCat=DataCategory("pdbx_seqtool_mapping_ref")
-            aCat.appendAttribute("ordinal")
-            aCat.appendAttribute("entity_id")
-            aCat.appendAttribute("auth_mon_id")
-            aCat.appendAttribute("auth_mon_num")
-            aCat.appendAttribute("pdb_chain_id")
-            aCat.appendAttribute("ref_mon_id")
-            aCat.appendAttribute("ref_mon_num")                        
-            aCat.append([1,2,3,4,5,6,7])
-            aCat.append([1,2,3,4,5,6,7])
-            aCat.append([1,2,3,4,5,6,7])
-            aCat.append([1,2,3,4,5,6,7])
-            aCat.append([7,6,5,4,3,2,1])
-            aCat.printIt()            
-            curContainer.append(aCat)
-            curContainer.printIt()
-            #
-            myDataList.append(curContainer)
-            pdbxW=PdbxWriter(ofh)
-            pdbxW.write(myDataList)
-            ofh.close()
-        except:
-            traceback.print_exc(file=self.lfh)
-            self.fail()
-
-    def testUpdateDataFile(self): 
-        """Test case -  update data file 
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            # Create a initial data file --
-            #
-            myDataList=[]
-
-            curContainer=DataContainer("myblock")
-            aCat=DataCategory("pdbx_seqtool_mapping_ref")
-            aCat.appendAttribute("ordinal")
-            aCat.appendAttribute("entity_id")
-            aCat.appendAttribute("auth_mon_id")
-            aCat.appendAttribute("auth_mon_num")
-            aCat.appendAttribute("pdb_chain_id")
-            aCat.appendAttribute("ref_mon_id")
-            aCat.appendAttribute("ref_mon_num")                        
-            aCat.append([9,2,3,4,5,6,7])
-            aCat.append([10,2,3,4,5,6,7])
-            aCat.append([11,2,3,4,5,6,7])
-            aCat.append([12,2,3,4,5,6,7])
-            
-            #self.lfh.write("Assigned data category state-----------------\n")            
-            #aCat.dumpIt(fh=self.lfh)
-
-            curContainer.append(aCat)
-            myDataList.append(curContainer)
-            ofh = open("test-output-1.cif", "w")            
-            pdbxW=PdbxWriter(ofh)
-            pdbxW.write(myDataList)
-            ofh.close()
-            #
-            #
-            # Read and update the data -
-            # 
-            myDataList=[]
-            ifh = open("test-output-1.cif", "r")
-            pRd=PdbxReader(ifh)
-            pRd.read(myDataList)
-            ifh.close()
-            #
-            myBlock=myDataList[0]
-            myBlock.printIt()
-            myCat=myBlock.getObj('pdbx_seqtool_mapping_ref')
-            myCat.printIt()
-            for iRow in range(0,myCat.getRowCount()):
-                myCat.setValue('some value', 'ref_mon_id',iRow)
-                myCat.setValue(100, 'ref_mon_num',iRow)
-            ofh = open("test-output-2.cif", "w")            
-            pdbxW=PdbxWriter(ofh)
-            pdbxW.write(myDataList)
-            ofh.close()
-            #
-            
-        except:
-            traceback.print_exc(file=self.lfh)
-            self.fail()
-
-    def testReadDataFile(self): 
-        """Test case -  read data file 
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            #
-            myDataList=[]
-            ifh = open(self.pathPdbxDataFile, "r")
-            pRd=PdbxReader(ifh)
-            pRd.read(myDataList)
-            ifh.close()            
-        except:
-            traceback.print_exc(file=self.lfh)
-            self.fail()
-
-    def testReadWriteDataFile(self): 
-        """Test case -  data file read write test
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            myDataList=[]            
-            ifh = open(self.pathPdbxDataFile, "r")
-            pRd=PdbxReader(ifh)
-            pRd.read(myDataList)
-            ifh.close()            
-            
-            ofh = open(self.pathOutputFile, "w")
-            pWr=PdbxWriter(ofh)
-            pWr.write(myDataList)        
-            ofh.close()
-        except:
-            traceback.print_exc(file=self.lfh)
-            self.fail()
-
-
-def simpleSuite():
-    suiteSelect = unittest.TestSuite()
-    suiteSelect.addTest(PdbxReadWriteTests("testSimpleInitialization"))
-    suiteSelect.addTest(PdbxReadWriteTests("testUpdateDataFile"))        
-    suiteSelect.addTest(PdbxReadWriteTests("testReadWriteDataFile"))    
-    return suiteSelect
-
-
-if __name__ == '__main__':
-    #
-    mySuite=simpleSuite()      
-    unittest.TextTestRunner(verbosity=2).run(mySuite)
-    #
-
+"""Test reading, writing, and updating files."""
+import logging
+from pathlib import Path
+from pdbx.reader import PdbxReader
+from pdbx.writer import PdbxWriter
+from pdbx.reader import DataCategory, DataContainer
+
+
+_LOGGER = logging.getLogger()
+DATA_DIR = Path("tests/data")
+
+
+def test_init_write_read(tmp_path):
+    """Test initialization, writing, and reading."""
+    attribute_name_list = [
+        'aOne', 'aTwo', 'aThree', 'aFour', 'aFive', 'aSix', 'aSeven',
+        'aEight', 'aNine', 'aTen']
+    row_list = [
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]
+    category_name = 'category'
+    container = DataContainer("myblock")
+    category = DataCategory(category_name, attribute_name_list, row_list)
+    category.print_it()
+    container.append(category)
+    container.print_it()
+    container_list = [container]
+    cif_path = Path(tmp_path) / Path("test-simple.cif")
+    with open(cif_path, "wt") as output_file:
+        writer = PdbxWriter(output_file)
+        writer.write(container_list)
+    container_list = []
+    with open(cif_path, "rt") as input_file:
+        reader = PdbxReader(input_file)
+        reader.read(container_list)
+    for container in container_list:
+        for object_name in container.get_object_name_list():
+            name, attr_list, row_list = container.get_object(
+                object_name).get()
+            _LOGGER.info("Recovered data category  %s\n", name)
+            _LOGGER.info("Attribute list           %r\n", repr(attr_list))
+            _LOGGER.info("Row list                 %r\n", repr(row_list))
+
+
+def test_update_data_file(tmp_path):
+    """Test updating of a data file."""
+    container = DataContainer("myblock")
+    category = DataCategory("pdbx_seqtool_mapping_ref")
+    category.append_attribute("ordinal")
+    category.append_attribute("entity_id")
+    category.append_attribute("auth_mon_id")
+    category.append_attribute("auth_mon_num")
+    category.append_attribute("pdb_chain_id")
+    category.append_attribute("ref_mon_id")
+    category.append_attribute("ref_mon_num")
+    category.append([9, 2, 3, 4, 5, 6, 7])
+    category.append([10, 2, 3, 4, 5, 6, 7])
+    category.append([11, 2, 3, 4, 5, 6, 7])
+    category.append([12, 2, 3, 4, 5, 6, 7])
+    container.append(category)
+    data_list = [container]
+    cif_path = Path(tmp_path) / Path("test-output-1.cif")
+    with open(cif_path, "wt") as output_file:
+        writer = PdbxWriter(output_file)
+        writer.write(data_list)
+    data_list = []
+    with open(cif_path, "rt") as input_file:
+        reader = PdbxReader(input_file)
+        reader.read(data_list)
+    block = data_list[0]
+    block.print_it()
+    category = block.get_object('pdbx_seqtool_mapping_ref')
+    category.print_it()
+    for irow in range(category.row_count):
+        category.set_value('some value', 'ref_mon_id', irow)
+        category.set_value(100, 'ref_mon_num', irow)
+    cif_path = Path(tmp_path) / Path("test-output-2.cif")
+    with open(cif_path, "wt") as output_file:
+        writer = PdbxWriter(output_file)
+        writer.write(data_list)
+
+def test_read_write_data_file(tmp_path):
+    """Data file read/write test."""
+    input_path = Path(DATA_DIR) / Path("1kip.cif")
+    with open(input_path, "rt") as input_file:
+        reader = PdbxReader(input_file)
+        data_list = []
+        reader.read(data_list)
+    output_path = Path(tmp_path) / Path("testOutputDataFile.cif")
+    with open(output_path, "wt") as output_path:
+        writer = PdbxWriter(output_path)
+        writer.write(data_list)
diff --git a/tests/writer_test.py b/tests/writer_test.py
index 1919257..b2c82d1 100644
--- a/tests/writer_test.py
+++ b/tests/writer_test.py
@@ -8,148 +8,34 @@
 #  5-Apr-2011 jdw   Using the double quote format preference
 # 24-Oct-2012 jdw   Update path and examples.
 ##
-"""
-Test implementing PDBx/mmCIF write and formatting operations.
-
-"""
-__docformat__ = "restructuredtext en"
-__author__    = "John Westbrook"
-__email__     = "jwest@rcsb.rutgers.edu"
-__license__   = "Creative Commons Attribution 3.0 Unported"
-__version__   = "V0.01"
-
-
-
-import sys, unittest, traceback
-import sys, time, os, os.path, shutil
-
-from pdbx.reader.reader  import PdbxReader
-from pdbx.writer.writer  import PdbxWriter
-from pdbx.reader.containers import *
-
-class writerTests(unittest.TestCase):
-    def setUp(self):
-        self.lfh=sys.stderr
-        self.verbose=False
-        self.pathPdbxDataFile     ="tests/data/1kip.cif"
-        self.pathOutputFile       ="testOutputDataFile.cif"
-
-    def tearDown(self):
-        pass
-
-    def testWriteDataFile(self): 
-        """Test case -  write data file 
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            #
-            myDataList=[]
-            ofh = open("test-output.cif", "w")
-            curContainer=DataContainer("myblock")
-            aCat=DataCategory("pdbx_seqtool_mapping_ref")
-            aCat.appendAttribute("ordinal")
-            aCat.appendAttribute("entity_id")
-            aCat.appendAttribute("auth_mon_id")
-            aCat.appendAttribute("auth_mon_num")
-            aCat.appendAttribute("pdb_chain_id")
-            aCat.appendAttribute("ref_mon_id")
-            aCat.appendAttribute("ref_mon_num")                        
-            aCat.append((1,2,3,4,5,6,7))
-            aCat.append((1,2,3,4,5,6,7))
-            aCat.append((1,2,3,4,5,6,7))
-            aCat.append((1,2,3,4,5,6,7))
-            curContainer.append(aCat)
-            myDataList.append(curContainer)
-            pdbxW=PdbxWriter(ofh)
-            pdbxW.write(myDataList)
-            ofh.close()
-        except:
-            traceback.print_exc(file=sys.stderr)
-            self.fail()
-
-    def testUpdateDataFile(self): 
-        """Test case -  write data file 
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        try:
-            # Create a initial data file --
-            #
-            myDataList=[]
-            ofh = open("test-output-1.cif", "w")
-            curContainer=DataContainer("myblock")
-            aCat=DataCategory("pdbx_seqtool_mapping_ref")
-            aCat.appendAttribute("ordinal")
-            aCat.appendAttribute("entity_id")
-            aCat.appendAttribute("auth_mon_id")
-            aCat.appendAttribute("auth_mon_num")
-            aCat.appendAttribute("pdb_chain_id")
-            aCat.appendAttribute("ref_mon_id")
-            aCat.appendAttribute("ref_mon_num")                        
-            aCat.append((1,2,3,4,5,6,7))
-            aCat.append((1,2,3,4,5,6,7))
-            aCat.append((1,2,3,4,5,6,7))
-            aCat.append((1,2,3,4,5,6,7))
-            curContainer.append(aCat)
-            myDataList.append(curContainer)
-            pdbxW=PdbxWriter(ofh)
-            pdbxW.write(myDataList)
-            ofh.close()
-            #
-            # Read and update the data -
-            # 
-            myDataList=[]
-            ifh = open("test-output-1.cif", "r")
-            pRd=PdbxReader(ifh)
-            pRd.read(myDataList)
-            ifh.close()
-            #
-            myBlock=myDataList[0]
-            myBlock.printIt()
-            myCat=myBlock.getObj('pdbx_seqtool_mapping_ref')
-            myCat.printIt()
-            for iRow in range(0,myCat.getRowCount()):
-                myCat.setValue('some value', 'ref_mon_id',iRow)
-                myCat.setValue(100, 'ref_mon_num',iRow)
-            ofh = open("test-output-2.cif", "w")            
-            pdbxW=PdbxWriter(ofh)
-            pdbxW.write(myDataList)
-            ofh.close()            
-            
-        except:
-            traceback.print_exc(file=sys.stderr)
-            self.fail()
-
-    def testReadDataFile(self): 
-        """Test case -  read data file 
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        myDataList=[]
-        ifh = open(self.pathPdbxDataFile, "r")
-        pRd=PdbxReader(ifh)
-        pRd.read(myDataList)
-        ifh.close()            
-
-    def testReadWriteDataFile(self): 
-        """Test case -  data file read write test
-        """
-        self.lfh.write("\nStarting %s %s\n" % (self.__class__.__name__,
-                                               sys._getframe().f_code.co_name))
-        myDataList=[]
-        ifh = open(self.pathPdbxDataFile, "r")            
-        pRd=PdbxReader(ifh)
-        pRd.read(myDataList)
-        ifh.close()            
-        
-        ofh = open(self.pathOutputFile, "w")
-        pWr=PdbxWriter(ofh)
-        pWr.write(myDataList)        
-        ofh.close()
-
-def suite():
-    return unittest.makeSuite(writerTests,'test')
-
-if __name__ == '__main__':
-    unittest.main()
+"""Test PDBx/mmCIF write and formatting operations."""
+import logging
+from pathlib import Path
+from pdbx.containers import DataContainer, DataCategory
+from pdbx.writer import PdbxWriter
+
+
+_LOGGER = logging.getLogger()
+
+
+def test_write_data_file(tmp_path):
+    """Test case -  write data file."""
+    output_path = Path(tmp_path) / Path("test-output.cif")
+    category = DataCategory("pdbx_seqtool_mapping_ref")
+    category.append_attribute("ordinal")
+    category.append_attribute("entity_id")
+    category.append_attribute("auth_mon_id")
+    category.append_attribute("auth_mon_num")
+    category.append_attribute("pdb_chain_id")
+    category.append_attribute("ref_mon_id")
+    category.append_attribute("ref_mon_num")
+    category.append((1, 2, 3, 4, 5, 6, 7))
+    category.append((1, 2, 3, 4, 5, 6, 7))
+    category.append((1, 2, 3, 4, 5, 6, 7))
+    category.append((1, 2, 3, 4, 5, 6, 7))
+    container = DataContainer("myblock")
+    container.append(category)
+    data_list = [container]
+    with open(output_path, "wt") as output_file:
+        writer = PdbxWriter(output_file)
+        writer.write(data_list)