diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 00000000..47c12129 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,11 @@ +name: Ruff +run-name: Ruff + +on: [ push, pull_request ] + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 diff --git a/m3u8/__init__.py b/m3u8/__init__.py index aeb70294..5eae2376 100644 --- a/m3u8/__init__.py +++ b/m3u8/__init__.py @@ -3,7 +3,6 @@ # license that can be found in the LICENSE file. import os -import sys from urllib.parse import urljoin, urlsplit from m3u8.httpclient import DefaultHTTPClient diff --git a/m3u8/model.py b/m3u8/model.py index cd1f7397..152aa7f0 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -2,7 +2,6 @@ # Use of this source code is governed by a MIT License # license that can be found in the LICENSE file. import decimal -import errno import os from m3u8.mixins import BasePathMixin, GroupedBasePathMixin @@ -1609,81 +1608,6 @@ def dumps(self): def __str__(self): return self.dumps() -class ImagePlaylist(BasePathMixin): - ''' - ImagePlaylist object representing a link to a - variant M3U8 image playlist with a specific bitrate. - - Attributes: - - `image_stream_info` is a named tuple containing the attributes: - `bandwidth`, `resolution` which is a tuple (w, h) of integers and `codecs`, - - More info: https://github.com/image-media-playlist/spec/blob/master/image_media_playlist_v0_4.pdf - ''' - - def __init__(self, base_uri, uri, image_stream_info): - self.uri = uri - self.base_uri = base_uri - - resolution = image_stream_info.get('resolution') - if resolution is not None: - values = resolution.split('x') - resolution_pair = (int(values[0]), int(values[1])) - else: - resolution_pair = None - - self.image_stream_info = StreamInfo( - bandwidth=image_stream_info.get('bandwidth'), - average_bandwidth=image_stream_info.get('average_bandwidth'), - video=image_stream_info.get('video'), - # Audio, subtitles, closed captions, video range and hdcp level should not exist in - # EXT-X-IMAGE-STREAM-INF, so just hardcode them to None. - audio=None, - subtitles=None, - closed_captions=None, - program_id=image_stream_info.get('program_id'), - resolution=resolution_pair, - codecs=image_stream_info.get('codecs'), - video_range=None, - hdcp_level=None, - frame_rate=None, - pathway_id=image_stream_info.get('pathway_id'), - stable_variant_id=image_stream_info.get('stable_variant_id') - ) - - def __str__(self): - image_stream_inf = [] - if self.image_stream_info.program_id: - image_stream_inf.append('PROGRAM-ID=%d' % - self.image_stream_info.program_id) - if self.image_stream_info.bandwidth: - image_stream_inf.append('BANDWIDTH=%d' % - self.image_stream_info.bandwidth) - if self.image_stream_info.average_bandwidth: - image_stream_inf.append('AVERAGE-BANDWIDTH=%d' % - self.image_stream_info.average_bandwidth) - if self.image_stream_info.resolution: - res = (str(self.image_stream_info.resolution[0]) + 'x' + - str(self.image_stream_info.resolution[1])) - image_stream_inf.append('RESOLUTION=' + res) - if self.image_stream_info.codecs: - image_stream_inf.append('CODECS=' + - quoted(self.image_stream_info.codecs)) - if self.uri: - image_stream_inf.append('URI=' + quoted(self.uri)) - if self.image_stream_info.pathway_id: - image_stream_inf.append( - 'PATHWAY-ID=' + quoted(self.image_stream_info.pathway_id) - ) - if self.image_stream_info.stable_variant_id: - image_stream_inf.append( - 'STABLE-VARIANT-ID=' + quoted(self.image_stream_info.stable_variant_id) - ) - - return '#EXT-X-IMAGE-STREAM-INF:' + ','.join(image_stream_inf) - - def find_key(keydata, keylist): if not keydata: return None diff --git a/m3u8/parser.py b/m3u8/parser.py index 9a2345db..47c10f36 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -5,7 +5,6 @@ import itertools import re from datetime import datetime, timedelta -from urllib.parse import urljoin as _urljoin try: from backports.datetime_fromisoformat import MonkeyPatch diff --git a/tests/m3u8server.py b/tests/m3u8server.py index 01fc234d..1d135e7f 100644 --- a/tests/m3u8server.py +++ b/tests/m3u8server.py @@ -14,7 +14,7 @@ @route("/path/to/redirect_me") -def simple(): +def redirect_route(): redirect("/simple.m3u8") @@ -25,14 +25,14 @@ def simple(): @route("/timeout_simple.m3u8") -def simple(): +def timeout_simple(): time.sleep(5) response.set_header("Content-Type", "application/vnd.apple.mpegurl") return m3u8_file("simple-playlist.m3u8") @route("/path/to/relative-playlist.m3u8") -def simple(): +def relative_playlist(): response.set_header("Content-Type", "application/vnd.apple.mpegurl") return m3u8_file("relative-playlist.m3u8") diff --git a/tests/test_loader.py b/tests/test_loader.py index 862a86f9..509b7714 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -3,6 +3,7 @@ # license that can be found in the LICENSE file. import os +import socket import urllib.parse import m3u8 import pytest @@ -140,8 +141,8 @@ def test_presence_of_base_uri_if_provided_when_loading_from_string(): def test_raise_timeout_exception_if_timeout_happens_when_loading_from_uri(): try: - obj = m3u8.load(playlists.TIMEOUT_SIMPLE_PLAYLIST_URI, timeout=1) - except: + m3u8.load(playlists.TIMEOUT_SIMPLE_PLAYLIST_URI, timeout=1) + except socket.timeout: assert True else: assert False diff --git a/tests/test_model.py b/tests/test_model.py index 6edbb197..492f19c4 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -7,7 +7,6 @@ import datetime import os -import sys import playlists import pytest @@ -135,30 +134,30 @@ def test_segment_discontinuity_attribute(): obj = m3u8.M3U8(playlists.DISCONTINUITY_PLAYLIST_WITH_PROGRAM_DATE_TIME) segments = obj.segments - assert segments[0].discontinuity == False - assert segments[5].discontinuity == True - assert segments[6].discontinuity == False + assert segments[0].discontinuity is False + assert segments[5].discontinuity is True + assert segments[6].discontinuity is False def test_segment_cue_out_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_PLAYLIST) segments = obj.segments - assert segments[1].cue_out == True - assert segments[2].cue_out == True - assert segments[3].cue_out == False + assert segments[1].cue_out is True + assert segments[2].cue_out is True + assert segments[3].cue_out is False def test_segment_cue_out_start_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_WITH_DURATION_PLAYLIST) - assert obj.segments[0].cue_out_start == True + assert obj.segments[0].cue_out_start is True def test_segment_cue_in_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_WITH_DURATION_PLAYLIST) - assert obj.segments[2].cue_in == True + assert obj.segments[2].cue_in is True def test_segment_cue_out_cont_dumps(): @@ -219,8 +218,8 @@ def test_segment_cue_out_in_dumps(): def test_segment_elemental_scte35_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_ELEMENTAL_PLAYLIST) segments = obj.segments - assert segments[4].cue_out == True - assert segments[9].cue_out == False + assert segments[4].cue_out is True + assert segments[9].cue_out is False assert ( segments[4].scte35 == "/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==" ) @@ -229,14 +228,14 @@ def test_segment_elemental_scte35_attribute(): def test_segment_envivio_scte35_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_ENVIVIO_PLAYLIST) segments = obj.segments - assert segments[3].cue_out == True + assert segments[3].cue_out is True assert ( segments[4].scte35 == "/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==" ) assert ( segments[5].scte35 == "/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==" ) - assert segments[7].cue_out == False + assert segments[7].cue_out is False def test_segment_unknown_scte35_attribute(): @@ -247,8 +246,8 @@ def test_segment_unknown_scte35_attribute(): def test_segment_cue_out_no_duration(): obj = m3u8.M3U8(playlists.CUE_OUT_NO_DURATION_PLAYLIST) - assert obj.segments[0].cue_out_start == True - assert obj.segments[2].cue_in == True + assert obj.segments[0].cue_out_start is True + assert obj.segments[2].cue_in is True def test_segment_asset_metadata_dumps(): @@ -301,7 +300,7 @@ def test_key_attribute_without_initialization_vector(): assert "AES-128" == obj.keys[0].method assert "/key" == obj.keys[0].uri - assert None == obj.keys[0].iv + assert None is obj.keys[0].iv def test_session_keys_on_clear_playlist(): @@ -341,7 +340,7 @@ def test_session_key_attribute_without_initialization_vector(): assert "AES-128" == obj.session_keys[0].method assert "/key" == obj.session_keys[0].uri - assert None == obj.session_keys[0].iv + assert None is obj.session_keys[0].iv def test_segments_attribute(): @@ -371,7 +370,7 @@ def test_segments_attribute_without_title(): assert "/foo/bar-1.ts" == obj.segments[0].uri assert 1500 == obj.segments[0].duration - assert None == obj.segments[0].title + assert None is obj.segments[0].title def test_segments_attribute_without_duration(): @@ -384,7 +383,7 @@ def test_segments_attribute_without_duration(): assert "/foo/bar-1.ts" == obj.segments[0].uri assert "Segment title" == obj.segments[0].title - assert None == obj.segments[0].duration + assert None is obj.segments[0].duration def test_segments_attribute_with_byterange(): @@ -502,34 +501,34 @@ def test_playlists_attribute(): assert "/url/1.m3u8" == obj.playlists[0].uri assert 1 == obj.playlists[0].stream_info.program_id assert 320000 == obj.playlists[0].stream_info.bandwidth - assert None == obj.playlists[0].stream_info.closed_captions - assert None == obj.playlists[0].stream_info.codecs + assert None is obj.playlists[0].stream_info.closed_captions + assert None is obj.playlists[0].stream_info.codecs - assert None == obj.playlists[0].media[0].uri + assert None is obj.playlists[0].media[0].uri assert "high" == obj.playlists[0].media[0].group_id assert "VIDEO" == obj.playlists[0].media[0].type - assert None == obj.playlists[0].media[0].language + assert None is obj.playlists[0].media[0].language assert "High" == obj.playlists[0].media[0].name - assert None == obj.playlists[0].media[0].default - assert None == obj.playlists[0].media[0].autoselect - assert None == obj.playlists[0].media[0].forced - assert None == obj.playlists[0].media[0].characteristics + assert None is obj.playlists[0].media[0].default + assert None is obj.playlists[0].media[0].autoselect + assert None is obj.playlists[0].media[0].forced + assert None is obj.playlists[0].media[0].characteristics assert "/url/2.m3u8" == obj.playlists[1].uri assert 1 == obj.playlists[1].stream_info.program_id assert 120000 == obj.playlists[1].stream_info.bandwidth - assert None == obj.playlists[1].stream_info.closed_captions + assert None is obj.playlists[1].stream_info.closed_captions assert "mp4a.40.5" == obj.playlists[1].stream_info.codecs - assert None == obj.playlists[1].media[0].uri + assert None is obj.playlists[1].media[0].uri assert "low" == obj.playlists[1].media[0].group_id assert "VIDEO" == obj.playlists[1].media[0].type - assert None == obj.playlists[1].media[0].language + assert None is obj.playlists[1].media[0].language assert "Low" == obj.playlists[1].media[0].name assert "YES" == obj.playlists[1].media[0].default assert "YES" == obj.playlists[1].media[0].autoselect - assert None == obj.playlists[1].media[0].forced - assert None == obj.playlists[1].media[0].characteristics + assert None is obj.playlists[1].media[0].forced + assert None is obj.playlists[1].media[0].characteristics assert [] == obj.iframe_playlists @@ -545,8 +544,8 @@ def test_playlists_attribute_without_program_id(): assert "/url/1.m3u8" == obj.playlists[0].uri assert 320000 == obj.playlists[0].stream_info.bandwidth - assert None == obj.playlists[0].stream_info.codecs - assert None == obj.playlists[0].stream_info.program_id + assert None is obj.playlists[0].stream_info.codecs + assert None is obj.playlists[0].stream_info.program_id def test_playlists_attribute_with_resolution(): @@ -554,7 +553,7 @@ def test_playlists_attribute_with_resolution(): assert 2 == len(obj.playlists) assert (512, 288) == obj.playlists[0].stream_info.resolution - assert None == obj.playlists[1].stream_info.resolution + assert None is obj.playlists[1].stream_info.resolution def test_iframe_playlists_attribute(): @@ -587,9 +586,9 @@ def test_iframe_playlists_attribute(): assert "avc1.4d001f" == obj.iframe_playlists[0].iframe_stream_info.codecs assert "/url/2.m3u8" == obj.iframe_playlists[1].uri - assert None == obj.iframe_playlists[1].iframe_stream_info.program_id + assert None is obj.iframe_playlists[1].iframe_stream_info.program_id assert "120000" == obj.iframe_playlists[1].iframe_stream_info.bandwidth - assert None == obj.iframe_playlists[1].iframe_stream_info.resolution + assert None is obj.iframe_playlists[1].iframe_stream_info.resolution assert "avc1.4d400d" == obj.iframe_playlists[1].iframe_stream_info.codecs @@ -599,7 +598,7 @@ def test_version_attribute(): assert 2 == obj.version mock_parser_data(obj, {}) - assert None == obj.version + assert None is obj.version def test_version_settable_as_int(): @@ -622,7 +621,7 @@ def test_allow_cache_attribute(): assert "no" == obj.allow_cache mock_parser_data(obj, {}) - assert None == obj.allow_cache + assert None is obj.allow_cache def test_files_attribute_should_list_all_files_including_segments_and_key(): @@ -1009,22 +1008,6 @@ def test_should_normalize_variant_streams_urls_if_base_path_passed_to_constructo assert obj.dumps().strip() == expected -def test_should_normalize_segments_and_key_urls_if_base_path_attribute_updated(): - base_path = "http://videoserver.com/hls/live" - - obj = m3u8.M3U8(playlists.PLAYLIST_WITH_ENCRYPTED_SEGMENTS_AND_IV) - obj.base_path = base_path # update later - - expected = ( - playlists.PLAYLIST_WITH_ENCRYPTED_SEGMENTS_AND_IV_SORTED.replace(", IV", ",IV") - .replace("../../../../hls", base_path) - .replace("/hls-key", base_path) - .strip() - ) - - assert obj.dumps() == expected - - def test_should_normalize_segments_and_key_urls_if_base_path_attribute_updated(): base_path = "http://videoserver.com/hls/live" @@ -1230,15 +1213,6 @@ def test_should_round_frame_rate(): assert expected == obj.dumps().strip() -@pytest.mark.skipif(sys.version_info >= (3,), reason="unicode not available in v3") -def test_m3u8_unicode_method(): - obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST) - - result = unicode(obj).strip() - expected = playlists.SIMPLE_PLAYLIST.strip() - assert result == expected - - def test_add_segment_to_playlist(): obj = m3u8.M3U8() @@ -1270,7 +1244,7 @@ def test_find_key_throws_when_no_match(): # deliberately empty ], ) - except KeyError as e: + except KeyError: threw = True finally: assert threw @@ -1572,13 +1546,6 @@ def test_add_content_steering_base_uri_update(): ) -def test_dump_should_work_for_variant_playlists_with_image_playlists(): - obj = m3u8.M3U8(playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS) - - expected = playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS.strip() - - assert expected == obj.dumps().strip() - def test_dump_should_work_for_variant_playlists_with_image_playlists(): obj = m3u8.M3U8(playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS) diff --git a/tests/test_parser.py b/tests/test_parser.py index 0e8878af..0a61077a 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -162,8 +162,8 @@ def test_should_parse_variant_playlist(): data = m3u8.parse(playlists.VARIANT_PLAYLIST) playlists_list = list(data["playlists"]) - assert True == data["is_variant"] - assert None == data["media_sequence"] + assert True is data["is_variant"] + assert None is data["media_sequence"] assert 4 == len(playlists_list) assert "http://example.com/low.m3u8" == playlists_list[0]["uri"] @@ -180,8 +180,8 @@ def test_should_parse_variant_playlist_with_cc_subtitles_and_audio(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_CC_SUBS_AND_AUDIO) playlists_list = list(data["playlists"]) - assert True == data["is_variant"] - assert None == data["media_sequence"] + assert True is data["is_variant"] + assert None is data["media_sequence"] assert 2 == len(playlists_list) assert "http://example.com/with-cc-hi.m3u8" == playlists_list[0]["uri"] @@ -252,7 +252,7 @@ def test_should_parse_variant_playlist_with_iframe_playlists(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_PLAYLISTS) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -271,7 +271,7 @@ def test_should_parse_variant_playlist_with_alt_iframe_playlists_layout(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_ALT_IFRAME_PLAYLISTS_LAYOUT) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -289,7 +289,7 @@ def test_should_parse_variant_playlist_with_alt_iframe_playlists_layout(): def test_should_parse_iframe_playlist(): data = m3u8.parse(playlists.IFRAME_PLAYLIST) - assert True == data["is_i_frames_only"] + assert True is data["is_i_frames_only"] assert 4.12 == data["segments"][0]["duration"] assert "9400@376" == data["segments"][0]["byterange"] assert "segment1.ts" == data["segments"][0]["uri"] @@ -299,7 +299,7 @@ def test_should_parse_variant_playlist_with_image_playlists(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS) image_playlists = list(data['image_playlists']) - assert True == data['is_variant'] + assert True is data['is_variant'] assert 2 == len(image_playlists) assert '320x180' == image_playlists[0]['image_stream_info']['resolution'] assert 'jpeg' == image_playlists[0]['image_stream_info']['codecs'] @@ -311,7 +311,7 @@ def test_should_parse_variant_playlist_with_image_playlists(): def test_should_parse_vod_image_playlist(): data = m3u8.parse(playlists.VOD_IMAGE_PLAYLIST) - assert True == data['is_images_only'] + assert True is data['is_images_only'] assert 8 == len(data['tiles']) assert 'preroll-ad-1.jpg' == data['segments'][0]['uri'] assert '640x360' == data['tiles'][0]['resolution'] @@ -322,7 +322,7 @@ def test_should_parse_vod_image_playlist(): def test_should_parse_vod_image_playlist2(): data = m3u8.parse(playlists.VOD_IMAGE_PLAYLIST2) - assert True == data['is_images_only'] + assert True is data['is_images_only'] assert '640x360' == data['tiles'][0]['resolution'] assert '4x3' == data['tiles'][0]['layout'] assert 2.002 == data['tiles'][0]['duration'] @@ -332,7 +332,7 @@ def test_should_parse_vod_image_playlist2(): def test_should_parse_live_image_playlist(): data = m3u8.parse(playlists.LIVE_IMAGE_PLAYLIST) - assert True == data['is_images_only'] + assert True is data['is_images_only'] assert 10 == len(data['segments']) assert 'content-123.jpg' == data['segments'][0]['uri'] assert 'content-124.jpg' == data['segments'][1]['uri'] @@ -348,7 +348,7 @@ def test_should_parse_live_image_playlist(): def test_should_parse_playlist_using_byteranges(): data = m3u8.parse(playlists.PLAYLIST_USING_BYTERANGES) - assert False == data["is_i_frames_only"] + assert False is data["is_i_frames_only"] assert 10 == data["segments"][0]["duration"] assert "76242@0" == data["segments"][0]["byterange"] assert "segment.ts" == data["segments"][0]["uri"] @@ -356,10 +356,10 @@ def test_should_parse_playlist_using_byteranges(): def test_should_parse_endlist_playlist(): data = m3u8.parse(playlists.SIMPLE_PLAYLIST) - assert True == data["is_endlist"] + assert True is data["is_endlist"] data = m3u8.parse(playlists.SLIDING_WINDOW_PLAYLIST) - assert False == data["is_endlist"] + assert False is data["is_endlist"] def test_should_parse_ALLOW_CACHE(): @@ -769,8 +769,8 @@ def test_gap(): data = m3u8.parse(playlists.GAP_PLAYLIST) assert data["segments"][0]["gap_tag"] is None - assert data["segments"][1]["gap_tag"] == True - assert data["segments"][2]["gap_tag"] == True + assert data["segments"][1]["gap_tag"] is True + assert data["segments"][2]["gap_tag"] is True assert data["segments"][3]["gap_tag"] is None @@ -781,7 +781,7 @@ def test_gap_in_parts(): assert data["segments"][0]["parts"][0].get("gap", None) is None assert data["segments"][0]["parts"][1]["gap_tag"] is None assert data["segments"][0]["parts"][1]["gap"] == "YES" - assert data["segments"][0]["parts"][2]["gap_tag"] == True + assert data["segments"][0]["parts"][2]["gap_tag"] is True assert data["segments"][0]["parts"][2].get("gap", None) is None @@ -789,7 +789,7 @@ def test_should_parse_variant_playlist_with_iframe_with_average_bandwidth(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_AVERAGE_BANDWIDTH) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -812,7 +812,7 @@ def test_should_parse_variant_playlist_with_iframe_with_video_range(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_VIDEO_RANGE) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -830,7 +830,7 @@ def test_should_parse_variant_playlist_with_iframe_with_hdcp_level(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_HDCP_LEVEL) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -868,13 +868,13 @@ def test_content_steering(): def test_cue_in_pops_scte35_data_and_duration(): data = m3u8.parse(playlists.CUE_OUT_ELEMENTAL_PLAYLIST) - assert data["segments"][9]["cue_in"] == True + assert data["segments"][9]["cue_in"] is True assert ( data["segments"][9]["scte35"] == "/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==" ) assert data["segments"][9]["scte35_duration"] == "50" - assert data["segments"][10]["cue_in"] == False + assert data["segments"][10]["cue_in"] is False assert data["segments"][10]["scte35"] is None assert data["segments"][10]["scte35_duration"] is None