bibtexparser 2.0.0b8__tar.gz → 2.0.0b9__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.0b8 → bibtexparser-2.0.0b9}/PKG-INFO +27 -8
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/README.md +5 -5
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/__init__.py +1 -1
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/entrypoint.py +91 -17
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/library.py +16 -12
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/latex_encoding.py +14 -4
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/names.py +10 -9
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/model.py +20 -19
- bibtexparser-2.0.0b9/bibtexparser/py.typed +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/splitter.py +55 -12
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/PKG-INFO +27 -8
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/SOURCES.txt +1 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/requires.txt +1 -1
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/setup.py +7 -1
- bibtexparser-2.0.0b9/tests/test_entrypoint.py +243 -0
- bibtexparser-2.0.0b8/tests/test_entrypoint.py +0 -11
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/LICENSE +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/exceptions.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/__init__.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/enclosing.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/fieldkeys.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/interpolate.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/middleware.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/month.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/parsestack.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/sorting_blocks.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/sorting_entry_fields.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/writer.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/dependency_links.txt +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/top_level.txt +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/setup.cfg +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/tests/test_library.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/tests/test_model.py +0 -0
- {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/tests/test_writer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: bibtexparser
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b9
|
|
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
|
|
@@ -10,12 +10,17 @@ Maintainer-email: code@mweiss.ch
|
|
|
10
10
|
License: MIT
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
17
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
18
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
19
|
Classifier: Operating System :: OS Independent
|
|
20
|
+
Requires-Python: >=3.9
|
|
16
21
|
Description-Content-Type: text/markdown
|
|
17
22
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: pylatexenc
|
|
23
|
+
Requires-Dist: pylatexenc~=2.10
|
|
19
24
|
Provides-Extra: test
|
|
20
25
|
Requires-Dist: pytest; extra == "test"
|
|
21
26
|
Requires-Dist: pytest-xdist; extra == "test"
|
|
@@ -23,12 +28,26 @@ Requires-Dist: pytest-cov; extra == "test"
|
|
|
23
28
|
Requires-Dist: jupyter; extra == "test"
|
|
24
29
|
Provides-Extra: docs
|
|
25
30
|
Requires-Dist: sphinx; extra == "docs"
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: author-email
|
|
33
|
+
Dynamic: classifier
|
|
34
|
+
Dynamic: description
|
|
35
|
+
Dynamic: description-content-type
|
|
36
|
+
Dynamic: home-page
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: maintainer
|
|
40
|
+
Dynamic: maintainer-email
|
|
41
|
+
Dynamic: provides-extra
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: requires-python
|
|
44
|
+
Dynamic: summary
|
|
26
45
|
|
|
27
46
|
# python-bibtexparser v2
|
|
28
47
|
|
|
29
|
-
Welcome to python-bibtexparser, a parser for `.bib` files with a long history and wide
|
|
48
|
+
Welcome to python-bibtexparser, a parser for `.bib` files with a long history and wide adoption.
|
|
30
49
|
|
|
31
|
-
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
|
|
50
|
+
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 still in beta stage, and does not contain all features of v1**. Install v2 using pip:
|
|
32
51
|
|
|
33
52
|
```bash
|
|
34
53
|
pip install bibtexparser --pre
|
|
@@ -87,7 +106,7 @@ bib_database = bibtexparser.parse_string(bibtex_string,
|
|
|
87
106
|
|
|
88
107
|
# Let's transform it back to a bibtex_string.
|
|
89
108
|
new_bibtex_string = bibtexparser.write_string(bib_database,
|
|
90
|
-
# Revert
|
|
109
|
+
# Revert above transformation
|
|
91
110
|
prepend_middleware=[MergeNameParts(), MergeCoAuthors()]
|
|
92
111
|
)
|
|
93
112
|
```
|
|
@@ -102,14 +121,14 @@ Consult the documentation for a list of available middleware, parsing options an
|
|
|
102
121
|
The architecture consists of the following components:
|
|
103
122
|
|
|
104
123
|
#### Library
|
|
105
|
-
Reflects the contents of a parsed bibtex files, including all comments, entries, strings,
|
|
124
|
+
Reflects the contents of a parsed bibtex files, including all comments, entries, strings, preambles and their metadata (e.g. order).
|
|
106
125
|
|
|
107
126
|
#### A Splitter
|
|
108
127
|
Splits a bibtex string into basic blocks (Entry, String, Preamble, ...), with correspondingly split content (e.g. fields on Entry, key-value on String, ...).
|
|
109
128
|
The splitter aims to be forgiving when facing invalid bibtex: A line starting with a block definition (``@....``) ends the previous block, even if not yet every bracket is closed, failing the parsing of the previous block. Correspondingly, one block type is "ParsingFailedBlock".
|
|
110
129
|
|
|
111
130
|
#### Middleware
|
|
112
|
-
Middleware layers transform a library and its blocks, for example by decoding latex special characters, interpolating string references,
|
|
131
|
+
Middleware layers transform a library and its blocks, for example by decoding latex special characters, interpolating string references, resolving crossreferences or re-ordering blocks. Thus, the choice of middleware allows to customize parsing and writing to ones specific usecase. Note: Middlewares, by default, do not mutate their input, but return a modified copy.
|
|
113
132
|
|
|
114
133
|
#### Writer
|
|
115
134
|
Writes the content of a bibtex library to a ``.bib`` file. Optional formatting parameters can be passed using a corresponding dedicated data structure.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# python-bibtexparser v2
|
|
2
2
|
|
|
3
|
-
Welcome to python-bibtexparser, a parser for `.bib` files with a long history and wide
|
|
3
|
+
Welcome to python-bibtexparser, a parser for `.bib` files with a long history and wide adoption.
|
|
4
4
|
|
|
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
|
|
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 still in beta stage, and does not contain all features of v1**. Install v2 using pip:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
pip install bibtexparser --pre
|
|
@@ -61,7 +61,7 @@ bib_database = bibtexparser.parse_string(bibtex_string,
|
|
|
61
61
|
|
|
62
62
|
# Let's transform it back to a bibtex_string.
|
|
63
63
|
new_bibtex_string = bibtexparser.write_string(bib_database,
|
|
64
|
-
# Revert
|
|
64
|
+
# Revert above transformation
|
|
65
65
|
prepend_middleware=[MergeNameParts(), MergeCoAuthors()]
|
|
66
66
|
)
|
|
67
67
|
```
|
|
@@ -76,14 +76,14 @@ Consult the documentation for a list of available middleware, parsing options an
|
|
|
76
76
|
The architecture consists of the following components:
|
|
77
77
|
|
|
78
78
|
#### Library
|
|
79
|
-
Reflects the contents of a parsed bibtex files, including all comments, entries, strings,
|
|
79
|
+
Reflects the contents of a parsed bibtex files, including all comments, entries, strings, preambles and their metadata (e.g. order).
|
|
80
80
|
|
|
81
81
|
#### A Splitter
|
|
82
82
|
Splits a bibtex string into basic blocks (Entry, String, Preamble, ...), with correspondingly split content (e.g. fields on Entry, key-value on String, ...).
|
|
83
83
|
The splitter aims to be forgiving when facing invalid bibtex: A line starting with a block definition (``@....``) ends the previous block, even if not yet every bracket is closed, failing the parsing of the previous block. Correspondingly, one block type is "ParsingFailedBlock".
|
|
84
84
|
|
|
85
85
|
#### Middleware
|
|
86
|
-
Middleware layers transform a library and its blocks, for example by decoding latex special characters, interpolating string references,
|
|
86
|
+
Middleware layers transform a library and its blocks, for example by decoding latex special characters, interpolating string references, resolving crossreferences or re-ordering blocks. Thus, the choice of middleware allows to customize parsing and writing to ones specific usecase. Note: Middlewares, by default, do not mutate their input, but return a modified copy.
|
|
87
87
|
|
|
88
88
|
#### Writer
|
|
89
89
|
Writes the content of a bibtex library to a ``.bib`` file. Optional formatting parameters can be passed using a corresponding dedicated data structure.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import codecs
|
|
1
2
|
import warnings
|
|
2
3
|
from typing import Iterable
|
|
3
4
|
from typing import List
|
|
@@ -20,9 +21,9 @@ def _build_parse_stack(
|
|
|
20
21
|
) -> List[Middleware]:
|
|
21
22
|
if parse_stack is not None and append_middleware is not None:
|
|
22
23
|
raise ValueError(
|
|
23
|
-
"Provided both parse_stack and append_middleware."
|
|
24
|
-
"Only one should be provided."
|
|
25
|
-
"(append_middleware should only be used with the default parse_stack,"
|
|
24
|
+
"Provided both parse_stack and append_middleware. "
|
|
25
|
+
"Only one should be provided. "
|
|
26
|
+
"(append_middleware should only be used with the default parse_stack, "
|
|
26
27
|
"i.e., when the passed parse_stack is None.)"
|
|
27
28
|
)
|
|
28
29
|
|
|
@@ -50,10 +51,10 @@ def _build_unparse_stack(
|
|
|
50
51
|
) -> List[Middleware]:
|
|
51
52
|
if unparse_stack is not None and prepend_middleware is not None:
|
|
52
53
|
raise ValueError(
|
|
53
|
-
"Provided both
|
|
54
|
-
"Only one should be provided."
|
|
55
|
-
"(prepend_middleware should only be used with the default
|
|
56
|
-
"i.e., when the passed
|
|
54
|
+
"Provided both unparse_stack and prepend_middleware. "
|
|
55
|
+
"Only one should be provided. "
|
|
56
|
+
"(prepend_middleware should only be used with the default unparse_stack, "
|
|
57
|
+
"i.e., when the passed unparse_stack is None.)"
|
|
57
58
|
)
|
|
58
59
|
|
|
59
60
|
if unparse_stack is None:
|
|
@@ -74,6 +75,52 @@ def _build_unparse_stack(
|
|
|
74
75
|
return list(prepend_middleware) + list(unparse_stack)
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
def _handle_deprecated_write_params(
|
|
79
|
+
unparse_stack: Optional[Iterable[Middleware]],
|
|
80
|
+
prepend_middleware: Optional[Iterable[Middleware]],
|
|
81
|
+
kwargs: dict,
|
|
82
|
+
function_name: str,
|
|
83
|
+
) -> tuple[Optional[Iterable[Middleware]], Optional[Iterable[Middleware]]]:
|
|
84
|
+
"""Handle deprecated parameter names for write functions.
|
|
85
|
+
|
|
86
|
+
:param unparse_stack: Current unparse_stack value
|
|
87
|
+
:param prepend_middleware: Current prepend_middleware value
|
|
88
|
+
:param kwargs: Dictionary of keyword arguments to check for deprecated params
|
|
89
|
+
:param function_name: Name of the calling function (for error messages)
|
|
90
|
+
:return: Tuple of (unparse_stack, prepend_middleware) with deprecated values migrated
|
|
91
|
+
"""
|
|
92
|
+
if "parse_stack" in kwargs:
|
|
93
|
+
warnings.warn(
|
|
94
|
+
"Parameter 'parse_stack' is deprecated. Use 'unparse_stack' instead.",
|
|
95
|
+
DeprecationWarning,
|
|
96
|
+
stacklevel=3,
|
|
97
|
+
)
|
|
98
|
+
if unparse_stack is not None:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"Cannot provide both 'parse_stack' (deprecated) and 'unparse_stack'. "
|
|
101
|
+
"Use 'unparse_stack' instead."
|
|
102
|
+
)
|
|
103
|
+
unparse_stack = kwargs.pop("parse_stack")
|
|
104
|
+
|
|
105
|
+
if "append_middleware" in kwargs:
|
|
106
|
+
warnings.warn(
|
|
107
|
+
"Parameter 'append_middleware' is deprecated. Use 'prepend_middleware' instead.",
|
|
108
|
+
DeprecationWarning,
|
|
109
|
+
stacklevel=3,
|
|
110
|
+
)
|
|
111
|
+
if prepend_middleware is not None:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
"Cannot provide both 'append_middleware' (deprecated) and 'prepend_middleware'. "
|
|
114
|
+
"Use 'prepend_middleware' instead."
|
|
115
|
+
)
|
|
116
|
+
prepend_middleware = kwargs.pop("append_middleware")
|
|
117
|
+
|
|
118
|
+
if kwargs:
|
|
119
|
+
raise TypeError(f"{function_name}() got unexpected keyword arguments: {', '.join(kwargs)}")
|
|
120
|
+
|
|
121
|
+
return unparse_stack, prepend_middleware
|
|
122
|
+
|
|
123
|
+
|
|
77
124
|
def parse_string(
|
|
78
125
|
bibtex_str: str,
|
|
79
126
|
parse_stack: Optional[Iterable[Middleware]] = None,
|
|
@@ -125,7 +172,13 @@ def parse_file(
|
|
|
125
172
|
|
|
126
173
|
:param encoding: Encoding of the .bib file. Default encoding is ``"UTF-8"``.
|
|
127
174
|
:return: Library: Parsed BibTeX library
|
|
175
|
+
:raises LookupError: If the specified encoding is not recognized.
|
|
128
176
|
"""
|
|
177
|
+
try:
|
|
178
|
+
codecs.lookup(encoding)
|
|
179
|
+
except LookupError:
|
|
180
|
+
raise LookupError(f"Unknown encoding: {encoding!r}")
|
|
181
|
+
|
|
129
182
|
with open(path, encoding=encoding) as f:
|
|
130
183
|
bibtex_str = f.read()
|
|
131
184
|
return parse_string(
|
|
@@ -136,27 +189,39 @@ def parse_file(
|
|
|
136
189
|
def write_file(
|
|
137
190
|
file: Union[str, TextIO],
|
|
138
191
|
library: Library,
|
|
139
|
-
|
|
140
|
-
|
|
192
|
+
unparse_stack: Optional[Iterable[Middleware]] = None,
|
|
193
|
+
prepend_middleware: Optional[Iterable[Middleware]] = None,
|
|
141
194
|
bibtex_format: Optional[BibtexFormat] = None,
|
|
195
|
+
encoding: str = "UTF-8",
|
|
196
|
+
**kwargs,
|
|
142
197
|
) -> None:
|
|
143
198
|
"""Write a BibTeX database to a file.
|
|
144
199
|
|
|
145
200
|
:param file: File to write to. Can be a file name or a file object.
|
|
146
201
|
:param library: BibTeX database to serialize.
|
|
147
|
-
:param
|
|
202
|
+
:param unparse_stack: List of middleware to apply to the database before writing.
|
|
148
203
|
If None, a default stack will be used.
|
|
149
|
-
:param
|
|
150
|
-
Only applicable if `
|
|
151
|
-
:param bibtex_format: Customized BibTeX format to use (optional).
|
|
204
|
+
:param prepend_middleware: List of middleware to prepend to the default stack.
|
|
205
|
+
Only applicable if `unparse_stack` is None.
|
|
206
|
+
:param bibtex_format: Customized BibTeX format to use (optional).
|
|
207
|
+
:param encoding: Encoding of the .bib file. Default encoding is ``"UTF-8"``.
|
|
208
|
+
|
|
209
|
+
.. deprecated:: (next version)
|
|
210
|
+
Parameters 'parse_stack' and 'append_middleware' are deprecated, will be deleted soon.
|
|
211
|
+
Use 'unparse_stack' and 'prepend_middleware' instead.
|
|
212
|
+
"""
|
|
213
|
+
unparse_stack, prepend_middleware = _handle_deprecated_write_params(
|
|
214
|
+
unparse_stack, prepend_middleware, kwargs, "write_file"
|
|
215
|
+
)
|
|
216
|
+
|
|
152
217
|
bibtex_str = write_string(
|
|
153
218
|
library=library,
|
|
154
|
-
unparse_stack=
|
|
155
|
-
prepend_middleware=
|
|
219
|
+
unparse_stack=unparse_stack,
|
|
220
|
+
prepend_middleware=prepend_middleware,
|
|
156
221
|
bibtex_format=bibtex_format,
|
|
157
222
|
)
|
|
158
223
|
if isinstance(file, str):
|
|
159
|
-
with open(file, "w") as f:
|
|
224
|
+
with open(file, "w", encoding=encoding) as f:
|
|
160
225
|
f.write(bibtex_str)
|
|
161
226
|
else:
|
|
162
227
|
file.write(bibtex_str)
|
|
@@ -167,6 +232,7 @@ def write_string(
|
|
|
167
232
|
unparse_stack: Optional[Iterable[Middleware]] = None,
|
|
168
233
|
prepend_middleware: Optional[Iterable[Middleware]] = None,
|
|
169
234
|
bibtex_format: Optional["BibtexFormat"] = None,
|
|
235
|
+
**kwargs,
|
|
170
236
|
) -> str:
|
|
171
237
|
"""Serialize a BibTeX database to a string.
|
|
172
238
|
|
|
@@ -174,9 +240,17 @@ def write_string(
|
|
|
174
240
|
:param unparse_stack: List of middleware to apply to the database before writing.
|
|
175
241
|
If None, a default stack will be used.
|
|
176
242
|
:param prepend_middleware: List of middleware to prepend to the default stack.
|
|
177
|
-
Only applicable if `
|
|
243
|
+
Only applicable if `unparse_stack` is None.
|
|
178
244
|
:param bibtex_format: Customized BibTeX format to use (optional).
|
|
245
|
+
|
|
246
|
+
.. deprecated:: (next version)
|
|
247
|
+
Parameters 'parse_stack' and 'append_middleware' are deprecated.
|
|
248
|
+
Use 'unparse_stack' and 'prepend_middleware' instead.
|
|
179
249
|
"""
|
|
250
|
+
unparse_stack, prepend_middleware = _handle_deprecated_write_params(
|
|
251
|
+
unparse_stack, prepend_middleware, kwargs, "write_string"
|
|
252
|
+
)
|
|
253
|
+
|
|
180
254
|
middleware: Middleware
|
|
181
255
|
for middleware in _build_unparse_stack(unparse_stack, prepend_middleware):
|
|
182
256
|
library = middleware.transform(library=library)
|
|
@@ -107,18 +107,22 @@ class Library:
|
|
|
107
107
|
def _cast_to_duplicate(
|
|
108
108
|
prev_block_with_same_key: Union[Entry, String], duplicate: Union[Entry, String]
|
|
109
109
|
):
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
if not (
|
|
111
|
+
isinstance(prev_block_with_same_key, type(duplicate))
|
|
112
|
+
or isinstance(duplicate, type(prev_block_with_same_key))
|
|
113
|
+
):
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"Internal BibtexParser Error. Duplicate blocks share no common type. "
|
|
116
|
+
f"Found {type(prev_block_with_same_key)} and {type(duplicate)}, but both should be "
|
|
117
|
+
"either instance of String or instance of Entry. "
|
|
118
|
+
"Please report this issue at the bibtexparser issue tracker."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if prev_block_with_same_key.key != duplicate.key:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"Internal BibtexParser Error. Duplicate blocks have different keys. "
|
|
124
|
+
"Please report this issue at the bibtexparser issue tracker."
|
|
125
|
+
)
|
|
122
126
|
|
|
123
127
|
return DuplicateBlockKeyBlock(
|
|
124
128
|
start_line=duplicate.start_line,
|
|
@@ -102,10 +102,15 @@ class LatexEncodingMiddleware(_PyStringTransformerMiddleware):
|
|
|
102
102
|
|
|
103
103
|
if encoder is not None and (keep_math is not None or enclose_urls is not None):
|
|
104
104
|
raise ValueError(
|
|
105
|
-
"Cannot specify both encoder and keep_math or enclose_urls."
|
|
105
|
+
"Cannot specify both encoder and keep_math or enclose_urls. "
|
|
106
106
|
"If you want to use a custom encoder, you have to specify it completely."
|
|
107
107
|
)
|
|
108
108
|
|
|
109
|
+
if encoder is not None and not isinstance(encoder, UnicodeToLatexEncoder):
|
|
110
|
+
raise TypeError(
|
|
111
|
+
f"encoder must be a UnicodeToLatexEncoder instance, got {type(encoder).__name__}"
|
|
112
|
+
)
|
|
113
|
+
|
|
109
114
|
# Defaults (not specified as defaults in args,
|
|
110
115
|
# to make sure we can identify if they were specified)
|
|
111
116
|
keep_math = keep_math if keep_math is not None else True
|
|
@@ -166,12 +171,17 @@ class LatexDecodingMiddleware(_PyStringTransformerMiddleware):
|
|
|
166
171
|
|
|
167
172
|
if decoder is not None and (keep_braced_groups is not None or keep_math_mode is not None):
|
|
168
173
|
raise ValueError(
|
|
169
|
-
"Cannot specify both
|
|
170
|
-
"`keep_braced_groups` or `
|
|
171
|
-
"If you want to use a custom
|
|
174
|
+
"Cannot specify both decoder and one of "
|
|
175
|
+
"`keep_braced_groups` or `keep_math_mode`. "
|
|
176
|
+
"If you want to use a custom decoder, "
|
|
172
177
|
"you have to specify it completely."
|
|
173
178
|
)
|
|
174
179
|
|
|
180
|
+
if decoder is not None and not isinstance(decoder, LatexNodes2Text):
|
|
181
|
+
raise TypeError(
|
|
182
|
+
f"decoder must be a LatexNodes2Text instance, got {type(decoder).__name__}"
|
|
183
|
+
)
|
|
184
|
+
|
|
175
185
|
# Defaults (not specified as defaults in args,
|
|
176
186
|
# to make sure we can identify if they were specified)
|
|
177
187
|
keep_braced_groups = keep_braced_groups if keep_braced_groups is not None else False
|
|
@@ -9,6 +9,7 @@ import dataclasses
|
|
|
9
9
|
from typing import List
|
|
10
10
|
from typing import Literal
|
|
11
11
|
from typing import Tuple
|
|
12
|
+
from typing import Union
|
|
12
13
|
|
|
13
14
|
from bibtexparser.model import Block
|
|
14
15
|
from bibtexparser.model import Entry
|
|
@@ -73,7 +74,7 @@ class SeparateCoAuthors(_NameTransformerMiddleware):
|
|
|
73
74
|
return "separate_coauthors"
|
|
74
75
|
|
|
75
76
|
# docstr-coverage: inherited
|
|
76
|
-
def _transform_field_value(self, name) -> List[str]:
|
|
77
|
+
def _transform_field_value(self, name: str) -> List[str]:
|
|
77
78
|
return split_multiple_persons_names(name)
|
|
78
79
|
|
|
79
80
|
|
|
@@ -86,7 +87,7 @@ class MergeCoAuthors(_NameTransformerMiddleware):
|
|
|
86
87
|
return "merge_coauthors"
|
|
87
88
|
|
|
88
89
|
# docstr-coverage: inherited
|
|
89
|
-
def _transform_field_value(self, name):
|
|
90
|
+
def _transform_field_value(self, name: Union[List[str], str]) -> str:
|
|
90
91
|
if isinstance(name, list):
|
|
91
92
|
return " and ".join(name)
|
|
92
93
|
return name
|
|
@@ -160,12 +161,12 @@ class SplitNameParts(_NameTransformerMiddleware):
|
|
|
160
161
|
def metadata_key(cls) -> str:
|
|
161
162
|
return "split_name_parts"
|
|
162
163
|
|
|
163
|
-
def _transform_field_value(self, name) -> List[NameParts]:
|
|
164
|
+
def _transform_field_value(self, name: List[str]) -> List[NameParts]:
|
|
164
165
|
if not isinstance(name, list):
|
|
165
166
|
raise ValueError(
|
|
166
167
|
"Expected a list of strings, got {}. "
|
|
167
|
-
"Make sure to use `SeparateCoAuthors` middleware"
|
|
168
|
-
"before using `SplitNameParts` middleware".format(name)
|
|
168
|
+
"Make sure to use `SeparateCoAuthors` middleware "
|
|
169
|
+
"before using `SplitNameParts` middleware.".format(name)
|
|
169
170
|
)
|
|
170
171
|
|
|
171
172
|
return [parse_single_name_into_parts(n) for n in name]
|
|
@@ -195,8 +196,8 @@ class MergeNameParts(_NameTransformerMiddleware):
|
|
|
195
196
|
def metadata_key(cls) -> str:
|
|
196
197
|
return "merge_name_parts"
|
|
197
198
|
|
|
198
|
-
def _transform_field_value(self, name) -> List[str]:
|
|
199
|
-
if not isinstance(name, list) and all(isinstance(n, NameParts) for n in name):
|
|
199
|
+
def _transform_field_value(self, name: List[NameParts]) -> List[str]:
|
|
200
|
+
if not (isinstance(name, list) and all(isinstance(n, NameParts) for n in name)):
|
|
200
201
|
raise ValueError(f"Expected a list of NameParts, got {name}. ")
|
|
201
202
|
|
|
202
203
|
if self.style == "last":
|
|
@@ -207,7 +208,7 @@ class MergeNameParts(_NameTransformerMiddleware):
|
|
|
207
208
|
raise ValueError(f"""Expected "first" or "last" style, got {self.style}. """)
|
|
208
209
|
|
|
209
210
|
|
|
210
|
-
def parse_single_name_into_parts(name, strict=True):
|
|
211
|
+
def parse_single_name_into_parts(name: str, strict: bool = True) -> NameParts:
|
|
211
212
|
"""
|
|
212
213
|
Parse a name into its constituent parts: First, von, Last, and Jr.
|
|
213
214
|
|
|
@@ -502,7 +503,7 @@ def parse_single_name_into_parts(name, strict=True):
|
|
|
502
503
|
return parts
|
|
503
504
|
|
|
504
505
|
|
|
505
|
-
def split_multiple_persons_names(names):
|
|
506
|
+
def split_multiple_persons_names(names: str) -> List[str]:
|
|
506
507
|
"""
|
|
507
508
|
Splits a string of multiple names.
|
|
508
509
|
|
|
@@ -4,10 +4,11 @@ from typing import Dict
|
|
|
4
4
|
from typing import List
|
|
5
5
|
from typing import Optional
|
|
6
6
|
from typing import Set
|
|
7
|
+
from typing import Tuple
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class Block(abc.ABC):
|
|
10
|
-
"""
|
|
11
|
+
"""An abstract superclass of all top-level building blocks of a bibtex file.
|
|
11
12
|
|
|
12
13
|
E.g. a ``@string`` block, a ``@preamble`` block, an ``@entry`` block, a comment, etc.
|
|
13
14
|
"""
|
|
@@ -67,7 +68,7 @@ class Block(abc.ABC):
|
|
|
67
68
|
See attribute ``parser_metadata`` for more information."""
|
|
68
69
|
self._parser_metadata[key] = value
|
|
69
70
|
|
|
70
|
-
def __eq__(self, other):
|
|
71
|
+
def __eq__(self, other: object) -> bool:
|
|
71
72
|
# make sure they have the same type and same content
|
|
72
73
|
return (
|
|
73
74
|
isinstance(other, self.__class__)
|
|
@@ -108,10 +109,10 @@ class String(Block):
|
|
|
108
109
|
def value(self, value: str):
|
|
109
110
|
self._value = value
|
|
110
111
|
|
|
111
|
-
def __str__(self):
|
|
112
|
+
def __str__(self) -> str:
|
|
112
113
|
return f"String (line: {self.start_line}, key: `{self.key}`): `{self.value}`"
|
|
113
114
|
|
|
114
|
-
def __repr__(self):
|
|
115
|
+
def __repr__(self) -> str:
|
|
115
116
|
return (
|
|
116
117
|
f"String(key=`{self.key}`, value=`{self.value}`, "
|
|
117
118
|
f"start_line={self.start_line}, raw=`{self.raw}`)"
|
|
@@ -134,10 +135,10 @@ class Preamble(Block):
|
|
|
134
135
|
def value(self, value: str):
|
|
135
136
|
self._value = value
|
|
136
137
|
|
|
137
|
-
def __str__(self):
|
|
138
|
+
def __str__(self) -> str:
|
|
138
139
|
return f"Preamble (line: {self.start_line}): `{self.value}`"
|
|
139
140
|
|
|
140
|
-
def __repr__(self):
|
|
141
|
+
def __repr__(self) -> str:
|
|
141
142
|
return f"Preamble(value=`{self.value}`, " f"start_line={self.start_line}, raw=`{self.raw}`)"
|
|
142
143
|
|
|
143
144
|
|
|
@@ -157,10 +158,10 @@ class ExplicitComment(Block):
|
|
|
157
158
|
def comment(self, value: str):
|
|
158
159
|
self._comment = value
|
|
159
160
|
|
|
160
|
-
def __str__(self):
|
|
161
|
+
def __str__(self) -> str:
|
|
161
162
|
return f"ExplicitComment (line: {self.start_line}): `{self.comment}`"
|
|
162
163
|
|
|
163
|
-
def __repr__(self):
|
|
164
|
+
def __repr__(self) -> str:
|
|
164
165
|
return (
|
|
165
166
|
f"ExplicitComment(comment=`{self.comment}`, "
|
|
166
167
|
f"start_line={self.start_line}, raw=`{self.raw}`)"
|
|
@@ -183,10 +184,10 @@ class ImplicitComment(Block):
|
|
|
183
184
|
def comment(self, value: str):
|
|
184
185
|
self._comment = value
|
|
185
186
|
|
|
186
|
-
def __str__(self):
|
|
187
|
+
def __str__(self) -> str:
|
|
187
188
|
return f"ImplicitComment (line: {self.start_line}): `{self.comment}`"
|
|
188
189
|
|
|
189
|
-
def __repr__(self):
|
|
190
|
+
def __repr__(self) -> str:
|
|
190
191
|
return (
|
|
191
192
|
f"ImplicitComment(comment=`{self.comment}`, "
|
|
192
193
|
f"start_line={self.start_line}, raw=`{self.raw}`)"
|
|
@@ -224,7 +225,7 @@ class Field:
|
|
|
224
225
|
"""The line number of the first line of this field in the originally parsed string."""
|
|
225
226
|
return self._start_line
|
|
226
227
|
|
|
227
|
-
def __eq__(self, other):
|
|
228
|
+
def __eq__(self, other: object) -> bool:
|
|
228
229
|
# make sure they have the same type and same content
|
|
229
230
|
return (
|
|
230
231
|
isinstance(other, self.__class__)
|
|
@@ -232,10 +233,10 @@ class Field:
|
|
|
232
233
|
and self.__dict__ == other.__dict__
|
|
233
234
|
)
|
|
234
235
|
|
|
235
|
-
def __str__(self):
|
|
236
|
+
def __str__(self) -> str:
|
|
236
237
|
return f"Field (line: {self.start_line}, key: `{self.key}`): `{self.value}`"
|
|
237
238
|
|
|
238
|
-
def __repr__(self):
|
|
239
|
+
def __repr__(self) -> str:
|
|
239
240
|
return f"Field(key=`{self.key}`, value=`{self.value}`, " f"start_line={self.start_line})"
|
|
240
241
|
|
|
241
242
|
|
|
@@ -256,7 +257,7 @@ class Entry(Block):
|
|
|
256
257
|
self._fields = fields
|
|
257
258
|
|
|
258
259
|
@property
|
|
259
|
-
def entry_type(self):
|
|
260
|
+
def entry_type(self) -> str:
|
|
260
261
|
"""The type of the entry, e.g. ``article`` in ``@article{Cesar2013, ...}``."""
|
|
261
262
|
return self._entry_type
|
|
262
263
|
|
|
@@ -265,7 +266,7 @@ class Entry(Block):
|
|
|
265
266
|
self._entry_type = value
|
|
266
267
|
|
|
267
268
|
@property
|
|
268
|
-
def key(self):
|
|
269
|
+
def key(self) -> str:
|
|
269
270
|
"""The key of the entry, e.g. ``Cesar2013`` in ``@article{Cesar2013, ...}``."""
|
|
270
271
|
return self._key
|
|
271
272
|
|
|
@@ -343,7 +344,7 @@ class Entry(Block):
|
|
|
343
344
|
"""
|
|
344
345
|
self.set_field(Field(key, value))
|
|
345
346
|
|
|
346
|
-
def __delitem__(self, key):
|
|
347
|
+
def __delitem__(self, key: str) -> None:
|
|
347
348
|
"""Dict-mimicking index.
|
|
348
349
|
|
|
349
350
|
This serves for partial v1.x backwards compatibility,
|
|
@@ -351,7 +352,7 @@ class Entry(Block):
|
|
|
351
352
|
"""
|
|
352
353
|
self.pop(key)
|
|
353
354
|
|
|
354
|
-
def items(self):
|
|
355
|
+
def items(self) -> List[Tuple[str, Any]]:
|
|
355
356
|
"""Dict-mimicking, for partial v1.x backwards compatibility.
|
|
356
357
|
|
|
357
358
|
For newly written code, it's recommended to use `entry.entry_type`,
|
|
@@ -361,12 +362,12 @@ class Entry(Block):
|
|
|
361
362
|
("ID", self.key),
|
|
362
363
|
] + [(f.key, f.value) for f in self.fields]
|
|
363
364
|
|
|
364
|
-
def __str__(self):
|
|
365
|
+
def __str__(self) -> str:
|
|
365
366
|
lines = [f"Entry (line: {self.start_line}, type: `{self.entry_type}`, key: `{self.key}`):"]
|
|
366
367
|
lines.extend([f"\t`{f.key}` = `{f.value}`" for f in self.fields])
|
|
367
368
|
return "\n".join(lines)
|
|
368
369
|
|
|
369
|
-
def __repr__(self):
|
|
370
|
+
def __repr__(self) -> str:
|
|
370
371
|
return (
|
|
371
372
|
f"Entry(entry_type=`{self.entry_type}`, key=`{self.key}`, "
|
|
372
373
|
f"fields=`{self.fields.__repr__()}`, start_line={self.start_line})"
|
|
File without changes
|
|
@@ -46,7 +46,7 @@ class Splitter:
|
|
|
46
46
|
|
|
47
47
|
self._reset_block_status(current_char_index=0)
|
|
48
48
|
|
|
49
|
-
def _reset_block_status(self, current_char_index):
|
|
49
|
+
def _reset_block_status(self, current_char_index: int) -> None:
|
|
50
50
|
self._open_brackets = 0
|
|
51
51
|
self._is_quote_open = False
|
|
52
52
|
self._expected_next: Optional[List[str]] = None
|
|
@@ -57,6 +57,24 @@ class Splitter:
|
|
|
57
57
|
self._implicit_comment_start_line = self._current_line
|
|
58
58
|
self._implicit_comment_start: Optional[int] = current_char_index
|
|
59
59
|
|
|
60
|
+
def _is_at_line_start(self, pos: int) -> bool:
|
|
61
|
+
"""Check if position is at the start of a line (after optional whitespace).
|
|
62
|
+
|
|
63
|
+
This is used to determine whether an @ sign should be treated as a new
|
|
64
|
+
block start (for error recovery) or as content within a field value.
|
|
65
|
+
We only want to abort parsing and start a new block if the @ is at the
|
|
66
|
+
beginning of a line, to avoid false positives with @ signs in content.
|
|
67
|
+
"""
|
|
68
|
+
# Scan backwards from pos to find either newline or non-whitespace
|
|
69
|
+
for i in range(pos - 1, -1, -1):
|
|
70
|
+
char = self.bibstr[i]
|
|
71
|
+
if char == "\n":
|
|
72
|
+
return True
|
|
73
|
+
elif not char.isspace():
|
|
74
|
+
return False
|
|
75
|
+
# Start of string counts as line start
|
|
76
|
+
return True
|
|
77
|
+
|
|
60
78
|
def _end_implicit_comment(self, end_char_index) -> Optional[ImplicitComment]:
|
|
61
79
|
if self._implicit_comment_start is None:
|
|
62
80
|
return # No implicit comment started
|
|
@@ -122,7 +140,11 @@ class Splitter:
|
|
|
122
140
|
return m.start()
|
|
123
141
|
else:
|
|
124
142
|
num_additional_brackets -= 1
|
|
125
|
-
elif m.group(0).startswith("@"):
|
|
143
|
+
elif m.group(0).startswith("@") and self._is_at_line_start(m.start()):
|
|
144
|
+
# Only abort if the @ is at the start of a line.
|
|
145
|
+
# This allows @ signs in field values (e.g., "LeQua @ {CLEF}")
|
|
146
|
+
# while still providing error recovery when a new block starts
|
|
147
|
+
# on a new line within an unclosed block.
|
|
126
148
|
self._unaccepted_mark = m
|
|
127
149
|
raise BlockAbortedException(
|
|
128
150
|
abort_reason=f"Unexpected block start: `{m.group(0)}`. "
|
|
@@ -131,7 +153,7 @@ class Splitter:
|
|
|
131
153
|
)
|
|
132
154
|
|
|
133
155
|
def _move_to_comma_or_closing_curly_bracket(
|
|
134
|
-
self, currently_quote_escaped=False, num_open_curls=0
|
|
156
|
+
self, currently_quote_escaped: bool = False, num_open_curls: int = 0
|
|
135
157
|
) -> int:
|
|
136
158
|
"""Index of the end of the field, taking quote-escape into account."""
|
|
137
159
|
|
|
@@ -151,6 +173,24 @@ class Splitter:
|
|
|
151
173
|
|
|
152
174
|
# Handle "escape" characters
|
|
153
175
|
if next_mark.group(0) == '"' and not num_open_curls > 0:
|
|
176
|
+
# Check for {"} escape sequence when inside quotes (issue #487).
|
|
177
|
+
# Per https://tug.ctan.org/info/bibtex/tamethebeast/ttb_en.pdf,
|
|
178
|
+
# {"} represents a literal quote in a quoted field.
|
|
179
|
+
# We verify this by checking:
|
|
180
|
+
# 1. We're inside quotes
|
|
181
|
+
# 2. The " is surrounded by { and }
|
|
182
|
+
# 3. There's more content after the } (i.e., pos+2 is in bounds)
|
|
183
|
+
# This distinguishes {"} escapes from cases like @article{"}
|
|
184
|
+
# where " closes the field and } closes the entry.
|
|
185
|
+
if currently_quote_escaped:
|
|
186
|
+
pos = next_mark.start()
|
|
187
|
+
if (
|
|
188
|
+
pos > 0
|
|
189
|
+
and pos + 2 < len(self.bibstr)
|
|
190
|
+
and self.bibstr[pos - 1] == "{"
|
|
191
|
+
and self.bibstr[pos + 1] == "}"
|
|
192
|
+
):
|
|
193
|
+
continue
|
|
154
194
|
currently_quote_escaped = not currently_quote_escaped
|
|
155
195
|
continue
|
|
156
196
|
elif next_mark.group(0) == "{" and not currently_quote_escaped:
|
|
@@ -169,8 +209,11 @@ class Splitter:
|
|
|
169
209
|
self._unaccepted_mark = next_mark
|
|
170
210
|
return next_mark.start()
|
|
171
211
|
|
|
172
|
-
# Sanity-check: If new block is starting, we abort
|
|
173
|
-
|
|
212
|
+
# Sanity-check: If new block is starting at line start, we abort.
|
|
213
|
+
# We only abort if the @ is at the start of a line to allow @ signs
|
|
214
|
+
# in field values (e.g., "LeQua @ {CLEF}") while still providing
|
|
215
|
+
# error recovery when a new block starts on a new line.
|
|
216
|
+
elif next_mark.group(0).startswith("@") and self._is_at_line_start(next_mark.start()):
|
|
174
217
|
self._unaccepted_mark = next_mark
|
|
175
218
|
|
|
176
219
|
if currently_quote_escaped:
|
|
@@ -291,8 +334,8 @@ class Splitter:
|
|
|
291
334
|
f"due to syntactical error in bibtex:\n {e.abort_reason}"
|
|
292
335
|
)
|
|
293
336
|
logger.info(
|
|
294
|
-
"We will try to continue parsing, but this might lead to unexpected results."
|
|
295
|
-
"The failed block will be stored in the `failed_blocks`of the library."
|
|
337
|
+
"We will try to continue parsing, but this might lead to unexpected results. "
|
|
338
|
+
"The failed block will be stored in the `failed_blocks` of the library."
|
|
296
339
|
)
|
|
297
340
|
library.add(
|
|
298
341
|
ParsingFailedBlock(
|
|
@@ -308,14 +351,14 @@ class Splitter:
|
|
|
308
351
|
"python-bibtexparser detected an invalid state. Please report this bug."
|
|
309
352
|
)
|
|
310
353
|
logger.error(e.message)
|
|
311
|
-
raise
|
|
312
|
-
except Exception
|
|
313
|
-
# For unknown
|
|
354
|
+
raise
|
|
355
|
+
except Exception:
|
|
356
|
+
# For unknown exceptions, we want to fail hard and get the info in our issue tracker.
|
|
314
357
|
logger.error(
|
|
315
|
-
f"Unexpected exception while parsing `{m_val}` block (line {start_line})"
|
|
358
|
+
f"Unexpected exception while parsing `{m_val}` block (line {start_line}). "
|
|
316
359
|
"Please report this bug."
|
|
317
360
|
)
|
|
318
|
-
raise
|
|
361
|
+
raise
|
|
319
362
|
|
|
320
363
|
self._reset_block_status(current_char_index=self._current_char_index + 1)
|
|
321
364
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: bibtexparser
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b9
|
|
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
|
|
@@ -10,12 +10,17 @@ Maintainer-email: code@mweiss.ch
|
|
|
10
10
|
License: MIT
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
17
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
18
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
19
|
Classifier: Operating System :: OS Independent
|
|
20
|
+
Requires-Python: >=3.9
|
|
16
21
|
Description-Content-Type: text/markdown
|
|
17
22
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: pylatexenc
|
|
23
|
+
Requires-Dist: pylatexenc~=2.10
|
|
19
24
|
Provides-Extra: test
|
|
20
25
|
Requires-Dist: pytest; extra == "test"
|
|
21
26
|
Requires-Dist: pytest-xdist; extra == "test"
|
|
@@ -23,12 +28,26 @@ Requires-Dist: pytest-cov; extra == "test"
|
|
|
23
28
|
Requires-Dist: jupyter; extra == "test"
|
|
24
29
|
Provides-Extra: docs
|
|
25
30
|
Requires-Dist: sphinx; extra == "docs"
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: author-email
|
|
33
|
+
Dynamic: classifier
|
|
34
|
+
Dynamic: description
|
|
35
|
+
Dynamic: description-content-type
|
|
36
|
+
Dynamic: home-page
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: maintainer
|
|
40
|
+
Dynamic: maintainer-email
|
|
41
|
+
Dynamic: provides-extra
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: requires-python
|
|
44
|
+
Dynamic: summary
|
|
26
45
|
|
|
27
46
|
# python-bibtexparser v2
|
|
28
47
|
|
|
29
|
-
Welcome to python-bibtexparser, a parser for `.bib` files with a long history and wide
|
|
48
|
+
Welcome to python-bibtexparser, a parser for `.bib` files with a long history and wide adoption.
|
|
30
49
|
|
|
31
|
-
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
|
|
50
|
+
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 still in beta stage, and does not contain all features of v1**. Install v2 using pip:
|
|
32
51
|
|
|
33
52
|
```bash
|
|
34
53
|
pip install bibtexparser --pre
|
|
@@ -87,7 +106,7 @@ bib_database = bibtexparser.parse_string(bibtex_string,
|
|
|
87
106
|
|
|
88
107
|
# Let's transform it back to a bibtex_string.
|
|
89
108
|
new_bibtex_string = bibtexparser.write_string(bib_database,
|
|
90
|
-
# Revert
|
|
109
|
+
# Revert above transformation
|
|
91
110
|
prepend_middleware=[MergeNameParts(), MergeCoAuthors()]
|
|
92
111
|
)
|
|
93
112
|
```
|
|
@@ -102,14 +121,14 @@ Consult the documentation for a list of available middleware, parsing options an
|
|
|
102
121
|
The architecture consists of the following components:
|
|
103
122
|
|
|
104
123
|
#### Library
|
|
105
|
-
Reflects the contents of a parsed bibtex files, including all comments, entries, strings,
|
|
124
|
+
Reflects the contents of a parsed bibtex files, including all comments, entries, strings, preambles and their metadata (e.g. order).
|
|
106
125
|
|
|
107
126
|
#### A Splitter
|
|
108
127
|
Splits a bibtex string into basic blocks (Entry, String, Preamble, ...), with correspondingly split content (e.g. fields on Entry, key-value on String, ...).
|
|
109
128
|
The splitter aims to be forgiving when facing invalid bibtex: A line starting with a block definition (``@....``) ends the previous block, even if not yet every bracket is closed, failing the parsing of the previous block. Correspondingly, one block type is "ParsingFailedBlock".
|
|
110
129
|
|
|
111
130
|
#### Middleware
|
|
112
|
-
Middleware layers transform a library and its blocks, for example by decoding latex special characters, interpolating string references,
|
|
131
|
+
Middleware layers transform a library and its blocks, for example by decoding latex special characters, interpolating string references, resolving crossreferences or re-ordering blocks. Thus, the choice of middleware allows to customize parsing and writing to ones specific usecase. Note: Middlewares, by default, do not mutate their input, but return a modified copy.
|
|
113
132
|
|
|
114
133
|
#### Writer
|
|
115
134
|
Writes the content of a bibtex library to a ``.bib`` file. Optional formatting parameters can be passed using a corresponding dedicated data structure.
|
|
@@ -29,16 +29,22 @@ setuptools.setup(
|
|
|
29
29
|
description="Bibtex parser for python 3",
|
|
30
30
|
long_description_content_type="text/markdown",
|
|
31
31
|
long_description=load_readme(),
|
|
32
|
+
python_requires=">=3.9",
|
|
32
33
|
packages=setuptools.find_packages(include=["bibtexparser", "bibtexparser.*"]),
|
|
34
|
+
package_data={"bibtexparser": ["py.typed"]},
|
|
33
35
|
classifiers=[
|
|
34
36
|
"Development Status :: 4 - Beta",
|
|
35
37
|
"Programming Language :: Python :: 3",
|
|
38
|
+
"Programming Language :: Python :: 3.9",
|
|
39
|
+
"Programming Language :: Python :: 3.10",
|
|
40
|
+
"Programming Language :: Python :: 3.11",
|
|
41
|
+
"Programming Language :: Python :: 3.12",
|
|
36
42
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
37
43
|
"License :: OSI Approved :: MIT License",
|
|
38
44
|
"Operating System :: OS Independent",
|
|
39
45
|
],
|
|
40
46
|
install_requires=[
|
|
41
|
-
"pylatexenc
|
|
47
|
+
"pylatexenc~=2.10",
|
|
42
48
|
],
|
|
43
49
|
extras_require={
|
|
44
50
|
"test": [
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Testing the parse_file and write_file functions."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from bibtexparser import parse_file
|
|
10
|
+
from bibtexparser import write_file
|
|
11
|
+
from bibtexparser import write_string
|
|
12
|
+
from bibtexparser.library import Library
|
|
13
|
+
from bibtexparser.model import Entry
|
|
14
|
+
from bibtexparser.model import Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_gbk():
|
|
18
|
+
library = parse_file("tests/resources/gbk_test.bib", encoding="gbk")
|
|
19
|
+
assert library.entries[0]["author"] == "凯撒"
|
|
20
|
+
assert library.entries[0]["title"] == "Test Title"
|
|
21
|
+
assert library.entries[0]["year"] == "2013"
|
|
22
|
+
assert library.entries[0]["journal"] == "测试期刊"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_write_file_default_encoding():
|
|
26
|
+
"""Test write_file uses UTF-8 by default."""
|
|
27
|
+
entry = Entry(
|
|
28
|
+
entry_type="article",
|
|
29
|
+
key="test2024",
|
|
30
|
+
fields=[
|
|
31
|
+
Field(key="author", value="Müller"),
|
|
32
|
+
Field(key="title", value="Ångström measurements"),
|
|
33
|
+
],
|
|
34
|
+
)
|
|
35
|
+
library = Library([entry])
|
|
36
|
+
|
|
37
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bib", delete=False) as f:
|
|
38
|
+
temp_path = f.name
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
write_file(temp_path, library)
|
|
42
|
+
# Read back and verify
|
|
43
|
+
with open(temp_path, encoding="UTF-8") as f:
|
|
44
|
+
content = f.read()
|
|
45
|
+
assert "Müller" in content
|
|
46
|
+
assert "Ångström" in content
|
|
47
|
+
finally:
|
|
48
|
+
os.unlink(temp_path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_write_file_gbk_encoding():
|
|
52
|
+
"""Test write_file with GBK encoding for Chinese characters."""
|
|
53
|
+
entry = Entry(
|
|
54
|
+
entry_type="article",
|
|
55
|
+
key="test2024",
|
|
56
|
+
fields=[
|
|
57
|
+
Field(key="author", value="凯撒"),
|
|
58
|
+
Field(key="title", value="Test Title"),
|
|
59
|
+
Field(key="journal", value="测试期刊"),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
library = Library([entry])
|
|
63
|
+
|
|
64
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bib", delete=False) as f:
|
|
65
|
+
temp_path = f.name
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
write_file(temp_path, library, encoding="gbk")
|
|
69
|
+
# Read back with GBK and verify
|
|
70
|
+
with open(temp_path, encoding="gbk") as f:
|
|
71
|
+
content = f.read()
|
|
72
|
+
assert "凯撒" in content
|
|
73
|
+
assert "测试期刊" in content
|
|
74
|
+
finally:
|
|
75
|
+
os.unlink(temp_path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_write_file_roundtrip_gbk():
|
|
79
|
+
"""Test round-trip: parse GBK file, write with GBK, parse again."""
|
|
80
|
+
# Parse original GBK file
|
|
81
|
+
library = parse_file("tests/resources/gbk_test.bib", encoding="gbk")
|
|
82
|
+
original_author = library.entries[0]["author"]
|
|
83
|
+
original_journal = library.entries[0]["journal"]
|
|
84
|
+
|
|
85
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bib", delete=False) as f:
|
|
86
|
+
temp_path = f.name
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Write with GBK encoding
|
|
90
|
+
write_file(temp_path, library, encoding="gbk")
|
|
91
|
+
# Parse back
|
|
92
|
+
library2 = parse_file(temp_path, encoding="gbk")
|
|
93
|
+
assert library2.entries[0]["author"] == original_author
|
|
94
|
+
assert library2.entries[0]["journal"] == original_journal
|
|
95
|
+
finally:
|
|
96
|
+
os.unlink(temp_path)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Deprecation warning tests for write_file and write_string
|
|
100
|
+
def test_write_file_deprecated_parse_stack_parameter():
|
|
101
|
+
"""Test that using deprecated 'parse_stack' parameter issues a warning."""
|
|
102
|
+
library = Library([])
|
|
103
|
+
|
|
104
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bib", delete=False) as f:
|
|
105
|
+
temp_path = f.name
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
with warnings.catch_warnings(record=True) as w:
|
|
109
|
+
warnings.simplefilter("always")
|
|
110
|
+
write_file(temp_path, library, parse_stack=[])
|
|
111
|
+
assert len(w) == 1
|
|
112
|
+
assert issubclass(w[0].category, DeprecationWarning)
|
|
113
|
+
assert "parse_stack" in str(w[0].message)
|
|
114
|
+
assert "unparse_stack" in str(w[0].message)
|
|
115
|
+
finally:
|
|
116
|
+
os.unlink(temp_path)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_write_file_deprecated_append_middleware_parameter():
|
|
120
|
+
"""Test that using deprecated 'append_middleware' parameter issues a warning."""
|
|
121
|
+
library = Library([])
|
|
122
|
+
|
|
123
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bib", delete=False) as f:
|
|
124
|
+
temp_path = f.name
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
with warnings.catch_warnings(record=True) as w:
|
|
128
|
+
warnings.simplefilter("always")
|
|
129
|
+
write_file(temp_path, library, append_middleware=[])
|
|
130
|
+
assert len(w) == 1
|
|
131
|
+
assert issubclass(w[0].category, DeprecationWarning)
|
|
132
|
+
assert "append_middleware" in str(w[0].message)
|
|
133
|
+
assert "prepend_middleware" in str(w[0].message)
|
|
134
|
+
finally:
|
|
135
|
+
os.unlink(temp_path)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_write_file_both_parse_stack_and_unparse_stack_raises_error():
|
|
139
|
+
"""Test that providing both parse_stack and unparse_stack raises ValueError."""
|
|
140
|
+
library = Library([])
|
|
141
|
+
|
|
142
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bib", delete=False) as f:
|
|
143
|
+
temp_path = f.name
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with pytest.raises(ValueError) as excinfo:
|
|
147
|
+
write_file(temp_path, library, parse_stack=[], unparse_stack=[])
|
|
148
|
+
assert "parse_stack" in str(excinfo.value)
|
|
149
|
+
assert "unparse_stack" in str(excinfo.value)
|
|
150
|
+
assert "Use 'unparse_stack' instead" in str(excinfo.value)
|
|
151
|
+
finally:
|
|
152
|
+
os.unlink(temp_path)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_write_file_both_append_and_prepend_middleware_raises_error():
|
|
156
|
+
"""Test that providing both append_middleware and prepend_middleware raises ValueError."""
|
|
157
|
+
library = Library([])
|
|
158
|
+
|
|
159
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bib", delete=False) as f:
|
|
160
|
+
temp_path = f.name
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
with pytest.raises(ValueError) as excinfo:
|
|
164
|
+
write_file(temp_path, library, append_middleware=[], prepend_middleware=[])
|
|
165
|
+
assert "append_middleware" in str(excinfo.value)
|
|
166
|
+
assert "prepend_middleware" in str(excinfo.value)
|
|
167
|
+
assert "Use 'prepend_middleware' instead" in str(excinfo.value)
|
|
168
|
+
finally:
|
|
169
|
+
os.unlink(temp_path)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_write_file_unexpected_keyword_argument_raises_error():
|
|
173
|
+
"""Test that unexpected keyword arguments raise TypeError."""
|
|
174
|
+
library = Library([])
|
|
175
|
+
|
|
176
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bib", delete=False) as f:
|
|
177
|
+
temp_path = f.name
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
with pytest.raises(TypeError) as excinfo:
|
|
181
|
+
write_file(temp_path, library, unknown_param="value")
|
|
182
|
+
assert "unexpected keyword arguments" in str(excinfo.value)
|
|
183
|
+
assert "unknown_param" in str(excinfo.value)
|
|
184
|
+
finally:
|
|
185
|
+
os.unlink(temp_path)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_write_string_deprecated_parse_stack_parameter():
|
|
189
|
+
"""Test that using deprecated 'parse_stack' parameter issues a warning."""
|
|
190
|
+
library = Library([])
|
|
191
|
+
|
|
192
|
+
with warnings.catch_warnings(record=True) as w:
|
|
193
|
+
warnings.simplefilter("always")
|
|
194
|
+
write_string(library, parse_stack=[])
|
|
195
|
+
assert len(w) == 1
|
|
196
|
+
assert issubclass(w[0].category, DeprecationWarning)
|
|
197
|
+
assert "parse_stack" in str(w[0].message)
|
|
198
|
+
assert "unparse_stack" in str(w[0].message)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_write_string_deprecated_append_middleware_parameter():
|
|
202
|
+
"""Test that using deprecated 'append_middleware' parameter issues a warning."""
|
|
203
|
+
library = Library([])
|
|
204
|
+
|
|
205
|
+
with warnings.catch_warnings(record=True) as w:
|
|
206
|
+
warnings.simplefilter("always")
|
|
207
|
+
write_string(library, append_middleware=[])
|
|
208
|
+
assert len(w) == 1
|
|
209
|
+
assert issubclass(w[0].category, DeprecationWarning)
|
|
210
|
+
assert "append_middleware" in str(w[0].message)
|
|
211
|
+
assert "prepend_middleware" in str(w[0].message)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_write_string_both_parse_stack_and_unparse_stack_raises_error():
|
|
215
|
+
"""Test that providing both parse_stack and unparse_stack raises ValueError."""
|
|
216
|
+
library = Library([])
|
|
217
|
+
|
|
218
|
+
with pytest.raises(ValueError) as excinfo:
|
|
219
|
+
write_string(library, parse_stack=[], unparse_stack=[])
|
|
220
|
+
assert "parse_stack" in str(excinfo.value)
|
|
221
|
+
assert "unparse_stack" in str(excinfo.value)
|
|
222
|
+
assert "Use 'unparse_stack' instead" in str(excinfo.value)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_write_string_both_append_and_prepend_middleware_raises_error():
|
|
226
|
+
"""Test that providing both append_middleware and prepend_middleware raises ValueError."""
|
|
227
|
+
library = Library([])
|
|
228
|
+
|
|
229
|
+
with pytest.raises(ValueError) as excinfo:
|
|
230
|
+
write_string(library, append_middleware=[], prepend_middleware=[])
|
|
231
|
+
assert "append_middleware" in str(excinfo.value)
|
|
232
|
+
assert "prepend_middleware" in str(excinfo.value)
|
|
233
|
+
assert "Use 'prepend_middleware' instead" in str(excinfo.value)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_write_string_unexpected_keyword_argument_raises_error():
|
|
237
|
+
"""Test that unexpected keyword arguments raise TypeError."""
|
|
238
|
+
library = Library([])
|
|
239
|
+
|
|
240
|
+
with pytest.raises(TypeError) as excinfo:
|
|
241
|
+
write_string(library, unknown_param="value")
|
|
242
|
+
assert "unexpected keyword arguments" in str(excinfo.value)
|
|
243
|
+
assert "unknown_param" in str(excinfo.value)
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""Testing the parse_file function."""
|
|
2
|
-
|
|
3
|
-
from bibtexparser import parse_file
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_gbk():
|
|
7
|
-
library = parse_file("tests/resources/gbk_test.bib", encoding="gbk")
|
|
8
|
-
assert library.entries[0]["author"] == "凯撒"
|
|
9
|
-
assert library.entries[0]["title"] == "Test Title"
|
|
10
|
-
assert library.entries[0]["year"] == "2013"
|
|
11
|
-
assert library.entries[0]["journal"] == "测试期刊"
|
|
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
|
{bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/sorting_entry_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|