Repository: sciunto-org/python-bibtexparser
Pull request #165 (2017-05-25 00:03:57)

Feat/better strings
Adds strings and string expression representations as detailed in issue #98.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11679866/badge)](https://coveralls.io/builds/11679866)

Coverage increased (+0.6%) to 96.447% when pulling **3342cbdd892b99e5b10d61596c0ecd70b0c6aa7f on omangin:feat/better-strings** into **8679713d01972ef173b88657395456e0fe3f2c95 on sciunto-org:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11679866/badge)](https://coveralls.io/builds/11679866)

Coverage increased (+0.6%) to 96.447% when pulling **3342cbdd892b99e5b10d61596c0ecd70b0c6aa7f on omangin:feat/better-strings** into **8679713d01972ef173b88657395456e0fe3f2c95 on sciunto-org:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11679866/badge)](https://coveralls.io/builds/11679866)

Coverage increased (+0.6%) to 96.447% when pulling **3342cbdd892b99e5b10d61596c0ecd70b0c6aa7f on omangin:feat/better-strings** into **8679713d01972ef173b88657395456e0fe3f2c95 on sciunto-org:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11679866/badge)](https://coveralls.io/builds/11679866)

Coverage increased (+0.6%) to 96.447% when pulling **3342cbdd892b99e5b10d61596c0ecd70b0c6aa7f on omangin:feat/better-strings** into **8679713d01972ef173b88657395456e0fe3f2c95 on sciunto-org:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11679866/badge)](https://coveralls.io/builds/11679866)

Coverage increased (+0.6%) to 96.447% when pulling **3342cbdd892b99e5b10d61596c0ecd70b0c6aa7f on omangin:feat/better-strings** into **8679713d01972ef173b88657395456e0fe3f2c95 on sciunto-org:master**.

omangin: One test is failing because of the outdated pypy3 version. Should be fixed after merging #164.
sciunto: You can rebase, #164 has been merged.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/11686575/badge)](https://coveralls.io/builds/11686575)

Coverage increased (+0.6%) to 96.447% when pulling **79022ab2c5455f474e4ad10605886e0496eabc17 on omangin:feat/better-strings** into **19051fdaeb3eea869aef1f7534d0a678f12f1b8c on sciunto-org:master**.

sciunto: I'll try to have a look soon...
sciunto: @Phyks Do you have time to have a look. I'm running out of time and energy these days... :(
Phyks: Sure, I'll try to have a look by the end of the week.
Phyks: @sciunto I gave a look at the PR, and it looks good. At least, I did not see anything making me scream. However, I should emphasize that I am highly unfamiliar with Bibtex strings (have never used them previously) and may have missed something :/
sciunto: If it looks good to both of you, feel free to merge. I'm not going to have brain time for that, for the next couple of months. ;)

Patch:
diff --git a/bibtexparser/bibdatabase.py b/bibtexparser/bibdatabase.py
index 79da290..6004de5 100644
--- a/bibtexparser/bibdatabase.py
+++ b/bibtexparser/bibdatabase.py
@@ -23,26 +23,31 @@ STANDARD_TYPES = set([
     'proceedings',
     'techreport',
     'unpublished'])
-COMMON_STRINGS = {
-    'jan': 'January',
-    'feb': 'February',
-    'mar': 'March',
-    'apr': 'April',
-    'may': 'May',
-    'jun': 'June',
-    'jul': 'July',
-    'aug': 'August',
-    'sep': 'September',
-    'oct': 'October',
-    'nov': 'November',
-    'dec': 'December',
-    }
+COMMON_STRINGS = OrderedDict([
+    ('jan', 'January'),
+    ('feb', 'February'),
+    ('mar', 'March'),
+    ('apr', 'April'),
+    ('may', 'May'),
+    ('jun', 'June'),
+    ('jul', 'July'),
+    ('aug', 'August'),
+    ('sep', 'September'),
+    ('oct', 'October'),
+    ('nov', 'November'),
+    ('dec', 'December'),
+    ])
+
+
+class UndefinedString(KeyError):
+    pass
 
 
 class BibDatabase(object):
     """
     Bibliographic database object that follows the data structure of a BibTeX file.
     """
+
     def __init__(self):
         #: List of BibTeX entries, for example `@book{...}`, `@article{...}`, etc. Each entry is a simple dict with
         #: BibTeX field-value pairs, for example `'author': 'Bird, R.B. and Armstrong, R.C. and Hassager, O.'` Each
@@ -93,16 +98,17 @@ class BibDatabase(object):
 
     def expand_string(self, name):
         try:
-            return self.strings[name]
+            return BibDataStringExpression.expand_if_expression(
+                self.strings[name])
         except KeyError:
-            raise(KeyError("Unknown string: {}.".format(name)))
+            raise(UndefinedString(name))
 
 
 class BibDataString(object):
     """
     Represents a bibtex string.
 
-    This object enables mainting string expressions as list of strings
+    This object enables maintaining string expressions as list of strings
     and BibDataString. Can be interpolated from Bibdatabase.
     """
 
@@ -110,6 +116,9 @@ class BibDataString(object):
         self._bibdatabase = bibdatabase
         self.name = name.lower()
 
+    def __eq__(self, other):
+        return isinstance(other, BibDataString) and self.name == other.name
+
     def __repr__(self):
         return "BibDataString({})".format(self.name.__repr__())
 
@@ -120,3 +129,97 @@ class BibDataString(object):
         :returns: string
         """
         return self._bibdatabase.expand_string(self.name)
+
+    def get_dependencies(self, known_dependencies=set()):
+        """Recursively tracks strings on which the expression depends.
+
+        :param kown_dependencies: dependencies to ignore
+        """
+        raise NotImplementedError
+
+    @staticmethod
+    def expand_string(string_or_bibdatastring):
+        """
+        Eventually replaces a bibdatastring by its value.
+
+        :param string_or_bibdatastring: the parsed token
+        :type string_expr: string or BibDataString
+        :returns: string
+        """
+        if isinstance(string_or_bibdatastring, BibDataString):
+            return string_or_bibdatastring.get_value()
+        else:
+            return string_or_bibdatastring
+
+
+class BibDataStringExpression(object):
+    """
+    Represents a bibtex string expression.
+
+    String expressions are sequences of regular strings and bibtex strings.
+    This object enables maintaining string expressions as list of strings.
+    The expression are represented as lists of regular strings and
+    BibDataStrings. They can be interpolated from Bibdatabase.
+
+    BibDataStringExpression(e)
+
+    :param e: list of strings and BibDataStrings
+    """
+
+    def __init__(self, expression):
+        self.expr = expression
+
+    def __eq__(self, other):
+        return isinstance(other, BibDataStringExpression) and self.expr == other.expr
+
+    def __repr__(self):
+        return "BibDataStringExpression({})".format(self.expr.__repr__())
+
+    def get_value(self):
+        """
+        Replaces bibdatastrings by their values in the expression.
+
+        :returns: string
+        """
+        return ''.join([BibDataString.expand_string(s) for s in self.expr])
+
+    def apply_on_strings(self, fun):
+        """
+        Maps a function on strings in expression, keeping unchanged
+        BibDataStrings.
+
+        :param fun: function from strings to strings
+        """
+        self.expr = [s if isinstance(s, BibDataString) else fun(s)
+                     for s in self.expr]
+
+    @staticmethod
+    def expand_if_expression(string_or_expression):
+        """
+        Eventually replaces a BibDataStringExpression by its value.
+
+        :param string_or_expression: the object to expand
+        :type string_expr: string or BibDataStringExpression
+        :returns: string
+        """
+        if isinstance(string_or_expression, BibDataStringExpression):
+            return string_or_expression.get_value()
+        else:
+            return string_or_expression
+
+    @staticmethod
+    def expression_if_needed(tokens):
+        """Build expression only if tokens are not a regular value.
+        """
+        if len(tokens) == 1 and not isinstance(tokens[0], BibDataString):
+            return tokens[0]
+        else:
+            return BibDataStringExpression(tokens)
+
+
+def as_text(text_string_or_expression):
+    if isinstance(text_string_or_expression,
+                  (BibDataString, BibDataStringExpression)):
+        return text_string_or_expression.get_value()
+    else:
+        return TEXT_TYPE(text_string_or_expression)
diff --git a/bibtexparser/bibtexexpression.py b/bibtexparser/bibtexexpression.py
index 857ad7c..28747dd 100644
--- a/bibtexparser/bibtexexpression.py
+++ b/bibtexparser/bibtexexpression.py
@@ -1,9 +1,11 @@
 import pyparsing as pp
 
+from .bibdatabase import BibDataStringExpression
+
 
 # General helpers
 
-def strip_after_new_lines(s):
+def _strip_after_new_lines(s):
     """Removes leading and trailing whitespaces in all but first line."""
     lines = s.splitlines()
     if len(lines) > 1:
@@ -11,6 +13,18 @@ def strip_after_new_lines(s):
     return '\n'.join(lines)
 
 
+def strip_after_new_lines(s):
+    """Removes leading and trailing whitespaces in all but first line.
+
+    :param s: string or BibDataStringExpression
+    """
+    if isinstance(s, BibDataStringExpression):
+        s.apply_on_strings(_strip_after_new_lines)
+        return s
+    else:
+        return _strip_after_new_lines(s)
+
+
 def add_logger_parse_action(expr, log_func):
     """Register a callback on expression parsing with the adequate message."""
     def action(s, l, t):
@@ -50,8 +64,6 @@ def field_to_pair(s, l, t):
     :returns: (name, value).
     """
     f = t.get('Field')
-    # Not sure it is desirable here to strip but it is for conformance
-    # to previous implementation
     return (f.get('FieldName'),
             strip_after_new_lines(f.get('Value')))
 
diff --git a/bibtexparser/bparser.py b/bibtexparser/bparser.py
index 98557b7..2019d0a 100644
--- a/bibtexparser/bparser.py
+++ b/bibtexparser/bparser.py
@@ -11,7 +11,8 @@ import sys
 import io
 import logging
 
-from bibtexparser.bibdatabase import BibDatabase, BibDataString, STANDARD_TYPES
+from bibtexparser.bibdatabase import (BibDatabase, BibDataString, as_text,
+                                      BibDataStringExpression, STANDARD_TYPES)
 from bibtexparser.bibtexexpression import BibtexExpression
 
 logger = logging.getLogger(__name__)
@@ -65,6 +66,7 @@ class BibTexParser(object):
                  customization=None,
                  ignore_nonstandard_types=True,
                  homogenize_fields=False,
+                 interpolate_strings=True,
                  common_strings=False):
         """
         Creates a parser for rading BibTeX files
@@ -95,6 +97,9 @@ class BibTexParser(object):
         #: Default: `False`.
         self.homogenize_fields = homogenize_fields
 
+        #: Interpolate Bibtex Strings or keep the structure
+        self.interpolate_strings = interpolate_strings
+
         # On some sample data files, the character encoding detection simply
         # hangs We are going to default to utf8, and mandate it.
         self.encoding = 'utf8'
@@ -157,9 +162,14 @@ class BibTexParser(object):
         self._expr.set_string_name_parse_action(
             lambda s, l, t:
                 BibDataString(self.bib_database, t[0]))
+        if self.interpolate_strings:
+            maybe_interpolate = lambda expr: as_text(expr)
+        else:
+            maybe_interpolate = lambda expr: expr
         self._expr.set_string_expression_parse_action(
             lambda s, l, t:
-                self._interpolate_string_expression(t))
+                maybe_interpolate(
+                    BibDataStringExpression.expression_if_needed(t)))
 
         # Add notice to logger
         self._expr.add_log_function(logger.debug)
@@ -282,31 +292,9 @@ class BibTexParser(object):
         if string_key in self.bib_database.strings:
             logger.warning('Overwritting existing string for key: %s.',
                            string_key)
-        logger.debug('Store string: {} -> {}'.format(string_key, string))
+        logger.debug(u'Store string: {} -> {}'.format(string_key, string))
         self.bib_database.strings[string_key] = self._clean_val(string)
 
-    def _interpolate_string_expression(self, string_expr):
-        """
-        Replaces bibdatastrings by their values in an expression.
-
-        :param string_expr: the parsed string as a list
-        :type string_expr: list
-        """
-        return ''.join([self._expand_string(s) for s in string_expr])
-
-    def _expand_string(self, string_or_bibdatastring):
-        """
-        Eventually replaces a bibdatastring by its value.
-
-        :param string_or_bibdatastring: the parsed token
-        :type string_expr: string or BibDataString
-        :returns: string
-        """
-        if isinstance(string_or_bibdatastring, BibDataString):
-            return string_or_bibdatastring.get_value()
-        else:
-            return string_or_bibdatastring
-
     def _add_preamble(self, preamble):
         """
         Stores a preamble.
