From 6bf75998cad09c152bb14cd168768c66ae78cfa4 Mon Sep 17 00:00:00 2001 From: Jeong Yunwon Date: Mon, 18 Jul 2022 04:37:14 +0900 Subject: [PATCH 1/3] Update datetime from CPython 3.10.5 --- Lib/datetime.py | 186 +++++++++++++++++++++++++++++++----------------- 1 file changed, 119 insertions(+), 67 deletions(-) diff --git a/Lib/datetime.py b/Lib/datetime.py index a964b202e..d087c9852 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -4,9 +4,14 @@ See http://www.iana.org/time-zones/repository/tz-link.html for time zone and DST data sources. """ +__all__ = ("date", "datetime", "time", "timedelta", "timezone", "tzinfo", + "MINYEAR", "MAXYEAR") + + import time as _time import math as _math import sys +from operator import index as _index def _cmp(x, y): return 0 if x == y else 1 if x > y else -1 @@ -376,27 +381,10 @@ def _check_utc_offset(name, offset): "-timedelta(hours=24) and timedelta(hours=24)" % (name, offset)) -def _check_int_field(value): - if isinstance(value, int): - return value - if not isinstance(value, float): - try: - value = value.__int__() - except AttributeError: - pass - else: - if isinstance(value, int): - return value - raise TypeError('__int__ returned non-int (type %s)' % - type(value).__name__) - raise TypeError('an integer is required (got type %s)' % - type(value).__name__) - raise TypeError('integer argument expected, got float') - def _check_date_fields(year, month, day): - year = _check_int_field(year) - month = _check_int_field(month) - day = _check_int_field(day) + year = _index(year) + month = _index(month) + day = _index(day) if not MINYEAR <= year <= MAXYEAR: raise ValueError('year must be in %d..%d' % (MINYEAR, MAXYEAR), year) if not 1 <= month <= 12: @@ -407,10 +395,10 @@ def _check_date_fields(year, month, day): return year, month, day def _check_time_fields(hour, minute, second, microsecond, fold): - hour = _check_int_field(hour) - minute = _check_int_field(minute) - second = _check_int_field(second) - microsecond = _check_int_field(microsecond) + hour = _index(hour) + minute = _index(minute) + second = _index(second) + microsecond = _index(microsecond) if not 0 <= hour <= 23: raise ValueError('hour must be in 0..23', hour) if not 0 <= minute <= 59: @@ -718,31 +706,31 @@ class timedelta: if isinstance(other, timedelta): return self._cmp(other) == 0 else: - return False + return NotImplemented def __le__(self, other): if isinstance(other, timedelta): return self._cmp(other) <= 0 else: - _cmperror(self, other) + return NotImplemented def __lt__(self, other): if isinstance(other, timedelta): return self._cmp(other) < 0 else: - _cmperror(self, other) + return NotImplemented def __ge__(self, other): if isinstance(other, timedelta): return self._cmp(other) >= 0 else: - _cmperror(self, other) + return NotImplemented def __gt__(self, other): if isinstance(other, timedelta): return self._cmp(other) > 0 else: - _cmperror(self, other) + return NotImplemented def _cmp(self, other): assert isinstance(other, timedelta) @@ -869,6 +857,40 @@ class date: except Exception: raise ValueError(f'Invalid isoformat string: {date_string!r}') + @classmethod + def fromisocalendar(cls, year, week, day): + """Construct a date from the ISO year, week number and weekday. + + This is the inverse of the date.isocalendar() function""" + # Year is bounded this way because 9999-12-31 is (9999, 52, 5) + if not MINYEAR <= year <= MAXYEAR: + raise ValueError(f"Year is out of range: {year}") + + if not 0 < week < 53: + out_of_range = True + + if week == 53: + # ISO years have 53 weeks in them on years starting with a + # Thursday and leap years starting on a Wednesday + first_weekday = _ymd2ord(year, 1, 1) % 7 + if (first_weekday == 4 or (first_weekday == 3 and + _is_leap(year))): + out_of_range = False + + if out_of_range: + raise ValueError(f"Invalid week: {week}") + + if not 0 < day < 8: + raise ValueError(f"Invalid weekday: {day} (range is [1, 7])") + + # Now compute the offset from (Y, 1, 1) in days: + day_offset = (week - 1) * 7 + (day - 1) + + # Calculate the ordinal day for monday, week 1 + day_1 = _isoweek1monday(year) + ord_day = day_1 + day_offset + + return cls(*_ord2ymd(ord_day)) # Conversions to string @@ -1014,7 +1036,7 @@ class date: if isinstance(other, timedelta): o = self.toordinal() + other.days if 0 < o <= _MAXORDINAL: - return date.fromordinal(o) + return type(self).fromordinal(o) raise OverflowError("result out of range") return NotImplemented @@ -1042,7 +1064,7 @@ class date: return self.toordinal() % 7 or 7 def isocalendar(self): - """Return a 3-tuple containing ISO year, week number, and weekday. + """Return a named tuple containing ISO year, week number, and weekday. The first ISO week of the year is the (Mon-Sun) week containing the year's first Thursday; everything else derives @@ -1067,7 +1089,7 @@ class date: if today >= _isoweek1monday(year+1): year += 1 week = 0 - return year, week+1, day+1 + return _IsoCalendarDate(year, week+1, day+1) # Pickle support. @@ -1157,6 +1179,36 @@ class tzinfo: else: return (self.__class__, args, state) + +class IsoCalendarDate(tuple): + + def __new__(cls, year, week, weekday, /): + return super().__new__(cls, (year, week, weekday)) + + @property + def year(self): + return self[0] + + @property + def week(self): + return self[1] + + @property + def weekday(self): + return self[2] + + def __reduce__(self): + # This code is intended to pickle the object without making the + # class public. See https://bugs.python.org/msg352381 + return (tuple, (tuple(self),)) + + def __repr__(self): + return (f'{self.__class__.__name__}' + f'(year={self[0]}, week={self[1]}, weekday={self[2]})') + + +_IsoCalendarDate = IsoCalendarDate +del IsoCalendarDate _tzinfo_class = tzinfo class time: @@ -1261,31 +1313,31 @@ class time: if isinstance(other, time): return self._cmp(other, allow_mixed=True) == 0 else: - return False + return NotImplemented def __le__(self, other): if isinstance(other, time): return self._cmp(other) <= 0 else: - _cmperror(self, other) + return NotImplemented def __lt__(self, other): if isinstance(other, time): return self._cmp(other) < 0 else: - _cmperror(self, other) + return NotImplemented def __ge__(self, other): if isinstance(other, time): return self._cmp(other) >= 0 else: - _cmperror(self, other) + return NotImplemented def __gt__(self, other): if isinstance(other, time): return self._cmp(other) > 0 else: - _cmperror(self, other) + return NotImplemented def _cmp(self, other, allow_mixed=False): assert isinstance(other, time) @@ -1369,7 +1421,8 @@ class time: part is omitted if self.microsecond == 0. The optional argument timespec specifies the number of additional - terms of the time to include. + terms of the time to include. Valid options are 'auto', 'hours', + 'minutes', 'seconds', 'milliseconds' and 'microseconds'. """ s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) @@ -1495,7 +1548,7 @@ class time: self._tzinfo = tzinfo def __reduce_ex__(self, protocol): - return (time, self._getstate(protocol)) + return (self.__class__, self._getstate(protocol)) def __reduce__(self): return self.__reduce_ex__(2) @@ -1506,6 +1559,7 @@ time.min = time(0, 0, 0) time.max = time(23, 59, 59, 999999) time.resolution = timedelta(microseconds=1) + class datetime(date): """datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]]) @@ -1598,7 +1652,7 @@ class datetime(date): y, m, d, hh, mm, ss, weekday, jday, dst = converter(t) ss = min(ss, 59) # clamp out leap seconds if the platform has them result = cls(y, m, d, hh, mm, ss, us, tz) - if tz is None: + if tz is None and not utc: # As of version 2015f max fold in IANA database is # 23 hours at 1969-09-30 13:00:00 in Kwajalein. # Let's probe 24 hours in the past to detect a transition: @@ -1619,7 +1673,7 @@ class datetime(date): probe2 = cls(y, m, d, hh, mm, ss, us, tz) if probe2 == result: result._fold = 1 - else: + elif tz is not None: result = tz.fromutc(result) return result @@ -1798,17 +1852,10 @@ class datetime(date): ts = (self - _EPOCH) // timedelta(seconds=1) localtm = _time.localtime(ts) local = datetime(*localtm[:6]) - try: - # Extract TZ data if available - gmtoff = localtm.tm_gmtoff - zone = localtm.tm_zone - except AttributeError: - delta = local - datetime(*_time.gmtime(ts)[:6]) - zone = _time.strftime('%Z', localtm) - tz = timezone(delta, zone) - else: - tz = timezone(timedelta(seconds=gmtoff), zone) - return tz + # Extract TZ data + gmtoff = localtm.tm_gmtoff + zone = localtm.tm_zone + return timezone(timedelta(seconds=gmtoff), zone) def astimezone(self, tz=None): if tz is None: @@ -1860,7 +1907,8 @@ class datetime(date): time, default 'T'. The optional argument timespec specifies the number of additional - terms of the time to include. + terms of the time to include. Valid options are 'auto', 'hours', + 'minutes', 'seconds', 'milliseconds' and 'microseconds'. """ s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) + _format_time(self._hour, self._minute, self._second, @@ -2031,10 +2079,10 @@ class datetime(date): hour, rem = divmod(delta.seconds, 3600) minute, second = divmod(rem, 60) if 0 < delta.days <= _MAXORDINAL: - return datetime.combine(date.fromordinal(delta.days), - time(hour, minute, second, - delta.microseconds, - tzinfo=self._tzinfo)) + return type(self).combine(date.fromordinal(delta.days), + time(hour, minute, second, + delta.microseconds, + tzinfo=self._tzinfo)) raise OverflowError("result out of range") __radd__ = __add__ @@ -2133,6 +2181,7 @@ def _isoweek1monday(year): week1monday += 7 return week1monday + class timezone(tzinfo): __slots__ = '_offset', '_name' @@ -2167,9 +2216,9 @@ class timezone(tzinfo): return (self._offset, self._name) def __eq__(self, other): - if type(other) != timezone: - return False - return self._offset == other._offset + if isinstance(other, timezone): + return self._offset == other._offset + return NotImplemented def __hash__(self): return hash(self._offset) @@ -2226,7 +2275,7 @@ class timezone(tzinfo): raise TypeError("fromutc() argument must be a datetime instance" " or None") - _maxoffset = timedelta(hours=23, minutes=59) + _maxoffset = timedelta(hours=24, microseconds=-1) _minoffset = -_maxoffset @staticmethod @@ -2250,8 +2299,11 @@ class timezone(tzinfo): return f'UTC{sign}{hours:02d}:{minutes:02d}' timezone.utc = timezone._create(timedelta(0)) -timezone.min = timezone._create(timezone._minoffset) -timezone.max = timezone._create(timezone._maxoffset) +# bpo-37642: These attributes are rounded to the nearest minute for backwards +# compatibility, even though the constructor will accept a wider range of +# values. This may change in the future. +timezone.min = timezone._create(-timedelta(hours=23, minutes=59)) +timezone.max = timezone._create(timedelta(hours=23, minutes=59)) _EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) # Some time zone algebra. For a datetime x, let @@ -2275,7 +2327,7 @@ _EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) # This is again a requirement for a sane tzinfo class. # # 4. (x+k).s = x.s -# This follows from #2, and that datimetimetz+timedelta preserves tzinfo. +# This follows from #2, and that datetime.timetz+timedelta preserves tzinfo. # # 5. (x+k).n = x.n + k # Again follows from how arithmetic is defined. @@ -2458,13 +2510,13 @@ else: # Clean up unused names del (_DAYNAMES, _DAYS_BEFORE_MONTH, _DAYS_IN_MONTH, _DI100Y, _DI400Y, _DI4Y, _EPOCH, _MAXORDINAL, _MONTHNAMES, _build_struct_time, - _check_date_fields, _check_int_field, _check_time_fields, + _check_date_fields, _check_time_fields, _check_tzinfo_arg, _check_tzname, _check_utc_offset, _cmp, _cmperror, _date_class, _days_before_month, _days_before_year, _days_in_month, - _format_time, _format_offset, _is_leap, _isoweek1monday, _math, + _format_time, _format_offset, _index, _is_leap, _isoweek1monday, _math, _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, _divide_and_round, _parse_isoformat_date, _parse_isoformat_time, - _parse_hh_mm_ss_ff) + _parse_hh_mm_ss_ff, _IsoCalendarDate) # XXX Since import * above excludes names that start with _, # docstring does not get overwritten. In the future, it may be # appropriate to maintain a single module level docstring and From 413e8250f0a5e56bac770baa7ff792da69a245fa Mon Sep 17 00:00:00 2001 From: CPython developers <> Date: Mon, 18 Jul 2022 04:56:35 +0900 Subject: [PATCH 2/3] Update {test_}json from CPython 3.10.5 --- Lib/json/__init__.py | 15 +---- Lib/json/encoder.py | 2 +- Lib/json/tool.py | 58 ++++++++++++---- Lib/test/test_json/__init__.py | 2 +- Lib/test/test_json/test_decode.py | 4 -- Lib/test/test_json/test_recursion.py | 21 ++++-- Lib/test/test_json/test_speedups.py | 9 +++ Lib/test/test_json/test_tool.py | 99 +++++++++++++++++++++++++--- 8 files changed, 160 insertions(+), 50 deletions(-) diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index 1ba8b48bd..e4c21daaf 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -133,7 +133,7 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, If ``check_circular`` is false, then the circular reference check for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). + result in an ``RecursionError`` (or worse). If ``allow_nan`` is false, then it will be a ``ValueError`` to serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) @@ -195,7 +195,7 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, If ``check_circular`` is false, then the circular reference check for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). + result in an ``RecursionError`` (or worse). If ``allow_nan`` is false, then it will be a ``ValueError`` to serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in @@ -329,8 +329,6 @@ def loads(s, *, cls=None, object_hook=None, parse_float=None, To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` kwarg; otherwise ``JSONDecoder`` is used. - - The ``encoding`` argument is ignored and deprecated since Python 3.1. """ if isinstance(s, str): if s.startswith('\ufeff'): @@ -342,15 +340,6 @@ def loads(s, *, cls=None, object_hook=None, parse_float=None, f'not {s.__class__.__name__}') s = s.decode(detect_encoding(s), 'surrogatepass') - if "encoding" in kw: - import warnings - warnings.warn( - "'encoding' is ignored and deprecated. It will be removed in Python 3.9", - DeprecationWarning, - stacklevel=2 - ) - del kw['encoding'] - if (cls is None and object_hook is None and parse_int is None and parse_float is None and parse_constant is None and object_pairs_hook is None and not kw): diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index c8c78b9c2..21bff2c1a 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -116,7 +116,7 @@ class JSONEncoder(object): If check_circular is true, then lists, dicts, and custom encoded objects will be checked for circular references during encoding to - prevent an infinite recursion (which would cause an OverflowError). + prevent an infinite recursion (which would cause an RecursionError). Otherwise, no such check takes place. If allow_nan is true, then NaN, Infinity, and -Infinity will be diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 8db9ea40a..0490b8c0b 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -13,6 +13,7 @@ Usage:: import argparse import json import sys +from pathlib import Path def main(): @@ -25,31 +26,60 @@ def main(): help='a JSON file to be validated or pretty-printed', default=sys.stdin) parser.add_argument('outfile', nargs='?', - type=argparse.FileType('w', encoding="utf-8"), + type=Path, help='write the output of infile to outfile', - default=sys.stdout) + default=None) parser.add_argument('--sort-keys', action='store_true', default=False, help='sort the output of dictionaries alphabetically by key') + parser.add_argument('--no-ensure-ascii', dest='ensure_ascii', action='store_false', + help='disable escaping of non-ASCII characters') parser.add_argument('--json-lines', action='store_true', default=False, - help='parse input using the jsonlines format') + help='parse input using the JSON Lines format. ' + 'Use with --no-indent or --compact to produce valid JSON Lines output.') + group = parser.add_mutually_exclusive_group() + group.add_argument('--indent', default=4, type=int, + help='separate items with newlines and use this number ' + 'of spaces for indentation') + group.add_argument('--tab', action='store_const', dest='indent', + const='\t', help='separate items with newlines and use ' + 'tabs for indentation') + group.add_argument('--no-indent', action='store_const', dest='indent', + const=None, + help='separate items with spaces rather than newlines') + group.add_argument('--compact', action='store_true', + help='suppress all whitespace separation (most compact)') options = parser.parse_args() - infile = options.infile - outfile = options.outfile - sort_keys = options.sort_keys - json_lines = options.json_lines - with infile, outfile: + dump_args = { + 'sort_keys': options.sort_keys, + 'indent': options.indent, + 'ensure_ascii': options.ensure_ascii, + } + if options.compact: + dump_args['indent'] = None + dump_args['separators'] = ',', ':' + + with options.infile as infile: try: - if json_lines: + if options.json_lines: objs = (json.loads(line) for line in infile) else: - objs = (json.load(infile), ) - for obj in objs: - json.dump(obj, outfile, sort_keys=sort_keys, indent=4) - outfile.write('\n') + objs = (json.load(infile),) + + if options.outfile is None: + out = sys.stdout + else: + out = options.outfile.open('w', encoding='utf-8') + with out as outfile: + for obj in objs: + json.dump(obj, outfile, **dump_args) + outfile.write('\n') except ValueError as e: raise SystemExit(e) if __name__ == '__main__': - main() + try: + main() + except BrokenPipeError as exc: + sys.exit(exc.errno) diff --git a/Lib/test/test_json/__init__.py b/Lib/test/test_json/__init__.py index e236bb23e..f43bded1a 100644 --- a/Lib/test/test_json/__init__.py +++ b/Lib/test/test_json/__init__.py @@ -6,6 +6,7 @@ import unittest from test import support from test.support import import_helper + # import json with and without accelerations # XXX RUSTPYTHON: we don't import _json as fresh since the fresh module isn't placed # into the sys.modules cache, and therefore the vm can't recognize the _json.Scanner class @@ -40,7 +41,6 @@ class TestPyTest(PyTest): 'json.encoder') class TestCTest(CTest): - @unittest.expectedFailure def test_cjson(self): self.assertEqual(self.json.scanner.make_scanner.__module__, '_json') self.assertEqual(self.json.decoder.scanstring.__module__, '_json') diff --git a/Lib/test/test_json/test_decode.py b/Lib/test/test_json/test_decode.py index 1ab3a2aa7..e48e36c54 100644 --- a/Lib/test/test_json/test_decode.py +++ b/Lib/test/test_json/test_decode.py @@ -97,10 +97,6 @@ class TestDecode: d = self.json.JSONDecoder() self.assertRaises(ValueError, d.raw_decode, 'a'*42, -50000) - def test_deprecated_encode(self): - with self.assertWarns(DeprecationWarning): - self.loads('{}', encoding='fake') - class TestPyDecode(TestDecode, PyTest): pass # TODO: RUSTPYTHON class TestCDecode(TestDecode, CTest): # pass diff --git a/Lib/test/test_json/test_recursion.py b/Lib/test/test_json/test_recursion.py index 877dc448b..9919d7fbe 100644 --- a/Lib/test/test_json/test_recursion.py +++ b/Lib/test/test_json/test_recursion.py @@ -1,3 +1,4 @@ +from test import support from test.test_json import PyTest, CTest @@ -52,7 +53,7 @@ class TestRecursion: return [JSONTestObject] else: return 'JSONTestObject' - return pyjson.JSONEncoder.default(o) + return self.json.JSONEncoder.default(o) enc = RecursiveJSONEncoder() self.assertEqual(enc.encode(JSONTestObject), '"JSONTestObject"') @@ -69,11 +70,14 @@ class TestRecursion: # test that loading highly-nested objects doesn't segfault when C # accelerations are used. See #12017 with self.assertRaises(RecursionError): - self.loads('{"a":' * 100000 + '1' + '}' * 100000) + with support.infinite_recursion(): + self.loads('{"a":' * 100000 + '1' + '}' * 100000) with self.assertRaises(RecursionError): - self.loads('{"a":' * 100000 + '[1]' + '}' * 100000) + with support.infinite_recursion(): + self.loads('{"a":' * 100000 + '[1]' + '}' * 100000) with self.assertRaises(RecursionError): - self.loads('[' * 100000 + '1' + ']' * 100000) + with support.infinite_recursion(): + self.loads('[' * 100000 + '1' + ']' * 100000) def test_highly_nested_objects_encoding(self): # See #12051 @@ -81,9 +85,11 @@ class TestRecursion: for x in range(100000): l, d = [l], {'k':d} with self.assertRaises(RecursionError): - self.dumps(l) + with support.infinite_recursion(): + self.dumps(l) with self.assertRaises(RecursionError): - self.dumps(d) + with support.infinite_recursion(): + self.dumps(d) def test_endless_recursion(self): # See #12051 @@ -93,7 +99,8 @@ class TestRecursion: return [o] with self.assertRaises(RecursionError): - EndlessJSONEncoder(check_circular=False).encode(5j) + with support.infinite_recursion(): + EndlessJSONEncoder(check_circular=False).encode(5j) class TestPyRecursion(TestRecursion, PyTest): pass diff --git a/Lib/test/test_json/test_speedups.py b/Lib/test/test_json/test_speedups.py index 1bf34ecba..214bfa45c 100644 --- a/Lib/test/test_json/test_speedups.py +++ b/Lib/test/test_json/test_speedups.py @@ -62,6 +62,15 @@ class TestEncode(CTest): with self.assertRaises(ZeroDivisionError): enc('spam', 4) + def test_bad_markers_argument_to_encoder(self): + # https://bugs.python.org/issue45269 + with self.assertRaisesRegex( + TypeError, + r'make_encoder\(\) argument 1 must be dict or None, not int', + ): + self.json.encoder.c_make_encoder(1, None, None, None, ': ', ', ', + False, False, False) + # TODO: RUSTPYTHON, translate the encoder to Rust @unittest.expectedFailure def test_bad_bool_args(self): diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 9f2329ad2..1d7fca6ef 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -1,8 +1,10 @@ +import errno import os import sys import textwrap import unittest -from subprocess import Popen, PIPE +import subprocess + from test import support from test.support import os_helper from test.support.script_helper import assert_python_ok @@ -85,10 +87,9 @@ class TestTool(unittest.TestCase): def test_stdin_stdout(self): args = sys.executable, '-m', 'json.tool' - with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) as proc: - out, err = proc.communicate(self.data.encode()) - self.assertEqual(out.splitlines(), self.expect.encode().splitlines()) - self.assertEqual(err, b'') + process = subprocess.run(args, input=self.data, capture_output=True, text=True, check=True) + self.assertEqual(process.stdout, self.expect) + self.assertEqual(process.stderr, '') def _create_infile(self, data=None): infile = os_helper.TESTFN @@ -124,7 +125,16 @@ class TestTool(unittest.TestCase): outfile = os_helper.TESTFN + '.out' rc, out, err = assert_python_ok('-m', 'json.tool', infile, outfile) self.addCleanup(os.remove, outfile) - with open(outfile, "r") as fp: + with open(outfile, "r", encoding="utf-8") as fp: + self.assertEqual(fp.read(), self.expect) + self.assertEqual(rc, 0) + self.assertEqual(out, b'') + self.assertEqual(err, b'') + + def test_writing_in_place(self): + infile = self._create_infile() + rc, out, err = assert_python_ok('-m', 'json.tool', infile, infile) + with open(infile, "r", encoding="utf-8") as fp: self.assertEqual(fp.read(), self.expect) self.assertEqual(rc, 0) self.assertEqual(out, b'') @@ -132,10 +142,9 @@ class TestTool(unittest.TestCase): def test_jsonlines(self): args = sys.executable, '-m', 'json.tool', '--json-lines' - with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) as proc: - out, err = proc.communicate(self.jsonlines_raw.encode()) - self.assertEqual(out.splitlines(), self.jsonlines_expect.encode().splitlines()) - self.assertEqual(err, b'') + process = subprocess.run(args, input=self.jsonlines_raw, capture_output=True, text=True, check=True) + self.assertEqual(process.stdout, self.jsonlines_expect) + self.assertEqual(process.stderr, '') def test_help_flag(self): rc, out, err = assert_python_ok('-m', 'json.tool', '-h') @@ -150,3 +159,73 @@ class TestTool(unittest.TestCase): self.assertEqual(out.splitlines(), self.expect_without_sort_keys.encode().splitlines()) self.assertEqual(err, b'') + + def test_indent(self): + input_ = '[1, 2]' + expect = textwrap.dedent('''\ + [ + 1, + 2 + ] + ''') + args = sys.executable, '-m', 'json.tool', '--indent', '2' + process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True) + self.assertEqual(process.stdout, expect) + self.assertEqual(process.stderr, '') + + def test_no_indent(self): + input_ = '[1,\n2]' + expect = '[1, 2]\n' + args = sys.executable, '-m', 'json.tool', '--no-indent' + process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True) + self.assertEqual(process.stdout, expect) + self.assertEqual(process.stderr, '') + + def test_tab(self): + input_ = '[1, 2]' + expect = '[\n\t1,\n\t2\n]\n' + args = sys.executable, '-m', 'json.tool', '--tab' + process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True) + self.assertEqual(process.stdout, expect) + self.assertEqual(process.stderr, '') + + def test_compact(self): + input_ = '[ 1 ,\n 2]' + expect = '[1,2]\n' + args = sys.executable, '-m', 'json.tool', '--compact' + process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True) + self.assertEqual(process.stdout, expect) + self.assertEqual(process.stderr, '') + + def test_no_ensure_ascii_flag(self): + infile = self._create_infile('{"key":"💩"}') + outfile = os_helper.TESTFN + '.out' + self.addCleanup(os.remove, outfile) + assert_python_ok('-m', 'json.tool', '--no-ensure-ascii', infile, outfile) + with open(outfile, "rb") as f: + lines = f.read().splitlines() + # asserting utf-8 encoded output file + expected = [b'{', b' "key": "\xf0\x9f\x92\xa9"', b"}"] + self.assertEqual(lines, expected) + + def test_ensure_ascii_default(self): + infile = self._create_infile('{"key":"💩"}') + outfile = os_helper.TESTFN + '.out' + self.addCleanup(os.remove, outfile) + assert_python_ok('-m', 'json.tool', infile, outfile) + with open(outfile, "rb") as f: + lines = f.read().splitlines() + # asserting an ascii encoded output file + expected = [b'{', rb' "key": "\ud83d\udca9"', b"}"] + self.assertEqual(lines, expected) + + @unittest.skipIf(sys.platform =="win32", "The test is failed with ValueError on Windows") + def test_broken_pipe_error(self): + cmd = [sys.executable, '-m', 'json.tool'] + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE) + # bpo-39828: Closing before json.tool attempts to write into stdout. + proc.stdout.close() + proc.communicate(b'"{}"') + self.assertEqual(proc.returncode, errno.EPIPE) From 2f4000b23990937bf07baaf8d19f7d77c4228462 Mon Sep 17 00:00:00 2001 From: Jeong Yunwon Date: Mon, 18 Jul 2022 05:01:29 +0900 Subject: [PATCH 3/3] mark faiing tests from test_json --- Lib/test/test_json/__init__.py | 2 ++ Lib/test/test_json/test_speedups.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Lib/test/test_json/__init__.py b/Lib/test/test_json/__init__.py index f43bded1a..b919af232 100644 --- a/Lib/test/test_json/__init__.py +++ b/Lib/test/test_json/__init__.py @@ -41,6 +41,8 @@ class TestPyTest(PyTest): 'json.encoder') class TestCTest(CTest): + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_cjson(self): self.assertEqual(self.json.scanner.make_scanner.__module__, '_json') self.assertEqual(self.json.decoder.scanstring.__module__, '_json') diff --git a/Lib/test/test_json/test_speedups.py b/Lib/test/test_json/test_speedups.py index 214bfa45c..66ce1d1b3 100644 --- a/Lib/test/test_json/test_speedups.py +++ b/Lib/test/test_json/test_speedups.py @@ -62,6 +62,8 @@ class TestEncode(CTest): with self.assertRaises(ZeroDivisionError): enc('spam', 4) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_bad_markers_argument_to_encoder(self): # https://bugs.python.org/issue45269 with self.assertRaisesRegex(