Skip to content

Commit

Permalink
Merge pull request #495 from itamarst/494-python-312-support
Browse files Browse the repository at this point in the history
Python 3.11 and 3.12 support
  • Loading branch information
itamarst committed Oct 30, 2023
2 parents 74e0aa2 + 3f7da18 commit 3551a8c
Show file tree
Hide file tree
Showing 28 changed files with 1,357 additions and 815 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:

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

steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v2"
- uses: "actions/checkout@v3"
- uses: "actions/setup-python@v4"
with:
python-version: "${{ matrix.python-version }}"
- name: "Install dependencies"
Expand All @@ -28,7 +28,7 @@ jobs:
python -VV
python -m site
python -m pip install --upgrade pip setuptools wheel
python -m pip install --upgrade virtualenv tox tox-gh-actions
python -m pip install --upgrade virtualenv tox tox-gh-actions
- name: "Run tox targets for ${{ matrix.python-version }}"
run: "python -m tox"
6 changes: 1 addition & 5 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
Eliot: Logging that tells you *why* it happened
================================================

.. image:: https://travis-ci.org/itamarst/eliot.png?branch=master
:target: http://travis-ci.org/itamarst/eliot
:alt: Build Status

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.

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

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.

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

* `Read the documentation <https://eliot.readthedocs.io>`_.
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# Ensure JSON serialization is part of benchmark:
to_file(open("/dev/null", "w"))

N = 10000
N = 100_000


def run():
Expand Down
19 changes: 19 additions & 0 deletions docs/source/news.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
What's New
==========

1.15.0
^^^^^^

Enhancements:

* Switched to JSON serialization with ``orjson`` (on CPython), which is much faster.
* Added support for Python 3.11 and 3.12.

Changes:

* JSON customization is now done with a default function rather than an encoder class.
* ``NaN``, ``inf``, and ``-inf`` are now serialized to JSON as ``null``, as per ``orjson`` this is more standards compliant.

Deprecation and removals:

* The deprecated support for serializing ``bytes`` in JSON log messages has been removed.
* Dropped support for Python 3.6 and 3.7.
* Removed the deprecated ``eliot.logwriter.ThreadedFileWriter``.

1.14.0
^^^^^^

Expand Down
18 changes: 9 additions & 9 deletions docs/source/outputting/output.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,28 +45,28 @@ Customizing JSON Encoding
-------------------------

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

.. code-block:: python
from eliot.json import EliotJSONEncoder
from eliot.json import json_default
from eliot import to_file
class MyClass:
def __init__(self, x):
self.x = x
class MyEncoder(EliotJSONEncoder):
def default(self, obj):
if isinstance(obj, MyClass):
return {"x": obj.x}
return EliotJSONEncoder.default(self, obj)
def default(self, obj):
if isinstance(obj, MyClass):
return {"x": obj.x}
return json_default(obj)
to_file(open("eliot.log", "ab"), encoder=MyEncoder)
to_file(open("eliot.log", "ab"), default=default)
For more details on JSON encoding see the Python `JSON documentation <https://docs.python.org/3/library/json.html>`_.
Note that Eliot uses `orjson <https://pypi.org/project/orjson/>`_ on CPython (for performance), and the built-in JSON module on PyPy.

.. _add_global_fields:

Expand Down
24 changes: 5 additions & 19 deletions eliot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,6 @@
Eliot: Logging for Complex & Distributed Systems.
"""
from warnings import warn
from sys import version_info

# Enable asyncio contextvars support in Python 3.5/3.6:
if version_info < (3, 7):
# On Python 3.5.2 and earlier, some of the necessary attributes aren't exposed:
if version_info < (3, 5, 3):
raise RuntimeError(
"This version of Eliot doesn't work on Python 3.5.2 or earlier. "
"Either upgrade to Python 3.5.3 or later (on Ubuntu 16.04 "
"you can use https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa "
"to get Python 3.6), or pin Eliot to version 1.7."
)
import aiocontextvars

dir(aiocontextvars) # pacify pyflakes
del aiocontextvars

# Expose the public API:
from ._message import Message
Expand All @@ -34,7 +18,7 @@
from ._validation import Field, fields, MessageType, ActionType, ValidationError
from ._traceback import write_traceback, writeFailure
from ._errors import register_exception_extractor
from ._version import get_versions


# Backwards compatibility:
def add_destination(destination):
Expand Down Expand Up @@ -69,6 +53,7 @@ def use_asyncio_context():
remove_destination = removeDestination
add_global_fields = addGlobalFields


# Backwards compatibility for old versions of eliot-tree, which rely on
# eliot._parse:
def _parse_compat():
Expand Down Expand Up @@ -127,5 +112,6 @@ def _parse_compat():
]


__version__ = get_versions()["version"]
del get_versions
from . import _version

__version__ = _version.get_versions()["version"]
55 changes: 0 additions & 55 deletions eliot/_bytesjson.py

This file was deleted.

88 changes: 66 additions & 22 deletions eliot/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@

import traceback
import inspect
import json as pyjson
from threading import Lock
from functools import wraps
from io import IOBase
import warnings

from pyrsistent import PClass, field

from . import _bytesjson as bytesjson
from zope.interface import Interface, implementer

from ._traceback import write_traceback, TRACEBACK_MESSAGE
from ._message import EXCEPTION_FIELD, MESSAGE_TYPE_FIELD, REASON_FIELD
from ._util import saferepr, safeunicode
from .json import EliotJSONEncoder
from .json import (
json_default,
_encoder_to_default_function,
_dumps_bytes,
_dumps_unicode,
)
from ._validation import ValidationError


Expand Down Expand Up @@ -260,12 +264,19 @@ class MemoryLogger(object):
not mutate this list.
"""