diff --git a/bibtexparser/bwriter.py b/bibtexparser/bwriter.py
index 365f977..428d116 100644
--- a/bibtexparser/bwriter.py
+++ b/bibtexparser/bwriter.py
@@ -3,8 +3,12 @@
 # Author: Francois Boulogne
 # License:
 
+
 import logging
-from bibtexparser.bibdatabase import BibDatabase
+from bibtexparser.bibdatabase import (BibDatabase, COMMON_STRINGS,
+                                      BibDataString,
+                                      BibDataStringExpression)
+
 
 logger = logging.getLogger(__name__)
 
@@ -18,6 +22,15 @@ def to_bibtex(parsed):
     return BibTexWriter().write(parsed)
 
 
+def _str_or_expr_to_bibtex(e):
+    if isinstance(e, BibDataStringExpression):
+        return ' # '.join([_str_or_expr_to_bibtex(s) for s in e.expr])
+    elif isinstance(e, BibDataString):
+        return e.name
+    else:
+        return '{' + e + '}'
+
+
 class BibTexWriter(object):
     """
     Writer to convert a :class:`BibDatabase` object to a string or file formatted as a BibTeX file.
@@ -38,7 +51,7 @@ class BibTexWriter(object):
 
     _valid_contents = ['entries', 'comments', 'preambles', 'strings']
 
-    def __init__(self):
+    def __init__(self, write_common_strings=False):
         #: List of BibTeX elements to write, valid values are `entries`, `comments`, `preambles`, `strings`.
         self.contents = ['comments', 'preambles', 'strings', 'entries']
         #: Character(s) for indenting BibTeX field-value pairs. Default: single space.
@@ -57,10 +70,10 @@ class BibTexWriter(object):
         #: (common in functional languages), use this to enable
         #: comma first syntax as the bwritter output
         self.comma_first = False
-
         #: internal variable used if self.align_values = True
         self._max_field_width = 0
-
+        #: Whether common strings are written
+        self.common_strings = write_common_strings
 
     def write(self, bib_database):
         """
