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.
Files changed (31) hide show
  1. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/PKG-INFO +2 -2
  2. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/README.md +1 -1
  3. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/__init__.py +2 -0
  4. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/library.py +25 -5
  5. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/__init__.py +1 -0
  6. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/enclosing.py +4 -4
  7. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/interpolate.py +2 -2
  8. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/middleware.py +3 -5
  9. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/month.py +6 -6
  10. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/names.py +105 -41
  11. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/sorting_entry_fields.py +4 -4
  12. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/model.py +26 -0
  13. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/PKG-INFO +2 -2
  14. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/SOURCES.txt +1 -0
  15. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/setup.py +3 -1
  16. bibtexparser-2.0.0b6/tests/test_library.py +75 -0
  17. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/tests/test_model.py +29 -0
  18. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/LICENSE +0 -0
  19. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/entrypoint.py +0 -0
  20. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/exceptions.py +0 -0
  21. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/latex_encoding.py +0 -0
  22. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/parsestack.py +0 -0
  23. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/middlewares/sorting_blocks.py +0 -0
  24. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/splitter.py +0 -0
  25. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser/writer.py +0 -0
  26. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/dependency_links.txt +0 -0
  27. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/requires.txt +0 -0
  28. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/bibtexparser.egg-info/top_level.txt +0 -0
  29. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/setup.cfg +0 -0
  30. {bibtexparser-2.0.0b4 → bibtexparser-2.0.0b6}/tests/test_entrypoint.py +0 -0
  31. {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.0b4
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 python-bibtexparser --pre
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 python-bibtexparser --pre
8
+ pip install bibtexparser --pre
9
9
  ```
10
10
 
11
11
  Or you can install the latest development version directly from the main branch:
@@ -4,3 +4,5 @@ import bibtexparser.model
4
4
  from bibtexparser.entrypoint import parse_file, parse_string, write_file, write_string
5
5
  from bibtexparser.library import Library
6
6
  from bibtexparser.writer import BibtexFormat
7
+
8
+ __version__ = "2.0.0b6"
@@ -24,7 +24,9 @@ class Library:
24
24
  if blocks is not None:
25
25
  self.add(blocks)
26
26
 
27
- def add(self, blocks: Union[List[Block], Block]):
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(new_block, DuplicateBlockKeyBlock)
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(new_block, old_block, fail_on_duplicate_key=False)
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
- return list(self._entries_by_key.values())
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
- @staticmethod
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
- @staticmethod
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
- @staticmethod
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
- @staticmethod
66
- @abc.abstractmethod
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
- pass
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
- @staticmethod
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
- @staticmethod
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
- @staticmethod
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
- @staticmethod
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
- @staticmethod
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
- @staticmethod
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
- @staticmethod
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
- return [n.merge_first_name_first for n in name]
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
- escaped = next(nameiter)
233
-
234
- # BibTeX doesn't allow whitespace escaping. Copy the slash and fall
235
- # through to the normal case to handle the whitespace.
236
- if escaped in whitespace:
237
- word.append(char)
238
- char = escaped
239
-
240
- else:
241
- # Is this the first character in a brace?
242
- if bracestart:
243
- bracestart = False
244
- controlseq = escaped.isalpha()
245
- specialchar = True
246
-
247
- # Can we use it to determine the case?
248
- elif (case == -1) and escaped.isalpha():
249
- if escaped.isupper():
250
- case = 1
251
- else:
252
- case = 0
253
-
254
- # Copy the escape to the current word and go to the next
255
- # character in the input.
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
- split = len(lcases) - lcases[::-1].index(0)
430
- if split == len(lcases):
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
- next(namesiter)
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
 
@@ -22,8 +22,8 @@ class SortFieldsAlphabeticallyMiddleware(BlockMiddleware):
22
22
  return entry
23
23
 
24
24
  # docstr-coverage: inherited
25
- @staticmethod
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
- @staticmethod
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.0b4
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 python-bibtexparser --pre
38
+ pip install bibtexparser --pre
39
39
  ```
40
40
 
41
41
  Or you can install the latest development version directly from the main branch:
@@ -24,5 +24,6 @@ bibtexparser/middlewares/parsestack.py
24
24
  bibtexparser/middlewares/sorting_blocks.py
25
25
  bibtexparser/middlewares/sorting_entry_fields.py
26
26
  tests/test_entrypoint.py
27
+ tests/test_library.py
27
28
  tests/test_model.py
28
29
  tests/test_writer.py
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env python
2
2
  import setuptools
3
3
 
4
- version = "2.0.0b4"
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