Repository: sciunto-org/python-bibtexparser

Issue:
Records with no ID parsed incorrectly
Given the following record:

> @misc{
author = {AMAP},
title = {Summary – The Greenland Ice Sheet in a Changing Climate: Snow, Water, Ice and Permafrost in the Arctic (SWIPA)},
publisher = {Arctic Monitoring and Assessment Programme (AMAP)},
pages = {22 pp.},
year = {2009},
type = {Generic},
}

The parser sets ID as "author = {AMAP" and doesn't recognize the author field.

Changing `re.split(',\s*\n|\n\s*,', record)` to `re.split('(?<={)\s*\n|,\s*\n|\n\s*,', record)` line 239 of bparser.py seems to fix the problem.

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

Coverage decreased (-0.2%) to 97.43% when pulling **a2a628a32acff1b75da9a10ab0f475c81dc53a4f on omangin:fix/163** into **3344a1aa8e3059a8283aa96c99a68f272d686d9d on sciunto-org:master**.

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

Coverage decreased (-0.2%) to 97.43% when pulling **a2a628a32acff1b75da9a10ab0f475c81dc53a4f on omangin:fix/163** into **3344a1aa8e3059a8283aa96c99a68f272d686d9d on sciunto-org:master**.


Patch:
diff --git a/bibtexparser/bibtexexpression.py b/bibtexparser/bibtexexpression.py
index ac679e4..e15bb21 100644
--- a/bibtexparser/bibtexexpression.py
+++ b/bibtexparser/bibtexexpression.py
@@ -34,36 +34,35 @@ def add_logger_parse_action(expr, log_func):
 
 # Parse action helpers
 # Helpers for returning values from the parsed tokens. Shaped as pyparsing's
-# parse actions. In pyparsing wording:
-# s, l, t, stand for string, location, token
+# parse actions. See pyparsing documentation for the arguments.
 
-def first_token(s, l, t):
+def first_token(string_, location, token):
     # TODO Handle this case correctly!
-    assert(len(t) == 1)
-    return t[0]
+    assert(len(token) == 1)
+    return token[0]
 
 
-def remove_trailing_newlines(s, l, t):
-    if t[0]:
-        return t[0].rstrip('\n')
+def remove_trailing_newlines(string_, location, token):
+    if token[0]:
+        return token[0].rstrip('\n')
 
 
-def remove_braces(s, l, t):
-    if len(t[0]) < 1:
+def remove_braces(string_, location, token):
+    if len(token[0]) < 1:
         return ''
     else:
-        start = 1 if t[0][0] == '{' else 0
-        end = -1 if t[0][-1] == '}' else None
-        return t[0][start:end]
+        start = 1 if token[0][0] == '{' else 0
+        end = -1 if token[0][-1] == '}' else None
+        return token[0][start:end]
 
 
-def field_to_pair(s, l, t):
+def field_to_pair(string_, location, token):
     """
     Looks for parsed element named 'Field'.
 
     :returns: (name, value).
     """
-    f = t.get('Field')
+    f = token.get('Field')
     return (f.get('FieldName'),
             strip_after_new_lines(f.get('Value')))
 
@@ -149,9 +148,28 @@ class BibtexExpression(object):
         entry_type.setParseAction(first_token)
 
         # Entry key: any character up to a ',' without leading and trailing
-        # spaces.
-        key = pp.SkipTo(',')('Key')  # Exclude @',\#}{~%
-        key.setParseAction(lambda s, l, t: first_token(s, l, t).strip())
+        # spaces. Also exclude spaces and prevent it from being empty.
+        key = pp.SkipTo(',')('Key')  # TODO Maybe also exclude @',\#}{~%
+
+        def citekeyParseAction(string_, location, token):
+            """Parse action for validating citekeys.
+
+            It ensures citekey is not empty and has no space.
+
+            :args: see pyparsing documentation.
+            """
+            key = first_token(string_, location, token).strip()
+            if len(key) < 1:
+                raise self.ParseException(
+                    string_, loc=location, msg="Empty citekeys are not allowed.")
+            for i, c in enumerate(key):
+                if c.isspace():
+                    raise self.ParseException(
+                        string_, loc=(location + i),
+                        msg="Whitespace not allowed in citekeys.")
+            return key
+
+        key.setParseAction(citekeyParseAction)
 
         # Field name: word of letters, digits, dashes and underscores
         field_name = pp.Word(pp.alphanums + '_-().+')('FieldName')


Test patch:
diff --git a/bibtexparser/tests/test_bibtexexpression.py b/bibtexparser/tests/test_bibtexexpression.py
index 1306861..1bcab43 100644
--- a/bibtexparser/tests/test_bibtexexpression.py
+++ b/bibtexparser/tests/test_bibtexexpression.py
@@ -58,6 +58,26 @@ class TestBibtexExpression(unittest.TestCase):
     def test_entry_declaration_after_space(self):
         self.expr.entry.parseString('  @journal{key, name = {abcd}}')
 
+    def test_entry_declaration_no_key(self):
+        with self.assertRaises(self.expr.ParseException):
+            self.expr.entry.parseString('@misc{name = {abcd}}')
+
+    def test_entry_declaration_no_key_new_line(self):
+        with self.assertRaises(self.expr.ParseException):
+            self.expr.entry.parseString('@misc{\n name = {abcd}}')
+
+    def test_entry_declaration_no_key_comma(self):
+        with self.assertRaises(self.expr.ParseException):
+            self.expr.entry.parseString('@misc{, \nname = {abcd}}')
+
+    def test_entry_declaration_no_key_keyvalue_without_space(self):
+        with self.assertRaises(self.expr.ParseException):
+            self.expr.entry.parseString('@misc{\nname=aaa}')
+
+    def test_entry_declaration_key_with_whitespace(self):
+        with self.assertRaises(self.expr.ParseException):
+            self.expr.entry.parseString('@misc{ xx yy, \n name = aaa}')
+
     def test_string_declaration_after_space(self):
         self.expr.string_def.parseString('  @string{ name = {abcd}}')
 
diff --git a/bibtexparser/tests/test_bparser.py b/bibtexparser/tests/test_bparser.py
index 479ac34..516411a 100644
--- a/bibtexparser/tests/test_bparser.py
+++ b/bibtexparser/tests/test_bparser.py
@@ -612,6 +612,13 @@ class TestBibtexParserList(unittest.TestCase):
         self.assertEqual(res_dict, expected_dict)
         self.assertEqual(bib.preambles, ["Blah blah"])
 
+    def test_no_citekey_parsed_as_comment(self):
+        bib = BibTexParser('@BOOK{, title = "bla"}')
+        self.assertEqual(bib.entries, [])
+        self.assertEqual(bib.preambles, [])
+        self.assertEqual(bib.strings, {})
+        self.assertEqual(bib.comments, ['@BOOK{, title = "bla"}'])
+
 
 if __name__ == '__main__':
     unittest.main()