@@ -107,16 +120,19 @@ class BibTexWriter(object):
         # first those keys which are both in self.display_order and in entry.keys
         display_order = [i for i in self.display_order if i in entry]
         # then all the other fields sorted alphabetically
-        more_fields = [i for i in sorted(entry) if i not in self.display_order]
         display_order += [i for i in sorted(entry) if i not in self.display_order]
-
+        if self.comma_first:
+            field_fmt = u"\n{indent}, {field:<{field_max_w}} = {value}"
+        else:
+            field_fmt = u",\n{indent}{field:<{field_max_w}} = {value}"
         # Write field = value lines
         for field in [i for i in display_order if i not in ['ENTRYTYPE', 'ID']]:
             try:
-                if self.comma_first:
-                    bibtex += "\n" + self.indent + ", " + "{0:<{1}}".format(field, self._max_field_width) + " = {" + entry[field] + "}"
-                else:
-                    bibtex += ",\n" + self.indent + "{0:<{1}}".format(field, self._max_field_width) + " = {" + entry[field] + "}"
+                bibtex += field_fmt.format(
+                    indent=self.indent,
+                    field=field,
+                    field_max_w=self._max_field_width,
+                    value=_str_or_expr_to_bibtex(entry[field]))
             except TypeError:
                 raise TypeError(u"The field %s in entry %s must be a string"
                                 % (field, entry['ID']))
