diff --git a/devscripts/prepare_manpage.py b/devscripts/prepare_manpage.py index 47188e9923..623f86ecba 100644 --- a/devscripts/prepare_manpage.py +++ b/devscripts/prepare_manpage.py @@ -66,8 +66,7 @@ def convert_code_blocks(readme): def move_sections(readme): MOVE_TAG_TEMPLATE = '' - sections = re.findall(r'(?m)^%s$' % ( - re.escape(MOVE_TAG_TEMPLATE).replace(r'\%', '%') % '(.+)'), readme) + sections = re.findall(r'(?m)^%s$' % (re.escape(MOVE_TAG_TEMPLATE) % '(.+)'), readme) for section_name in sections: move_tag = MOVE_TAG_TEMPLATE % section_name diff --git a/test/helper.py b/test/helper.py index 4f65a09d13..4ba16fd6a4 100644 --- a/test/helper.py +++ b/test/helper.py @@ -3,7 +3,6 @@ import json import os.path import re -import ssl import sys import types @@ -318,36 +317,6 @@ def _repr(v): 'Missing keys in test definition: {}'.format(', '.join(sorted(missing_keys)))) -def assertRegexpMatches(self, text, regexp, msg=None): - if hasattr(self, 'assertRegexp'): - return self.assertRegexp(text, regexp, msg) - else: - m = re.match(regexp, text) - if not m: - note = f'Regexp didn\'t match: {regexp!r} not found' - if len(text) < 1000: - note += f' in {text!r}' - if msg is None: - msg = note - else: - msg = note + ', ' + msg - self.assertTrue(m, msg) - - -def assertGreaterEqual(self, got, expected, msg=None): - if not (got >= expected): - if msg is None: - msg = f'{got!r} not greater than or equal to {expected!r}' - self.assertTrue(got >= expected, msg) - - -def assertLessEqual(self, got, expected, msg=None): - if not (got <= expected): - if msg is None: - msg = f'{got!r} not less than or equal to {expected!r}' - self.assertTrue(got <= expected, msg) - - def assertEqual(self, got, expected, msg=None): if got != expected: if msg is None: @@ -366,12 +335,7 @@ def _report_warning(w, *args, **kwargs): def http_server_port(httpd): - if os.name == 'java' and isinstance(httpd.socket, ssl.SSLSocket): - # In Jython SSLSocket is not a subclass of socket.socket - sock = httpd.socket.sock - else: - sock = httpd.socket - return sock.getsockname()[1] + return httpd.server_address[1] def verify_address_availability(address): diff --git a/test/test_YoutubeDL.py b/test/test_YoutubeDL.py index 0f84d7e169..e507bad9df 100644 --- a/test/test_YoutubeDL.py +++ b/test/test_YoutubeDL.py @@ -15,7 +15,7 @@ import copy import json -from test.helper import FakeYDL, assertRegexpMatches, try_rm +from test.helper import FakeYDL, try_rm from yt_dlp import YoutubeDL from yt_dlp.extractor.common import InfoExtractor from yt_dlp.postprocessor.common import PostProcessor @@ -860,10 +860,10 @@ def gen(): def test_format_note(self): ydl = YoutubeDL() self.assertEqual(ydl._format_note({}), '') - assertRegexpMatches(self, ydl._format_note({ + self.assertRegex(ydl._format_note({ 'vbr': 10, }), r'^\s*10k$') - assertRegexpMatches(self, ydl._format_note({ + self.assertRegex(ydl._format_note({ 'fps': 30, }), r'^30fps$') diff --git a/test/test_download.py b/test/test_download.py index a9199c49d9..8875ca2696 100755 --- a/test/test_download.py +++ b/test/test_download.py @@ -13,8 +13,6 @@ import json from test.helper import ( - assertGreaterEqual, - assertLessEqual, expect_info_dict, expect_warnings, get_params, @@ -201,8 +199,8 @@ def try_rm_tcs_files(tcs=None): num_entries = len(res_dict.get('entries', [])) if 'playlist_mincount' in test_case: mincount = test_case['playlist_mincount'] - assertGreaterEqual( - self, num_entries, mincount, + self.assertGreaterEqual( + num_entries, mincount, f'Expected at least {mincount} entries in playlist {test_url}, but got only {num_entries}') if 'playlist_count' in test_case: count = test_case['playlist_count'] @@ -212,8 +210,8 @@ def try_rm_tcs_files(tcs=None): f'Expected exactly {count} entries in playlist {test_url}, but got {got}') if 'playlist_maxcount' in test_case: maxcount = test_case['playlist_maxcount'] - assertLessEqual( - self, num_entries, maxcount, + self.assertLessEqual( + num_entries, maxcount, f'Expected at most {maxcount} entries in playlist {test_url}, but got more') if 'playlist_duration_sum' in test_case: got_duration = sum(e['duration'] for e in res_dict['entries']) @@ -241,8 +239,8 @@ def try_rm_tcs_files(tcs=None): if params.get('test'): expected_minsize = max(expected_minsize, 10000) got_fsize = os.path.getsize(tc_filename) - assertGreaterEqual( - self, got_fsize, expected_minsize, + self.assertGreaterEqual( + got_fsize, expected_minsize, f'Expected {tc_filename} to be at least {format_bytes(expected_minsize)}, ' f'but it\'s only {format_bytes(got_fsize)} ') if 'md5' in tc: diff --git a/test/test_networking.py b/test/test_networking.py index 7c5aa4bb56..dd8e7d9626 100644 --- a/test/test_networking.py +++ b/test/test_networking.py @@ -984,28 +984,15 @@ def test_verify_cert_error_text(self, handler): ): validate_and_send(rh, Request(f'https://127.0.0.1:{self.https_port}/headers')) - @pytest.mark.parametrize('req,match,version_check', [ + @pytest.mark.parametrize('req,match', [ # https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1256 - # bpo-39603: Check implemented in 3.7.9+, 3.8.5+ - ( - Request('http://127.0.0.1', method='GET\n'), - 'method can\'t contain control characters', - lambda v: v < (3, 7, 9) or (3, 8, 0) <= v < (3, 8, 5), - ), + (Request('http://127.0.0.1', method='GET\n'), 'method can\'t contain control characters'), # https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1265 - # bpo-38576: Check implemented in 3.7.8+, 3.8.3+ - ( - Request('http://127.0.0. 1', method='GET'), - 'URL can\'t contain control characters', - lambda v: v < (3, 7, 8) or (3, 8, 0) <= v < (3, 8, 3), - ), + (Request('http://127.0.0. 1', method='GET'), 'URL can\'t contain control characters'), # https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1288C31-L1288C50 - (Request('http://127.0.0.1', headers={'foo\n': 'bar'}), 'Invalid header name', None), + (Request('http://127.0.0.1', headers={'foo\n': 'bar'}), 'Invalid header name'), ]) - def test_httplib_validation_errors(self, handler, req, match, version_check): - if version_check and version_check(sys.version_info): - pytest.skip(f'Python {sys.version} version does not have the required validation for this test.') - + def test_httplib_validation_errors(self, handler, req, match): with handler() as rh: with pytest.raises(RequestError, match=match) as exc_info: validate_and_send(rh, req) diff --git a/test/test_utils.py b/test/test_utils.py index 17e43f9b36..7d34fded35 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1347,15 +1347,8 @@ def test_extract_attributes(self): self.assertEqual(extract_attributes(''), {'_:funny-name1': '1'}) self.assertEqual(extract_attributes(''), {'x': 'Fáilte 世界 \U0001f600'}) self.assertEqual(extract_attributes(''), {'x': 'décompose\u0301'}) - # "Narrow" Python builds don't support unicode code points outside BMP. - try: - chr(0x10000) - supports_outside_bmp = True - except ValueError: - supports_outside_bmp = False - if supports_outside_bmp: - self.assertEqual(extract_attributes(''), {'x': 'Smile \U0001f600!'}) - # Malformed HTML should not break attributes extraction on older Python + self.assertEqual(extract_attributes(''), {'x': 'Smile \U0001f600!'}) + # Malformed HTML should not break attribute extraction self.assertEqual(extract_attributes(''), {}) def test_clean_html(self): diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 051d1b0fb4..9bd9423218 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -2398,8 +2398,7 @@ def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, ins selectors = [] current_selector = None for type_, string_, start, _, _ in tokens: - # ENCODING is only defined in Python 3.x - if type_ == getattr(tokenize, 'ENCODING', None): + if type_ == tokenize.ENCODING: continue elif type_ in [tokenize.NAME, tokenize.NUMBER]: current_selector = FormatSelector(SINGLE, string_, []) diff --git a/yt_dlp/aes.py b/yt_dlp/aes.py index e5a2e67cff..a501bbaff9 100644 --- a/yt_dlp/aes.py +++ b/yt_dlp/aes.py @@ -293,7 +293,7 @@ def aes_decrypt_text(data, password, key_size_bytes): - Mode of operation is 'counter' @param {str} data Base64 encoded string - @param {str,unicode} password Password (will be encoded with utf-8) + @param {str} password Password (will be encoded with UTF-8) @param {int} key_size_bytes Possible values: 16 for 128-Bit, 24 for 192-Bit or 32 for 256-Bit @returns {str} Decrypted data """ diff --git a/yt_dlp/compat/__init__.py b/yt_dlp/compat/__init__.py index ad1268143c..b297fe6ac4 100644 --- a/yt_dlp/compat/__init__.py +++ b/yt_dlp/compat/__init__.py @@ -8,9 +8,8 @@ del passthrough_module -# HTMLParseError has been deprecated in Python 3.3 and removed in -# Python 3.5. Introducing dummy exception for Python >3.5 for compatible -# and uniform cross-version exception handling +# HTMLParseError was deprecated in Python 3.3 and removed in Python 3.5. +# Keep a replacement for API compatibility and uniform exception handling. class compat_HTMLParseError(ValueError): pass diff --git a/yt_dlp/downloader/ism.py b/yt_dlp/downloader/ism.py index 62c3a3b7fd..e7ddb0bbd5 100644 --- a/yt_dlp/downloader/ism.py +++ b/yt_dlp/downloader/ism.py @@ -154,7 +154,7 @@ def write_piff_header(stream, params): sample_entry_payload += u16.pack(0x18) # depth sample_entry_payload += s16.pack(-1) # pre defined - codec_private_data = binascii.unhexlify(params['codec_private_data'].encode()) + codec_private_data = binascii.unhexlify(params['codec_private_data']) if fourcc in ('H264', 'AVC1'): sps, pps = codec_private_data.split(u32.pack(1))[1:] avcc_payload = u8.pack(1) # configuration version diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py index 5278c07bde..180785f6a2 100644 --- a/yt_dlp/extractor/common.py +++ b/yt_dlp/extractor/common.py @@ -432,29 +432,28 @@ class InfoExtractor: chapter: Name or title of the chapter the video belongs to. chapter_number: Number of the chapter the video belongs to, as an integer. - chapter_id: Id of the chapter the video belongs to, as a unicode string. + chapter_id: Id of the chapter the video belongs to. The following fields should only be used when the video is an episode of some series, programme or podcast: series: Title of the series or programme the video episode belongs to. - series_id: Id of the series or programme the video episode belongs to, as a unicode string. + series_id: Id of the series or programme the video episode belongs to. season: Title of the season the video episode belongs to. season_number: Number of the season the video episode belongs to, as an integer. - season_id: Id of the season the video episode belongs to, as a unicode string. + season_id: Id of the season the video episode belongs to. episode: Title of the video episode. Unlike mandatory video title field, this field should denote the exact title of the video episode without any kind of decoration. episode_number: Number of the video episode within a season, as an integer. - episode_id: Id of the video episode, as a unicode string. + episode_id: Id of the video episode. The following fields should only be used when the media is a track or a part of a music album: track: Title of the track. track_number: Number of the track within an album or a disc, as an integer. - track_id: Id of the track (useful in case of custom indexing, e.g. 6.iii), - as a unicode string. + track_id: Id of the track (useful for custom indexing, e.g. 6.iii). artists: List of artists of the track. composers: List of composers of the piece. genres: List of genres of the track. @@ -487,7 +486,7 @@ class InfoExtractor: creator: Use "creators" instead. The creator of the video. - Unless mentioned otherwise, the fields should be Unicode strings. + Unless mentioned otherwise, the fields should be strings. Unless mentioned otherwise, None is equivalent to absence of information. diff --git a/yt_dlp/extractor/itv.py b/yt_dlp/extractor/itv.py index 65e6443729..187e755c1f 100644 --- a/yt_dlp/extractor/itv.py +++ b/yt_dlp/extractor/itv.py @@ -114,7 +114,7 @@ def _get_subtitles(self, video_id, variants, ios_playlist_url, headers, *args, * # See: https://github.com/yt-dlp/yt-dlp/issues/986 platform_tag_subs, featureset_subs = next( ((platform_tag, featureset) - for platform_tag, featuresets in reversed(list(variants.items())) for featureset in featuresets + for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets if try_get(featureset, lambda x: x[2]) == 'outband-webvtt'), (None, None)) @@ -143,7 +143,7 @@ def _real_extract(self, url): # See: https://github.com/yt-dlp/yt-dlp/issues/986 platform_tag_video, featureset_video = next( ((platform_tag, featureset) - for platform_tag, featuresets in reversed(list(variants.items())) for featureset in featuresets + for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets if set(try_get(featureset, lambda x: x[:2]) or []) == {'aes', 'hls'}), (None, None)) if not platform_tag_video or not featureset_video: diff --git a/yt_dlp/extractor/youtube/pot/_director.py b/yt_dlp/extractor/youtube/pot/_director.py index a33d48a144..13616a940f 100644 --- a/yt_dlp/extractor/youtube/pot/_director.py +++ b/yt_dlp/extractor/youtube/pot/_director.py @@ -76,7 +76,7 @@ def error(self, message: str, cause=None): if self.log_level <= self.LogLevel.ERROR: self.__ie._downloader.report_error( self._format_msg(message), is_error=False, - tb=''.join(traceback.format_exception(None, cause, cause.__traceback__)) if cause else None) + tb=''.join(traceback.format_exception(cause)) if cause else None) class PoTokenCache: diff --git a/yt_dlp/networking/_helper.py b/yt_dlp/networking/_helper.py index 661a2c3b51..c75306bfae 100644 --- a/yt_dlp/networking/_helper.py +++ b/yt_dlp/networking/_helper.py @@ -108,9 +108,7 @@ def make_ssl_context( context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) context.check_hostname = verify context.verify_mode = ssl.CERT_REQUIRED if verify else ssl.CERT_NONE - # OpenSSL 1.1.1+ Python 3.8+ keylog file - if hasattr(context, 'keylog_filename'): - context.keylog_filename = os.environ.get('SSLKEYLOGFILE') or None + context.keylog_filename = os.environ.get('SSLKEYLOGFILE') or None # Some servers may reject requests if ALPN extension is not sent. See: # https://github.com/python/cpython/issues/85140 diff --git a/yt_dlp/networking/_requests.py b/yt_dlp/networking/_requests.py index c7629c7c63..409fb13e75 100644 --- a/yt_dlp/networking/_requests.py +++ b/yt_dlp/networking/_requests.py @@ -123,7 +123,7 @@ def _real_read(self, amt: int | None = None) -> bytes: # Work around issue with `.read(amt)` then `.read()` # See: https://github.com/urllib3/urllib3/issues/3636 if amt is None: - # compat: py3.9: Python 3.9 preallocates the whole read buffer, read in chunks + # Read in chunks to avoid preallocating a large buffer read_chunk = functools.partial(self.fp.read, 1 << 20, decode_content=True) return b''.join(iter(read_chunk, b'')) # Interact with urllib3 response directly. diff --git a/yt_dlp/networking/_urllib.py b/yt_dlp/networking/_urllib.py index e3b4903456..5e6167dd7a 100644 --- a/yt_dlp/networking/_urllib.py +++ b/yt_dlp/networking/_urllib.py @@ -296,13 +296,9 @@ class UrllibResponseAdapter(Response): """ def __init__(self, res: http.client.HTTPResponse | urllib.response.addinfourl): - # addinfourl: In Python 3.9+, .status was introduced and .getcode() was deprecated [1] - # HTTPResponse: .getcode() was deprecated, .status always existed [2] - # 1. https://docs.python.org/3/library/urllib.request.html#urllib.response.addinfourl.getcode - # 2. https://docs.python.org/3.10/library/http.client.html#http.client.HTTPResponse.status super().__init__( fp=res, headers=res.headers, url=res.url, - status=getattr(res, 'status', None) or res.getcode(), reason=getattr(res, 'reason', None)) + status=res.status, reason=getattr(res, 'reason', None)) def read(self, amt=None): if self.closed: diff --git a/yt_dlp/utils/_utils.py b/yt_dlp/utils/_utils.py index 81d4ee3bc8..f7cba13d21 100644 --- a/yt_dlp/utils/_utils.py +++ b/yt_dlp/utils/_utils.py @@ -238,11 +238,9 @@ def find_xpath_attr(node, xpath, key, val=None): expr = xpath + (f'[@{key}]' if val is None else f"[@{key}='{val}']") return node.find(expr) -# On python2.6 the xml.etree.ElementTree.Element methods don't support -# the namespace parameter - def xpath_with_ns(path, ns_map): + """Expand namespace-prefixed names to Clark notation.""" components = [c.split(':') for c in path.split('/')] replaced = [] for c in components: @@ -876,7 +874,7 @@ def __init__(self, args, *remaining, env=None, text=False, shell=False, **kwargs self.__text_mode = kwargs.get('encoding') or kwargs.get('errors') or text or kwargs.get('universal_newlines') if text is True: - kwargs['universal_newlines'] = True # For 3.6 compatibility + kwargs['text'] = True kwargs.setdefault('encoding', 'utf-8') kwargs.setdefault('errors', 'replace') @@ -1012,7 +1010,7 @@ def __msg(self): def format_traceback(self): return join_nonempty( self.traceback and ''.join(traceback.format_tb(self.traceback)), - self.cause and ''.join(traceback.format_exception(None, self.cause, self.cause.__traceback__)[1:]), + self.cause and ''.join(traceback.format_exception(self.cause)[1:]), delim='\n') or None def __setattr__(self, name, value): @@ -1960,11 +1958,7 @@ def setproctitle(title): libc = ctypes.cdll.LoadLibrary('libc.so.6') except OSError: return - except TypeError: - # LoadLibrary in Windows Python 2.7.13 only expects - # a bytestring, but since unicode_literals turns - # every string into a unicode string, it fails. - return + title_bytes = title.encode() buf = ctypes.create_string_buffer(len(title_bytes)) buf.value = title_bytes @@ -2655,11 +2649,10 @@ def multipart_encode(data, boundary=None): Encode a dict to RFC 7578-compliant form-data data: - A dict where keys and values can be either Unicode or bytes-like - objects. + A dict where keys and values can be either str or bytes-like objects. boundary: - If specified a Unicode object, it's used as the boundary. Otherwise - a random boundary is generated. + An ASCII string to use as the boundary. If omitted, a random boundary + is generated. Reference: https://tools.ietf.org/html/rfc7578 """ @@ -3422,7 +3415,7 @@ def ass_subtitles_timecode(seconds): def dfxp2srt(dfxp_data): """ @param dfxp_data A bytes-like object containing DFXP data - @returns A unicode object containing converted SRT data + @returns A string containing the converted SRT data """ LEGACY_NAMESPACES = ( (b'http://www.w3.org/ns/ttml', [