Skip to content

Commit 3551a8c

Browse files
authored
Merge pull request #495 from itamarst/494-python-312-support
Python 3.11 and 3.12 support
2 parents 74e0aa2 + 3f7da18 commit 3551a8c

28 files changed

+1357
-815
lines changed

.github/workflows/main.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ jobs:
1515

1616
strategy:
1717
matrix:
18-
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy3"]
18+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9", "pypy3.10"]
1919

2020
steps:
21-
- uses: "actions/checkout@v2"
22-
- uses: "actions/setup-python@v2"
21+
- uses: "actions/checkout@v3"
22+
- uses: "actions/setup-python@v4"
2323
with:
2424
python-version: "${{ matrix.python-version }}"
2525
- name: "Install dependencies"
@@ -28,7 +28,7 @@ jobs:
2828
python -VV
2929
python -m site
3030
python -m pip install --upgrade pip setuptools wheel
31-
python -m pip install --upgrade virtualenv tox tox-gh-actions
31+
python -m pip install --upgrade virtualenv tox tox-gh-actions
3232
3333
- name: "Run tox targets for ${{ matrix.python-version }}"
3434
run: "python -m tox"

README.rst

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
Eliot: Logging that tells you *why* it happened
22
================================================
33

4-
.. image:: https://travis-ci.org/itamarst/eliot.png?branch=master
5-
:target: http://travis-ci.org/itamarst/eliot
6-
:alt: Build Status
7-
84
Python's built-in ``logging`` and other similar systems output a stream of factoids: they're interesting, but you can't really tell what's going on.
95

106
* Why is your application slow?
@@ -29,7 +25,7 @@ Eliot supports a range of use cases and 3rd party libraries:
2925

3026
Eliot is only used to generate your logs; you will might need tools like Logstash and ElasticSearch to aggregate and store logs if you are using multiple processes across multiple machines.
3127

32-
Eliot supports Python 3.6, 3.7, 3.8, 3.9, and 3.10, as well as PyPy3.
28+
Eliot supports Python 3.8-3.12, as well as PyPy3.
3329
It is maintained by Itamar Turner-Trauring, and released under the Apache 2.0 License.
3430

3531
* `Read the documentation <https://eliot.readthedocs.io>`_.

benchmarks/serialization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# Ensure JSON serialization is part of benchmark:
1616
to_file(open("/dev/null", "w"))
1717

18-
N = 10000
18+
N = 100_000
1919

2020

2121
def run():

docs/source/news.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
What's New
22
==========
33

4+
1.15.0
5+
^^^^^^
6+
7+
Enhancements:
8+
9+
* Switched to JSON serialization with ``orjson`` (on CPython), which is much faster.
10+
* Added support for Python 3.11 and 3.12.
11+
12+
Changes:
13+
14+
* JSON customization is now done with a default function rather than an encoder class.
15+
* ``NaN``, ``inf``, and ``-inf`` are now serialized to JSON as ``null``, as per ``orjson`` this is more standards compliant.
16+
17+
Deprecation and removals:
18+
19+
* The deprecated support for serializing ``bytes`` in JSON log messages has been removed.
20+
* Dropped support for Python 3.6 and 3.7.
21+
* Removed the deprecated ``eliot.logwriter.ThreadedFileWriter``.
22+
423
1.14.0
524
^^^^^^
625

docs/source/outputting/output.rst

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,28 @@ Customizing JSON Encoding
4545
-------------------------
4646

4747
If you're using Eliot's JSON output you may wish to customize encoding.
48-
By default Eliot uses ``eliot.json.EliotJSONEncoder`` (a subclass of ``json.JSONEncoder``) to encode objects.
49-
You can customize encoding by passing a custom subclass to either ``eliot.FileDestination`` or ``eliot.to_file``:
48+
By default Eliot uses ``eliot.json.json_default`` to encode objects that the default JSON serializer doesn't handle.
49+
You can customize encoding by passing a custom function to either ``eliot.FileDestination`` or ``eliot.to_file``:
5050