@@ -132,5 +148,13 @@ class BibTexWriter(object):
                         for preamble in bib_database.preambles])
 
     def _strings_to_bibtex(self, bib_database):
-        return ''.join(['@string{{{0} = "{1}"}}\n{2}'.format(name, value, self.entry_separator)
-                        for name, value in bib_database.strings.items()])
+        return ''.join([
+            u'@string{{{name} = {value}}}\n{sep}'.format(
+                name=name,
+                value=_str_or_expr_to_bibtex(value),
+                sep=self.entry_separator)
+            for name, value in bib_database.strings.items()
+            if (self.common_strings or
+                name not in COMMON_STRINGS or  # user defined string
+                value != COMMON_STRINGS[name]  # string has been updated
+                )])
diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst
index 1bd9a76..e93ecf3 100644
--- a/docs/source/tutorial.rst
+++ b/docs/source/tutorial.rst
@@ -338,3 +338,50 @@ They are sometimes coded like this ``\'{e}`` but this is not the correct way, ``
 .. Note::
 
     If you want to mix different customization functions, you can write your own function.
+
+
+Using bibtex strings
+--------------------
+
+.. Warning:: support for bibtex strings representation is still an experimental feature; the way strings are represented is likely to change in future releases.
+
+Bibtex strings and string expressions are expanded by default into the value they represent.
+This behavior is controlled by the ``interpolate_string`` argument of the BibTexParser. It defaults to ``True`` but can be set to ``False``, in which case bibtex strings and string expressions from input files are represented with the :class:`bibdatabase.BibDataString` and :class:`bibdatabase.BibDataStringExpression` from the :mod:`bibdatabase` module. Both classes retain the intrinsic structure of the string or expression so that they can be written to a new file, the same way. Each instance provides a :func:`get_value` method to interpolate the string or expression and the module also provide an :func:`bibdatabase.as_text` helper to expand a string or an expression when needed.
+
+Using the code would yield the following output.
+
+.. code-block:: python
+
+    from bibtexparser.bparser import BibTexParser
+    from bibtexparser.bibdatabase import as_text
+
+
+    bibtex = """@STRING{ jean = "Jean"}
+    
+    @ARTICLE{Cesar2013,
+      author = jean # { César},
+      title = {An amazing title},
+      year = {2013},
+      month = jan,
+      volume = {12},
+      pages = {12--23},
+      journal = {Nice Journal},
+    }
+    """
+
+    bp = BibTexParser(interpolate_strings=False)
+    bib_database = bp.parse(bibtex)
+    bib_database.entries[0]
+    as_text(bd.entries[0]['author'])
+
+.. code-block:: python
+
+    {'ENTRYTYPE': 'article',
+     'ID': 'Cesar2013',
+     'author': BibDataStringExpression([BibDataString('jean'), ' César']),
+     'journal': 'Nice Journal',
+     'month': BibDataStringExpression([BibDataString('jan')]),
+     'pages': '12--23',
+     'title': 'An amazing title',
+     }
+    'Jean César'


