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.
Files changed (34) hide show
  1. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/PKG-INFO +27 -8
  2. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/README.md +5 -5
  3. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/__init__.py +1 -1
  4. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/entrypoint.py +91 -17
  5. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/library.py +16 -12
  6. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/latex_encoding.py +14 -4
  7. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/names.py +10 -9
  8. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/model.py +20 -19
  9. bibtexparser-2.0.0b9/bibtexparser/py.typed +0 -0
  10. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/splitter.py +55 -12
  11. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/PKG-INFO +27 -8
  12. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/SOURCES.txt +1 -0
  13. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/requires.txt +1 -1
  14. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/setup.py +7 -1
  15. bibtexparser-2.0.0b9/tests/test_entrypoint.py +243 -0
  16. bibtexparser-2.0.0b8/tests/test_entrypoint.py +0 -11
  17. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/LICENSE +0 -0
  18. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/exceptions.py +0 -0
  19. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/__init__.py +0 -0
  20. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/enclosing.py +0 -0
  21. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/fieldkeys.py +0 -0
  22. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/interpolate.py +0 -0
  23. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/middleware.py +0 -0
  24. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/month.py +0 -0
  25. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/parsestack.py +0 -0
  26. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/sorting_blocks.py +0 -0
  27. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/middlewares/sorting_entry_fields.py +0 -0
  28. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser/writer.py +0 -0
  29. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/dependency_links.txt +0 -0
  30. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/bibtexparser.egg-info/top_level.txt +0 -0
  31. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/setup.cfg +0 -0
  32. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/tests/test_library.py +0 -0
  33. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/tests/test_model.py +0 -0
  34. {bibtexparser-2.0.0b8 → bibtexparser-2.0.0b9}/tests/test_writer.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: bibtexparser
3
- Version: 2.0.0b8
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>=2.10
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 adaption.
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 an early beta, and does not contain all features of v1**. Install v2 using pip:
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 aboves transfomration
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, preamples and their metadata (e.g. order).
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, resoling crossreferences or re-ordering blocks. Thus, the choice of middleware allows to customize parsing and writing to ones specific usecase. Note: Middlewares, by default, no not mutate their input, but return a modified copy.
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 adaption.
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 an early beta, and does not contain all features of v1**. Install v2 using pip:
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 aboves transfomration
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, preamples and their metadata (e.g. order).
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, resoling crossreferences or re-ordering blocks. Thus, the choice of middleware allows to customize parsing and writing to ones specific usecase. Note: Middlewares, by default, no not mutate their input, but return a modified copy.
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.
@@ -8,4 +8,4 @@ from bibtexparser.entrypoint import write_string
8
8
  from bibtexparser.library import Library
9
9
  from bibtexparser.writer import BibtexFormat
10
10
 
11
- __version__ = "2.0.0b8"
11
+ __version__ = "2.0.0b9"
@@ -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 parse_stack and append_middleware."
54
- "Only one should be provided."
55
- "(prepend_middleware should only be used with the default parse_stack,"
56
- "i.e., when the passed parse_stack is None.)"
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
- parse_stack: Optional[Iterable[Middleware]] = None,
140
- append_middleware: Optional[Iterable[Middleware]] = None,
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 parse_stack: List of middleware to apply to the database before writing.
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 append_middleware: List of middleware to append to the default stack.
150
- Only applicable if `parse_stack` is None.
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=parse_stack,
155
- prepend_middleware=append_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 `parse_stack` is None.
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
- assert isinstance(prev_block_with_same_key, type(duplicate)) or isinstance(
111
- duplicate, type(prev_block_with_same_key)
112
- ), (
113
- "Internal BibtexParser Error. Duplicate blocks share no common type."
114
- f"Found {type(prev_block_with_same_key)} and {type(duplicate)}, but both should be"
115
- f"either instance of String or instance of Entry."
116
- f"Please report this issue at the bibtexparser issue tracker.",
117
- )
118
-
119
- assert (
120
- prev_block_with_same_key.key == duplicate.key
121
- ), "Internal BibtexParser Error. Duplicate blocks have different keys."
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 encoder and one of "
170
- "`keep_braced_groups` or `keep_braced_groups`."
171
- "If you want to use a custom encoder, "
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
- """A abstract superclass of all top-level building blocks of a bibtex file.
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
- elif next_mark.group(0).startswith("@"):
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 e
312
- except Exception as e:
313
- # For unknown exeptions, we want to fail hard and get the info in our issue tracker.
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 e
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
1
+ Metadata-Version: 2.4
2
2
  Name: bibtexparser
3
- Version: 2.0.0b8
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>=2.10
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 adaption.
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 an early beta, and does not contain all features of v1**. Install v2 using pip:
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 aboves transfomration
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, preamples and their metadata (e.g. order).
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, resoling crossreferences or re-ordering blocks. Thus, the choice of middleware allows to customize parsing and writing to ones specific usecase. Note: Middlewares, by default, no not mutate their input, but return a modified copy.
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.
@@ -6,6 +6,7 @@ bibtexparser/entrypoint.py
6
6
  bibtexparser/exceptions.py
7
7
  bibtexparser/library.py
8
8
  bibtexparser/model.py
9
+ bibtexparser/py.typed
9
10
  bibtexparser/splitter.py
10
11
  bibtexparser/writer.py
11
12
  bibtexparser.egg-info/PKG-INFO
@@ -1,4 +1,4 @@
1
- pylatexenc>=2.10
1
+ pylatexenc~=2.10
2
2
 
3
3
  [docs]
4
4
  sphinx
@@ -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>=2.10",
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