mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-18 20:47:54 +00:00
Merge pull request #1344 from tahoe-lafs/3072-python-3.12-support
Python 3.12 support, compatability with Eliot 1.15 Fixes ticket:4071
This commit is contained in:
commit
e3c8ff0fc6
@ -46,8 +46,8 @@ export PIP_FIND_LINKS="file://${WHEELHOUSE_PATH}"
|
||||
# setuptools 45 requires Python 3.5 or newer. Even though we upgraded pip
|
||||
# above, it may still not be able to get us a compatible version unless we
|
||||
# explicitly ask for one.
|
||||
"${PIP}" install --upgrade setuptools==44.0.0 wheel
|
||||
"${PIP}" install --upgrade setuptools wheel
|
||||
|
||||
# Just about every user of this image wants to use tox from the bootstrap
|
||||
# virtualenv so go ahead and install it now.
|
||||
"${PIP}" install "tox~=3.0"
|
||||
"${PIP}" install "tox~=4.0"
|
||||
|
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@ -45,16 +45,17 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# On macOS don't bother with 3.8, just to get faster builds.
|
||||
- os: macos-12
|
||||
python-version: "3.9"
|
||||
- os: macos-12
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
# We only support PyPy on Linux at the moment.
|
||||
- os: ubuntu-latest
|
||||
python-version: "pypy-3.8"
|
||||
- os: ubuntu-latest
|
||||
python-version: "pypy-3.9"
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.12"
|
||||
- os: windows-latest
|
||||
python-version: "3.12"
|
||||
|
||||
steps:
|
||||
# See https://github.com/actions/checkout. A fetch-depth of 0
|
||||
@ -72,7 +73,7 @@ jobs:
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
pip install --upgrade "tox<4" tox-gh-actions setuptools
|
||||
pip install --upgrade tox tox-gh-actions setuptools
|
||||
pip list
|
||||
|
||||
- name: Display tool versions
|
||||
@ -169,7 +170,7 @@ jobs:
|
||||
- false
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.10"
|
||||
python-version: "3.12"
|
||||
force-foolscap: true
|
||||
steps:
|
||||
|
||||
@ -204,7 +205,7 @@ jobs:
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
pip install --upgrade "tox<4"
|
||||
pip install --upgrade tox
|
||||
pip list
|
||||
|
||||
- name: Display tool versions
|
||||
@ -264,7 +265,7 @@ jobs:
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
pip install --upgrade "tox<4"
|
||||
pip install --upgrade tox
|
||||
pip list
|
||||
|
||||
- name: Display tool versions
|
||||
|
@ -30,6 +30,7 @@ from allmydata.util.deferredutil import async_to_deferred
|
||||
if sys.platform.startswith('win'):
|
||||
pytest.skip('Skipping Tor tests on Windows', allow_module_level=True)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info[:2] > (3, 11), reason='Chutney still does not support 3.12')
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_onion_service_storage(reactor, request, temp_dir, flog_gatherer, tor_network, tor_introducer_furl):
|
||||
"""
|
||||
@ -140,6 +141,7 @@ def _create_anonymous_node(reactor, name, web_port, request, temp_dir, flog_gath
|
||||
print("okay, launched")
|
||||
return result
|
||||
|
||||
@pytest.mark.skipif(sys.version_info[:2] > (3, 11), reason='Chutney still does not support 3.12')
|
||||
@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='This test has issues on macOS')
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_anonymous_client(reactor, request, temp_dir, flog_gatherer, tor_network, introducer_furl):
|
||||
|
1
newsfragments/3072.feature
Normal file
1
newsfragments/3072.feature
Normal file
@ -0,0 +1 @@
|
||||
Added support for Python 3.12, and work with Eliot 1.15
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
15
setup.py
15
setup.py
@ -112,7 +112,7 @@ install_requires = [
|
||||
"magic-wormhole >= 0.10.2",
|
||||
|
||||
# We want a new enough version to support custom JSON encoders.
|
||||
"eliot < 1.15.0", # temporary cap, to be fixed in PR #1344
|
||||
"eliot >= 1.14.0",
|
||||
|
||||
"pyrsistent",
|
||||
|
||||
@ -157,10 +157,6 @@ install_requires = [
|
||||
"filelock",
|
||||
]
|
||||
|
||||
setup_requires = [
|
||||
'setuptools >= 28.8.0', # for PEP-440 style versions
|
||||
]
|
||||
|
||||
tor_requires = [
|
||||
# 23.5 added support for custom TLS contexts in web_agent(), which is
|
||||
# needed for the HTTP storage client to run over Tor.
|
||||
@ -384,8 +380,8 @@ setup(name="tahoe-lafs", # also set in __init__.py
|
||||
package_dir = {'':'src'},
|
||||
packages=find_packages('src') + ['allmydata.test.plugins'],
|
||||
classifiers=trove_classifiers,
|
||||
# We support Python 3.8 or later, 3.12 is untested for now
|
||||
python_requires=">=3.8, <3.12",
|
||||
# We support Python 3.8 or later, 3.13 is untested for now
|
||||
python_requires=">=3.8, <3.13",
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
# Duplicate the Twisted pywin32 dependency here. See
|
||||
@ -409,8 +405,8 @@ setup(name="tahoe-lafs", # also set in __init__.py
|
||||
# selected here are just the current versions at the time.
|
||||
# Bumping them to keep up with future releases is fine as long
|
||||
# as those releases are known to actually work.
|
||||
"pip==22.0.3",
|
||||
"wheel==0.37.1",
|
||||
"pip==23.3.1",
|
||||
"wheel==0.41.3",
|
||||
"subunitreporter==23.8.0",
|
||||
"python-subunit==1.4.2",
|
||||
"junitxml==0.7",
|
||||
@ -446,7 +442,6 @@ setup(name="tahoe-lafs", # also set in __init__.py
|
||||
"allmydata": ["ported-modules.txt"],
|
||||
},
|
||||
include_package_data=True,
|
||||
setup_requires=setup_requires,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'tahoe = allmydata.scripts.runner:run',
|
||||
|
@ -3,16 +3,6 @@ Decentralized storage grid.
|
||||
|
||||
community web site: U{https://tahoe-lafs.org/}
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2, PY3
|
||||
if PY2:
|
||||
# Don't import future str() so we don't break Foolscap serialization on Python 2.
|
||||
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, max, min # noqa: F401
|
||||
from past.builtins import unicode as str
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
@ -52,12 +42,6 @@ __appname__ = "tahoe-lafs"
|
||||
# https://tahoe-lafs.org/trac/tahoe-lafs/wiki/Versioning
|
||||
__full_version__ = __appname__ + '/' + str(__version__)
|
||||
|
||||
|
||||
# Install Python 3 module locations in Python 2:
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
|
||||
|
||||
# Monkey-patch 3rd party libraries:
|
||||
from ._monkeypatch import patch
|
||||
patch()
|
||||
@ -72,8 +56,7 @@ del patch
|
||||
#
|
||||
# Also note that BytesWarnings only happen if Python is run with -b option, so
|
||||
# in practice this should only affect tests.
|
||||
if PY3:
|
||||
import warnings
|
||||
# Error on BytesWarnings, to catch things like str(b""), but only for
|
||||
# allmydata code.
|
||||
warnings.filterwarnings("error", category=BytesWarning, module=".*allmydata.*")
|
||||
import warnings
|
||||
# Error on BytesWarnings, to catch things like str(b""), but only for
|
||||
# allmydata code.
|
||||
warnings.filterwarnings("error", category=BytesWarning, module=".*allmydata.*")
|
||||
|
@ -12,6 +12,7 @@ from base64 import urlsafe_b64encode
|
||||
from functools import partial
|
||||
from configparser import NoSectionError
|
||||
|
||||
from six import ensure_text
|
||||
from foolscap.furl import (
|
||||
decode_furl,
|
||||
)
|
||||
@ -989,6 +990,9 @@ class _Client(node.Node, pollmixin.PollMixin):
|
||||
static_servers = servers_yaml.get("storage", {})
|
||||
log.msg("found %d static servers in private/servers.yaml" %
|
||||
len(static_servers))
|
||||
static_servers = {
|
||||
ensure_text(key): value for (key, value) in static_servers.items()
|
||||
}
|
||||
self.storage_broker.set_static_servers(static_servers)
|
||||
except EnvironmentError:
|
||||
pass
|
||||
|
@ -78,7 +78,7 @@ _READONLY_PEERS = Field(
|
||||
|
||||
def _serialize_existing_shares(existing_shares):
|
||||
return {
|
||||
server: list(shares)
|
||||
ensure_str(server): list(shares)
|
||||
for (server, shares)
|
||||
in existing_shares.items()
|
||||
}
|
||||
@ -91,7 +91,7 @@ _EXISTING_SHARES = Field(
|
||||
|
||||
def _serialize_happiness_mappings(happiness_mappings):
|
||||
return {
|
||||
sharenum: base32.b2a(serverid)
|
||||
str(sharenum): ensure_str(base32.b2a(serverid))
|
||||
for (sharenum, serverid)
|
||||
in happiness_mappings.items()
|
||||
}
|
||||
@ -112,7 +112,7 @@ _UPLOAD_TRACKERS = Field(
|
||||
u"upload_trackers",
|
||||
lambda trackers: list(
|
||||
dict(
|
||||
server=tracker.get_name(),
|
||||
server=ensure_str(tracker.get_name()),
|
||||
shareids=sorted(tracker.buckets.keys()),
|
||||
)
|
||||
for tracker
|
||||
@ -123,7 +123,7 @@ _UPLOAD_TRACKERS = Field(
|
||||
|
||||
_ALREADY_SERVERIDS = Field(
|
||||
u"already_serverids",
|
||||
lambda d: d,
|
||||
lambda d: {str(k): v for k, v in d.items()},
|
||||
u"Some servers which are already holding some shares that we were interested in uploading.",
|
||||
)
|
||||
|
||||
|
@ -32,7 +32,6 @@ Ported to Python 3.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from six import ensure_text
|
||||
from typing import Union, Callable, Any, Optional, cast, Dict
|
||||
from os import urandom
|
||||
import re
|
||||
@ -273,7 +272,6 @@ class StorageFarmBroker(service.MultiService):
|
||||
# doesn't really matter but it makes the logging behavior more
|
||||
# predictable and easier to test (and at least one test does depend on
|
||||
# this sorted order).
|
||||
servers = {ensure_text(key): value for (key, value) in servers.items()}
|
||||
for (server_id, server) in sorted(servers.items()):
|
||||
try:
|
||||
storage_server = self._make_storage_server(
|
||||
|
@ -125,5 +125,5 @@ if sys.platform == "win32":
|
||||
initialize()
|
||||
|
||||
from eliot import to_file
|
||||
from allmydata.util.eliotutil import eliot_json_encoder
|
||||
to_file(open("eliot.log", "wb"), encoder=eliot_json_encoder)
|
||||
from allmydata.util.jsonbytes import AnyBytesJSONEncoder
|
||||
to_file(open("eliot.log", "wb"), encoder=AnyBytesJSONEncoder)
|
||||
|
@ -1352,6 +1352,26 @@ class _TestCaseMixin(object):
|
||||
def assertRaises(self, *a, **kw):
|
||||
return self._dummyCase.assertRaises(*a, **kw)
|
||||
|
||||
def failUnless(self, *args, **kwargs):
|
||||
"""Backwards compatibility method."""
|
||||
self.assertTrue(*args, **kwargs)
|
||||
|
||||
def failIf(self, *args, **kwargs):
|
||||
"""Backwards compatibility method."""
|
||||
self.assertFalse(*args, **kwargs)
|
||||
|
||||
def failIfEqual(self, *args, **kwargs):
|
||||
"""Backwards compatibility method."""
|
||||
self.assertNotEqual(*args, **kwargs)
|
||||
|
||||
def failUnlessEqual(self, *args, **kwargs):
|
||||
"""Backwards compatibility method."""
|
||||
self.assertEqual(*args, **kwargs)
|
||||
|
||||
def failUnlessReallyEqual(self, *args, **kwargs):
|
||||
"""Backwards compatibility method."""
|
||||
self.assertReallyEqual(*args, **kwargs)
|
||||
|
||||
|
||||
class SyncTestCase(_TestCaseMixin, TestCase):
|
||||
"""
|
||||
|
@ -29,6 +29,7 @@ from eliot import (
|
||||
ILogger,
|
||||
)
|
||||
from eliot.testing import (
|
||||
MemoryLogger,
|
||||
swap_logger,
|
||||
check_for_errors,
|
||||
)
|
||||
@ -37,8 +38,8 @@ from twisted.python.monkey import (
|
||||
MonkeyPatcher,
|
||||
)
|
||||
|
||||
from ..util.eliotutil import (
|
||||
MemoryLogger,
|
||||
from ..util.jsonbytes import (
|
||||
AnyBytesJSONEncoder
|
||||
)
|
||||
|
||||
_NAME = Field.for_types(
|
||||
@ -146,7 +147,7 @@ def with_logging(
|
||||
"""
|
||||
@wraps(test_method)
|
||||
def run_with_logging(*args, **kwargs):
|
||||
validating_logger = MemoryLogger()
|
||||
validating_logger = MemoryLogger(encoder=AnyBytesJSONEncoder)
|
||||
original = swap_logger(None)
|
||||
try:
|
||||
swap_logger(_TwoLoggers(original, validating_logger))
|
||||
|
@ -850,6 +850,7 @@ class StorageClients(SyncTestCase):
|
||||
actionType=u"storage-client:broker:set-static-servers",
|
||||
succeeded=True,
|
||||
),
|
||||
encoder_=json.AnyBytesJSONEncoder
|
||||
)
|
||||
def test_static_servers(self, logger):
|
||||
"""
|
||||
@ -884,6 +885,7 @@ class StorageClients(SyncTestCase):
|
||||
actionType=u"storage-client:broker:make-storage-server",
|
||||
succeeded=False,
|
||||
),
|
||||
encoder_=json.AnyBytesJSONEncoder
|
||||
)
|
||||
def test_invalid_static_server(self, logger):
|
||||
"""
|
||||
|
@ -507,7 +507,7 @@ class TestUtil(unittest.TestCase):
|
||||
"""
|
||||
remove a simple prefix properly
|
||||
"""
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
remove_prefix(b"foobar", b"foo"),
|
||||
b"bar"
|
||||
)
|
||||
@ -523,7 +523,7 @@ class TestUtil(unittest.TestCase):
|
||||
"""
|
||||
removing a zero-length prefix does nothing
|
||||
"""
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
remove_prefix(b"foobar", b""),
|
||||
b"foobar",
|
||||
)
|
||||
@ -532,7 +532,7 @@ class TestUtil(unittest.TestCase):
|
||||
"""
|
||||
removing a prefix which is the whole string is empty
|
||||
"""
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
remove_prefix(b"foobar", b"foobar"),
|
||||
b"",
|
||||
)
|
||||
|
@ -47,7 +47,6 @@ from eliot import (
|
||||
Message,
|
||||
MessageType,
|
||||
fields,
|
||||
FileDestination,
|
||||
MemoryLogger,
|
||||
)
|
||||
from eliot.twisted import DeferredContext
|
||||
@ -64,7 +63,6 @@ from twisted.internet.task import deferLater
|
||||
from twisted.internet import reactor
|
||||
|
||||
from ..util.eliotutil import (
|
||||
eliot_json_encoder,
|
||||
log_call_deferred,
|
||||
_parse_destination_description,
|
||||
_EliotLogging,
|
||||
@ -188,8 +186,8 @@ class ParseDestinationDescriptionTests(SyncTestCase):
|
||||
"""
|
||||
reactor = object()
|
||||
self.assertThat(
|
||||
_parse_destination_description("file:-")(reactor),
|
||||
Equals(FileDestination(stdout, encoder=eliot_json_encoder)),
|
||||
_parse_destination_description("file:-")(reactor).file,
|
||||
Equals(stdout),
|
||||
)
|
||||
|
||||
|
||||
|
@ -28,14 +28,14 @@ INTRODUCERS_CFG_FURLS_COMMENTED="""introducers:
|
||||
|
||||
class MultiIntroTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
async def setUp(self):
|
||||
# setup tahoe.cfg and basedir/private/introducers
|
||||
# create a custom tahoe.cfg
|
||||
self.basedir = os.path.dirname(self.mktemp())
|
||||
c = open(os.path.join(self.basedir, "tahoe.cfg"), "w")
|
||||
config = {'hide-ip':False, 'listen': 'tcp',
|
||||
'port': None, 'location': None, 'hostname': 'example.net'}
|
||||
write_node_config(c, config)
|
||||
await write_node_config(c, config)
|
||||
c.write("[storage]\n")
|
||||
c.write("enabled = false\n")
|
||||
c.close()
|
||||
@ -63,8 +63,7 @@ class MultiIntroTests(unittest.TestCase):
|
||||
# assertions
|
||||
self.failUnlessEqual(ic_count, len(connections["introducers"]))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_read_introducer_furl_from_tahoecfg(self):
|
||||
async def test_read_introducer_furl_from_tahoecfg(self):
|
||||
"""
|
||||
The deprecated [client]introducer.furl item is still read and respected.
|
||||
"""
|
||||
@ -72,7 +71,7 @@ class MultiIntroTests(unittest.TestCase):
|
||||
c = open(os.path.join(self.basedir, "tahoe.cfg"), "w")
|
||||
config = {'hide-ip':False, 'listen': 'tcp',
|
||||
'port': None, 'location': None, 'hostname': 'example.net'}
|
||||
write_node_config(c, config)
|
||||
await write_node_config(c, config)
|
||||
fake_furl = "furl1"
|
||||
c.write("[client]\n")
|
||||
c.write("introducer.furl = %s\n" % fake_furl)
|
||||
@ -139,14 +138,14 @@ introducers:
|
||||
"""
|
||||
|
||||
class NoDefault(unittest.TestCase):
|
||||
def setUp(self):
|
||||
async def setUp(self):
|
||||
# setup tahoe.cfg and basedir/private/introducers
|
||||
# create a custom tahoe.cfg
|
||||
self.basedir = os.path.dirname(self.mktemp())
|
||||
c = open(os.path.join(self.basedir, "tahoe.cfg"), "w")
|
||||
config = {'hide-ip':False, 'listen': 'tcp',
|
||||
'port': None, 'location': None, 'hostname': 'example.net'}
|
||||
write_node_config(c, config)
|
||||
await write_node_config(c, config)
|
||||
c.write("[storage]\n")
|
||||
c.write("enabled = false\n")
|
||||
c.close()
|
||||
|
@ -26,7 +26,7 @@ from typing import Union, Callable, Tuple, Iterable
|
||||
from queue import Queue
|
||||
from cbor2 import dumps
|
||||
from pycddl import ValidationError as CDDLValidationError
|
||||
from hypothesis import assume, given, strategies as st
|
||||
from hypothesis import assume, given, strategies as st, settings as hypothesis_settings
|
||||
from fixtures import Fixture, TempDir, MonkeyPatch
|
||||
from treq.testing import StubTreq
|
||||
from klein import Klein
|
||||
@ -442,6 +442,9 @@ class CustomHTTPServerTests(SyncTestCase):
|
||||
result_of(client.get_version())
|
||||
|
||||
@given(length=st.integers(min_value=1, max_value=1_000_000))
|
||||
# On Python 3.12 we're getting weird deadline issues in CI, so disabling
|
||||
# for now.
|
||||
@hypothesis_settings(deadline=None)
|
||||
def test_limited_content_fits(self, length):
|
||||
"""
|
||||
``http_client.limited_content()`` returns the body if it is less than
|
||||
|
@ -23,7 +23,7 @@ def assert_soup_has_favicon(testcase, soup):
|
||||
``BeautifulSoup`` object ``soup`` contains the tahoe favicon link.
|
||||
"""
|
||||
links = soup.find_all(u'link', rel=u'shortcut icon')
|
||||
testcase.assert_(
|
||||
testcase.assertTrue(
|
||||
any(t[u'href'] == u'/icon.png' for t in links), soup)
|
||||
|
||||
|
||||
@ -92,6 +92,6 @@ def assert_soup_has_text(testcase, soup, text):
|
||||
``BeautifulSoup`` object ``soup`` contains the passed in ``text`` anywhere
|
||||
as a text node.
|
||||
"""
|
||||
testcase.assert_(
|
||||
testcase.assertTrue(
|
||||
soup.find_all(string=re.compile(re.escape(text))),
|
||||
soup)
|
||||
|
@ -117,7 +117,7 @@ class TestStreamingLogs(AsyncTestCase):
|
||||
proto.transport.loseConnection()
|
||||
yield proto.is_closed
|
||||
|
||||
self.assertThat(len(messages), Equals(3))
|
||||
self.assertThat(len(messages), Equals(3), messages)
|
||||
self.assertThat(messages[0]["action_type"], Equals("test:cli:some-exciting-action"))
|
||||
self.assertThat(messages[0]["arguments"],
|
||||
Equals(["hello", "good-\\xff-day", 123, {"a": 35}, [None]]))
|
||||
|
@ -1,195 +0,0 @@
|
||||
"""
|
||||
Bring in some Eliot updates from newer versions of Eliot than we can
|
||||
depend on in Python 2. The implementations are copied from Eliot 1.14 and
|
||||
only changed enough to add Python 2 compatibility.
|
||||
|
||||
Every API in this module (except ``eliot_json_encoder``) should be obsolete as
|
||||
soon as we depend on Eliot 1.14 or newer.
|
||||
|
||||
When that happens:
|
||||
|
||||
* replace ``capture_logging``
|
||||
with ``partial(eliot.testing.capture_logging, encoder_=eliot_json_encoder)``
|
||||
* replace ``validateLogging``
|
||||
with ``partial(eliot.testing.validateLogging, encoder_=eliot_json_encoder)``
|
||||
* replace ``MemoryLogger``
|
||||
with ``partial(eliot.MemoryLogger, encoder=eliot_json_encoder)``
|
||||
|
||||
Ported to Python 3.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
|
||||
|
||||
import json as pyjson
|
||||
from functools import wraps, partial
|
||||
|
||||
from eliot import (
|
||||
MemoryLogger as _MemoryLogger,
|
||||
)
|
||||
|
||||
from eliot.testing import (
|
||||
check_for_errors,
|
||||
swap_logger,
|
||||
)
|
||||
|
||||
from .jsonbytes import AnyBytesJSONEncoder
|
||||
|
||||
# There are currently a number of log messages that include non-UTF-8 bytes.
|
||||
# Allow these, at least for now. Later when the whole test suite has been
|
||||
# converted to our SyncTestCase or AsyncTestCase it will be easier to turn
|
||||
# this off and then attribute log failures to specific codepaths so they can
|
||||
# be fixed (and then not regressed later) because those instances will result
|
||||
# in test failures instead of only garbage being written to the eliot log.
|
||||
eliot_json_encoder = AnyBytesJSONEncoder
|
||||
|
||||
class _CustomEncoderMemoryLogger(_MemoryLogger):
|
||||
"""
|
||||
Override message validation from the Eliot-supplied ``MemoryLogger`` to
|
||||
use our chosen JSON encoder.
|
||||
|
||||
This is only necessary on Python 2 where we use an old version of Eliot
|
||||
that does not parameterize the encoder.
|
||||
"""
|
||||
def __init__(self, encoder=eliot_json_encoder):
|
||||
"""
|
||||
@param encoder: A JSONEncoder subclass to use when encoding JSON.
|
||||
"""
|
||||
self._encoder = encoder
|
||||
super(_CustomEncoderMemoryLogger, self).__init__()
|
||||
|
||||
def _validate_message(self, dictionary, serializer):
|
||||
"""Validate an individual message.
|
||||
|
||||
As a side-effect, the message is replaced with its serialized contents.
|
||||
|
||||
@param dictionary: A message C{dict} to be validated. Might be mutated
|
||||
by the serializer!
|
||||
|
||||
@param serializer: C{None} or a serializer.
|
||||
|
||||
@raises TypeError: If a field name is not unicode, or the dictionary
|
||||
fails to serialize to JSON.
|
||||
|
||||
@raises eliot.ValidationError: If serializer was given and validation
|
||||
failed.
|
||||
"""
|
||||
if serializer is not None:
|
||||
serializer.validate(dictionary)
|
||||
for key in dictionary:
|
||||
if not isinstance(key, str):
|
||||
if isinstance(key, bytes):
|
||||
key.decode("utf-8")
|
||||
else:
|
||||
raise TypeError(dictionary, "%r is not unicode" % (key,))
|
||||
if serializer is not None:
|
||||
serializer.serialize(dictionary)
|
||||
|
||||
try:
|
||||
pyjson.dumps(dictionary, cls=self._encoder)
|
||||
except Exception as e:
|
||||
raise TypeError("Message %s doesn't encode to JSON: %s" % (dictionary, e))
|
||||
|
||||
if PY2:
|
||||
MemoryLogger = partial(_CustomEncoderMemoryLogger, encoder=eliot_json_encoder)
|
||||
else:
|
||||
MemoryLogger = partial(_MemoryLogger, encoder=eliot_json_encoder)
|
||||
|
||||
def validateLogging(
|
||||
assertion, *assertionArgs, **assertionKwargs
|
||||
):
|
||||
"""
|
||||
Decorator factory for L{unittest.TestCase} methods to add logging
|
||||
validation.
|
||||
|
||||
1. The decorated test method gets a C{logger} keyword argument, a
|
||||
L{MemoryLogger}.
|
||||
2. All messages logged to this logger will be validated at the end of
|
||||
the test.
|
||||
3. Any unflushed logged tracebacks will cause the test to fail.
|
||||
|
||||
For example:
|
||||
|
||||
from unittest import TestCase
|
||||
from eliot.testing import assertContainsFields, validateLogging
|
||||
|
||||
class MyTests(TestCase):
|
||||
def assertFooLogging(self, logger):
|
||||
assertContainsFields(self, logger.messages[0], {"key": 123})
|
||||
|
||||
|
||||
@param assertion: A callable that will be called with the
|
||||
L{unittest.TestCase} instance, the logger and C{assertionArgs} and
|
||||
C{assertionKwargs} once the actual test has run, allowing for extra
|
||||
logging-related assertions on the effects of the test. Use L{None} if you
|
||||
want the cleanup assertions registered but no custom assertions.
|
||||
|
||||
@param assertionArgs: Additional positional arguments to pass to
|
||||
C{assertion}.
|
||||
|
||||
@param assertionKwargs: Additional keyword arguments to pass to
|
||||
C{assertion}.
|
||||
|
||||
@param encoder_: C{json.JSONEncoder} subclass to use when validating JSON.
|
||||
"""
|
||||
encoder_ = assertionKwargs.pop("encoder_", eliot_json_encoder)
|
||||
def decorator(function):
|
||||
@wraps(function)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
skipped = False
|
||||
|
||||
kwargs["logger"] = logger = MemoryLogger(encoder=encoder_)
|
||||
self.addCleanup(check_for_errors, logger)
|
||||
# TestCase runs cleanups in reverse order, and we want this to
|
||||
# run *before* tracebacks are checked:
|
||||
if assertion is not None:
|
||||
self.addCleanup(
|
||||
lambda: skipped
|
||||
or assertion(self, logger, *assertionArgs, **assertionKwargs)
|
||||
)
|
||||
try:
|
||||
return function(self, *args, **kwargs)
|
||||
except self.skipException:
|
||||
skipped = True
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
# PEP 8 variant:
|
||||
validate_logging = validateLogging
|
||||
|
||||
def capture_logging(
|
||||
assertion, *assertionArgs, **assertionKwargs
|
||||
):
|
||||
"""
|
||||
Capture and validate all logging that doesn't specify a L{Logger}.
|
||||
|
||||
See L{validate_logging} for details on the rest of its behavior.
|
||||
"""
|
||||
encoder_ = assertionKwargs.pop("encoder_", eliot_json_encoder)
|
||||
def decorator(function):
|
||||
@validate_logging(
|
||||
assertion, *assertionArgs, encoder_=encoder_, **assertionKwargs
|
||||
)
|
||||
@wraps(function)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
logger = kwargs["logger"]
|
||||
previous_logger = swap_logger(logger)
|
||||
|
||||
def cleanup():
|
||||
swap_logger(previous_logger)
|
||||
|
||||
self.addCleanup(cleanup)
|
||||
return function(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
@ -3,17 +3,6 @@ Tools aimed at the interaction between Tahoe-LAFS implementation and Eliot.
|
||||
|
||||
Ported to Python 3.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from __future__ import (
|
||||
unicode_literals,
|
||||
print_function,
|
||||
absolute_import,
|
||||
division,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MemoryLogger",
|
||||
@ -26,11 +15,6 @@ __all__ = [
|
||||
"capture_logging",
|
||||
]
|
||||
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
|
||||
from six import ensure_text
|
||||
|
||||
from sys import (
|
||||
stdout,
|
||||
)
|
||||
@ -42,6 +26,7 @@ from logging import (
|
||||
)
|
||||
from json import loads
|
||||
|
||||
from six import ensure_text
|
||||
from zope.interface import (
|
||||
implementer,
|
||||
)
|
||||
@ -61,6 +46,11 @@ from eliot import (
|
||||
write_traceback,
|
||||
start_action,
|
||||
)
|
||||
from eliot.testing import (
|
||||
MemoryLogger,
|
||||
capture_logging,
|
||||
)
|
||||
|
||||
from eliot._validation import (
|
||||
ValidationError,
|
||||
)
|
||||
@ -87,11 +77,8 @@ from twisted.internet.defer import (
|
||||
)
|
||||
from twisted.application.service import Service
|
||||
|
||||
from ._eliot_updates import (
|
||||
MemoryLogger,
|
||||
eliot_json_encoder,
|
||||
capture_logging,
|
||||
)
|
||||
from .jsonbytes import AnyBytesJSONEncoder
|
||||
|
||||
|
||||
def validateInstanceOf(t):
|
||||
"""
|
||||
@ -309,7 +296,7 @@ class _DestinationParser(object):
|
||||
rotateLength=rotate_length,
|
||||
maxRotatedFiles=max_rotated_files,
|
||||
)
|
||||
return lambda reactor: FileDestination(get_file(), eliot_json_encoder)
|
||||
return lambda reactor: FileDestination(get_file(), encoder=AnyBytesJSONEncoder)
|
||||
|
||||
|
||||
_parse_destination_description = _DestinationParser().parse
|
||||
|
@ -61,6 +61,9 @@ class UTF8BytesJSONEncoder(json.JSONEncoder):
|
||||
"""
|
||||
A JSON encoder than can also encode UTF-8 encoded strings.
|
||||
"""
|
||||
def default(self, o):
|
||||
return bytes_to_unicode(False, o)
|
||||
|
||||
def encode(self, o, **kwargs):
|
||||
return json.JSONEncoder.encode(
|
||||
self, bytes_to_unicode(False, o), **kwargs)
|
||||
@ -77,6 +80,9 @@ class AnyBytesJSONEncoder(json.JSONEncoder):
|
||||
Bytes are decoded to strings using UTF-8, if that fails to decode then the
|
||||
bytes are quoted.
|
||||
"""
|
||||
def default(self, o):
|
||||
return bytes_to_unicode(True, o)
|
||||
|
||||
def encode(self, o, **kwargs):
|
||||
return json.JSONEncoder.encode(
|
||||
self, bytes_to_unicode(True, o), **kwargs)
|
||||
|
20
tox.ini
20
tox.ini
@ -11,6 +11,7 @@ python =
|
||||
3.9: py39-coverage
|
||||
3.10: py310-coverage
|
||||
3.11: py311-coverage
|
||||
3.12: py312-coverage
|
||||
pypy-3.8: pypy38
|
||||
pypy-3.9: pypy39
|
||||
|
||||
@ -18,11 +19,14 @@ python =
|
||||
twisted = 1
|
||||
|
||||
[tox]
|
||||
envlist = typechecks,codechecks,py{38,39,310,311}-{coverage},pypy27,pypy38,pypy39,integration
|
||||
minversion = 2.4
|
||||
envlist = typechecks,codechecks,py{38,39,310,311,312}-{coverage},pypy27,pypy38,pypy39,integration
|
||||
minversion = 4
|
||||
|
||||
[testenv]
|
||||
passenv = TAHOE_LAFS_* PIP_* SUBUNITREPORTER_* USERPROFILE HOMEDRIVE HOMEPATH
|
||||
# Install code the real way, for maximum realism.
|
||||
usedevelop = False
|
||||
|
||||
passenv = TAHOE_LAFS_*,PIP_*,SUBUNITREPORTER_*,USERPROFILE,HOMEDRIVE,HOMEPATH,COLUMNS
|
||||
deps =
|
||||
# We pull in certify *here* to avoid bug #2913. Basically if a
|
||||
# `setup_requires=...` causes a package to be installed (with setuptools)
|
||||
@ -40,10 +44,6 @@ deps =
|
||||
# with the above pins.
|
||||
certifi
|
||||
|
||||
# We add usedevelop=False because testing against a true installation gives
|
||||
# more useful results.
|
||||
usedevelop = False
|
||||
|
||||
extras =
|
||||
# Get general testing environment dependencies so we can run the tests
|
||||
# how we like.
|
||||
@ -56,6 +56,7 @@ setenv =
|
||||
# Define TEST_SUITE in the environment as an aid to constructing the
|
||||
# correct test command below.
|
||||
TEST_SUITE = allmydata
|
||||
COLUMNS = 80
|
||||
|
||||
commands =
|
||||
# As an aid to debugging, dump all of the Python packages and their
|
||||
@ -81,6 +82,7 @@ commands =
|
||||
coverage: coverage xml
|
||||
|
||||
[testenv:integration]
|
||||
usedevelop = False
|
||||
basepython = python3
|
||||
platform = mylinux: linux
|
||||
mymacos: darwin
|
||||
@ -140,7 +142,7 @@ commands = mypy src
|
||||
|
||||
|
||||
[testenv:draftnews]
|
||||
passenv = TAHOE_LAFS_* PIP_* SUBUNITREPORTER_* USERPROFILE HOMEDRIVE HOMEPATH
|
||||
passenv = TAHOE_LAFS_*,PIP_*,SUBUNITREPORTER_*,USERPROFILE,HOMEDRIVE,HOMEPATH,COLUMNS
|
||||
deps =
|
||||
# see comment in [testenv] about "certifi"
|
||||
certifi
|
||||
@ -150,7 +152,7 @@ commands =
|
||||
|
||||
[testenv:news]
|
||||
# On macOS, git invoked from Tox needs $HOME.
|
||||
passenv = TAHOE_LAFS_* PIP_* SUBUNITREPORTER_* USERPROFILE HOMEDRIVE HOMEPATH HOME
|
||||
passenv = TAHOE_LAFS_*,PIP_*,SUBUNITREPORTER_*,USERPROFILE,HOMEDRIVE,HOMEPATH,COLUMNS
|
||||
whitelist_externals =
|
||||
git
|
||||
deps =
|
||||
|
Loading…
Reference in New Issue
Block a user