Test patch:
diff --git a/bibtexparser/tests/data/article_with_strings.bib b/bibtexparser/tests/data/article_with_strings.bib
new file mode 100644
index 0000000..744163d
--- /dev/null
+++ b/bibtexparser/tests/data/article_with_strings.bib
@@ -0,0 +1,15 @@
+@STRING{ nice_journal = "Nice Journal" }
+@STRING ( jean={Jean} )
+@STRING{cesar = {César}}
+
+@ARTICLE{Cesar2013,
+  author = jean # " " # cesar,
+  title = {An amazing title},
+  year = {2013},
+  month = jan,
+  volume = {12},
+  pages = {12-23},
+  journal = nice_journal,
+  comments = {A comment},
+  keyword = {keyword1, keyword2},
+}
diff --git a/bibtexparser/tests/data/article_with_strings_output.bib b/bibtexparser/tests/data/article_with_strings_output.bib
new file mode 100644
index 0000000..0c70dbf
--- /dev/null
+++ b/bibtexparser/tests/data/article_with_strings_output.bib
@@ -0,0 +1,18 @@
+@string{nice_journal = {Nice Journal}}
+
+@string{jean = {Jean}}
+
+@string{cesar = {César}}
+
+@article{Cesar2013,
+ author = jean # { } # cesar,
+ comments = {A comment},
+ journal = nice_journal,
+ keyword = {keyword1, keyword2},
+ month = jan,
+ pages = {12-23},
+ title = {An amazing title},
+ volume = {12},
+ year = {2013}
+}
+
diff --git a/bibtexparser/tests/data/common_strings.bib b/bibtexparser/tests/data/common_strings.bib
new file mode 100644
index 0000000..a0c386d
--- /dev/null
+++ b/bibtexparser/tests/data/common_strings.bib
@@ -0,0 +1,24 @@
+@string{jan = {January}}
+
+@string{feb = {February}}
+
+@string{mar = {March}}
+
+@string{apr = {April}}
+
+@string{may = {May}}
+
+@string{jun = {June}}
+
+@string{jul = {July}}
+
+@string{aug = {August}}
+
+@string{sep = {September}}
+
+@string{oct = {October}}
+
+@string{nov = {November}}
+
+@string{dec = {December}}
+
diff --git a/bibtexparser/tests/test_bibdatabase.py b/bibtexparser/tests/test_bibdatabase.py
index c4296da..964109b 100644
--- a/bibtexparser/tests/test_bibdatabase.py
+++ b/bibtexparser/tests/test_bibdatabase.py
@@ -1,5 +1,6 @@
 import unittest
-from bibtexparser.bibdatabase import BibDatabase
+from bibtexparser.bibdatabase import (BibDatabase, BibDataString,
+                                      BibDataStringExpression)
 
 
 class TestBibDatabase(unittest.TestCase):
@@ -11,7 +12,7 @@ class TestBibDatabase(unittest.TestCase):
                 'volume': '1',
                 'title': 'Dynamics of Polymeric Liquid',
                 'author': 'Bird, R.B. and Armstrong, R.C. and Hassager, O.'
-               }]
+                }]
 
     def test_entries_list_method(self):
         bib_db = BibDatabase()