5151
.. code-block:: python
5252
53-
from eliot.json import EliotJSONEncoder
53+
from eliot.json import json_default
5454
from eliot import to_file
5555
5656
5757
class MyClass:
5858
def __init__(self, x):
5959
self.x = x
6060
61-
class MyEncoder(EliotJSONEncoder):
62-
def default(self, obj):
63-
if isinstance(obj, MyClass):
64-
return {"x": obj.x}
65-
return EliotJSONEncoder.default(self, obj)
61+
def default(self, obj):
62+
if isinstance(obj, MyClass):
63+
return {"x": obj.x}
64+
return json_default(obj)
6665
67-
to_file(open("eliot.log", "ab"), encoder=MyEncoder)
66+
to_file(open("eliot.log", "ab"), default=default)
6867
6968
For more details on JSON encoding see the Python `JSON documentation <https://docs.python.org/3/library/json.html>`_.
69+
Note that Eliot uses `orjson <https://pypi.org/project/orjson/>`_ on CPython (for performance), and the built-in JSON module on PyPy.
7070

7171
.. _add_global_fields:
7272

eliot/__init__.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,6 @@
22
Eliot: Logging for Complex & Distributed Systems.
33
"""
44
from warnings import warn
5-
from sys import version_info
6-
7-
# Enable asyncio contextvars support in Python 3.5/3.6:
8-
if version_info < (3, 7):
9-
# On Python 3.5.2 and earlier, some of the necessary attributes aren't exposed:
10-
if version_info < (3, 5, 3):
11-
raise RuntimeError(
12-
"This version of Eliot doesn't work on Python 3.5.2 or earlier. "
13-
"Either upgrade to Python 3.5.3 or later (on Ubuntu 16.04 "
14-
"you can use https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa "
15-
"to get Python 3.6), or pin Eliot to version 1.7."
16-
)
17-
import aiocontextvars
18-
19-
dir(aiocontextvars) # pacify pyflakes
20-
del aiocontextvars
215

226
# Expose the public API:
237
from ._message import Message
@@ -34,7 +18,7 @@
3418
from ._validation import Field, fields, MessageType, ActionType, ValidationError
3519
from ._traceback import write_traceback, writeFailure
3620
from ._errors import register_exception_extractor
37-
from ._version import get_versions
21+
3822

3923
# Backwards compatibility:
4024
def add_destination(destination):
@@ -69,6 +53,7 @@ def use_asyncio_context():
6953
remove_destination = removeDestination
7054
add_global_fields = addGlobalFields
7155

56+
7257
# Backwards compatibility for old versions of eliot-tree, which rely on
7358
# eliot._parse:
7459
def _parse_compat():
@@ -127,5 +112,6 @@ def _parse_compat():
127112
]
128113

129114

130-
__version__ = get_versions()["version"]
131-
del get_versions
115+
from . import _version
116+
117+
__version__ = _version.get_versions()["version"]

eliot/_bytesjson.py

Lines changed: 0 additions & 55 deletions
This file was deleted.

eliot/_output.py

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44

55
import traceback
66
import inspect
7-
import json as pyjson
87
from threading import Lock
98
from functools import wraps
109
from io import IOBase
10+
import warnings
1111

1212
from pyrsistent import PClass, field
1313

14-
from . import _bytesjson as bytesjson
1514
from zope.interface import Interface, implementer
1615

1716
from ._traceback import write_traceback, TRACEBACK_MESSAGE
1817
from ._message import EXCEPTION_FIELD, MESSAGE_TYPE_FIELD, REASON_FIELD
1918
from ._util import saferepr, safeunicode
20-
from .json import EliotJSONEncoder
19+
from .json import (
20+
json_default,
21+
_encoder_to_default_function,
22+
_dumps_bytes,
23+
_dumps_unicode,
24+
)
2125
from ._validation import ValidationError
2226

2327

@@ -260,12 +264,19 @@ class MemoryLogger(object):
260264
not mutate this list.
261265
"""
262266

263-
def __init__(self, encoder=EliotJSONEncoder):
267+
def __init__(self, encoder=None, json_default=json_default):
264268
"""
265-
@param encoder: A JSONEncoder subclass to use when encoding JSON.
269+
@param encoder: DEPRECATED. A JSONEncoder subclass to use when
270+
encoding JSON.
271+
272+
@param json_default: A callable that handles objects the default JSON
273+
serializer can't handle.
266274
"""
275+
json_default = _json_default_from_encoder_and_json_default(
276+
encoder, json_default
277+
)
267278
self._lock = Lock()
268-
self._encoder = encoder
279+
self._json_default = json_default
269280
self.reset()
270281