def __init__(self, encoder=EliotJSONEncoder):
def __init__(self, encoder=None, json_default=json_default):
"""
@param encoder: A JSONEncoder subclass to use when encoding JSON.
@param encoder: DEPRECATED. A JSONEncoder subclass to use when
encoding JSON.
@param json_default: A callable that handles objects the default JSON
serializer can't handle.
"""
json_default = _json_default_from_encoder_and_json_default(
encoder, json_default
)
self._lock = Lock()
self._encoder = encoder
self._json_default = json_default
self.reset()

@exclusively
Expand Down Expand Up @@ -346,7 +357,7 @@ def _validate_message(self, dictionary, serializer):
serializer.serialize(dictionary)

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

Expand Down Expand Up @@ -409,13 +420,26 @@ def reset(self):
self._failed_validations = []


def _json_default_from_encoder_and_json_default(encoder, json_default):
if encoder is not None:
warnings.warn(
"Using a JSON encoder subclass is no longer supported, please switch to using a default function",
DeprecationWarning,
stacklevel=3,
)
from .json import json_default as default_json_default

if json_default is not default_json_default:
raise RuntimeError("Can't pass in both encoder and default function")

json_default = _encoder_to_default_function(encoder())
return json_default


class FileDestination(PClass):
"""
Callable that writes JSON messages to a file.
On Python 3 the file may support either C{bytes} or C{unicode}. On
Python 2 only C{bytes} are supported since that is what all files expect
in practice.
Callable that writes JSON messages to a file that accepts either C{bytes}
or C{str}.
@ivar file: The file to which messages will be written.
Expand All @@ -425,48 +449,68 @@ class FileDestination(PClass):
"""

file = field(mandatory=True)
encoder = field(mandatory=True)
_json_default = field(mandatory=True)
_dumps = field(mandatory=True)
_linebreak = field(mandatory=True)

def __new__(cls, file, encoder=EliotJSONEncoder):
def __new__(cls, file, encoder=None, json_default=json_default):
"""
Use ``json_default`` to pass in a default function for JSON dumping.
The ``encoder`` parameter is deprecated.
"""
if isinstance(file, IOBase) and not file.writable():
raise RuntimeError("Given file {} is not writeable.")

json_default = _json_default_from_encoder_and_json_default(
encoder, json_default
)

unicodeFile = False
try:
file.write(b"")
except TypeError:
unicodeFile = True

if unicodeFile:
# On Python 3 native json module outputs unicode:
_dumps = pyjson.dumps
_dumps = _dumps_unicode
_linebreak = "\n"
else:
_dumps = bytesjson.dumps
_dumps = _dumps_bytes
_linebreak = b"\n"
return PClass.__new__(
cls, file=file, _dumps=_dumps, _linebreak=_linebreak, encoder=encoder
cls,
file=file,
_dumps=_dumps,
_linebreak=_linebreak,
_json_default=json_default,
)

def __call__(self, message):
"""
@param message: A message dictionary.
"""
self.file.write(self._dumps(message, cls=self.encoder) + self._linebreak)
self.file.write(
self._dumps(message, default=self._json_default) + self._linebreak
)
self.file.flush()


def to_file(output_file, encoder=EliotJSONEncoder):
def to_file(output_file, encoder=None, json_default=json_default):
"""
Add a destination that writes a JSON message per line to the given file.
@param output_file: A file-like object.
@param encoder: A JSONEncoder subclass to use when encoding JSON.
@param encoder: DEPRECATED. A JSONEncoder subclass to use when encoding
JSON.
@param json_default: A callable that handles objects the default JSON
serializer can't handle.
"""
Logger._destinations.add(FileDestination(file=output_file, encoder=encoder))
Logger._destinations.add(
FileDestination(file=output_file, encoder=encoder, json_default=json_default)
)


# The default Logger, used when none is specified:
Expand Down
Loading

0 comments on commit 3551a8c

Please sign in to comment.