@@ -24,5 +25,77 @@ class TestBibDatabase(unittest.TestCase):
         self.assertEqual(bib_db.entries_dict, bib_db.get_entry_dict())
 
 
+class TestBibDataString(unittest.TestCase):
+
+    def setUp(self):
+        self.bd = BibDatabase()
+
+    def test_name_is_lower(self):
+        bds = BibDataString(self.bd, 'nAmE')
+        self.assertTrue(bds.name.islower())
+
+    def test_raises_KeyError(self):
+        bds = BibDataString(self.bd, 'name')
+        with self.assertRaises(KeyError):
+            bds.get_value()
+
+    def test_get_value(self):
+        bds = BibDataString(self.bd, 'name')
+        self.bd.strings['name'] = 'value'
+        self.assertEqual(bds.get_value(), 'value')
+
+    def test_expand_string(self):
+        bds = BibDataString(self.bd, 'name')
+        self.bd.strings['name'] = 'value'
+        self.assertEqual(BibDataString.expand_string('name'), 'name')
+        self.assertEqual(BibDataString.expand_string(bds), 'value')
+
+    def test_get_value_string_is_defined_by_expression(self):
+        self.bd.strings['name'] = 'string'
+        exp = BibDataStringExpression(['this is a ',
+                                       BibDataString(self.bd, 'name')])
+        self.bd.strings['exp'] = exp
+        bds = BibDataString(self.bd, 'exp')
+        self.assertEqual(bds.get_value(), 'this is a string')
+
+    def test_strings_are_equal_iif_name_is_equal(self):
+        self.bd.strings['a'] = 'foo'
+        self.bd.strings['b'] = 'foo'
+        a1 = BibDataString(self.bd, 'a')
+        a2 = BibDataString(self.bd, 'a')
+        b = BibDataString(self.bd, 'b')
+        self.assertEqual(a1, a2)
+        self.assertNotEqual(a1, b)
+        self.assertNotEqual(a1, b)
+        self.assertNotEqual(a1, "foo")
+
+
+class TestBibDataStringExpression(unittest.TestCase):
+
+    def setUp(self):
+        self.bd = BibDatabase()
+        self.bd.strings['name'] = 'value'
+        self.bds = BibDataString(self.bd, 'name')
+
+    def test_get_value(self):
+        exp = BibDataStringExpression(
+            ["The string has value: ", self.bds, '.'])
+        self.assertEqual(exp.get_value(), 'The string has value: value.')
+
+    def test_raises_KeyError(self):
+        bds = BibDataString(self.bd, 'unknown')
+        exp = BibDataStringExpression([bds, self.bds, 'text'])
+        with self.assertRaises(KeyError):
+            exp.get_value()
+
+    def test_equations_are_equal_iif_same(self):
+        a1 = BibDataString(self.bd, 'a')
+        a2 = BibDataString(self.bd, 'a')
+        exp = BibDataStringExpression([a1, self.bds, 'text'])
+        self.assertEqual(exp, BibDataStringExpression([a2, self.bds, 'text']))
+        self.assertNotEqual(exp, BibDataStringExpression(['foo', self.bds, 'text']))
+        self.assertNotEqual(exp, 'foovaluetext')
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/bibtexparser/tests/test_bibtex_strings.py b/bibtexparser/tests/test_bibtex_strings.py
index 775ba27..3a70ab4 100644
--- a/bibtexparser/tests/test_bibtex_strings.py
+++ b/bibtexparser/tests/test_bibtex_strings.py
@@ -1,12 +1,16 @@
+import io
 import unittest
 import codecs
 import bibtexparser
-from bibtexparser.bibdatabase import BibDatabase
+from bibtexparser.bibdatabase import (BibDatabase, BibDataString,
+                                      BibDataStringExpression)
 from bibtexparser.bparser import BibTexParser
+from bibtexparser.bwriter import BibTexWriter
 from collections import OrderedDict
 
 
 class TestStringParse(unittest.TestCase):
+
     def test_single_string_parse_count(self):
         bibtex_str = '@string{name1 = "value1"}\n\n'
         bib_database = bibtexparser.loads(bibtex_str)
@@ -46,13 +50,13 @@ class TestStringParse(unittest.TestCase):
         self.assertEqual(res, expected)
 
 
-
 class TestStringWrite(unittest.TestCase):
+
     def test_single_string_write(self):
         bib_database = BibDatabase()
         bib_database.strings['name1'] = 'value1'
         result = bibtexparser.dumps(bib_database)
-        expected = '@string{name1 = "value1"}\n\n'
+        expected = '@string{name1 = {value1}}\n\n'
         self.assertEqual(result, expected)
 
     def test_multiple_string_write(self):
