Skip to content

Commit 15b0548

Browse files
authored
Merge pull request #92 from testing-cabal/python3.14
Drop Python 3.9 support/add Python 3.14 support
2 parents 014d967 + 6979249 commit 15b0548

File tree

13 files changed

+409
-425
lines changed

13 files changed

+409
-425
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ${{ matrix.os }}
1111
strategy:
1212
matrix:
13-
python-version: ["3.9", "3.10", "3.11", "3.12", "pypy3.9", '3.13']
13+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10"]
1414
os: ["ubuntu-latest"]
1515
steps:
1616
- uses: actions/checkout@v5

configure.ac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ AM_PROG_CC_C_O
2424
AC_PROG_INSTALL
2525
AC_PROG_LN_S
2626
AC_PROG_LIBTOOL
27-
AM_PATH_PYTHON([3.6])
27+
AM_PATH_PYTHON([3.10])
2828

2929
AS_IF([test "$GCC" = "yes"],
3030
[

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ classifiers = [
1515
"Programming Language :: Python :: 3.10",
1616
"Programming Language :: Python :: 3.11",
1717
"Programming Language :: Python :: 3.12",
18-
"Programming Language :: Python :: 3.9",
1918
"Programming Language :: Python :: 3.13",
19+
"Programming Language :: Python :: 3.14",
2020
"Programming Language :: Python",
2121
"Topic :: Software Development :: Testing",
2222
]
@@ -27,7 +27,7 @@ keywords = ["python", "streaming", "test"]
2727
license = { text = "Apache-2.0 or BSD" }
2828
name = "python-subunit"
2929
readme = "README.md"
30-
requires-python = ">=3.9"
30+
requires-python = ">=3.10"
3131

3232
[project.urls]
3333
"Bug Tracker" = "https://bugs.launchpad.net/subunit"

python/subunit/__init__.py

Lines changed: 51 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ def test_script_two(self):
123123
import iso8601
124124

125125
from testtools import ExtendedToOriginalDecorator, content, content_type
126-
from testtools.compat import _b, _u
127126
from testtools.content import TracebackContent
128127

129128
from testtools.testresult.real import _StringException as RemoteException
@@ -218,27 +217,27 @@ def write(self, bytes):
218217
pass
219218

220219
def read(self, len=0):
221-
return _b("")
220+
return b""
222221

223222

224223
class _ParserState(object):
225224
"""State for the subunit parser."""
226225

227226
def __init__(self, parser):
228227
self.parser = parser
229-
self._test_sym = (_b("test"), _b("testing"))
230-
self._colon_sym = _b(":")
231-
self._error_sym = (_b("error"),)
232-
self._failure_sym = (_b("failure"),)
233-
self._progress_sym = (_b("progress"),)
234-
self._skip_sym = _b("skip")
235-
self._success_sym = (_b("success"), _b("successful"))
236-
self._tags_sym = (_b("tags"),)
237-
self._time_sym = (_b("time"),)
238-
self._xfail_sym = (_b("xfail"),)
239-
self._uxsuccess_sym = (_b("uxsuccess"),)
240-
self._start_simple = _u(" [")
241-
self._start_multipart = _u(" [ multipart")
228+
self._test_sym = (b"test", b"testing")
229+
self._colon_sym = b":"
230+
self._error_sym = (b"error",)
231+
self._failure_sym = (b"failure",)
232+
self._progress_sym = (b"progress",)
233+
self._skip_sym = b"skip"
234+
self._success_sym = (b"success", b"successful")
235+
self._tags_sym = (b"tags",)
236+
self._time_sym = (b"time",)
237+
self._xfail_sym = (b"xfail",)
238+
self._uxsuccess_sym = (b"uxsuccess",)
239+
self._start_simple = " ["
240+
self._start_multipart = " [ multipart"
242241

243242
def addError(self, offset, line):
244243
"""An 'error:' directive has been read."""
@@ -296,7 +295,7 @@ def lineReceived(self, line):
296295

297296
def lostConnection(self):
298297
"""Connection lost."""
299-
self.parser._lostConnectionInTest(_u("unknown state of "))
298+
self.parser._lostConnectionInTest("unknown state of ")
300299

301300
def startTest(self, offset, line):
302301
"""A test start command received."""
@@ -376,7 +375,7 @@ def addSuccess(self, offset, line):
376375

377376
def lostConnection(self):
378377
"""Connection lost."""
379-
self.parser._lostConnectionInTest(_u(""))
378+
self.parser._lostConnectionInTest("")
380379

381380

382381
class _OutSideTest(_ParserState):
@@ -412,7 +411,7 @@ def lineReceived(self, line):
412411

413412
def lostConnection(self):
414413
"""Connection lost."""
415-
self.parser._lostConnectionInTest(_u("%s report of ") % self._outcome_label())
414+
self.parser._lostConnectionInTest("%s report of " % self._outcome_label())
416415

417416
def _outcome_label(self):
418417
"""The label to describe this outcome."""
@@ -523,9 +522,9 @@ def __init__(self, client, stream=None, forward_stream=None):
523522
# start with outside test.
524523
self._state = self._outside_test
525524
# Avoid casts on every call
526-
self._plusminus = _b("+-")
527-
self._push_sym = _b("push")
528-
self._pop_sym = _b("pop")
525+
self._plusminus = b"+-"
526+
self._push_sym = b"push"
527+
self._pop_sym = b"pop"
529528

530529
def _handleProgress(self, offset, line):
531530
"""Process a progress directive."""
@@ -555,15 +554,15 @@ def _handleTime(self, offset, line):
555554
try:
556555
event_time = iso8601.parse_date(line[offset:-1].decode())
557556
except TypeError:
558-
raise TypeError(_u("Failed to parse %r, got %r") % (line, sys.exc_info()[1]))
557+
raise TypeError("Failed to parse %r, got %r" % (line, sys.exc_info()[1]))
559558
self.client.time(event_time)
560559

561560
def lineReceived(self, line):
562561
"""Call the appropriate local method for the received line."""
563562
self._state.lineReceived(line)
564563

565564
def _lostConnectionInTest(self, state_string):
566-
error_string = _u("lost connection during %stest '%s'") % (state_string, self.current_test_description)
565+
error_string = "lost connection during %stest '%s'" % (state_string, self.current_test_description)
567566
self.client.addError(self._current_test, RemoteError(error_string))
568567
self.client.stopTest(self._current_test)
569568

@@ -615,14 +614,14 @@ def __init__(self, stream):
615614
testresult.TestResult.__init__(self)
616615
stream = make_stream_binary(stream)
617616
self._stream = stream
618-
self._progress_fmt = _b("progress: ")
619-
self._bytes_eol = _b("\n")
620-
self._progress_plus = _b("+")
621-
self._progress_push = _b("push")
622-
self._progress_pop = _b("pop")
623-
self._empty_bytes = _b("")
624-
self._start_simple = _b(" [\n")
625-
self._end_simple = _b("]\n")
617+
self._progress_fmt = b"progress: "
618+
self._bytes_eol = b"\n"
619+
self._progress_plus = b"+"
620+
self._progress_push = b"push"
621+
self._progress_pop = b"pop"
622+
self._empty_bytes = b""
623+
self._start_simple = b" [\n"
624+
self._end_simple = b"]\n"
626625

627626
def addError(self, test, error=None, details=None):
628627
"""Report an error in test test.
@@ -690,7 +689,7 @@ def _addOutcome(self, outcome, test, error=None, details=None, error_permitted=T
690689
:param error_permitted: If True then one and only one of error or
691690
details must be supplied. If False then error must not be supplied
692691
and details is still optional."""
693-
self._stream.write(_b("%s: " % outcome) + self._test_id(test))
692+
self._stream.write(("%s: " % outcome).encode() + self._test_id(test))
694693
if error_permitted:
695694
if error is None and details is None:
696695
raise ValueError
@@ -705,7 +704,7 @@ def _addOutcome(self, outcome, test, error=None, details=None, error_permitted=T
705704
elif details is not None:
706705
self._write_details(details)
707706
else:
708-
self._stream.write(_b("\n"))
707+
self._stream.write(b"\n")
709708
if details is not None or error is not None:
710709
self._stream.write(self._end_simple)
711710

@@ -714,8 +713,8 @@ def addSkip(self, test, reason=None, details=None):
714713
if reason is None:
715714
self._addOutcome("skip", test, error=None, details=details)
716715
else:
717-
self._stream.write(_b("skip: %s [\n" % test.id()))
718-
self._stream.write(_b("%s\n" % reason))
716+
self._stream.write(("skip: %s [\n" % test.id()).encode())
717+
self._stream.write(("%s\n" % reason).encode())
719718
self._stream.write(self._end_simple)
720719

721720
def addSuccess(self, test, details=None):
@@ -746,7 +745,7 @@ def _test_id(self, test):
746745
def startTest(self, test):
747746
"""Mark a test as starting its test run."""
748747
super(TestProtocolClient, self).startTest(test)
749-
self._stream.write(_b("test: ") + self._test_id(test) + _b("\n"))
748+
self._stream.write(b"test: " + self._test_id(test) + b"\n")
750749
self._stream.flush()
751750

752751
def stopTest(self, test):
@@ -765,7 +764,7 @@ def progress(self, offset, whence):
765764
"""
766765
if whence == PROGRESS_CUR and offset > -1:
767766
prefix = self._progress_plus
768-
offset = _b(str(offset))
767+
offset = str(offset).encode()
769768
elif whence == PROGRESS_PUSH:
770769
prefix = self._empty_bytes
771770
offset = self._progress_push
@@ -774,16 +773,16 @@ def progress(self, offset, whence):
774773
offset = self._progress_pop
775774
else:
776775
prefix = self._empty_bytes
777-
offset = _b(str(offset))
776+
offset = str(offset).encode()
778777
self._stream.write(self._progress_fmt + prefix + offset + self._bytes_eol)
779778

780779
def tags(self, new_tags, gone_tags):
781780
"""Inform the client about tags added/removed from the stream."""
782781
if not new_tags and not gone_tags:
783782
return
784783
tags = set([tag.encode("utf8") for tag in new_tags])
785-
tags.update([_b("-") + tag.encode("utf8") for tag in gone_tags])
786-
tag_line = _b("tags: ") + _b(" ").join(tags) + _b("\n")
784+
tags.update([b"-" + tag.encode("utf8") for tag in gone_tags])
785+
tag_line = b"tags: " + b" ".join(tags) + b"\n"
787786
self._stream.write(tag_line)
788787

789788
def time(self, a_datetime):
@@ -793,28 +792,28 @@ def time(self, a_datetime):
793792
"""
794793
time = a_datetime.astimezone(iso8601.UTC)
795794
self._stream.write(
796-
_b(
797-
"time: %04d-%02d-%02d %02d:%02d:%02d.%06dZ\n"
798-
% (time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond)
799-
)
795+
b"time: %04d-%02d-%02d %02d:%02d:%02d.%06dZ\n"
796+
% (time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond)
800797
)
801798

802799
def _write_details(self, details):
803800
"""Output details to the stream.
804801
805802
:param details: An extended details dict for a test outcome.
806803
"""
807-
self._stream.write(_b(" [ multipart\n"))
804+
self._stream.write(b" [ multipart\n")
808805
for name, content in sorted(details.items()): # noqa: F402
809-
self._stream.write(_b("Content-Type: %s/%s" % (content.content_type.type, content.content_type.subtype)))
806+
self._stream.write(
807+
("Content-Type: %s/%s" % (content.content_type.type, content.content_type.subtype)).encode()
808+
)
810809
parameters = content.content_type.parameters
811810
if parameters:
812-
self._stream.write(_b(";"))
811+
self._stream.write(b";")
813812
param_strs = []
814813
for param, value in sorted(parameters.items()):
815814
param_strs.append("%s=%s" % (param, value))
816-
self._stream.write(_b(",".join(param_strs)))
817-
self._stream.write(_b("\n%s\n" % name))
815+
self._stream.write(",".join(param_strs).encode())
816+
self._stream.write(b"\n" + name.encode() + b"\n")
818817
encoder = chunked.Encoder(self._stream)
819818
list(map(encoder.write, content.iter_bytes()))
820819
encoder.close()
@@ -831,7 +830,7 @@ def addDuration(self, test, duration):
831830
pass
832831

833832

834-
def RemoteError(description=_u("")):
833+
def RemoteError(description=""):
835834
return (RemoteException, RemoteException(description), None)
836835

837836

@@ -880,7 +879,7 @@ def run(self, result=None):
880879
if result is None:
881880
result = self.defaultTestResult()
882881
result.startTest(self)
883-
result.addError(self, RemoteError(_u("Cannot run RemotedTestCases.\n")))
882+
result.addError(self, RemoteError("Cannot run RemotedTestCases.\n"))
884883
result.stopTest(self)
885884

886885
def _strclass(self):
@@ -1323,7 +1322,7 @@ def _unwrap_text(stream):
13231322
except exceptions:
13241323
# Cannot read from the stream: try via writes
13251324
try:
1326-
stream.write(_b(""))
1325+
stream.write(b"")
13271326
except TypeError:
13281327
return stream.buffer
13291328
return stream

python/subunit/chunked.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717

1818
"""Encoder/decoder for http style chunked encoding."""
1919

20-
from testtools.compat import _b
21-
22-
empty = _b("")
20+
empty = b""
2321

2422

2523
class Decoder(object):
@@ -47,11 +45,11 @@ def __init__(self, output, strict=True):
4745
self.state = self._read_length
4846
self.body_length = 0
4947
self.strict = strict
50-
self._match_chars = _b("0123456789abcdefABCDEF\r\n")
51-
self._slash_n = _b("\n")
52-
self._slash_r = _b("\r")
53-
self._slash_rn = _b("\r\n")
54-
self._slash_nr = _b("\n\r")
48+
self._match_chars = b"0123456789abcdefABCDEF\r\n"
49+
self._slash_n = b"\n"
50+
self._slash_r = b"\r"
51+
self._slash_rn = b"\r\n"
52+
self._slash_nr = b"\n\r"
5553

5654
def close(self):
5755
"""Close the decoder.
@@ -164,7 +162,7 @@ def flush(self, extra_len=0):
164162
buffer_size = self.buffer_size
165163
self.buffered_bytes = []
166164
self.buffer_size = 0
167-
self.output.write(_b("%X\r\n" % (buffer_size + extra_len)))
165+
self.output.write(b"%X\r\n" % (buffer_size + extra_len))
168166
if buffer_size:
169167
self.output.write(empty.join(buffered_bytes))
170168
return True
@@ -182,4 +180,4 @@ def write(self, bytes):
182180
def close(self):
183181
"""Finish the stream. This does not close the output stream."""
184182
self.flush()
185-
self.output.write(_b("0\r\n"))
183+
self.output.write(b"0\r\n")

python/subunit/details.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@
1919
from io import BytesIO
2020

2121
from testtools import content, content_type
22-
from testtools.compat import _b
2322

2423
from subunit import chunked
2524

26-
end_marker = _b("]\n")
27-
quoted_marker = _b(" ]")
28-
empty = _b("")
25+
end_marker = b"]\n"
26+
quoted_marker = b" ]"
27+
empty = b""
2928

3029

3130
class DetailsParser(object):
@@ -36,7 +35,7 @@ class SimpleDetailsParser(DetailsParser):
3635
"""Parser for single-part [] delimited details."""
3736

3837
def __init__(self, state):
39-
self._message = _b("")
38+
self._message = b""
4039
self._state = state
4140

4241
def lineReceived(self, line):

0 commit comments

Comments
 (0)