271282
@exclusively
@@ -346,7 +357,7 @@ def _validate_message(self, dictionary, serializer):
346357
serializer.serialize(dictionary)
347358

348359
try:
349-
pyjson.dumps(dictionary, cls=self._encoder)
360+
_dumps_unicode(dictionary, default=self._json_default)
350361
except Exception as e:
351362
raise TypeError("Message %s doesn't encode to JSON: %s" % (dictionary, e))
352363

@@ -409,13 +420,26 @@ def reset(self):
409420
self._failed_validations = []
410421

411422

423+
def _json_default_from_encoder_and_json_default(encoder, json_default):
424+
if encoder is not None:
425+
warnings.warn(
426+
"Using a JSON encoder subclass is no longer supported, please switch to using a default function",
427+
DeprecationWarning,
428+
stacklevel=3,
429+
)
430+
from .json import json_default as default_json_default
431+
432+
if json_default is not default_json_default:
433+
raise RuntimeError("Can't pass in both encoder and default function")
434+
435+
json_default = _encoder_to_default_function(encoder())
436+
return json_default
437+
438+
412439
class FileDestination(PClass):
413440
"""
414-
Callable that writes JSON messages to a file.
415-
416-
On Python 3 the file may support either C{bytes} or C{unicode}. On
417-
Python 2 only C{bytes} are supported since that is what all files expect
418-
in practice.
441+
Callable that writes JSON messages to a file that accepts either C{bytes}
442+
or C{str}.
419443
420444
@ivar file: The file to which messages will be written.
421445
@@ -425,48 +449,68 @@ class FileDestination(PClass):
425449
"""
426450

427451
file = field(mandatory=True)
428-
encoder = field(mandatory=True)
452+
_json_default = field(mandatory=True)
429453
_dumps = field(mandatory=True)
430454
_linebreak = field(mandatory=True)
431455

432-
def __new__(cls, file, encoder=EliotJSONEncoder):
456+
def __new__(cls, file, encoder=None, json_default=json_default):
457+
"""
458+
Use ``json_default`` to pass in a default function for JSON dumping.
459+
460+
The ``encoder`` parameter is deprecated.
461+
"""
433462
if isinstance(file, IOBase) and not file.writable():
434463
raise RuntimeError("Given file {} is not writeable.")
435464

465+
json_default = _json_default_from_encoder_and_json_default(
466+
encoder, json_default
467+
)
468+
436469
unicodeFile = False
437470
try:
438471
file.write(b"")
439472
except TypeError:
440473
unicodeFile = True
441474

442475
if unicodeFile:
443-
# On Python 3 native json module outputs unicode:
444-
_dumps = pyjson.dumps
476+
_dumps = _dumps_unicode
445477
_linebreak = "\n"
446478
else:
447-
_dumps = bytesjson.dumps
479+
_dumps = _dumps_bytes
448480
_linebreak = b"\n"
449481
return PClass.__new__(
450-
cls, file=file, _dumps=_dumps, _linebreak=_linebreak, encoder=encoder
482+
cls,
483+
file=file,
484+
_dumps=_dumps,
485+
_linebreak=_linebreak,
486+
_json_default=json_default,
451487
)
452488

453489
def __call__(self, message):
454490
"""
455491
@param message: A message dictionary.
456492
"""
457-
self.file.write(self._dumps(message, cls=self.encoder) + self._linebreak)
493+
self.file.write(
494+
self._dumps(message, default=self._json_default) + self._linebreak
495+
)
458496
self.file.flush()
459497

460498

461-
def to_file(output_file, encoder=EliotJSONEncoder):
499+
def to_file(output_file, encoder=None, json_default=json_default):
462500
"""
463501
Add a destination that writes a JSON message per line to the given file.
464502
465503
@param output_file: A file-like object.
466504
467-
@param encoder: A JSONEncoder subclass to use when encoding JSON.
505+
@param encoder: DEPRECATED. A JSONEncoder subclass to use when encoding
506+
JSON.
507+
508+
@param json_default: A callable that handles objects the default JSON
509+
serializer can't handle.
468510
"""
469-
Logger._destinations.add(FileDestination(file=output_file, encoder=encoder))
511+
Logger._destinations.add(
512+
FileDestination(file=output_file, encoder=encoder, json_default=json_default)
513+
)
470514

471515

472516
# The default Logger, used when none is specified:

0 commit comments

Comments
 (0)