@@ -60,5 +64,36 @@ class TestStringWrite(unittest.TestCase):
         bib_database.strings['name1'] = 'value1'
         bib_database.strings['name2'] = 'value2'  # Order is important!
         result = bibtexparser.dumps(bib_database)
-        expected = '@string{name1 = "value1"}\n\n@string{name2 = "value2"}\n\n'
+        expected = '@string{name1 = {value1}}\n\n@string{name2 = {value2}}\n\n'
+        self.assertEqual(result, expected)
+
+    def test_ignore_common_strings(self):
+        bib_database = BibDatabase()
+        bib_database.load_common_strings()
+        result = bibtexparser.dumps(bib_database)
+        self.assertEqual(result, '')
+
+    def test_ignore_common_strings_only_if_not_overloaded(self):
+        bib_database = BibDatabase()
+        bib_database.load_common_strings()
+        bib_database.strings['jan'] = 'Janvier'
+        result = bibtexparser.dumps(bib_database)
+        self.assertEqual(result, '@string{jan = {Janvier}}\n\n')
+
+    def test_write_common_strings(self):
+        bib_database = BibDatabase()
+        bib_database.load_common_strings()
+        writer = BibTexWriter(write_common_strings=True)
+        result = bibtexparser.dumps(bib_database, writer=writer)
+        with io.open('bibtexparser/tests/data/common_strings.bib') as f:
+            expected = f.read()
+        self.assertEqual(result, expected)
+
+    def test_write_dependent_strings(self):
+        bib_database = BibDatabase()
+        bib_database.strings['title'] = 'Mr'
+        expr = BibDataStringExpression([BibDataString(bib_database, 'title'), 'Smith'])
+        bib_database.strings['name'] = expr
+        result = bibtexparser.dumps(bib_database)
+        expected = '@string{title = {Mr}}\n\n@string{name = title # {Smith}}\n\n'
         self.assertEqual(result, expected)
diff --git a/bibtexparser/tests/test_bparser.py b/bibtexparser/tests/test_bparser.py
index ebc83d0..33cfd3f 100644
--- a/bibtexparser/tests/test_bparser.py
+++ b/bibtexparser/tests/test_bparser.py
@@ -6,6 +6,8 @@ import unittest
 import codecs
 
 from bibtexparser.bparser import BibTexParser
+from bibtexparser.bibdatabase import (COMMON_STRINGS, BibDataStringExpression,
+                                      BibDataString)
 from bibtexparser.customization import *
 from bibtexparser import customization
 
@@ -518,5 +520,53 @@ class TestBibtexParserList(unittest.TestCase):
             }]
         self.assertEqual(res, expected)
 
+    def test_string_definitions(self):
+        with open('bibtexparser/tests/data/article_with_strings.bib', 'r') as bibfile:
+            bib = BibTexParser(bibfile.read(), common_strings=True)
+        res = dict(bib.strings)
+        expected = COMMON_STRINGS.copy()
+        expected.update({
+                'nice_journal': 'Nice Journal',
+                'jean': 'Jean',
+                'cesar': "César",
+                })
+        self.assertEqual(res, expected)
+
+    def test_string_is_interpolated(self):
+        with open('bibtexparser/tests/data/article_with_strings.bib', 'r') as bibfile:
+            bib = BibTexParser(bibfile.read(), common_strings=True,
+                               interpolate_strings=True)
+        res = bib.get_entry_list()
+        expected = [{
+            'keyword': 'keyword1, keyword2',
+            'ENTRYTYPE': 'article',
+            'year': '2013',
+            'month': 'January',
+            'journal': 'Nice Journal',
+            'ID': 'Cesar2013',
+            'pages': '12-23',
+            'title': 'An amazing title',
+            'comments': 'A comment',
+            'author': 'Jean César',
+            'volume': '12',
+            }]
+        self.assertEqual(res, expected)
+
+    def test_string_is_not_interpolated(self):
+        with open('bibtexparser/tests/data/article_with_strings.bib', 'r') as bibfile:
+            bib = BibTexParser(bibfile.read(), common_strings=True,
+                               interpolate_strings=False)
+        res = bib.get_entry_list()[0]
+        self.assertIsInstance(res['month'], BibDataStringExpression)
+        self.assertEqual(len(res['month'].expr), 1)
+        self.assertEqual(res['month'].get_value(), 'January')
+        self.assertIsInstance(res['author'], BibDataStringExpression)
+        self.assertEqual(len(res['author'].expr), 3)
+        self.assertEqual(res['author'].get_value(), 'Jean César')
+        self.assertIsInstance(res['journal'], BibDataStringExpression)
+        self.assertEqual(len(res['journal'].expr), 1)
+        self.assertEqual(res['journal'].get_value(), 'Nice Journal')
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/bibtexparser/tests/test_bwriter.py b/bibtexparser/tests/test_bwriter.py
index c5d0262..c06a670 100644
--- a/bibtexparser/tests/test_bwriter.py
+++ b/bibtexparser/tests/test_bwriter.py
@@ -6,6 +6,8 @@
 from __future__ import unicode_literals
 
 import unittest
