bibtexparser 2.0.0b4__tar.gz → 2.0.0b6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/PKG-INFO +2 -2
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/README.md +1 -1
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/__init__.py +2 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/library.py +25 -5
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/__init__.py +1 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/enclosing.py +4 -4
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/interpolate.py +2 -2
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/middleware.py +3 -5
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/month.py +6 -6
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/names.py +105 -41
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/sorting_entry_fields.py +4 -4
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/model.py +26 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/PKG-INFO +2 -2
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/SOURCES.txt +1 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/setup.py +3 -1
- bibtexparser-2.0.0b6/tests/test_library.py +75 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/tests/test_model.py +29 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/LICENSE +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/entrypoint.py +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/exceptions.py +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/latex_encoding.py +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/parsestack.py +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/sorting_blocks.py +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/splitter.py +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/writer.py +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/dependency_links.txt +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/requires.txt +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/top_level.txt +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/setup.cfg +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/tests/test_entrypoint.py +0 -0
- {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/tests/test_writer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bibtexparser
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b6
|
|
4
4
|
Summary: Bibtex parser for python 3
|
|
5
5
|
Home-page: https://github.com/sciunto-org/python-bibtexparser
|
|
6
6
|
Author: Michael Weiss and other contributors
|
|
@@ -35,7 +35,7 @@ Welcome to python-bibtexparser, a parser for `.bib` files with a long history an
|
|
|
35
35
|
Bibtexparser is available in two versions: V1 and V2. For new projects, we recommend using v2 which, in the long run, will provide an overall more robust and faster experience. **For now, however, note that v2 is an early beta, and does not contain all features of v1**. Install v2 using pip:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
pip install
|
|
38
|
+
pip install bibtexparser --pre
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Or you can install the latest development version directly from the main branch:
|
|
@@ -5,7 +5,7 @@ Welcome to python-bibtexparser, a parser for `.bib` files with a long history an
|
|
|
5
5
|
Bibtexparser is available in two versions: V1 and V2. For new projects, we recommend using v2 which, in the long run, will provide an overall more robust and faster experience. **For now, however, note that v2 is an early beta, and does not contain all features of v1**. Install v2 using pip:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pip install
|
|
8
|
+
pip install bibtexparser --pre
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Or you can install the latest development version directly from the main branch:
|
|
@@ -24,7 +24,9 @@ class Library:
|
|
|
24
24
|
if blocks is not None:
|
|
25
25
|
self.add(blocks)
|
|
26
26
|
|
|
27
|
-
def add(
|
|
27
|
+
def add(
|
|
28
|
+
self, blocks: Union[List[Block], Block], fail_on_duplicate_key: bool = False
|
|
29
|
+
):
|
|
28
30
|
"""Add blocks to library.
|
|
29
31
|
|
|
30
32
|
The adding is key-safe, i.e., it is made sure that no duplicate keys are added.
|
|
@@ -32,14 +34,30 @@ class Library:
|
|
|
32
34
|
a DuplicateKeyBlock.
|
|
33
35
|
|
|
34
36
|
:param blocks: Block or list of blocks to add.
|
|
37
|
+
:param fail_on_duplicate_key: If True, raises ValueError if a block was replaced with a DuplicateKeyBlock.
|
|
35
38
|
"""
|
|
36
39
|
if isinstance(blocks, Block):
|
|
37
40
|
blocks = [blocks]
|
|
38
41
|
|
|
42
|
+
_added_blocks = []
|
|
39
43
|
for block in blocks:
|
|
40
44
|
# This may replace block with a DuplicateEntryKeyBlock
|
|
41
45
|
block = self._add_to_dicts(block)
|
|
42
46
|
self._blocks.append(block)
|
|
47
|
+
_added_blocks.append(block)
|
|
48
|
+
|
|
49
|
+
if fail_on_duplicate_key:
|
|
50
|
+
duplicate_keys = []
|
|
51
|
+
for original, added in zip(blocks, _added_blocks):
|
|
52
|
+
if not original is added and isinstance(added, DuplicateBlockKeyBlock):
|
|
53
|
+
duplicate_keys.append(added.key)
|
|
54
|
+
|
|
55
|
+
if len(duplicate_keys) > 0:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Duplicate keys found: {duplicate_keys}. "
|
|
58
|
+
f"Duplicate entries have been added to the library as DuplicateBlockKeyBlock."
|
|
59
|
+
f"Use `library.failed_blocks` to access them. "
|
|
60
|
+
)
|
|
43
61
|
|
|
44
62
|
def remove(self, blocks: Union[List[Block], Block]):
|
|
45
63
|
"""Remove blocks from library.
|
|
@@ -73,19 +91,19 @@ class Library:
|
|
|
73
91
|
except ValueError:
|
|
74
92
|
raise ValueError("Block to replace is not in library.")
|
|
75
93
|
|
|
76
|
-
self._blocks.insert(index, new_block)
|
|
77
94
|
block_after_add = self._add_to_dicts(new_block)
|
|
95
|
+
self._blocks.insert(index, block_after_add)
|
|
78
96
|
|
|
79
97
|
if (
|
|
80
98
|
new_block is not block_after_add
|
|
81
|
-
and isinstance(
|
|
99
|
+
and isinstance(block_after_add, DuplicateBlockKeyBlock)
|
|
82
100
|
and fail_on_duplicate_key
|
|
83
101
|
):
|
|
84
102
|
# Revert changes to old_block
|
|
85
103
|
# Don't fail on duplicate key, as this would lead to an infinite recursion
|
|
86
104
|
# (should never happen for a clean library, but could happen if the user
|
|
87
105
|
# tampered with the internals of the library).
|
|
88
|
-
self.replace(
|
|
106
|
+
self.replace(block_after_add, old_block, fail_on_duplicate_key=False)
|
|
89
107
|
raise ValueError("Duplicate key found.")
|
|
90
108
|
|
|
91
109
|
@staticmethod
|
|
@@ -160,7 +178,9 @@ class Library:
|
|
|
160
178
|
@property
|
|
161
179
|
def entries(self) -> List[Entry]:
|
|
162
180
|
"""All entry (@article, ...) blocks in the library, preserving order of insertion."""
|
|
163
|
-
|
|
181
|
+
# Note: Taking this from the entries dict would be faster, but does not preserve order
|
|
182
|
+
# e.g. in cases where `replace` has been called.
|
|
183
|
+
return [b for b in self._blocks if isinstance(b, Entry)]
|
|
164
184
|
|
|
165
185
|
@property
|
|
166
186
|
def entries_dict(self) -> Dict[str, Entry]:
|
|
@@ -7,6 +7,7 @@ from bibtexparser.middlewares.latex_encoding import (
|
|
|
7
7
|
LatexDecodingMiddleware,
|
|
8
8
|
LatexEncodingMiddleware,
|
|
9
9
|
)
|
|
10
|
+
from bibtexparser.middlewares.middleware import BlockMiddleware, LibraryMiddleware
|
|
10
11
|
from bibtexparser.middlewares.month import (
|
|
11
12
|
MonthAbbreviationMiddleware,
|
|
12
13
|
MonthIntMiddleware,
|
|
@@ -37,8 +37,8 @@ class RemoveEnclosingMiddleware(BlockMiddleware):
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
# docstr-coverage: inherited
|
|
40
|
-
@
|
|
41
|
-
def metadata_key() -> str:
|
|
40
|
+
@classmethod
|
|
41
|
+
def metadata_key(cls) -> str:
|
|
42
42
|
return REMOVED_ENCLOSING_KEY
|
|
43
43
|
|
|
44
44
|
@staticmethod
|
|
@@ -109,8 +109,8 @@ class AddEnclosingMiddleware(BlockMiddleware):
|
|
|
109
109
|
self._enclose_integers = enclose_integers
|
|
110
110
|
|
|
111
111
|
# docstr-coverage: inherited
|
|
112
|
-
@
|
|
113
|
-
def metadata_key() -> str:
|
|
112
|
+
@classmethod
|
|
113
|
+
def metadata_key(cls) -> str:
|
|
114
114
|
return "remove_enclosing"
|
|
115
115
|
|
|
116
116
|
def _enclose(
|
|
@@ -28,8 +28,8 @@ class ResolveStringReferencesMiddleware(LibraryMiddleware):
|
|
|
28
28
|
super().__init__(allow_inplace_modification)
|
|
29
29
|
|
|
30
30
|
# docstr-coverage: inherited
|
|
31
|
-
@
|
|
32
|
-
def metadata_key() -> str:
|
|
31
|
+
@classmethod
|
|
32
|
+
def metadata_key(cls) -> str:
|
|
33
33
|
return "ResolveStringReferences"
|
|
34
34
|
|
|
35
35
|
# docstr-coverage: inherited
|
|
@@ -62,14 +62,12 @@ class BlockMiddleware(Middleware, abc.ABC):
|
|
|
62
62
|
except if `allow_inplace_modification` is true.
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
|
-
@
|
|
66
|
-
|
|
67
|
-
def metadata_key() -> str:
|
|
65
|
+
@classmethod
|
|
66
|
+
def metadata_key(cls) -> str:
|
|
68
67
|
"""Identifier of the middleware.
|
|
69
|
-
|
|
70
68
|
This key is used to identify the middleware in a blocks metadata.
|
|
71
69
|
"""
|
|
72
|
-
|
|
70
|
+
return cls.__name__
|
|
73
71
|
|
|
74
72
|
# docstr-coverage: inherited
|
|
75
73
|
def transform(self, library: "Library") -> "Library":
|
|
@@ -80,8 +80,8 @@ class MonthLongStringMiddleware(_MonthInterpolator):
|
|
|
80
80
|
and unenclosed."""
|
|
81
81
|
|
|
82
82
|
# docstr-coverage: inherited
|
|
83
|
-
@
|
|
84
|
-
def metadata_key() -> str:
|
|
83
|
+
@classmethod
|
|
84
|
+
def metadata_key(cls) -> str:
|
|
85
85
|
return "MonthLongStringMiddleware"
|
|
86
86
|
|
|
87
87
|
# docstr-coverage: inherited
|
|
@@ -124,8 +124,8 @@ class MonthAbbreviationMiddleware(_MonthInterpolator):
|
|
|
124
124
|
The created abbreviations are always lowercase and unenclosed."""
|
|
125
125
|
|
|
126
126
|
# docstr-coverage: inherited
|
|
127
|
-
@
|
|
128
|
-
def metadata_key() -> str:
|
|
127
|
+
@classmethod
|
|
128
|
+
def metadata_key(cls) -> str:
|
|
129
129
|
return "MonthAbbreviationMiddleware"
|
|
130
130
|
|
|
131
131
|
# docstr-coverage: inherited
|
|
@@ -159,8 +159,8 @@ class MonthIntMiddleware(_MonthInterpolator):
|
|
|
159
159
|
The created int-months are always integers and unenclosed."""
|
|
160
160
|
|
|
161
161
|
# docstr-coverage: inherited
|
|
162
|
-
@
|
|
163
|
-
def metadata_key() -> str:
|
|
162
|
+
@classmethod
|
|
163
|
+
def metadata_key(cls) -> str:
|
|
164
164
|
return "MonthIntMiddleware"
|
|
165
165
|
|
|
166
166
|
# docstr-coverage: inherited
|
|
@@ -5,7 +5,7 @@ Much of the code is taken from Blair Bonnetts never merged v0 pull request
|
|
|
5
5
|
"""
|
|
6
6
|
import abc
|
|
7
7
|
import dataclasses
|
|
8
|
-
from typing import List, Tuple
|
|
8
|
+
from typing import List, Literal, Tuple
|
|
9
9
|
|
|
10
10
|
from bibtexparser.model import Block, Entry, Field, MiddlewareErrorBlock
|
|
11
11
|
|
|
@@ -62,8 +62,8 @@ class SeparateCoAuthors(_NameTransformerMiddleware):
|
|
|
62
62
|
"""Middleware to separate multi-person fields (e.g. co-authors, co-editors)."""
|
|
63
63
|
|
|
64
64
|
# docstr-coverage: inherited
|
|
65
|
-
@
|
|
66
|
-
def metadata_key() -> str:
|
|
65
|
+
@classmethod
|
|
66
|
+
def metadata_key(cls) -> str:
|
|
67
67
|
return "separate_coauthors"
|
|
68
68
|
|
|
69
69
|
# docstr-coverage: inherited
|
|
@@ -75,8 +75,8 @@ class MergeCoAuthors(_NameTransformerMiddleware):
|
|
|
75
75
|
"""Middleware to merge multi-person-list fields (e.g. co-authors, co-editors)."""
|
|
76
76
|
|
|
77
77
|
# docstr-coverage: inherited
|
|
78
|
-
@
|
|
79
|
-
def metadata_key() -> str:
|
|
78
|
+
@classmethod
|
|
79
|
+
def metadata_key(cls) -> str:
|
|
80
80
|
return "merge_coauthors"
|
|
81
81
|
|
|
82
82
|
# docstr-coverage: inherited
|
|
@@ -114,6 +114,35 @@ class NameParts:
|
|
|
114
114
|
]
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
+
@property
|
|
118
|
+
def merge_last_name_first(self) -> str:
|
|
119
|
+
"""Merging the name parts into a single string, last-name-first (with commas) format.
|
|
120
|
+
|
|
121
|
+
The structure of the output is: `von Last, Jr, First`
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def escape_last_slash(string: str) -> str:
|
|
125
|
+
"""Escape the last slash in a string if it is not escaped."""
|
|
126
|
+
# Find the number of trailing slashes
|
|
127
|
+
stripped = string.rstrip("\\")
|
|
128
|
+
num_slashes = len(string) - len(stripped)
|
|
129
|
+
if num_slashes % 2 == 0:
|
|
130
|
+
# Even number: everything is escaped
|
|
131
|
+
return string
|
|
132
|
+
else:
|
|
133
|
+
# Odd number: need to escape one.
|
|
134
|
+
return string + "\\"
|
|
135
|
+
|
|
136
|
+
first = " ".join(self.first) if self.first else None
|
|
137
|
+
von = " ".join(self.von) if self.von else None
|
|
138
|
+
last = " ".join(self.last) if self.last else None
|
|
139
|
+
jr = " ".join(self.jr) if self.jr else None
|
|
140
|
+
|
|
141
|
+
von_last = " ".join(name for name in [von, last] if name)
|
|
142
|
+
return ", ".join(
|
|
143
|
+
escape_last_slash(name) for name in [von_last, jr, first] if name
|
|
144
|
+
)
|
|
145
|
+
|
|
117
146
|
|
|
118
147
|
class SplitNameParts(_NameTransformerMiddleware):
|
|
119
148
|
"""Middleware to split a persons name into its parts (first, von, last, jr).
|
|
@@ -123,8 +152,8 @@ class SplitNameParts(_NameTransformerMiddleware):
|
|
|
123
152
|
"""
|
|
124
153
|
|
|
125
154
|
# docstr-coverage: inherited
|
|
126
|
-
@
|
|
127
|
-
def metadata_key() -> str:
|
|
155
|
+
@classmethod
|
|
156
|
+
def metadata_key(cls) -> str:
|
|
128
157
|
return "split_name_parts"
|
|
129
158
|
|
|
130
159
|
def _transform_field_value(self, name) -> List[NameParts]:
|
|
@@ -142,18 +171,38 @@ class MergeNameParts(_NameTransformerMiddleware):
|
|
|
142
171
|
"""Middleware to merge a persons name parts (first, von, last, jr) into a single string.
|
|
143
172
|
|
|
144
173
|
Name fields (e.g. author, editor, translator) are expected to be lists of NameParts.
|
|
174
|
+
|
|
175
|
+
The merging style indicates whether:
|
|
176
|
+
- the merging is done without commas in first-name-first order ("first"), or
|
|
177
|
+
- the merging is done with commas in last-name-first order ("last").
|
|
145
178
|
"""
|
|
146
179
|
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
style: Literal["last", "first"] = "last",
|
|
183
|
+
allow_inplace_modification: bool = True,
|
|
184
|
+
name_fields: Tuple[str] = ("author", "editor", "translator"),
|
|
185
|
+
):
|
|
186
|
+
self.style = style
|
|
187
|
+
super().__init__(allow_inplace_modification, name_fields)
|
|
188
|
+
|
|
147
189
|
# docstr-coverage: inherited
|
|
148
|
-
@
|
|
149
|
-
def metadata_key() -> str:
|
|
190
|
+
@classmethod
|
|
191
|
+
def metadata_key(cls) -> str:
|
|
150
192
|
return "merge_name_parts"
|
|
151
193
|
|
|
152
194
|
def _transform_field_value(self, name) -> List[str]:
|
|
153
195
|
if not isinstance(name, list) and all(isinstance(n, NameParts) for n in name):
|
|
154
196
|
raise ValueError("Expected a list of NameParts, got {}. ".format(name))
|
|
155
197
|
|
|
156
|
-
|
|
198
|
+
if self.style == "last":
|
|
199
|
+
return [n.merge_last_name_first for n in name]
|
|
200
|
+
elif self.style == "first":
|
|
201
|
+
return [n.merge_first_name_first for n in name]
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError(
|
|
204
|
+
"""Expected "first" or "last" style, got {}. """.format(self.style)
|
|
205
|
+
)
|
|
157
206
|
|
|
158
207
|
|
|
159
208
|
def parse_single_name_into_parts(name, strict=True):
|
|
@@ -229,33 +278,38 @@ def parse_single_name_into_parts(name, strict=True):
|
|
|
229
278
|
for char in nameiter:
|
|
230
279
|
# An escape.
|
|
231
280
|
if char == "\\":
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
bracestart
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
281
|
+
try:
|
|
282
|
+
escaped = next(nameiter)
|
|
283
|
+
|
|
284
|
+
# BibTeX doesn't allow whitespace escaping. Copy the slash and fall
|
|
285
|
+
# through to the normal case to handle the whitespace.
|
|
286
|
+
if escaped in whitespace:
|
|
287
|
+
word.append(char)
|
|
288
|
+
char = escaped
|
|
289
|
+
|
|
290
|
+
else:
|
|
291
|
+
# Is this the first character in a brace?
|
|
292
|
+
if bracestart:
|
|
293
|
+
bracestart = False
|
|
294
|
+
controlseq = escaped.isalpha()
|
|
295
|
+
specialchar = True
|
|
296
|
+
|
|
297
|
+
# Can we use it to determine the case?
|
|
298
|
+
elif (case == -1) and escaped.isalpha():
|
|
299
|
+
if escaped.isupper():
|
|
300
|
+
case = 1
|
|
301
|
+
else:
|
|
302
|
+
case = 0
|
|
303
|
+
|
|
304
|
+
# Copy the escape to the current word and go to the next
|
|
305
|
+
# character in the input.
|
|
306
|
+
word.append(char)
|
|
307
|
+
word.append(escaped)
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
# If we're at the end of the string, then the \ is just a \.
|
|
311
|
+
except StopIteration:
|
|
256
312
|
word.append(char)
|
|
257
|
-
word.append(escaped)
|
|
258
|
-
continue
|
|
259
313
|
|
|
260
314
|
# Start of a braced expression.
|
|
261
315
|
if char == "{":
|
|
@@ -424,11 +478,17 @@ def parse_single_name_into_parts(name, strict=True):
|
|
|
424
478
|
else:
|
|
425
479
|
lcases = cases[0]
|
|
426
480
|
|
|
427
|
-
|
|
481
|
+
def rindex(l, x, default):
|
|
482
|
+
"""Returns the index of the rightmost occurence of x in l."""
|
|
483
|
+
for i in range(len(l) - 1, -1, -1):
|
|
484
|
+
if l[i] == x:
|
|
485
|
+
return i
|
|
486
|
+
return default
|
|
487
|
+
|
|
488
|
+
# Check if at least one of the words is lowercase
|
|
428
489
|
if 0 in lcases:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
split = 0 # Last cannot be empty.
|
|
490
|
+
# Excluding the last word, find the index of the last lower word
|
|
491
|
+
split = rindex(lcases[:-1], 0, -1) + 1
|
|
432
492
|
parts.von = sections[0][:split]
|
|
433
493
|
parts.last = sections[0][split:]
|
|
434
494
|
|
|
@@ -504,7 +564,11 @@ def split_multiple_persons_names(names):
|
|
|
504
564
|
|
|
505
565
|
# Escaped character.
|
|
506
566
|
if char == "\\":
|
|
507
|
-
|
|
567
|
+
try:
|
|
568
|
+
next(namesiter)
|
|
569
|
+
# If we're at the end of the string, then the \ is just a \.
|
|
570
|
+
except StopIteration:
|
|
571
|
+
pass
|
|
508
572
|
pos += 1
|
|
509
573
|
continue
|
|
510
574
|
|
{bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/sorting_entry_fields.py
RENAMED
|
@@ -22,8 +22,8 @@ class SortFieldsAlphabeticallyMiddleware(BlockMiddleware):
|
|
|
22
22
|
return entry
|
|
23
23
|
|
|
24
24
|
# docstr-coverage: inherited
|
|
25
|
-
@
|
|
26
|
-
def metadata_key() -> str:
|
|
25
|
+
@classmethod
|
|
26
|
+
def metadata_key(cls) -> str:
|
|
27
27
|
return "sorted_fields_alphabetically"
|
|
28
28
|
|
|
29
29
|
|
|
@@ -71,6 +71,6 @@ class SortFieldsCustomMiddleware(BlockMiddleware):
|
|
|
71
71
|
return entry
|
|
72
72
|
|
|
73
73
|
# docstr-coverage: inherited
|
|
74
|
-
@
|
|
75
|
-
def metadata_key() -> str:
|
|
74
|
+
@classmethod
|
|
75
|
+
def metadata_key(cls) -> str:
|
|
76
76
|
return "sorted_fields_custom"
|
|
@@ -304,6 +304,16 @@ class Entry(Block):
|
|
|
304
304
|
else:
|
|
305
305
|
self._fields.append(field)
|
|
306
306
|
|
|
307
|
+
def pop_field(self, key: str) -> Optional[Field]:
|
|
308
|
+
"""Removes and returns the field with the given key, if present."""
|
|
309
|
+
try:
|
|
310
|
+
field = self.fields_dict.pop(key)
|
|
311
|
+
except KeyError:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
self._fields = [f for f in self._fields if f.key != key]
|
|
315
|
+
return field
|
|
316
|
+
|
|
307
317
|
def __getitem__(self, key: str) -> Any:
|
|
308
318
|
"""Dict-mimicking index.
|
|
309
319
|
|
|
@@ -318,6 +328,22 @@ class Entry(Block):
|
|
|
318
328
|
return self.key
|
|
319
329
|
return self.fields_dict[key].value
|
|
320
330
|
|
|
331
|
+
def __setitem__(self, key: str, value: Any):
|
|
332
|
+
"""Dict-mimicking index.
|
|
333
|
+
|
|
334
|
+
This serves for partial v1.x backwards compatibility,
|
|
335
|
+
as well as for a shorthand for `set_field`.
|
|
336
|
+
"""
|
|
337
|
+
self.set_field(Field(key, value))
|
|
338
|
+
|
|
339
|
+
def __delitem__(self, key):
|
|
340
|
+
"""Dict-mimicking index.
|
|
341
|
+
|
|
342
|
+
This serves for partial v1.x backwards compatibility,
|
|
343
|
+
as well as for a shorthand for `pop_field`.
|
|
344
|
+
"""
|
|
345
|
+
self.pop_field(key)
|
|
346
|
+
|
|
321
347
|
def items(self):
|
|
322
348
|
"""Dict-mimicking, for partial v1.x backwards compatibility.
|
|
323
349
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bibtexparser
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b6
|
|
4
4
|
Summary: Bibtex parser for python 3
|
|
5
5
|
Home-page: https://github.com/sciunto-org/python-bibtexparser
|
|
6
6
|
Author: Michael Weiss and other contributors
|
|
@@ -35,7 +35,7 @@ Welcome to python-bibtexparser, a parser for `.bib` files with a long history an
|
|
|
35
35
|
Bibtexparser is available in two versions: V1 and V2. For new projects, we recommend using v2 which, in the long run, will provide an overall more robust and faster experience. **For now, however, note that v2 is an early beta, and does not contain all features of v1**. Install v2 using pip:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
pip install
|
|
38
|
+
pip install bibtexparser --pre
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Or you can install the latest development version directly from the main branch:
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
import setuptools
|
|
3
3
|
|
|
4
|
-
version =
|
|
4
|
+
version = None
|
|
5
5
|
|
|
6
6
|
with open("bibtexparser/__init__.py") as fh:
|
|
7
7
|
for line in fh:
|
|
8
8
|
if line.startswith("__version__"):
|
|
9
9
|
version = line.strip().split()[-1][1:-1]
|
|
10
10
|
break
|
|
11
|
+
if not version:
|
|
12
|
+
raise RuntimeError("Could not determine version")
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def load_readme():
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from bibtexparser import Library
|
|
6
|
+
from bibtexparser.model import Entry, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_dummy_entry():
|
|
10
|
+
return Entry(
|
|
11
|
+
entry_type="article",
|
|
12
|
+
key="duplicateKey",
|
|
13
|
+
fields=[
|
|
14
|
+
Field(key="title", value="A title"),
|
|
15
|
+
Field(key="author", value="An author"),
|
|
16
|
+
],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_replace_with_duplicates():
|
|
21
|
+
"""Test that replace() works when there are duplicate values. See issue 404."""
|
|
22
|
+
library = Library()
|
|
23
|
+
library.add(get_dummy_entry())
|
|
24
|
+
library.add(get_dummy_entry())
|
|
25
|
+
# Test precondition
|
|
26
|
+
assert len(library.blocks) == 2
|
|
27
|
+
assert len(library.failed_blocks) == 1
|
|
28
|
+
|
|
29
|
+
replacement_entry = get_dummy_entry()
|
|
30
|
+
replacement_entry.fields_dict["title"].value = "A new title"
|
|
31
|
+
|
|
32
|
+
library.replace(
|
|
33
|
+
library.failed_blocks[0], replacement_entry, fail_on_duplicate_key=False
|
|
34
|
+
)
|
|
35
|
+
assert len(library.blocks) == 2
|
|
36
|
+
assert len(library.failed_blocks) == 1
|
|
37
|
+
assert library.failed_blocks[0].ignore_error_block["title"] == "A new title"
|
|
38
|
+
|
|
39
|
+
replacement_entry_2 = get_dummy_entry()
|
|
40
|
+
replacement_entry_2.fields_dict["title"].value = "Another new title"
|
|
41
|
+
|
|
42
|
+
library.replace(
|
|
43
|
+
library.entries[0], replacement_entry_2, fail_on_duplicate_key=False
|
|
44
|
+
)
|
|
45
|
+
assert len(library.blocks) == 2
|
|
46
|
+
assert len(library.failed_blocks) == 1
|
|
47
|
+
# The new block replaces the previous "non-duplicate" and should thus not become a duplicate itself
|
|
48
|
+
assert library.entries[0].fields_dict["title"].value == "Another new title"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_replace_fail_on_duplicate():
|
|
52
|
+
library = Library()
|
|
53
|
+
replaceable_entry = get_dummy_entry()
|
|
54
|
+
replaceable_entry.key = "Just a regular entry, to be replaced"
|
|
55
|
+
future_duplicate_entry = get_dummy_entry()
|
|
56
|
+
library.add([replaceable_entry, future_duplicate_entry])
|
|
57
|
+
|
|
58
|
+
with pytest.raises(ValueError):
|
|
59
|
+
library.replace(
|
|
60
|
+
replaceable_entry, get_dummy_entry(), fail_on_duplicate_key=True
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert len(library.blocks) == 2
|
|
64
|
+
assert len(library.failed_blocks) == 0
|
|
65
|
+
assert library.entries[0].key == "Just a regular entry, to be replaced"
|
|
66
|
+
assert library.entries[1].key == "duplicateKey"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_fail_on_duplicate_add():
|
|
70
|
+
library = Library()
|
|
71
|
+
library.add(get_dummy_entry())
|
|
72
|
+
with pytest.raises(ValueError):
|
|
73
|
+
library.add(get_dummy_entry(), fail_on_duplicate_key=True)
|
|
74
|
+
assert len(library.blocks) == 2
|
|
75
|
+
assert len(library.failed_blocks) == 1
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from copy import copy, deepcopy
|
|
2
2
|
from textwrap import dedent
|
|
3
3
|
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
4
6
|
from bibtexparser.model import (
|
|
5
7
|
Entry,
|
|
6
8
|
ExplicitComment,
|
|
@@ -290,3 +292,30 @@ def test_entry_str():
|
|
|
290
292
|
)
|
|
291
293
|
|
|
292
294
|
assert str(entry) == expected
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_entry_fields_shorthand():
|
|
298
|
+
entry = Entry(
|
|
299
|
+
entry_type="article",
|
|
300
|
+
key="myEntry",
|
|
301
|
+
fields=[
|
|
302
|
+
Field("myFirstField", "firstValue"),
|
|
303
|
+
Field("mySecondField", "secondValue"),
|
|
304
|
+
],
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
entry["myFirstField"] = "changed_value"
|
|
308
|
+
assert entry["myFirstField"] == "changed_value"
|
|
309
|
+
assert entry.fields_dict["myFirstField"].value == "changed_value"
|
|
310
|
+
|
|
311
|
+
entry["myNewField"] = "new_value"
|
|
312
|
+
assert entry["myNewField"] == "new_value"
|
|
313
|
+
assert entry.fields_dict["myNewField"].key == "myNewField"
|
|
314
|
+
assert entry.fields_dict["myNewField"].value == "new_value"
|
|
315
|
+
assert entry.fields_dict["myNewField"].start_line is None
|
|
316
|
+
|
|
317
|
+
del entry["myNewField"]
|
|
318
|
+
assert "myNewField" not in entry.fields_dict
|
|
319
|
+
assert len([f for f in entry.fields if f.key == "myNewField"]) == 0
|
|
320
|
+
with pytest.raises(KeyError):
|
|
321
|
+
entry["myNewField"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|