Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-jsonlines for 
openSUSE:Factory checked in at 2022-09-30 17:57:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jsonlines (Old)
 and      /work/SRC/openSUSE:Factory/.python-jsonlines.new.2275 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-jsonlines"

Fri Sep 30 17:57:57 2022 rev:3 rq:1007080 version:3.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-jsonlines/python-jsonlines.changes        
2021-11-08 17:25:21.996739896 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-jsonlines.new.2275/python-jsonlines.changes  
    2022-09-30 17:58:16.101309395 +0200
@@ -1,0 +2,10 @@
+Thu Sep 29 14:16:46 UTC 2022 - Yogalakshmi Arunachalam <yarunacha...@suse.com>
+
+- Update to 3.0.0
+  * add type annotations; adopt mypy in strict mode (#58, #62)
+  * ignore UTF-8 BOM sequences in various scenarios (#69)
+  * support dumps() callables returning bytes again (#64)
+  * add basic support for rfc7464 text sequences (#61)
+  * drop support for numbers.Number in type= arguments (#63) 
+
+-------------------------------------------------------------------

Old:
----
  2.0.0.tar.gz

New:
----
  3.1.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-jsonlines.spec ++++++
--- /var/tmp/diff_new_pack.WBnVUk/_old  2022-09-30 17:58:16.573310404 +0200
+++ /var/tmp/diff_new_pack.WBnVUk/_new  2022-09-30 17:58:16.577310412 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-jsonlines
 #
-# Copyright (c) 2021 SUSE LLC
+# Copyright (c) 2022 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %global skip_python2 1
 Name:           python-jsonlines
-Version:        2.0.0
+Version:        3.1.0
 Release:        0
 Summary:        Library with helpers for the jsonlines file format
 License:        BSD-3-Clause

++++++ 2.0.0.tar.gz -> 3.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/.gitignore 
new/jsonlines-3.1.0/.gitignore
--- old/jsonlines-2.0.0/.gitignore      2021-01-04 17:03:18.000000000 +0100
+++ new/jsonlines-3.1.0/.gitignore      2022-07-01 18:35:18.000000000 +0200
@@ -4,8 +4,9 @@
 
 # Testing
 /.coverage
-/.tox/
+/coverage.xml
 /htmlcov/
+/.tox/
 
 # Packaging
 /.cache/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/LICENSE.rst 
new/jsonlines-3.1.0/LICENSE.rst
--- old/jsonlines-2.0.0/LICENSE.rst     2021-01-04 17:03:18.000000000 +0100
+++ new/jsonlines-3.1.0/LICENSE.rst     2022-07-01 18:35:18.000000000 +0200
@@ -1,6 +1,6 @@
 *(This is the OSI approved 3-clause "New BSD License".)*
 
-Copyright ?? 2016, Wouter Bolsterlee
+Copyright ?? 2016, wouter bolsterlee
 
 All rights reserved.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/README.rst 
new/jsonlines-3.1.0/README.rst
--- old/jsonlines-2.0.0/README.rst      2021-01-04 17:03:18.000000000 +0100
+++ new/jsonlines-3.1.0/README.rst      2022-07-01 18:35:18.000000000 +0200
@@ -7,6 +7,9 @@
 .. image:: https://pepy.tech/badge/jsonlines/month
    :target: https://pepy.tech/project/jsonlines
 
+.. image:: https://anaconda.org/anaconda/anaconda/badges/installer/conda.svg
+   :target: https://anaconda.org/anaconda/jsonlines
+
 =========
 jsonlines
 =========
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/doc/index.rst 
new/jsonlines-3.1.0/doc/index.rst
--- old/jsonlines-2.0.0/doc/index.rst   2021-01-04 17:03:18.000000000 +0100
+++ new/jsonlines-3.1.0/doc/index.rst   2022-07-01 18:35:18.000000000 +0200
@@ -179,6 +179,34 @@
 Version history
 ===============
 
+* 3.1.0, released at 2022-07-01
+
+  * Return number of chars/bytes written by :py:meth:`Writer.write()`
+    and :py:meth:`~Writer.write_all()`
+    (`#73 <https://github.com/wbolster/jsonlines/pull/73>`_)
+
+  * allow ``mode='x'`` in :py:func:`~jsonlines.open()`
+    to open a file for exclusive creation
+    (`#74 <https://github.com/wbolster/jsonlines/issues/74>`_)
+
+* 3.0.0, released at 2021-12-04
+
+  * add type annotations; adopt mypy in strict mode
+    (`#58 <https://github.com/wbolster/jsonlines/pull/58>`_,
+    `#62 <https://github.com/wbolster/jsonlines/pull/62>`_)
+
+  * ignore UTF-8 BOM sequences in various scenarios
+    (`#69 <https://github.com/wbolster/jsonlines/pull/69>`_)
+
+  * support ``dumps()`` callables returning bytes again
+    (`#64 <https://github.com/wbolster/jsonlines/issues/64>`_)
+
+  * add basic support for rfc7464 text sequences
+    (`#61 <https://github.com/wbolster/jsonlines/pull/61>`_)
+
+  * drop support for ``numbers.Number`` in ``type=`` arguments
+    (`#63 <https://github.com/wbolster/jsonlines/issues/63>`_)
+
 * 2.0.0, released at 2021-01-04
 
   * drop support for end-of-life Python versions; this package is now
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/jsonlines/jsonlines.py 
new/jsonlines-3.1.0/jsonlines/jsonlines.py
--- old/jsonlines-2.0.0/jsonlines/jsonlines.py  2021-01-04 17:03:18.000000000 
+0100
+++ new/jsonlines-3.1.0/jsonlines/jsonlines.py  2022-07-01 18:35:18.000000000 
+0200
@@ -3,9 +3,36 @@
 """
 
 import builtins
+import codecs
+import enum
+import io
 import json
-import numbers
+import os
+import sys
+import types
+import typing
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    Iterable,
+    Iterator,
+    List,
+    Optional,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+    cast,
+    overload,
+)
+
+if sys.version_info >= (3, 8):
+    from typing import Literal
+else:
+    from typing_extensions import Literal  # pragma: no cover
 
+import attr
 
 VALID_TYPES = {
     bool,
@@ -13,19 +40,59 @@
     float,
     int,
     list,
-    numbers.Number,
     str,
 }
 
+# Characters to skip at the beginning of a line. Note: at most one such
+# character is skipped per line.
+SKIPPABLE_SINGLE_INITIAL_CHARS = (
+    "\x1e",  # RFC7464 text sequence
+    codecs.BOM_UTF8.decode(),
+)
 
+
+class DumpsResultConversion(enum.Enum):
+    LeaveAsIs = enum.auto()
+    EncodeToBytes = enum.auto()
+    DecodeToString = enum.auto()
+
+
+# https://docs.python.org/3/library/functions.html#open
+Openable = Union[str, bytes, int, os.PathLike]
+
+LoadsCallable = Callable[[Union[str, bytes]], Any]
+DumpsCallable = Callable[[Any], Union[str, bytes]]
+
+# Currently, JSON structures cannot be typed properly:
+# - https://github.com/python/typing/issues/182
+# - https://github.com/python/mypy/issues/731
+JSONCollection = Union[Dict[str, Any], List[Any]]
+JSONScalar = Union[bool, float, int, str]
+JSONValue = Union[JSONCollection, JSONScalar]
+TJSONValue = TypeVar("TJSONValue", bound=JSONValue)
+
+TRW = TypeVar("TRW", bound="ReaderWriterBase")
+
+default_loads = json.loads
+
+
+def default_dumps(obj: Any) -> str:
+    """
+    Fake dumps() function to use as a default marker.
+    """
+    raise NotImplementedError  # pragma: no cover
+
+
+@attr.s(auto_exc=True, auto_attribs=True)
 class Error(Exception):
     """
     Base error class.
     """
 
-    pass
+    message: str
 
 
+@attr.s(auto_exc=True, auto_attribs=True, init=False)
 class InvalidLineError(Error, ValueError):
     """
     Error raised when an invalid line is encountered.
@@ -42,24 +109,30 @@
     """
 
     #: The invalid line
-    line = None
+    line: Union[str, bytes]
 
     #: The line number
-    lineno = None
+    lineno: int
 
-    def __init__(self, msg, line, lineno):
-        msg = f"{msg} (line {lineno})"
+    def __init__(self, message: str, line: Union[str, bytes], lineno: int) -> 
None:
         self.line = line.rstrip()
         self.lineno = lineno
-        super().__init__(msg)
+        super().__init__(f"{message} (line {lineno})")
 
 
+@attr.s(auto_attribs=True, repr=False)
 class ReaderWriterBase:
     """
     Base class with shared behaviour for both the reader and writer.
     """
 
-    def close(self):
+    _fp: Union[typing.IO[str], typing.IO[bytes], None] = attr.ib(
+        default=None, init=False
+    )
+    _closed: bool = attr.ib(default=False, init=False)
+    _should_close_fp: bool = attr.ib(default=False, init=False)
+
+    def close(self) -> None:
         """
         Close this reader/writer.
 
@@ -70,27 +143,30 @@
         if self._closed:
             return
         self._closed = True
-        if self._should_close_fp:
+        if self._fp is not None and self._should_close_fp:
             self._fp.close()
 
-    def __repr__(self):
-        name = getattr(self._fp, "name", None)
-        if name:
-            wrapping = repr(name)
-        else:
-            wrapping = "<{} at 0x{:x}>".format(type(self._fp).__name__, 
id(self._fp))
-        return "<jsonlines.{} at 0x{:x} wrapping {}>".format(
-            type(self).__name__, id(self), wrapping
-        )
+    def __repr__(self) -> str:
+        cls_name = type(self).__name__
+        wrapped = self._repr_for_wrapped()
+        return f"<jsonlines.{cls_name} at 0x{id(self):x} wrapping {wrapped}>"
 
-    def __enter__(self):
+    def _repr_for_wrapped(self) -> str:
+        raise NotImplementedError  # pragma: no cover
+
+    def __enter__(self: TRW) -> TRW:
         return self
 
-    def __exit__(self, *exc_info):
+    def __exit__(
+        self,
+        exc_type: Optional[Type[BaseException]],
+        exc_val: Optional[BaseException],
+        exc_tb: Optional[types.TracebackType],
+    ) -> None:
         self.close()
-        return False
 
 
+@attr.s(auto_attribs=True, repr=False)
 class Reader(ReaderWriterBase):
     """
     Reader for the jsonlines format.
@@ -100,33 +176,100 @@
     an open file or an ``io.TextIO`` instance, but it can also be
     something else as long as it yields strings when iterated over.
 
+    Instances are iterable and can be used as a context manager.
+
     The `loads` argument can be used to replace the standard json
     decoder. If specified, it must be a callable that accepts a
     (unicode) string and returns the decoded object.
 
-    Instances are iterable and can be used as a context manager.
-
-    :param file-like iterable: iterable yielding lines as strings
-    :param callable loads: custom json decoder callable
-    """
+    :param file_or_iterable: file-like object or iterable yielding lines as
+        strings
+    :param loads: custom json decoder callable
+    """
+
+    _file_or_iterable: Union[
+        typing.IO[str], typing.IO[bytes], Iterable[Union[str, bytes]]
+    ]
+    _line_iter: Iterator[Tuple[int, Union[bytes, str]]] = attr.ib(init=False)
+    _loads: LoadsCallable = attr.ib(default=default_loads, kw_only=True)
+
+    def __attrs_post_init__(self) -> None:
+        if isinstance(self._file_or_iterable, io.IOBase):
+            self._fp = cast(
+                Union[typing.IO[str], typing.IO[bytes]],
+                self._file_or_iterable,
+            )
 
-    def __init__(self, iterable, loads=None):
-        self._fp = iterable
-        self._should_close_fp = False
-        self._closed = False
-        if loads is None:
-            loads = json.loads
-        self._loads = loads
-        self._line_iter = enumerate(iterable, 1)
+        self._line_iter = enumerate(self._file_or_iterable, 1)
 
-    def read(self, type=None, allow_none=False, skip_empty=False):
+    # No type specified, None not allowed
+    @overload
+    def read(
+        self,
+        *,
+        type: Literal[None] = ...,
+        allow_none: Literal[False] = ...,
+        skip_empty: bool = ...,
+    ) -> JSONValue:
+        ...  # pragma: no cover
+
+    # No type specified, None allowed
+    @overload
+    def read(
+        self,
+        *,
+        type: Literal[None] = ...,
+        allow_none: Literal[True],
+        skip_empty: bool = ...,
+    ) -> Optional[JSONValue]:
+        ...  # pragma: no cover
+
+    # Type specified, None not allowed
+    @overload
+    def read(
+        self,
+        *,
+        type: Type[TJSONValue],
+        allow_none: Literal[False] = ...,
+        skip_empty: bool = ...,
+    ) -> TJSONValue:
+        ...  # pragma: no cover
+
+    # Type specified, None allowed
+    @overload
+    def read(
+        self,
+        *,
+        type: Type[TJSONValue],
+        allow_none: Literal[True],
+        skip_empty: bool = ...,
+    ) -> Optional[TJSONValue]:
+        ...  # pragma: no cover
+
+    # Generic definition
+    @overload
+    def read(
+        self,
+        *,
+        type: Optional[Type[Any]] = ...,
+        allow_none: bool = ...,
+        skip_empty: bool = ...,
+    ) -> Optional[JSONValue]:
+        ...  # pragma: no cover
+
+    def read(
+        self,
+        *,
+        type: Optional[Type[Any]] = None,
+        allow_none: bool = False,
+        skip_empty: bool = False,
+    ) -> Optional[JSONValue]:
         """
         Read and decode a line.
 
         The optional `type` argument specifies the expected data type.
         Supported types are ``dict``, ``list``, ``str``, ``int``,
-        ``float``, ``numbers.Number`` (accepts both integers and
-        floats), and ``bool``. When specified, non-conforming lines
+        ``float``, and ``bool``. When specified, non-conforming lines
         result in :py:exc:`InvalidLineError`.
 
         By default, input lines containing ``null`` (in JSON) are
@@ -158,8 +301,11 @@
                 )
                 raise exc from orig_exc
 
+        if line.startswith(SKIPPABLE_SINGLE_INITIAL_CHARS):
+            line = line[1:]
+
         try:
-            value = self._loads(line)
+            value: JSONValue = self._loads(line)
         except ValueError as orig_exc:
             exc = InvalidLineError(
                 f"line contains invalid json: {orig_exc}", line, lineno
@@ -173,8 +319,9 @@
 
         if type is not None:
             valid = isinstance(value, type)
-            if type in (int, numbers.Number):
-                valid = valid and not isinstance(value, bool)
+            if type is int and isinstance(value, bool):
+                # isinstance() is not sufficient, since bool is an int subclass
+                valid = False
             if not valid:
                 raise InvalidLineError(
                     "line does not match requested type", line, lineno
@@ -182,7 +329,73 @@
 
         return value
 
-    def iter(self, type=None, allow_none=False, skip_empty=False, 
skip_invalid=False):
+    # No type specified, None not allowed
+    @overload
+    def iter(
+        self,
+        *,
+        type: Literal[None] = ...,
+        allow_none: Literal[False] = ...,
+        skip_empty: bool = ...,
+        skip_invalid: bool = ...,
+    ) -> Iterator[JSONValue]:
+        ...  # pragma: no cover
+
+    # No type specified, None allowed
+    @overload
+    def iter(
+        self,
+        *,
+        type: Literal[None] = ...,
+        allow_none: Literal[True],
+        skip_empty: bool = ...,
+        skip_invalid: bool = ...,
+    ) -> Iterator[JSONValue]:
+        ...  # pragma: no cover
+
+    # Type specified, None not allowed
+    @overload
+    def iter(
+        self,
+        *,
+        type: Type[TJSONValue],
+        allow_none: Literal[False] = ...,
+        skip_empty: bool = ...,
+        skip_invalid: bool = ...,
+    ) -> Iterator[TJSONValue]:
+        ...  # pragma: no cover
+
+    # Type specified, None allowed
+    @overload
+    def iter(
+        self,
+        *,
+        type: Type[TJSONValue],
+        allow_none: Literal[True],
+        skip_empty: bool = ...,
+        skip_invalid: bool = ...,
+    ) -> Iterator[Optional[TJSONValue]]:
+        ...  # pragma: no cover
+
+    # Generic definition
+    @overload
+    def iter(
+        self,
+        *,
+        type: Optional[Type[TJSONValue]] = ...,
+        allow_none: bool = ...,
+        skip_empty: bool = ...,
+        skip_invalid: bool = ...,
+    ) -> Iterator[Optional[TJSONValue]]:
+        ...  # pragma: no cover
+
+    def iter(
+        self,
+        type: Optional[Type[Any]] = None,
+        allow_none: bool = False,
+        skip_empty: bool = False,
+        skip_invalid: bool = False,
+    ) -> Iterator[Optional[JSONValue]]:
         """
         Iterate over all lines.
 
@@ -209,17 +422,26 @@
         except EOFError:
             pass
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[Any]:
         """
         See :py:meth:`~Reader.iter()`.
         """
         return self.iter()
 
+    def _repr_for_wrapped(self) -> str:
+        if self._fp is not None:
+            return repr_for_fp(self._fp)
+        class_name = type(self._file_or_iterable).__name__
+        return f"<{class_name} at 0x{id(self._file_or_iterable):x}>"
+
 
+@attr.s(auto_attribs=True, repr=False)
 class Writer(ReaderWriterBase):
     """
     Writer for the jsonlines format.
 
+    Instances can be used as a context manager.
+
     The `fp` argument must be a file-like object with a ``.write()``
     method accepting either text (unicode) or bytes.
 
@@ -235,63 +457,144 @@
     When the `flush` argument is set to ``True``, the writer will call
     ``fp.flush()`` after each written line.
 
-    Instances can be used as a context manager.
-
-    :param file-like fp: writable file-like object
-    :param bool compact: whether to use a compact output format
-    :param bool sort_keys: whether to sort object keys
-    :param callable dumps: custom encoder callable
-    :param bool flush: whether to flush the file-like object after
-        writing each line
-    """
+    :param fp: writable file-like object
+    :param compact: whether to use a compact output format
+    :param sort_keys: whether to sort object keys
+    :param dumps: custom encoder callable
+    :param flush: whether to flush the file-like object after writing each line
+    """
+
+    _fp: Union[typing.IO[str], typing.IO[bytes]] = attr.ib(default=None)
+    _fp_is_binary: bool = attr.ib(default=False, init=False)
+    _compact: bool = attr.ib(default=False, kw_only=True)
+    _sort_keys: bool = attr.ib(default=False, kw_only=True)
+    _flush: bool = attr.ib(default=False, kw_only=True)
+    _dumps: DumpsCallable = attr.ib(default=default_dumps, kw_only=True)
+    _dumps_result_conversion: DumpsResultConversion = attr.ib(
+        default=DumpsResultConversion.LeaveAsIs, init=False
+    )
 
-    def __init__(self, fp, compact=False, sort_keys=False, dumps=None, 
flush=False):
-        self._closed = False
-        try:
-            fp.write("")
+    def __attrs_post_init__(self) -> None:
+        if isinstance(self._fp, io.TextIOBase):
             self._fp_is_binary = False
-        except TypeError:
+        elif isinstance(self._fp, io.IOBase):
             self._fp_is_binary = True
-        if dumps is None:
-            encoder_kwargs = dict(ensure_ascii=False, sort_keys=sort_keys)
-            if compact:
+        else:
+            try:
+                self._fp.write("")  # type: ignore[arg-type]
+            except TypeError:
+                self._fp_is_binary = True
+            else:
+                self._fp_is_binary = False
+
+        if self._dumps is default_dumps:
+            encoder_kwargs: Dict[str, Any] = dict(
+                ensure_ascii=False,
+                sort_keys=self._sort_keys,
+            )
+            if self._compact:
                 encoder_kwargs.update(separators=(",", ":"))
-            dumps = json.JSONEncoder(**encoder_kwargs).encode
-        self._fp = fp
-        self._should_close_fp = False
-        self._dumps = dumps
-        self._flush = flush
+            self._dumps = json.JSONEncoder(**encoder_kwargs).encode
 
-    def write(self, obj):
+        # Detect if str-to-bytes conversion (or vice versa) is needed for the
+        # combination of this file-like object and the used dumps() callable.
+        # This avoids checking this for each .write(). Note that this
+        # deliberately does not support ???dynamic??? return types that depend 
on
+        # input and dump options, like simplejson on Python 2 in some cases.
+        sample_dumps_result = self._dumps({})
+        if isinstance(sample_dumps_result, str) and self._fp_is_binary:
+            self._dumps_result_conversion = DumpsResultConversion.EncodeToBytes
+        elif isinstance(sample_dumps_result, bytes) and not self._fp_is_binary:
+            self._dumps_result_conversion = 
DumpsResultConversion.DecodeToString
+
+    def write(self, obj: Any) -> int:
         """
         Encode and write a single object.
 
         :param obj: the object to encode and write
+        :return: number of characters or bytes written
         """
         if self._closed:
             raise RuntimeError("writer is closed")
+
         line = self._dumps(obj)
-        if self._fp_is_binary:
-            line = line.encode("utf-8")
-            self._fp.write(line)
-            self._fp.write(b"\n")
-        else:
-            self._fp.write(line)
-            self._fp.write("\n")
+
+        # This handles either str or bytes, but the type checker does not know
+        # that this code always passes the right type of arguments.
+        if self._dumps_result_conversion == 
DumpsResultConversion.EncodeToBytes:
+            line = line.encode()  # type: ignore[union-attr]
+        elif self._dumps_result_conversion == 
DumpsResultConversion.DecodeToString:
+            line = line.decode()  # type: ignore[union-attr]
+
+        fp = self._fp
+        fp.write(line)  # type: ignore[arg-type]
+        fp.write(b"\n" if self._fp_is_binary else "\n")  # type: 
ignore[arg-type]
+
         if self._flush:
-            self._fp.flush()
+            fp.flush()
+
+        return len(line) + 1  # including newline
 
-    def write_all(self, iterable):
+    def write_all(self, iterable: Iterable[Any]) -> int:
         """
         Encode and write multiple objects.
 
         :param iterable: an iterable of objects
+        :return: number of characters or bytes written
         """
-        for obj in iterable:
-            self.write(obj)
+        return sum(self.write(obj) for obj in iterable)
 
+    def _repr_for_wrapped(self) -> str:
+        return repr_for_fp(self._fp)
 
-def open(name, mode="r", **kwargs):
+
+@overload
+def open(
+    file: Openable,
+    mode: Literal["r"] = ...,
+    *,
+    loads: Optional[LoadsCallable] = ...,
+) -> Reader:
+    ...  # pragma: no cover
+
+
+@overload
+def open(
+    file: Openable,
+    mode: Literal["w", "a", "x"],
+    *,
+    dumps: Optional[DumpsCallable] = ...,
+    compact: Optional[bool] = ...,
+    sort_keys: Optional[bool] = ...,
+    flush: Optional[bool] = ...,
+) -> Writer:
+    ...  # pragma: no cover
+
+
+@overload
+def open(
+    file: Openable,
+    mode: str = ...,
+    *,
+    loads: Optional[LoadsCallable] = ...,
+    dumps: Optional[DumpsCallable] = ...,
+    compact: Optional[bool] = ...,
+    sort_keys: Optional[bool] = ...,
+    flush: Optional[bool] = ...,
+) -> Union[Reader, Writer]:
+    ...  # pragma: no cover
+
+
+def open(
+    file: Openable,
+    mode: str = "r",
+    *,
+    loads: Optional[LoadsCallable] = None,
+    dumps: Optional[DumpsCallable] = None,
+    compact: Optional[bool] = None,
+    sort_keys: Optional[bool] = None,
+    flush: Optional[bool] = None,
+) -> Union[Reader, Writer]:
     """
     Open a jsonlines file for reading or writing.
 
@@ -312,17 +615,35 @@
         with jsonlines.open('out.jsonl', mode='w') as writer:
             writer.write(...)
 
-    :param file-like fp: name of the file to open
-    :param str mode: whether to open the file for reading (``r``),
+    :param file: name or ???path-like object??? of the file to open
+    :param mode: whether to open the file for reading (``r``),
         writing (``w``) or appending (``a``).
-    :param **kwargs: additional arguments, forwarded to the reader or writer
     """
-    if mode not in {"r", "w", "a"}:
-        raise ValueError("'mode' must be either 'r', 'w', or 'a'")
-    fp = builtins.open(name, mode=mode + "t", encoding="utf-8")
-    if mode == "r":
-        instance = Reader(fp, **kwargs)
-    else:
-        instance = Writer(fp, **kwargs)
+    if mode not in {"r", "w", "a", "x"}:
+        raise ValueError("'mode' must be either 'r', 'w', 'a', or 'x'")
+
+    cls = Reader if mode == "r" else Writer
+    encoding = "utf-8-sig" if mode == "r" else "utf-8"
+    fp = builtins.open(file, mode=mode + "t", encoding=encoding)
+    kwargs = dict(
+        loads=loads,
+        dumps=dumps,
+        compact=compact,
+        sort_keys=sort_keys,
+        flush=flush,
+    )
+    kwargs = {key: value for key, value in kwargs.items() if value is not None}
+    instance: Union[Reader, Writer] = cls(fp, **kwargs)
     instance._should_close_fp = True
     return instance
+
+
+def repr_for_fp(fp: typing.IO[Any]) -> str:
+    """
+    Helper to make a useful repr() for a file-like object.
+    """
+    name = getattr(fp, "name", None)
+    if name is not None:
+        return repr(name)
+    else:
+        return repr(fp)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/mypy.ini new/jsonlines-3.1.0/mypy.ini
--- old/jsonlines-2.0.0/mypy.ini        1970-01-01 01:00:00.000000000 +0100
+++ new/jsonlines-3.1.0/mypy.ini        2022-07-01 18:35:18.000000000 +0200
@@ -0,0 +1,16 @@
+[mypy]
+check_untyped_defs = True
+disallow_any_generics = True
+disallow_incomplete_defs = True
+disallow_subclassing_any = True
+disallow_untyped_calls = True
+disallow_untyped_decorators = True
+disallow_untyped_defs = True
+no_implicit_optional = True
+no_implicit_reexport = True
+show_error_codes = True
+strict_equality = True
+warn_redundant_casts = True
+warn_return_any = True
+warn_unused_configs = True
+warn_unused_ignores = True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/requirements-dev.txt 
new/jsonlines-3.1.0/requirements-dev.txt
--- old/jsonlines-2.0.0/requirements-dev.txt    2021-01-04 17:03:18.000000000 
+0100
+++ new/jsonlines-3.1.0/requirements-dev.txt    2022-07-01 18:35:18.000000000 
+0200
@@ -1,5 +1,6 @@
 black
 flake8
+mypy
 pytest>=3
 pytest-cov
 sphinx
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/setup.cfg 
new/jsonlines-3.1.0/setup.cfg
--- old/jsonlines-2.0.0/setup.cfg       2021-01-04 17:03:18.000000000 +0100
+++ new/jsonlines-3.1.0/setup.cfg       2022-07-01 18:35:18.000000000 +0200
@@ -1,7 +1,7 @@
 [metadata]
 name = jsonlines
-version = 2.0.0
-author = Wouter Bolsterlee
+version = 3.1.0
+author = wouter bolsterlee
 author_email = wou...@bolsterl.ee
 license = BSD
 license_file = LICENSE.rst
@@ -24,6 +24,12 @@
 [options]
 packages = jsonlines
 python_requires = >=3.6
+install_requires =
+    attrs>=19.2.0
+    typing_extensions; python_version < "3.8"
+
+[options.package_data]
+jsonlines = py.typed
 
 [build_sphinx]
 source-dir = doc/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jsonlines-2.0.0/tests/sample-crlf-line-separators.jsonl 
new/jsonlines-3.1.0/tests/sample-crlf-line-separators.jsonl
--- old/jsonlines-2.0.0/tests/sample-crlf-line-separators.jsonl 2021-01-04 
17:03:18.000000000 +0100
+++ new/jsonlines-3.1.0/tests/sample-crlf-line-separators.jsonl 1970-01-01 
01:00:00.000000000 +0100
@@ -1,2 +0,0 @@
-{"a": 1}
-{"b": 2}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/tests/sample-no-eol-at-eof.jsonl 
new/jsonlines-3.1.0/tests/sample-no-eol-at-eof.jsonl
--- old/jsonlines-2.0.0/tests/sample-no-eol-at-eof.jsonl        2021-01-04 
17:03:18.000000000 +0100
+++ new/jsonlines-3.1.0/tests/sample-no-eol-at-eof.jsonl        1970-01-01 
01:00:00.000000000 +0100
@@ -1,2 +0,0 @@
-{"a": 1}
-{"b": 2}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/tests/sample.jsonl 
new/jsonlines-3.1.0/tests/sample.jsonl
--- old/jsonlines-2.0.0/tests/sample.jsonl      2021-01-04 17:03:18.000000000 
+0100
+++ new/jsonlines-3.1.0/tests/sample.jsonl      1970-01-01 01:00:00.000000000 
+0100
@@ -1,2 +0,0 @@
-{"a": 1}
-{"b": 2}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/tests/test_jsonlines.py 
new/jsonlines-3.1.0/tests/test_jsonlines.py
--- old/jsonlines-2.0.0/tests/test_jsonlines.py 2021-01-04 17:03:18.000000000 
+0100
+++ new/jsonlines-3.1.0/tests/test_jsonlines.py 2022-07-01 18:35:18.000000000 
+0200
@@ -2,11 +2,13 @@
 Tests for the jsonlines library.
 """
 
+import codecs
 import collections
 import io
-import jsonlines
+import json
 import tempfile
 
+import jsonlines
 import pytest
 
 
@@ -14,7 +16,7 @@
 SAMPLE_TEXT = SAMPLE_BYTES.decode("utf-8")
 
 
-def test_reader():
+def test_reader() -> None:
     fp = io.BytesIO(SAMPLE_BYTES)
     with jsonlines.Reader(fp) as reader:
         it = iter(reader)
@@ -26,13 +28,61 @@
             reader.read()
 
 
-def test_reading_from_iterable():
+def test_reading_from_iterable() -> None:
     with jsonlines.Reader(["1", b"{}"]) as reader:
         assert list(reader) == [1, {}]
     assert "wrapping <list at " in repr(reader)
 
 
-def test_writer_text():
+def test_reader_rfc7464_text_sequences() -> None:
+    fp = io.BytesIO(b'\x1e"a"\x0a\x1e"b"\x0a')
+    with jsonlines.Reader(fp) as reader:
+        assert list(reader) == ["a", "b"]
+
+
+def test_reader_utf8_bom_bytes() -> None:
+    """
+    UTF-8 BOM is ignored, even if it occurs in the middle of a stream.
+    """
+    chunks = [
+        codecs.BOM_UTF8,
+        b"1\n",
+        codecs.BOM_UTF8,
+        b"2\n",
+    ]
+    fp = io.BytesIO(b"".join(chunks))
+    with jsonlines.Reader(fp) as reader:
+        assert list(reader) == [1, 2]
+
+
+def test_reader_utf8_bom_text() -> None:
+    """
+    Text version of ``test_reader_utf8_bom_bytes()``.
+    """
+    chunks = [
+        "1\n",
+        codecs.BOM_UTF8.decode(),
+        "2\n",
+    ]
+    fp = io.StringIO("".join(chunks))
+    with jsonlines.Reader(fp) as reader:
+        assert list(reader) == [1, 2]
+
+
+def test_reader_utf8_bom_bom_bom() -> None:
+    """
+    Too many UTF-8 BOM BOM BOM chars cause BOOM ???? BOOM.
+    """
+    reader = jsonlines.Reader([codecs.BOM_UTF8.decode() * 3 + "1\n"])
+    with pytest.raises(jsonlines.InvalidLineError) as excinfo:
+        reader.read()
+
+    exc = excinfo.value
+    assert "invalid json" in str(exc)
+    assert isinstance(exc.__cause__, json.JSONDecodeError)
+
+
+def test_writer_text() -> None:
     fp = io.StringIO()
     with jsonlines.Writer(fp) as writer:
         writer.write({"a": 1})
@@ -40,7 +90,7 @@
     assert fp.getvalue() == SAMPLE_TEXT
 
 
-def test_writer_binary():
+def test_writer_binary() -> None:
     fp = io.BytesIO()
     with jsonlines.Writer(fp) as writer:
         writer.write_all(
@@ -52,7 +102,7 @@
     assert fp.getvalue() == SAMPLE_BYTES
 
 
-def test_closing():
+def test_closing() -> None:
     reader = jsonlines.Reader([])
     reader.close()
     with pytest.raises(RuntimeError):
@@ -64,7 +114,7 @@
         writer.write(123)
 
 
-def test_invalid_lines():
+def test_invalid_lines() -> None:
     data = "[1, 2"
     with jsonlines.Reader(io.StringIO(data)) as reader:
         with pytest.raises(jsonlines.InvalidLineError) as excinfo:
@@ -72,9 +122,10 @@
         exc = excinfo.value
         assert "invalid json" in str(exc)
         assert exc.line == data
+        assert isinstance(exc.__cause__, json.JSONDecodeError)
 
 
-def test_skip_invalid():
+def test_skip_invalid() -> None:
     fp = io.StringIO("12\ninvalid\n34")
     reader = jsonlines.Reader(fp)
     it = reader.iter(skip_invalid=True)
@@ -82,7 +133,7 @@
     assert next(it) == 34
 
 
-def test_empty_strings_in_iterable():
+def test_empty_strings_in_iterable() -> None:
     input = ["123", "", "456"]
     it = iter(jsonlines.Reader(input))
     assert next(it) == 123
@@ -94,14 +145,14 @@
     assert list(it) == [123, 456]
 
 
-def test_invalid_utf8():
+def test_invalid_utf8() -> None:
     with jsonlines.Reader([b"\xff\xff"]) as reader:
         with pytest.raises(jsonlines.InvalidLineError) as excinfo:
             reader.read()
         assert "line is not valid utf-8" in str(excinfo.value)
 
 
-def test_empty_lines():
+def test_empty_lines() -> None:
     data_with_empty_line = b"1\n\n2\n"
     with jsonlines.Reader(io.BytesIO(data_with_empty_line)) as reader:
         assert reader.read()
@@ -114,9 +165,16 @@
         assert list(reader.iter(skip_empty=True)) == [1, 2]
 
 
-def test_typed_reads():
-    with jsonlines.Reader(io.StringIO('12\n"foo"\n')) as reader:
+def test_typed_reads() -> None:
+    with jsonlines.Reader(io.StringIO('12\ntrue\n"foo"\n')) as reader:
         assert reader.read(type=int) == 12
+
+        with pytest.raises(jsonlines.InvalidLineError) as excinfo:
+            reader.read(type=int)
+        exc = excinfo.value
+        assert "does not match requested type" in str(exc)
+        assert exc.line == 'true'
+
         with pytest.raises(jsonlines.InvalidLineError) as excinfo:
             reader.read(type=float)
         exc = excinfo.value
@@ -124,7 +182,15 @@
         assert exc.line == '"foo"'
 
 
-def test_typed_iteration():
+def test_typed_read_invalid_type() -> None:
+    reader = jsonlines.Reader([])
+    with pytest.raises(ValueError) as excinfo:
+        reader.read(type="nope")  # type: ignore[call-overload]
+    exc = excinfo.value
+    assert str(exc) == "invalid type specified"
+
+
+def test_typed_iteration() -> None:
     fp = io.StringIO("1\n2\n")
     with jsonlines.Reader(fp) as reader:
         actual = list(reader.iter(type=int))
@@ -139,7 +205,7 @@
         assert "does not match requested type" in str(exc)
 
 
-def test_writer_flags():
+def test_writer_flags() -> None:
     fp = io.BytesIO()
     with jsonlines.Writer(fp, compact=True, sort_keys=True) as writer:
         writer.write(
@@ -153,21 +219,36 @@
     assert fp.getvalue() == b'{"a":1,"b":2}\n'
 
 
-def test_custom_dumps():
+def test_custom_dumps() -> None:
     fp = io.BytesIO()
     writer = jsonlines.Writer(fp, dumps=lambda obj: "oh hai")
     with writer:
-        writer.write({})
+        nbytes = writer.write({})
+        assert nbytes == len(b"oh hai\n")
+
     assert fp.getvalue() == b"oh hai\n"
 
 
-def test_custom_loads():
+def test_custom_dumps_bytes() -> None:
+    """
+    A custom dump function that returns bytes (e.g. ???orjson???) should work.
+    """
+
+    fp = io.BytesIO()
+    writer = jsonlines.Writer(fp, dumps=lambda obj: b"some bytes")
+    with writer:
+        writer.write(123)
+
+    assert fp.getvalue() == b"some bytes\n"
+
+
+def test_custom_loads() -> None:
     fp = io.BytesIO(b"{}\n")
     with jsonlines.Reader(fp, loads=lambda s: "uh what") as reader:
         assert reader.read() == "uh what"
 
 
-def test_open_reading():
+def test_open_reading() -> None:
     with tempfile.NamedTemporaryFile("wb") as fp:
         fp.write(b"123\n")
         fp.flush()
@@ -175,7 +256,19 @@
             assert list(reader) == [123]
 
 
-def test_open_writing():
+def test_open_reading_with_utf8_bom() -> None:
+    """
+    The ``.open()`` helper ignores a UTF-8 BOM.
+    """
+    with tempfile.NamedTemporaryFile("wb") as fp:
+        fp.write(codecs.BOM_UTF8)
+        fp.write(b"123\n")
+        fp.flush()
+        with jsonlines.open(fp.name) as reader:
+            assert list(reader) == [123]
+
+
+def test_open_writing() -> None:
     with tempfile.NamedTemporaryFile("w+b") as fp:
         with jsonlines.open(fp.name, mode="w") as writer:
             writer.write(123)
@@ -183,16 +276,25 @@
     assert fp.name in repr(writer)
 
 
-def test_open_and_append_writing():
+def test_open_and_append_writing() -> None:
     with tempfile.NamedTemporaryFile("w+b") as fp:
         with jsonlines.open(fp.name, mode="w") as writer:
-            writer.write(123)
+            nbytes = writer.write(123)
+            assert nbytes == len(str(123)) + 1
         with jsonlines.open(fp.name, mode="a") as writer:
-            writer.write(456)
+            nbytes = writer.write(456)
+            assert nbytes == len(str(456)) + 1
         assert fp.read() == b"123\n456\n"
 
 
-def test_open_invalid_mode():
+def test_open_invalid_mode() -> None:
     with pytest.raises(ValueError) as excinfo:
         jsonlines.open("foo", mode="foo")
     assert "mode" in str(excinfo.value)
+
+
+def test_single_char_stripping() -> None:
+    """ "
+    Sanity check that a helper constant actually contains single-char strings.
+    """
+    assert all(len(s) == 1 for s in 
jsonlines.jsonlines.SKIPPABLE_SINGLE_INITIAL_CHARS)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/tests/test_typing.py 
new/jsonlines-3.1.0/tests/test_typing.py
--- old/jsonlines-2.0.0/tests/test_typing.py    1970-01-01 01:00:00.000000000 
+0100
+++ new/jsonlines-3.1.0/tests/test_typing.py    2022-07-01 18:35:18.000000000 
+0200
@@ -0,0 +1,96 @@
+"""
+This file should give any type checking errors.
+"""
+
+import io
+import json
+import random
+
+import numbers
+from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional
+
+
+if not TYPE_CHECKING:
+
+    def reveal_type(obj: Any) -> None:
+        pass
+
+
+import jsonlines
+
+
+def something_with_reader() -> None:
+
+    reader: jsonlines.Reader
+    reader = jsonlines.Reader(io.StringIO())
+    reader = jsonlines.Reader(io.BytesIO())
+    reader = jsonlines.Reader(['"text"'])
+    reader = jsonlines.Reader([b'"bytes"'])
+
+    r1 = reader.read()
+    r2 = reader.read(allow_none=True)
+    r3: numbers.Number = reader.read(type=random.choice([int, float]))
+
+    # For debugging:
+    # reveal_type(r1)
+    # reveal_type(r2)
+    # reveal_type(r3)
+
+    some_int: int = reader.read(type=int)
+    maybe_int: Optional[int] = reader.read(type=int, allow_none=True)
+
+    some_float: float = reader.read(type=float)
+    maybe_float: Optional[float] = reader.read(type=float, allow_none=True)
+
+    some_bool: bool = reader.read(type=bool)
+    maybe_bool: Optional[bool] = reader.read(type=bool, allow_none=True)
+
+    some_dict: Dict[str, Any] = reader.read(type=dict)
+    optional_dict: Optional[Dict[str, Any]] = reader.read(type=dict, 
allow_none=True)
+
+    some_list: List[Any] = reader.read(type=list)
+    maybe_list: Optional[List[Any]] = reader.read(type=list, allow_none=True)
+
+    iter_int: Iterable[int] = reader.iter(type=int)
+    iter_str: Iterable[str] = reader.iter(type=str)
+    iter_dict: Iterable[Dict[str, Any]] = reader.iter(type=dict)
+    iter_optional_str: Iterable[Optional[str]] = reader.iter(type=str, 
allow_none=True)
+
+    locals()  # Silence flake8 F841
+
+
+def something_with_writer() -> None:
+    writer: jsonlines.Writer
+    writer = jsonlines.Writer(io.StringIO())
+    writer = jsonlines.Writer(io.BytesIO())
+
+    locals()  # Silence flake8 F841
+
+
+def something_with_open() -> None:
+    name = "/nonexistent"
+
+    reader: jsonlines.Reader
+    reader = jsonlines.open(name)
+    reader = jsonlines.open(name, "r")
+    reader = jsonlines.open(name, mode="r")
+    reader = jsonlines.open(
+        name,
+        mode="r",
+        loads=json.loads,
+    )
+
+    writer: jsonlines.Writer
+    writer = jsonlines.open(name, "w")
+    writer = jsonlines.open(name, mode="w")
+    writer = jsonlines.open(name, "a")
+    writer = jsonlines.open(
+        name,
+        mode="w",
+        dumps=json.dumps,
+        compact=True,
+        sort_keys=True,
+        flush=True,
+    )
+
+    locals()  # Silence flake8 F841
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonlines-2.0.0/tox.ini new/jsonlines-3.1.0/tox.ini
--- old/jsonlines-2.0.0/tox.ini 2021-01-04 17:03:18.000000000 +0100
+++ new/jsonlines-3.1.0/tox.ini 2022-07-01 18:35:18.000000000 +0200
@@ -1,5 +1,5 @@
 [tox]
-envlist = py36,py37,py38,py39,flake8
+envlist = py310,py39,py38,py37,py36,linters
 
 [testenv]
 deps = -rrequirements-dev.txt
@@ -11,3 +11,4 @@
 commands =
   flake8 jsonlines/ tests/
   black --check jsonlines/ tests/
+  mypy --strict jsonlines/ tests/

Reply via email to