+import os
+import io
 import sys
 
 from bibtexparser.bparser import BibTexParser
@@ -13,45 +15,37 @@ from bibtexparser.bwriter import BibTexWriter, to_bibtex
 from bibtexparser.customization import author
 
 
+def _data_path(filename):
+    return os.path.join('bibtexparser/tests/data', filename)
+
+
 class TestBibtexWriterList(unittest.TestCase):
 
-    ###########
-    # ARTICLE
-    ###########
     def test_article(self):
-        with open('bibtexparser/tests/data/article.bib', 'r') as bibfile:
+        with io.open(_data_path('article.bib'), 'r') as bibfile:
             bib = BibTexParser(bibfile.read())
 
-        with open('bibtexparser/tests/data/article_output.bib', 'r') as bibfile:
+        with io.open(_data_path('article_output.bib'), 'r') as bibfile:
             expected = bibfile.read()
         result = to_bibtex(bib)
-        if not sys.version_info >= (3, 0):
-            if isinstance(result, unicode):
-                result = result.encode('utf-8')
         self.maxDiff = None
         self.assertEqual(expected, result)
 
-    ###########
-    # BOOK
-    ###########
     def test_book(self):
-        with open('bibtexparser/tests/data/book.bib', 'r') as bibfile:
+        with io.open(_data_path('book.bib'), 'r') as bibfile:
             bib = BibTexParser(bibfile.read())
 
-        with open('bibtexparser/tests/data/book_output.bib', 'r') as bibfile:
+        with io.open(_data_path('book_output.bib'), 'r') as bibfile:
             expected = bibfile.read()
         result = to_bibtex(bib)
         self.maxDiff = None
         self.assertEqual(expected, result)
 
-    ###########
-    # COMMA FIRST
-    ###########
     def test_comma_first(self):
-        with open('bibtexparser/tests/data/book.bib', 'r') as bibfile:
+        with io.open(_data_path('book.bib'), 'r') as bibfile:
             bib = BibTexParser(bibfile.read())
 
-        with open('bibtexparser/tests/data/book_comma_first.bib', 'r') as bibfile:
+        with io.open(_data_path('book_comma_first.bib'), 'r') as bibfile:
             expected = bibfile.read()
         writer = BibTexWriter()
         writer.indent = '   '
@@ -60,24 +54,28 @@ class TestBibtexWriterList(unittest.TestCase):
         self.maxDiff = None
         self.assertEqual(expected, result)
 
-    ###########
-    # MULTIPLE
-    ###########
     def test_multiple(self):
-        with open('bibtexparser/tests/data/multiple_entries.bib', 'r') as bibfile:
+        with io.open(_data_path('multiple_entries.bib'), 'r') as bibfile:
             bib = BibTexParser(bibfile.read())
 
-        with open('bibtexparser/tests/data/multiple_entries_output.bib', 'r') as bibfile:
+        with io.open(_data_path('multiple_entries_output.bib'), 'r') as bibfile:
             expected = bibfile.read()
         result = to_bibtex(bib)
         self.maxDiff = None
         self.assertEqual(expected, result)
 
-    ###########
-    # Exception
-    ###########
     def test_exception_typeerror(self):
-        with open('bibtexparser/tests/data/article.bib', 'r') as bibfile:
+        with io.open(_data_path('article.bib'), 'r') as bibfile:
             bib = BibTexParser(bibfile.read(), customization=author)
         self.assertRaises(TypeError, to_bibtex, bib)
 
+    def test_with_strings(self):
+        with io.open(_data_path('article_with_strings.bib'), 'r') as bibfile:
+            bib = BibTexParser(bibfile.read(), common_strings=True,
+                               interpolate_strings=False)
+        with io.open(_data_path(
+                'article_with_strings_output.bib'), 'r') as bibfile:
+            expected = bibfile.read()
+        result = to_bibtex(bib)
+        self.maxDiff = None
+        self.assertEqual(expected, result)