diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ad54157..26dda362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## v2.14.6 (2024-10-12) + +* Newsletters: + * Fix: Allow formatting newsletter date parameters. + * Change: Support apscheduler compatible cron expressions. +* UI: + * Fix: Round runtime before converting to human duration. + * Fix: Make recently added/watched rows touch scrollable. +* Other: + * Fix: Auto-updater not running. + + +## v2.14.5 (2024-09-20) + +* Activity: + * Fix: Display of 2k resolution on activity card. +* Notifications: + * Fix: ntfy notifications with special characters failing to send. +* Other: + * Fix: Memory leak with database closing. (#2404) + + ## v2.14.4 (2024-08-10) * Notifications: @@ -8,7 +30,7 @@ * UI: * Fix: macOS platform capitalization. * Other: - * Fix: Remove deprecated getdefaultlocale (Thanks @teodorstelian) (#2364, #2345) + * Fix: Remove deprecated getdefaultlocale. (Thanks @teodorstelian) (#2364, #2345) ## v2.14.3 (2024-06-19) diff --git a/data/interfaces/default/css/tautulli.css b/data/interfaces/default/css/tautulli.css index f8b15890..2835488d 100644 --- a/data/interfaces/default/css/tautulli.css +++ b/data/interfaces/default/css/tautulli.css @@ -1478,7 +1478,8 @@ a:hover .dashboard-stats-square { text-align: center; position: relative; z-index: 0; - overflow: hidden; + overflow: auto; + scrollbar-width: none; } .dashboard-recent-media { width: 100%; diff --git a/data/interfaces/default/index.html b/data/interfaces/default/index.html index 6e4818c7..582ac93f 100644 --- a/data/interfaces/default/index.html +++ b/data/interfaces/default/index.html @@ -92,10 +92,10 @@

Recently Added

@@ -936,10 +936,14 @@ count: recently_added_count, media_type: recently_added_type }, + beforeSend: function () { + $(".dashboard-recent-media-row").animate({ scrollLeft: 0 }, 1000); + }, complete: function (xhr, status) { $("#recentlyAdded").html(xhr.responseText); $('#ajaxMsg').fadeOut(); - highlightAddedScrollerButton(); + highlightScrollerButton("#recently-added"); + paginateScroller("#recently-added", ".paginate-added"); } }); } @@ -955,57 +959,11 @@ recentlyAdded(recently_added_count, recently_added_type); } - function highlightAddedScrollerButton() { - var scroller = $("#recently-added-row-scroller"); - var numElems = scroller.find("li:visible").length; - scroller.width(numElems * 175); - if (scroller.width() > $("body").find(".container-fluid").width()) { - $("#recently-added-page-right").removeClass("disabled"); - } else { - $("#recently-added-page-right").addClass("disabled"); - } - } - - $(window).resize(function () { - highlightAddedScrollerButton(); - }); - - function resetScroller() { - leftTotal = 0; - $("#recently-added-row-scroller").animate({ left: leftTotal }, 1000); - $("#recently-added-page-left").addClass("disabled").blur(); - } - - var leftTotal = 0; - $(".paginate").click(function (e) { - e.preventDefault(); - var scroller = $("#recently-added-row-scroller"); - var containerWidth = $("body").find(".container-fluid").width(); - var scrollAmount = $(this).data("id") * parseInt((containerWidth - 15) / 175) * 175; - var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0); - - leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax); - scroller.animate({ left: leftTotal }, 250); - - if (leftTotal === 0) { - $("#recently-added-page-left").addClass("disabled").blur(); - } else { - $("#recently-added-page-left").removeClass("disabled"); - } - - if (leftTotal === leftMax) { - $("#recently-added-page-right").addClass("disabled").blur(); - } else { - $("#recently-added-page-right").removeClass("disabled"); - } - }); - $('#recently-added-toggles').on('change', function () { $('#recently-added-toggles > label').removeClass('active'); selected_filter = $('input[name=recently-added-toggle]:checked', '#recently-added-toggles'); $(selected_filter).closest('label').addClass('active'); recently_added_type = $(selected_filter).val(); - resetScroller(); setLocalStorage('home_stats_recently_added_type', recently_added_type); recentlyAdded(recently_added_count, recently_added_type); }); @@ -1013,7 +971,6 @@ $('#recently-added-count').change(function () { forceMinMax($(this)); recently_added_count = $(this).val(); - resetScroller(); setLocalStorage('home_stats_recently_added_count', recently_added_count); recentlyAdded(recently_added_count, recently_added_type); }); diff --git a/data/interfaces/default/js/script.js b/data/interfaces/default/js/script.js index 2df5daeb..5d75c69d 100644 --- a/data/interfaces/default/js/script.js +++ b/data/interfaces/default/js/script.js @@ -360,7 +360,8 @@ function humanDuration(ms, sig='dhm', units='ms', return_seconds=300000) { sig = 'dhms' } - ms = ms * factors[units]; + r = factors[sig.slice(-1)]; + ms = Math.round(ms * factors[units] / r) * r; h = ms % factors['d']; d = Math.trunc(ms / factors['d']); @@ -929,3 +930,50 @@ $('.modal').on('hide.bs.modal', function (e) { $.fn.hasScrollBar = function() { return this.get(0).scrollHeight > this.get(0).clientHeight; } + +function paginateScroller(scrollerId, buttonClass) { + $(buttonClass).click(function (e) { + e.preventDefault(); + var scroller = $(scrollerId + "-row-scroller"); + var scrollerParent = scroller.parent(); + var containerWidth = scrollerParent.width(); + var scrollCurrent = scrollerParent.scrollLeft(); + var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175; + var scrollMax = scroller.width() - Math.abs(scrollAmount); + var scrollTotal = Math.min(parseInt(scrollCurrent / 175) * 175 + scrollAmount, scrollMax); + scrollerParent.animate({ scrollLeft: scrollTotal }, 250); + }); +} + +function highlightScrollerButton(scrollerId) { + var scroller = $(scrollerId + "-row-scroller"); + var scrollerParent = scroller.parent(); + var buttonLeft = $(scrollerId + "-page-left"); + var buttonRight = $(scrollerId + "-page-right"); + + var numElems = scroller.find("li").length; + scroller.width(numElems * 175); + $(buttonLeft).addClass("disabled").blur(); + if (scroller.width() > scrollerParent.width()) { + $(buttonRight).removeClass("disabled"); + } else { + $(buttonRight).addClass("disabled"); + } + + scrollerParent.scroll(function () { + var scrollCurrent = $(this).scrollLeft(); + var scrollMax = scroller.width() - $(this).width(); + + if (scrollCurrent == 0) { + $(buttonLeft).addClass("disabled").blur(); + } else { + $(buttonLeft).removeClass("disabled"); + } + + if (scrollCurrent >= scrollMax) { + $(buttonRight).addClass("disabled").blur(); + } else { + $(buttonRight).removeClass("disabled"); + } + }); +} diff --git a/data/interfaces/default/library.html b/data/interfaces/default/library.html index b1fe8b6f..ba61153d 100644 --- a/data/interfaces/default/library.html +++ b/data/interfaces/default/library.html @@ -149,10 +149,10 @@ DOCUMENTATION :: END
@@ -175,10 +175,10 @@ DOCUMENTATION :: END
@@ -690,7 +690,8 @@ DOCUMENTATION :: END }, complete: function(xhr, status) { $("#library-recently-watched").html(xhr.responseText); - highlightWatchedScrollerButton(); + highlightScrollerButton("#recently-watched"); + paginateScroller("#recently-watched", ".paginate-watched"); } }); } @@ -706,7 +707,8 @@ DOCUMENTATION :: END }, complete: function(xhr, status) { $("#library-recently-added").html(xhr.responseText); - highlightAddedScrollerButton(); + highlightScrollerButton("#recently-added"); + paginateScroller("#recently-added", ".paginate-added"); } }); } @@ -716,83 +718,8 @@ DOCUMENTATION :: END recentlyAdded(); % endif - function highlightWatchedScrollerButton() { - var scroller = $("#recently-watched-row-scroller"); - var numElems = scroller.find("li").length; - scroller.width(numElems * 175); - if (scroller.width() > $("#library-recently-watched").width()) { - $("#recently-watched-page-right").removeClass("disabled"); - } else { - $("#recently-watched-page-right").addClass("disabled"); - } - } - - function highlightAddedScrollerButton() { - var scroller = $("#recently-added-row-scroller"); - var numElems = scroller.find("li").length; - scroller.width(numElems * 175); - if (scroller.width() > $("#library-recently-added").width()) { - $("#recently-added-page-right").removeClass("disabled"); - } else { - $("#recently-added-page-right").addClass("disabled"); - } - } - - $(window).resize(function() { - highlightWatchedScrollerButton(); - highlightAddedScrollerButton(); - }); - $('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 }); - var leftTotalWatched = 0; - $(".paginate-watched").click(function (e) { - e.preventDefault(); - var scroller = $("#recently-watched-row-scroller"); - var containerWidth = $("#library-recently-watched").width(); - var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175; - var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0); - - leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax); - scroller.animate({ left: leftTotalWatched }, 250); - - if (leftTotalWatched == 0) { - $("#recently-watched-page-left").addClass("disabled").blur(); - } else { - $("#recently-watched-page-left").removeClass("disabled"); - } - - if (leftTotalWatched == leftMax) { - $("#recently-watched-page-right").addClass("disabled").blur(); - } else { - $("#recently-watched-page-right").removeClass("disabled"); - } - }); - - var leftTotalAdded = 0; - $(".paginate-added").click(function (e) { - e.preventDefault(); - var scroller = $("#recently-added-row-scroller"); - var containerWidth = $("#library-recently-added").width(); - var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175; - var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0); - - leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax); - scroller.animate({ left: leftTotalAdded }, 250); - - if (leftTotalAdded == 0) { - $("#recently-added-page-left").addClass("disabled").blur(); - } else { - $("#recently-added-page-left").removeClass("disabled"); - } - - if (leftTotalAdded == leftMax) { - $("#recently-added-page-right").addClass("disabled").blur(); - } else { - $("#recently-added-page-right").removeClass("disabled"); - } - }); - $(document).ready(function () { // Javascript to enable link to tab diff --git a/data/interfaces/default/library_recently_added.html b/data/interfaces/default/library_recently_added.html index fb53f9a5..4cf56d8e 100644 --- a/data/interfaces/default/library_recently_added.html +++ b/data/interfaces/default/library_recently_added.html @@ -36,7 +36,7 @@ DOCUMENTATION :: END %>
-
+
    % for item in data:
  • diff --git a/data/interfaces/default/newsletter_config.html b/data/interfaces/default/newsletter_config.html index 10583707..3c1c68ee 100644 --- a/data/interfaces/default/newsletter_config.html +++ b/data/interfaces/default/newsletter_config.html @@ -50,7 +50,10 @@

Set the schedule for the newsletter. - Set the schedule for the newsletter using a custom crontab. Only standard cron values are valid. + + Set the schedule for the newsletter using a custom crontab. + Click here for a list of supported expressions. +

@@ -481,7 +484,7 @@ }); if (${newsletter['config']['custom_cron']}) { - $('#cron_value').val('${newsletter['cron']}'); + $('#cron_value').val('${newsletter['cron'] | n}'); } else { try { cron_widget.cron('value', '${newsletter['cron']}'); diff --git a/data/interfaces/default/recently_added.html b/data/interfaces/default/recently_added.html index 52920817..74d5e561 100644 --- a/data/interfaces/default/recently_added.html +++ b/data/interfaces/default/recently_added.html @@ -36,7 +36,7 @@ DOCUMENTATION :: END %> % if data:
-
+
    % for item in data:
    diff --git a/data/interfaces/default/user.html b/data/interfaces/default/user.html index 6ec74620..adfd6c39 100644 --- a/data/interfaces/default/user.html +++ b/data/interfaces/default/user.html @@ -125,10 +125,10 @@ DOCUMENTATION :: END
    @@ -666,52 +666,14 @@ DOCUMENTATION :: END }, complete: function(xhr, status) { $("#user-recently-watched").html(xhr.responseText); - highlightWatchedScrollerButton(); + highlightScrollerButton("#recently-watched"); + paginateScroller("#recently-watched", ".paginate-watched"); } }); } recentlyWatched(); - function highlightWatchedScrollerButton() { - var scroller = $("#recently-watched-row-scroller"); - var numElems = scroller.find("li").length; - scroller.width(numElems * 175); - if (scroller.width() > $("#user-recently-watched").width()) { - $("#recently-watched-page-right").removeClass("disabled"); - } else { - $("#recently-watched-page-right").addClass("disabled"); - } - } - - $(window).resize(function() { - highlightWatchedScrollerButton(); - }); - - var leftTotal = 0; - $(".paginate").click(function (e) { - e.preventDefault(); - var scroller = $("#recently-watched-row-scroller"); - var containerWidth = $("#user-recently-watched").width(); - var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175; - var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0); - - leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax); - scroller.animate({ left: leftTotal }, 250); - - if (leftTotal == 0) { - $("#recently-watched-page-left").addClass("disabled").blur(); - } else { - $("#recently-watched-page-left").removeClass("disabled"); - } - - if (leftTotal == leftMax) { - $("#recently-watched-page-right").addClass("disabled").blur(); - } else { - $("#recently-watched-page-right").removeClass("disabled"); - } - }); - $(document).ready(function () { // Javascript to enable link to tab var hash = document.location.hash; diff --git a/data/interfaces/default/user_recently_watched.html b/data/interfaces/default/user_recently_watched.html index 39243766..b9f8a8a1 100644 --- a/data/interfaces/default/user_recently_watched.html +++ b/data/interfaces/default/user_recently_watched.html @@ -31,7 +31,7 @@ DOCUMENTATION :: END from plexpy.helpers import page, short_season %>
    -
    +
      % for item in data:
    • diff --git a/lib/certifi/__init__.py b/lib/certifi/__init__.py index d321f1bc..f61d77fa 100644 --- a/lib/certifi/__init__.py +++ b/lib/certifi/__init__.py @@ -1,4 +1,4 @@ from .core import contents, where __all__ = ["contents", "where"] -__version__ = "2024.07.04" +__version__ = "2024.08.30" diff --git a/lib/certifi/cacert.pem b/lib/certifi/cacert.pem index a6581589..3c165a1b 100644 --- a/lib/certifi/cacert.pem +++ b/lib/certifi/cacert.pem @@ -4796,3 +4796,134 @@ PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG XSaQpYXFuXqUPoeovQA= -----END CERTIFICATE----- + +# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA CYBER Root CA" +# Serial: 85076849864375384482682434040119489222 +# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 +# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 +# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA12" +# Serial: 587887345431707215246142177076162061960426065942 +# MD5 Fingerprint: c6:89:ca:64:42:9b:62:08:49:0b:1e:7f:e9:07:3d:e8 +# SHA1 Fingerprint: 7a:22:1e:3d:de:1b:06:ac:9e:c8:47:70:16:8e:3c:e5:f7:6b:06:f4 +# SHA256 Fingerprint: 3f:03:4b:b5:70:4d:44:b2:d0:85:45:a0:20:57:de:93:eb:f3:90:5f:ce:72:1a:cb:c7:30:c0:6d:da:ee:90:4e +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw +NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF +KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt +p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd +J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur +FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J +hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K +h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF +AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld +mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ +mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA +8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV +55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ +yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA14" +# Serial: 575790784512929437950770173562378038616896959179 +# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 +# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f +# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA15" +# Serial: 126083514594751269499665114766174399806381178503 +# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 +# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d +# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- diff --git a/lib/importlib_metadata/__init__.py b/lib/importlib_metadata/__init__.py index 2c71d33c..46a14e64 100644 --- a/lib/importlib_metadata/__init__.py +++ b/lib/importlib_metadata/__init__.py @@ -1,24 +1,34 @@ +""" +APIs exposing metadata from third-party Python packages. + +This codebase is shared between importlib.metadata in the stdlib +and importlib_metadata in PyPI. See +https://github.com/python/importlib_metadata/wiki/Development-Methodology +for more detail. +""" + from __future__ import annotations -import os -import re import abc -import sys -import json -import zipp +import collections import email -import types -import inspect -import pathlib -import operator -import textwrap import functools import itertools +import operator +import os +import pathlib import posixpath -import collections +import re +import sys +import textwrap +import types +from contextlib import suppress +from importlib import import_module +from importlib.abc import MetaPathFinder +from itertools import starmap +from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast from . import _meta -from .compat import py39, py311 from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, @@ -27,12 +37,7 @@ from ._compat import ( from ._functools import method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath - -from contextlib import suppress -from importlib import import_module -from importlib.abc import MetaPathFinder -from itertools import starmap -from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast +from .compat import py39, py311 __all__ = [ 'Distribution', @@ -58,7 +63,7 @@ class PackageNotFoundError(ModuleNotFoundError): return f"No package metadata was found for {self.name}" @property - def name(self) -> str: # type: ignore[override] + def name(self) -> str: # type: ignore[override] # make readonly (name,) = self.args return name @@ -227,9 +232,26 @@ class EntryPoint: >>> ep.matches(attr='bong') True """ + self._disallow_dist(params) attrs = (getattr(self, param) for param in params) return all(map(operator.eq, params.values(), attrs)) + @staticmethod + def _disallow_dist(params): + """ + Querying by dist is not allowed (dist objects are not comparable). + >>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo') + Traceback (most recent call last): + ... + ValueError: "dist" is not suitable for matching... + """ + if "dist" in params: + raise ValueError( + '"dist" is not suitable for matching. ' + "Instead, use Distribution.entry_points.select() on a " + "located distribution." + ) + def _key(self): return self.name, self.value, self.group @@ -259,7 +281,7 @@ class EntryPoints(tuple): __slots__ = () - def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] + def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int """ Get the EntryPoint in self matching name. """ @@ -315,7 +337,7 @@ class PackagePath(pathlib.PurePosixPath): size: int dist: Distribution - def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override] + def read_text(self, encoding: str = 'utf-8') -> str: return self.locate().read_text(encoding=encoding) def read_binary(self) -> bytes: @@ -373,6 +395,17 @@ class Distribution(metaclass=abc.ABCMeta): """ Given a path to a file in this distribution, return a SimplePath to it. + + This method is used by callers of ``Distribution.files()`` to + locate files within the distribution. If it's possible for a + Distribution to represent files in the distribution as + ``SimplePath`` objects, it should implement this method + to resolve such objects. + + Some Distribution providers may elect not to resolve SimplePath + objects within the distribution by raising a + NotImplementedError, but consumers of such a Distribution would + be unable to invoke ``Distribution.files()``. """ @classmethod @@ -639,6 +672,9 @@ class Distribution(metaclass=abc.ABCMeta): return self._load_json('direct_url.json') def _load_json(self, filename): + # Deferred for performance (python/importlib_metadata#503) + import json + return pass_none(json.loads)( self.read_text(filename), object_hook=lambda data: types.SimpleNamespace(**data), @@ -723,7 +759,7 @@ class FastPath: True """ - @functools.lru_cache() # type: ignore + @functools.lru_cache() # type: ignore[misc] def __new__(cls, root): return super().__new__(cls) @@ -741,7 +777,10 @@ class FastPath: return [] def zip_children(self): - zip_path = zipp.Path(self.root) + # deferred for performance (python/importlib_metadata#502) + from zipp.compat.overlay import zipfile + + zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() self.joinpath = zip_path.joinpath @@ -1078,11 +1117,10 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - return _topmost(name) or ( - # python/typeshed#10328 - inspect.getmodulename(name) # type: ignore - or str(name) - ) + # Defer import of inspect for performance (python/cpython#118761) + import inspect + + return _topmost(name) or inspect.getmodulename(name) or str(name) def _top_level_inferred(dist): diff --git a/lib/importlib_metadata/_adapters.py b/lib/importlib_metadata/_adapters.py index 6223263e..3b516a2d 100644 --- a/lib/importlib_metadata/_adapters.py +++ b/lib/importlib_metadata/_adapters.py @@ -1,6 +1,6 @@ +import email.message import re import textwrap -import email.message from ._text import FoldedCase diff --git a/lib/importlib_metadata/_compat.py b/lib/importlib_metadata/_compat.py index df312b1c..01356d69 100644 --- a/lib/importlib_metadata/_compat.py +++ b/lib/importlib_metadata/_compat.py @@ -1,6 +1,5 @@ -import sys import platform - +import sys __all__ = ['install', 'NullFinder'] diff --git a/lib/importlib_metadata/_functools.py b/lib/importlib_metadata/_functools.py index 71f66bd0..5dda6a21 100644 --- a/lib/importlib_metadata/_functools.py +++ b/lib/importlib_metadata/_functools.py @@ -1,5 +1,5 @@ -import types import functools +import types # from jaraco.functools 3.3 diff --git a/lib/importlib_metadata/_meta.py b/lib/importlib_metadata/_meta.py index 1927d0f6..0942bbd9 100644 --- a/lib/importlib_metadata/_meta.py +++ b/lib/importlib_metadata/_meta.py @@ -1,9 +1,17 @@ from __future__ import annotations import os -from typing import Protocol -from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload - +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Protocol, + TypeVar, + Union, + overload, +) _T = TypeVar("_T") diff --git a/lib/importlib_resources/__init__.py b/lib/importlib_resources/__init__.py index 0d029abd..723c9f9e 100644 --- a/lib/importlib_resources/__init__.py +++ b/lib/importlib_resources/__init__.py @@ -1,4 +1,11 @@ -"""Read resources contained within a package.""" +""" +Read resources contained within a package. + +This codebase is shared between importlib.resources in the stdlib +and importlib_resources in PyPI. See +https://github.com/python/importlib_metadata/wiki/Development-Methodology +for more detail. +""" from ._common import ( as_file, @@ -7,7 +14,7 @@ from ._common import ( Anchor, ) -from .functional import ( +from ._functional import ( contents, is_resource, open_binary, diff --git a/lib/importlib_resources/_common.py b/lib/importlib_resources/_common.py index 8df6b39e..f065d493 100644 --- a/lib/importlib_resources/_common.py +++ b/lib/importlib_resources/_common.py @@ -66,10 +66,10 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: # zipimport.zipimporter does not support weak references, resulting in a # TypeError. That seems terrible. spec = package.__spec__ - reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore + reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore[union-attr] if reader is None: return None - return reader(spec.name) # type: ignore + return reader(spec.name) # type: ignore[union-attr] @functools.singledispatch @@ -93,12 +93,13 @@ def _infer_caller(): """ def is_this_file(frame_info): - return frame_info.filename == __file__ + return frame_info.filename == stack[0].filename def is_wrapper(frame_info): return frame_info.function == 'wrapper' - not_this_file = itertools.filterfalse(is_this_file, inspect.stack()) + stack = inspect.stack() + not_this_file = itertools.filterfalse(is_this_file, stack) # also exclude 'wrapper' due to singledispatch in the call stack callers = itertools.filterfalse(is_wrapper, not_this_file) return next(callers).frame @@ -182,7 +183,7 @@ def _(path): @contextlib.contextmanager def _temp_path(dir: tempfile.TemporaryDirectory): """ - Wrap tempfile.TemporyDirectory to return a pathlib object. + Wrap tempfile.TemporaryDirectory to return a pathlib object. """ with dir as result: yield pathlib.Path(result) diff --git a/lib/importlib_resources/functional.py b/lib/importlib_resources/_functional.py similarity index 100% rename from lib/importlib_resources/functional.py rename to lib/importlib_resources/_functional.py diff --git a/lib/importlib_resources/compat/py39.py b/lib/importlib_resources/compat/py39.py index ab87b9dc..ed5abd5e 100644 --- a/lib/importlib_resources/compat/py39.py +++ b/lib/importlib_resources/compat/py39.py @@ -5,6 +5,6 @@ __all__ = ['ZipPath'] if sys.version_info >= (3, 10): - from zipfile import Path as ZipPath # type: ignore + from zipfile import Path as ZipPath else: - from zipp import Path as ZipPath # type: ignore + from zipp import Path as ZipPath diff --git a/lib/importlib_resources/readers.py b/lib/importlib_resources/readers.py index 4a80a774..4f761c64 100644 --- a/lib/importlib_resources/readers.py +++ b/lib/importlib_resources/readers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections import contextlib import itertools @@ -5,6 +7,7 @@ import pathlib import operator import re import warnings +from collections.abc import Iterator from . import abc @@ -34,8 +37,10 @@ class FileReader(abc.TraversableResources): class ZipReader(abc.TraversableResources): def __init__(self, loader, module): - _, _, name = module.rpartition('.') - self.prefix = loader.prefix.replace('\\', '/') + name + '/' + self.prefix = loader.prefix.replace('\\', '/') + if loader.is_package(module): + _, _, name = module.rpartition('.') + self.prefix += name + '/' self.archive = loader.archive def open_resource(self, resource): @@ -133,27 +138,31 @@ class NamespaceReader(abc.TraversableResources): def __init__(self, namespace_path): if 'NamespacePath' not in str(namespace_path): raise ValueError('Invalid path') - self.path = MultiplexedPath(*map(self._resolve, namespace_path)) + self.path = MultiplexedPath(*filter(bool, map(self._resolve, namespace_path))) @classmethod - def _resolve(cls, path_str) -> abc.Traversable: + def _resolve(cls, path_str) -> abc.Traversable | None: r""" Given an item from a namespace path, resolve it to a Traversable. path_str might be a directory on the filesystem or a path to a zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or ``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``. + + path_str might also be a sentinel used by editable packages to + trigger other behaviors (see python/importlib_resources#311). + In that case, return None. """ - (dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) - return dir + dirs = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) + return next(dirs, None) @classmethod - def _candidate_paths(cls, path_str): + def _candidate_paths(cls, path_str: str) -> Iterator[abc.Traversable]: yield pathlib.Path(path_str) yield from cls._resolve_zip_path(path_str) @staticmethod - def _resolve_zip_path(path_str): + def _resolve_zip_path(path_str: str): for match in reversed(list(re.finditer(r'[\\/]', path_str))): with contextlib.suppress( FileNotFoundError, diff --git a/lib/importlib_resources/simple.py b/lib/importlib_resources/simple.py index 96f117fe..2e75299b 100644 --- a/lib/importlib_resources/simple.py +++ b/lib/importlib_resources/simple.py @@ -77,7 +77,7 @@ class ResourceHandle(Traversable): def __init__(self, parent: ResourceContainer, name: str): self.parent = parent - self.name = name # type: ignore + self.name = name # type: ignore[misc] def is_file(self): return True diff --git a/lib/importlib_resources/tests/_path.py b/lib/importlib_resources/tests/_path.py index 1f97c961..b144628c 100644 --- a/lib/importlib_resources/tests/_path.py +++ b/lib/importlib_resources/tests/_path.py @@ -2,15 +2,44 @@ import pathlib import functools from typing import Dict, Union +from typing import runtime_checkable +from typing import Protocol #### -# from jaraco.path 3.4.1 - -FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore +# from jaraco.path 3.7.1 -def build(spec: FilesSpec, prefix=pathlib.Path()): +class Symlink(str): + """ + A string indicating the target of a symlink. + """ + + +FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] + + +@runtime_checkable +class TreeMaker(Protocol): + def __truediv__(self, *args, **kwargs): ... # pragma: no cover + + def mkdir(self, **kwargs): ... # pragma: no cover + + def write_text(self, content, **kwargs): ... # pragma: no cover + + def write_bytes(self, content): ... # pragma: no cover + + def symlink_to(self, target): ... # pragma: no cover + + +def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] + + +def build( + spec: FilesSpec, + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] +): """ Build a set of files/directories, as described by the spec. @@ -25,21 +54,25 @@ def build(spec: FilesSpec, prefix=pathlib.Path()): ... "__init__.py": "", ... }, ... "baz.py": "# Some code", - ... } + ... "bar.py": Symlink("baz.py"), + ... }, + ... "bing": Symlink("foo"), ... } >>> target = getfixture('tmp_path') >>> build(spec, target) >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8') '# Some code' + >>> target.joinpath('bing/bar.py').read_text(encoding='utf-8') + '# Some code' """ for name, contents in spec.items(): - create(contents, pathlib.Path(prefix) / name) + create(contents, _ensure_tree_maker(prefix) / name) @functools.singledispatch def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore + build(content, prefix=path) # type: ignore[arg-type] @create.register @@ -52,5 +85,10 @@ def _(content: str, path): path.write_text(content, encoding='utf-8') +@create.register +def _(content: Symlink, path): + path.symlink_to(content) + + # end from jaraco.path #### diff --git a/lib/importlib_resources/tests/compat/py39.py b/lib/importlib_resources/tests/compat/py39.py index e158eb85..e01d276b 100644 --- a/lib/importlib_resources/tests/compat/py39.py +++ b/lib/importlib_resources/tests/compat/py39.py @@ -8,3 +8,6 @@ import_helper = try_import('import_helper') or from_test_support( 'modules_setup', 'modules_cleanup', 'DirsOnSysPath' ) os_helper = try_import('os_helper') or from_test_support('temp_dir') +warnings_helper = try_import('warnings_helper') or from_test_support( + 'ignore_warnings', 'check_warnings' +) diff --git a/lib/importlib_resources/tests/data01/__init__.py b/lib/importlib_resources/tests/data01/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/importlib_resources/tests/data01/binary.file b/lib/importlib_resources/tests/data01/binary.file deleted file mode 100644 index eaf36c1d..00000000 Binary files a/lib/importlib_resources/tests/data01/binary.file and /dev/null differ diff --git a/lib/importlib_resources/tests/data01/subdirectory/__init__.py b/lib/importlib_resources/tests/data01/subdirectory/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/importlib_resources/tests/data01/subdirectory/binary.file b/lib/importlib_resources/tests/data01/subdirectory/binary.file deleted file mode 100644 index 5bd8bb89..00000000 --- a/lib/importlib_resources/tests/data01/subdirectory/binary.file +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/lib/importlib_resources/tests/data01/utf-16.file b/lib/importlib_resources/tests/data01/utf-16.file deleted file mode 100644 index 2cb77229..00000000 Binary files a/lib/importlib_resources/tests/data01/utf-16.file and /dev/null differ diff --git a/lib/importlib_resources/tests/data01/utf-8.file b/lib/importlib_resources/tests/data01/utf-8.file deleted file mode 100644 index 1c0132ad..00000000 --- a/lib/importlib_resources/tests/data01/utf-8.file +++ /dev/null @@ -1 +0,0 @@ -Hello, UTF-8 world! diff --git a/lib/importlib_resources/tests/data02/__init__.py b/lib/importlib_resources/tests/data02/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/importlib_resources/tests/data02/one/__init__.py b/lib/importlib_resources/tests/data02/one/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/importlib_resources/tests/data02/one/resource1.txt b/lib/importlib_resources/tests/data02/one/resource1.txt deleted file mode 100644 index 61a813e4..00000000 --- a/lib/importlib_resources/tests/data02/one/resource1.txt +++ /dev/null @@ -1 +0,0 @@ -one resource diff --git a/lib/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt b/lib/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt deleted file mode 100644 index 48f587a2..00000000 --- a/lib/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt +++ /dev/null @@ -1 +0,0 @@ -a resource \ No newline at end of file diff --git a/lib/importlib_resources/tests/data02/two/__init__.py b/lib/importlib_resources/tests/data02/two/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/importlib_resources/tests/data02/two/resource2.txt b/lib/importlib_resources/tests/data02/two/resource2.txt deleted file mode 100644 index a80ce46e..00000000 --- a/lib/importlib_resources/tests/data02/two/resource2.txt +++ /dev/null @@ -1 +0,0 @@ -two resource diff --git a/lib/importlib_resources/tests/namespacedata01/binary.file b/lib/importlib_resources/tests/namespacedata01/binary.file deleted file mode 100644 index eaf36c1d..00000000 Binary files a/lib/importlib_resources/tests/namespacedata01/binary.file and /dev/null differ diff --git a/lib/importlib_resources/tests/namespacedata01/subdirectory/binary.file b/lib/importlib_resources/tests/namespacedata01/subdirectory/binary.file deleted file mode 100644 index 100f5064..00000000 --- a/lib/importlib_resources/tests/namespacedata01/subdirectory/binary.file +++ /dev/null @@ -1 +0,0 @@ -  \ No newline at end of file diff --git a/lib/importlib_resources/tests/namespacedata01/utf-16.file b/lib/importlib_resources/tests/namespacedata01/utf-16.file deleted file mode 100644 index 2cb77229..00000000 Binary files a/lib/importlib_resources/tests/namespacedata01/utf-16.file and /dev/null differ diff --git a/lib/importlib_resources/tests/namespacedata01/utf-8.file b/lib/importlib_resources/tests/namespacedata01/utf-8.file deleted file mode 100644 index 1c0132ad..00000000 --- a/lib/importlib_resources/tests/namespacedata01/utf-8.file +++ /dev/null @@ -1 +0,0 @@ -Hello, UTF-8 world! diff --git a/lib/importlib_resources/tests/test_contents.py b/lib/importlib_resources/tests/test_contents.py index 7dc3b0a6..741a7407 100644 --- a/lib/importlib_resources/tests/test_contents.py +++ b/lib/importlib_resources/tests/test_contents.py @@ -1,7 +1,6 @@ import unittest import importlib_resources as resources -from . import data01 from . import util @@ -19,16 +18,17 @@ class ContentsTests: assert self.expected <= contents -class ContentsDiskTests(ContentsTests, unittest.TestCase): - def setUp(self): - self.data = data01 +class ContentsDiskTests(ContentsTests, util.DiskSetup, unittest.TestCase): + pass class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): pass -class ContentsNamespaceTests(ContentsTests, unittest.TestCase): +class ContentsNamespaceTests(ContentsTests, util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' + expected = { # no __init__ because of namespace design 'binary.file', @@ -36,8 +36,3 @@ class ContentsNamespaceTests(ContentsTests, unittest.TestCase): 'utf-16.file', 'utf-8.file', } - - def setUp(self): - from . import namespacedata01 - - self.data = namespacedata01 diff --git a/lib/importlib_resources/tests/test_files.py b/lib/importlib_resources/tests/test_files.py index 3e86ec64..f1fe2337 100644 --- a/lib/importlib_resources/tests/test_files.py +++ b/lib/importlib_resources/tests/test_files.py @@ -1,3 +1,7 @@ +import os +import pathlib +import py_compile +import shutil import textwrap import unittest import warnings @@ -6,11 +10,8 @@ import contextlib import importlib_resources as resources from ..abc import Traversable -from . import data01 from . import util -from . import _path -from .compat.py39 import os_helper -from .compat.py312 import import_helper +from .compat.py39 import os_helper, import_helper @contextlib.contextmanager @@ -48,70 +49,146 @@ class FilesTests: resources.files(package=self.data) -class OpenDiskTests(FilesTests, unittest.TestCase): - def setUp(self): - self.data = data01 +class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase): + pass class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): pass -class OpenNamespaceTests(FilesTests, unittest.TestCase): - def setUp(self): - from . import namespacedata01 +class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' - self.data = namespacedata01 + def test_non_paths_in_dunder_path(self): + """ + Non-path items in a namespace package's ``__path__`` are ignored. + + As reported in python/importlib_resources#311, some tools + like Setuptools, when creating editable packages, will inject + non-paths into a namespace package's ``__path__``, a + sentinel like + ``__editable__.sample_namespace-1.0.finder.__path_hook__`` + to cause the ``PathEntryFinder`` to be called when searching + for packages. In that case, resources should still be loadable. + """ + import namespacedata01 + + namespacedata01.__path__.append( + '__editable__.sample_namespace-1.0.finder.__path_hook__' + ) + + resources.files(namespacedata01) class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase): ZIP_MODULE = 'namespacedata01' -class SiteDir: - def setUp(self): - self.fixtures = contextlib.ExitStack() - self.addCleanup(self.fixtures.close) - self.site_dir = self.fixtures.enter_context(os_helper.temp_dir()) - self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) - self.fixtures.enter_context(import_helper.isolated_modules()) +class DirectSpec: + """ + Override behavior of ModuleSetup to write a full spec directly. + """ + + MODULE = 'unused' + + def load_fixture(self, name): + self.tree_on_path(self.spec) -class ModulesFilesTests(SiteDir, unittest.TestCase): +class ModulesFiles: + spec = { + 'mod.py': '', + 'res.txt': 'resources are the best', + } + def test_module_resources(self): """ A module can have resources found adjacent to the module. """ - spec = { - 'mod.py': '', - 'res.txt': 'resources are the best', - } - _path.build(spec, self.site_dir) - import mod + import mod # type: ignore[import-not-found] actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8') - assert actual == spec['res.txt'] + assert actual == self.spec['res.txt'] -class ImplicitContextFilesTests(SiteDir, unittest.TestCase): - def test_implicit_files(self): +class ModuleFilesDiskTests(DirectSpec, util.DiskSetup, ModulesFiles, unittest.TestCase): + pass + + +class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.TestCase): + pass + + +class ImplicitContextFiles: + set_val = textwrap.dedent( + f""" + import {resources.__name__} as res + val = res.files().joinpath('res.txt').read_text(encoding='utf-8') + """ + ) + spec = { + 'somepkg': { + '__init__.py': set_val, + 'submod.py': set_val, + 'res.txt': 'resources are the best', + }, + 'frozenpkg': { + '__init__.py': set_val.replace(resources.__name__, 'c_resources'), + 'res.txt': 'resources are the best', + }, + } + + def test_implicit_files_package(self): """ Without any parameter, files() will infer the location as the caller. """ - spec = { - 'somepkg': { - '__init__.py': textwrap.dedent( - """ - import importlib_resources as res - val = res.files().joinpath('res.txt').read_text(encoding='utf-8') - """ - ), - 'res.txt': 'resources are the best', - }, - } - _path.build(spec, self.site_dir) assert importlib.import_module('somepkg').val == 'resources are the best' + def test_implicit_files_submodule(self): + """ + Without any parameter, files() will infer the location as the caller. + """ + assert importlib.import_module('somepkg.submod').val == 'resources are the best' + + def _compile_importlib(self): + """ + Make a compiled-only copy of the importlib resources package. + """ + bin_site = self.fixtures.enter_context(os_helper.temp_dir()) + c_resources = pathlib.Path(bin_site, 'c_resources') + sources = pathlib.Path(resources.__file__).parent + shutil.copytree(sources, c_resources, ignore=lambda *_: ['__pycache__']) + + for dirpath, _, filenames in os.walk(c_resources): + for filename in filenames: + source_path = pathlib.Path(dirpath) / filename + cfile = source_path.with_suffix('.pyc') + py_compile.compile(source_path, cfile) + pathlib.Path.unlink(source_path) + self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site)) + + def test_implicit_files_with_compiled_importlib(self): + """ + Caller detection works for compiled-only resources module. + + python/cpython#123085 + """ + self._compile_importlib() + assert importlib.import_module('frozenpkg').val == 'resources are the best' + + +class ImplicitContextFilesDiskTests( + DirectSpec, util.DiskSetup, ImplicitContextFiles, unittest.TestCase +): + pass + + +class ImplicitContextFilesZipTests( + DirectSpec, util.ZipSetup, ImplicitContextFiles, unittest.TestCase +): + pass + if __name__ == '__main__': unittest.main() diff --git a/lib/importlib_resources/tests/test_functional.py b/lib/importlib_resources/tests/test_functional.py index 69706cf7..1851edfb 100644 --- a/lib/importlib_resources/tests/test_functional.py +++ b/lib/importlib_resources/tests/test_functional.py @@ -1,31 +1,38 @@ import unittest import os -import contextlib +import importlib -try: - from test.support.warnings_helper import ignore_warnings, check_warnings -except ImportError: - # older Python versions - from test.support import ignore_warnings, check_warnings +from .compat.py39 import warnings_helper import importlib_resources as resources +from . import util + # Since the functional API forwards to Traversable, we only test # filesystem resources here -- not zip files, namespace packages etc. # We do test for two kinds of Anchor, though. class StringAnchorMixin: - anchor01 = 'importlib_resources.tests.data01' - anchor02 = 'importlib_resources.tests.data02' + anchor01 = 'data01' + anchor02 = 'data02' class ModuleAnchorMixin: - from . import data01 as anchor01 - from . import data02 as anchor02 + @property + def anchor01(self): + return importlib.import_module('data01') + + @property + def anchor02(self): + return importlib.import_module('data02') -class FunctionalAPIBase: +class FunctionalAPIBase(util.DiskSetup): + def setUp(self): + super().setUp() + self.load_fixture('data02') + def _gen_resourcetxt_path_parts(self): """Yield various names of a text file in anchor02, each in a subTest""" for path_parts in ( @@ -36,6 +43,12 @@ class FunctionalAPIBase: with self.subTest(path_parts=path_parts): yield path_parts + def assertEndsWith(self, string, suffix): + """Assert that `string` ends with `suffix`. + + Used to ignore an architecture-specific UTF-16 byte-order mark.""" + self.assertEqual(string[-len(suffix) :], suffix) + def test_read_text(self): self.assertEqual( resources.read_text(self.anchor01, 'utf-8.file'), @@ -76,13 +89,13 @@ class FunctionalAPIBase: ), '\x00\x01\x02\x03', ) - self.assertEqual( + self.assertEndsWith( # ignore the BOM resources.read_text( self.anchor01, 'utf-16.file', errors='backslashreplace', ), - 'Hello, UTF-16 world!\n'.encode('utf-16').decode( + 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode( errors='backslashreplace', ), ) @@ -128,9 +141,9 @@ class FunctionalAPIBase: 'utf-16.file', errors='backslashreplace', ) as f: - self.assertEqual( + self.assertEndsWith( # ignore the BOM f.read(), - 'Hello, UTF-16 world!\n'.encode('utf-16').decode( + 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode( errors='backslashreplace', ), ) @@ -163,32 +176,32 @@ class FunctionalAPIBase: self.assertTrue(is_resource(self.anchor02, *path_parts)) def test_contents(self): - with check_warnings((".*contents.*", DeprecationWarning)): + with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01) self.assertGreaterEqual( set(c), {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, ) - with contextlib.ExitStack() as cm: - cm.enter_context(self.assertRaises(OSError)) - cm.enter_context(check_warnings((".*contents.*", DeprecationWarning))) - + with self.assertRaises(OSError), warnings_helper.check_warnings(( + ".*contents.*", + DeprecationWarning, + )): list(resources.contents(self.anchor01, 'utf-8.file')) for path_parts in self._gen_resourcetxt_path_parts(): - with contextlib.ExitStack() as cm: - cm.enter_context(self.assertRaises(OSError)) - cm.enter_context(check_warnings((".*contents.*", DeprecationWarning))) - + with self.assertRaises(OSError), warnings_helper.check_warnings(( + ".*contents.*", + DeprecationWarning, + )): list(resources.contents(self.anchor01, *path_parts)) - with check_warnings((".*contents.*", DeprecationWarning)): + with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01, 'subdirectory') self.assertGreaterEqual( set(c), {'binary.file'}, ) - @ignore_warnings(category=DeprecationWarning) + @warnings_helper.ignore_warnings(category=DeprecationWarning) def test_common_errors(self): for func in ( resources.read_text, @@ -227,16 +240,16 @@ class FunctionalAPIBase: class FunctionalAPITest_StringAnchor( - unittest.TestCase, - FunctionalAPIBase, StringAnchorMixin, + FunctionalAPIBase, + unittest.TestCase, ): pass class FunctionalAPITest_ModuleAnchor( - unittest.TestCase, - FunctionalAPIBase, ModuleAnchorMixin, + FunctionalAPIBase, + unittest.TestCase, ): pass diff --git a/lib/importlib_resources/tests/test_open.py b/lib/importlib_resources/tests/test_open.py index 44f1018a..c40bb8c6 100644 --- a/lib/importlib_resources/tests/test_open.py +++ b/lib/importlib_resources/tests/test_open.py @@ -1,7 +1,6 @@ import unittest import importlib_resources as resources -from . import data01 from . import util @@ -65,16 +64,12 @@ class OpenTests: target.open(encoding='utf-8') -class OpenDiskTests(OpenTests, unittest.TestCase): - def setUp(self): - self.data = data01 +class OpenDiskTests(OpenTests, util.DiskSetup, unittest.TestCase): + pass -class OpenDiskNamespaceTests(OpenTests, unittest.TestCase): - def setUp(self): - from . import namespacedata01 - - self.data = namespacedata01 +class OpenDiskNamespaceTests(OpenTests, util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase): @@ -82,7 +77,7 @@ class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase): class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase): - ZIP_MODULE = 'namespacedata01' + MODULE = 'namespacedata01' if __name__ == '__main__': diff --git a/lib/importlib_resources/tests/test_path.py b/lib/importlib_resources/tests/test_path.py index c3e1cbb4..1e30f2bc 100644 --- a/lib/importlib_resources/tests/test_path.py +++ b/lib/importlib_resources/tests/test_path.py @@ -3,7 +3,6 @@ import pathlib import unittest import importlib_resources as resources -from . import data01 from . import util @@ -25,9 +24,7 @@ class PathTests: self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8')) -class PathDiskTests(PathTests, unittest.TestCase): - data = data01 - +class PathDiskTests(PathTests, util.DiskSetup, unittest.TestCase): def test_natural_path(self): """ Guarantee the internal implementation detail that diff --git a/lib/importlib_resources/tests/test_read.py b/lib/importlib_resources/tests/test_read.py index 97d90128..6780a2d1 100644 --- a/lib/importlib_resources/tests/test_read.py +++ b/lib/importlib_resources/tests/test_read.py @@ -1,7 +1,6 @@ import unittest import importlib_resources as resources -from . import data01 from . import util from importlib import import_module @@ -52,8 +51,8 @@ class ReadTests: ) -class ReadDiskTests(ReadTests, unittest.TestCase): - data = data01 +class ReadDiskTests(ReadTests, util.DiskSetup, unittest.TestCase): + pass class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): @@ -69,15 +68,12 @@ class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): self.assertEqual(result, bytes(range(4, 8))) -class ReadNamespaceTests(ReadTests, unittest.TestCase): - def setUp(self): - from . import namespacedata01 - - self.data = namespacedata01 +class ReadNamespaceTests(ReadTests, util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase): - ZIP_MODULE = 'namespacedata01' + MODULE = 'namespacedata01' def test_read_submodule_resource(self): submodule = import_module('namespacedata01.subdirectory') diff --git a/lib/importlib_resources/tests/test_reader.py b/lib/importlib_resources/tests/test_reader.py index 95c2fc85..0a77eb40 100644 --- a/lib/importlib_resources/tests/test_reader.py +++ b/lib/importlib_resources/tests/test_reader.py @@ -1,16 +1,21 @@ import os.path -import sys import pathlib import unittest from importlib import import_module from importlib_resources.readers import MultiplexedPath, NamespaceReader +from . import util -class MultiplexedPathTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.folder = pathlib.Path(__file__).parent / 'namespacedata01' + +class MultiplexedPathTest(util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' + + def setUp(self): + super().setUp() + self.folder = pathlib.Path(self.data.__path__[0]) + self.data01 = pathlib.Path(self.load_fixture('data01').__file__).parent + self.data02 = pathlib.Path(self.load_fixture('data02').__file__).parent def test_init_no_paths(self): with self.assertRaises(FileNotFoundError): @@ -31,9 +36,8 @@ class MultiplexedPathTest(unittest.TestCase): ) def test_iterdir_duplicate(self): - data01 = pathlib.Path(__file__).parent.joinpath('data01') contents = { - path.name for path in MultiplexedPath(self.folder, data01).iterdir() + path.name for path in MultiplexedPath(self.folder, self.data01).iterdir() } for remove in ('__pycache__', '__init__.pyc'): try: @@ -61,9 +65,8 @@ class MultiplexedPathTest(unittest.TestCase): path.open() def test_join_path(self): - data01 = pathlib.Path(__file__).parent.joinpath('data01') - prefix = str(data01.parent) - path = MultiplexedPath(self.folder, data01) + prefix = str(self.folder.parent) + path = MultiplexedPath(self.folder, self.data01) self.assertEqual( str(path.joinpath('binary.file'))[len(prefix) + 1 :], os.path.join('namespacedata01', 'binary.file'), @@ -83,10 +86,8 @@ class MultiplexedPathTest(unittest.TestCase): assert not path.joinpath('imaginary/foo.py').exists() def test_join_path_common_subdir(self): - data01 = pathlib.Path(__file__).parent.joinpath('data01') - data02 = pathlib.Path(__file__).parent.joinpath('data02') - prefix = str(data01.parent) - path = MultiplexedPath(data01, data02) + prefix = str(self.data02.parent) + path = MultiplexedPath(self.data01, self.data02) self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath) self.assertEqual( str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :], @@ -106,16 +107,8 @@ class MultiplexedPathTest(unittest.TestCase): ) -class NamespaceReaderTest(unittest.TestCase): - site_dir = str(pathlib.Path(__file__).parent) - - @classmethod - def setUpClass(cls): - sys.path.append(cls.site_dir) - - @classmethod - def tearDownClass(cls): - sys.path.remove(cls.site_dir) +class NamespaceReaderTest(util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' def test_init_error(self): with self.assertRaises(ValueError): @@ -125,7 +118,7 @@ class NamespaceReaderTest(unittest.TestCase): namespacedata01 = import_module('namespacedata01') reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) - root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + root = self.data.__path__[0] self.assertEqual( reader.resource_path('binary.file'), os.path.join(root, 'binary.file') ) @@ -134,9 +127,8 @@ class NamespaceReaderTest(unittest.TestCase): ) def test_files(self): - namespacedata01 = import_module('namespacedata01') - reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) - root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + reader = NamespaceReader(self.data.__spec__.submodule_search_locations) + root = self.data.__path__[0] self.assertIsInstance(reader.files(), MultiplexedPath) self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')") diff --git a/lib/importlib_resources/tests/test_resource.py b/lib/importlib_resources/tests/test_resource.py index dc2a108c..a0da6a35 100644 --- a/lib/importlib_resources/tests/test_resource.py +++ b/lib/importlib_resources/tests/test_resource.py @@ -1,9 +1,6 @@ -import sys import unittest import importlib_resources as resources -import pathlib -from . import data01 from . import util from importlib import import_module @@ -25,9 +22,8 @@ class ResourceTests: self.assertTrue(target.is_dir()) -class ResourceDiskTests(ResourceTests, unittest.TestCase): - def setUp(self): - self.data = data01 +class ResourceDiskTests(ResourceTests, util.DiskSetup, unittest.TestCase): + pass class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase): @@ -38,33 +34,39 @@ def names(traversable): return {item.name for item in traversable.iterdir()} -class ResourceLoaderTests(unittest.TestCase): +class ResourceLoaderTests(util.DiskSetup, unittest.TestCase): def test_resource_contents(self): package = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + file=self.data, path=self.data.__file__, contents=['A', 'B', 'C'] ) self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'}) def test_is_file(self): package = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + file=self.data, + path=self.data.__file__, + contents=['A', 'B', 'C', 'D/E', 'D/F'], ) self.assertTrue(resources.files(package).joinpath('B').is_file()) def test_is_dir(self): package = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + file=self.data, + path=self.data.__file__, + contents=['A', 'B', 'C', 'D/E', 'D/F'], ) self.assertTrue(resources.files(package).joinpath('D').is_dir()) def test_resource_missing(self): package = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + file=self.data, + path=self.data.__file__, + contents=['A', 'B', 'C', 'D/E', 'D/F'], ) self.assertFalse(resources.files(package).joinpath('Z').is_file()) -class ResourceCornerCaseTests(unittest.TestCase): +class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase): def test_package_has_no_reader_fallback(self): """ Test odd ball packages which: @@ -73,7 +75,7 @@ class ResourceCornerCaseTests(unittest.TestCase): # 3. Are not in a zip file """ module = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + file=self.data, path=self.data.__file__, contents=['A', 'B', 'C'] ) # Give the module a dummy loader. module.__loader__ = object() @@ -84,9 +86,7 @@ class ResourceCornerCaseTests(unittest.TestCase): self.assertFalse(resources.files(module).joinpath('A').is_file()) -class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): - ZIP_MODULE = 'data01' - +class ResourceFromZipsTest01(util.ZipSetup, unittest.TestCase): def test_is_submodule_resource(self): submodule = import_module('data01.subdirectory') self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file()) @@ -117,8 +117,8 @@ class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): assert not data.parent.exists() -class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): - ZIP_MODULE = 'data02' +class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase): + MODULE = 'data02' def test_unrelated_contents(self): """ @@ -135,7 +135,7 @@ class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): ) -class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase): +class DeletingZipsTest(util.ZipSetup, unittest.TestCase): """Having accessed resources in a zip file should not keep an open reference to the zip. """ @@ -217,24 +217,20 @@ class ResourceFromNamespaceTests: self.assertEqual(contents, {'binary.file'}) -class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, unittest.TestCase): - site_dir = str(pathlib.Path(__file__).parent) - - @classmethod - def setUpClass(cls): - sys.path.append(cls.site_dir) - - @classmethod - def tearDownClass(cls): - sys.path.remove(cls.site_dir) - - -class ResourceFromNamespaceZipTests( - util.ZipSetupBase, +class ResourceFromNamespaceDiskTests( + util.DiskSetup, ResourceFromNamespaceTests, unittest.TestCase, ): - ZIP_MODULE = 'namespacedata01' + MODULE = 'namespacedata01' + + +class ResourceFromNamespaceZipTests( + util.ZipSetup, + ResourceFromNamespaceTests, + unittest.TestCase, +): + MODULE = 'namespacedata01' if __name__ == '__main__': diff --git a/lib/importlib_resources/tests/util.py b/lib/importlib_resources/tests/util.py index fb827d2f..a4eafac3 100644 --- a/lib/importlib_resources/tests/util.py +++ b/lib/importlib_resources/tests/util.py @@ -6,10 +6,10 @@ import types import pathlib import contextlib -from . import data01 from ..abc import ResourceReader from .compat.py39 import import_helper, os_helper from . import zip as zip_ +from . import _path from importlib.machinery import ModuleSpec @@ -68,7 +68,7 @@ def create_package(file=None, path=None, is_package=True, contents=()): ) -class CommonTests(metaclass=abc.ABCMeta): +class CommonTestsBase(metaclass=abc.ABCMeta): """ Tests shared by test_open, test_path, and test_read. """ @@ -84,34 +84,34 @@ class CommonTests(metaclass=abc.ABCMeta): """ Passing in the package name should succeed. """ - self.execute(data01.__name__, 'utf-8.file') + self.execute(self.data.__name__, 'utf-8.file') def test_package_object(self): """ Passing in the package itself should succeed. """ - self.execute(data01, 'utf-8.file') + self.execute(self.data, 'utf-8.file') def test_string_path(self): """ Passing in a string for the path should succeed. """ path = 'utf-8.file' - self.execute(data01, path) + self.execute(self.data, path) def test_pathlib_path(self): """ Passing in a pathlib.PurePath object for the path should succeed. """ path = pathlib.PurePath('utf-8.file') - self.execute(data01, path) + self.execute(self.data, path) def test_importing_module_as_side_effect(self): """ The anchor package can already be imported. """ - del sys.modules[data01.__name__] - self.execute(data01.__name__, 'utf-8.file') + del sys.modules[self.data.__name__] + self.execute(self.data.__name__, 'utf-8.file') def test_missing_path(self): """ @@ -141,24 +141,66 @@ class CommonTests(metaclass=abc.ABCMeta): self.execute(package, 'utf-8.file') -class ZipSetupBase: - ZIP_MODULE = 'data01' +fixtures = dict( + data01={ + '__init__.py': '', + 'binary.file': bytes(range(4)), + 'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'), + 'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'), + 'subdirectory': { + '__init__.py': '', + 'binary.file': bytes(range(4, 8)), + }, + }, + data02={ + '__init__.py': '', + 'one': {'__init__.py': '', 'resource1.txt': 'one resource'}, + 'two': {'__init__.py': '', 'resource2.txt': 'two resource'}, + 'subdirectory': {'subsubdir': {'resource.txt': 'a resource'}}, + }, + namespacedata01={ + 'binary.file': bytes(range(4)), + 'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'), + 'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'), + 'subdirectory': { + 'binary.file': bytes(range(12, 16)), + }, + }, +) + +class ModuleSetup: def setUp(self): self.fixtures = contextlib.ExitStack() self.addCleanup(self.fixtures.close) self.fixtures.enter_context(import_helper.isolated_modules()) + self.data = self.load_fixture(self.MODULE) + def load_fixture(self, module): + self.tree_on_path({module: fixtures[module]}) + return importlib.import_module(module) + + +class ZipSetup(ModuleSetup): + MODULE = 'data01' + + def tree_on_path(self, spec): temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) modules = pathlib.Path(temp_dir) / 'zipped modules.zip' - src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE) self.fixtures.enter_context( - import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules))) + import_helper.DirsOnSysPath(str(zip_.make_zip_file(spec, modules))) ) - self.data = importlib.import_module(self.ZIP_MODULE) + +class DiskSetup(ModuleSetup): + MODULE = 'data01' + + def tree_on_path(self, spec): + temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) + _path.build(spec, pathlib.Path(temp_dir)) + self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir)) -class ZipSetup(ZipSetupBase): +class CommonTests(DiskSetup, CommonTestsBase): pass diff --git a/lib/importlib_resources/tests/zip.py b/lib/importlib_resources/tests/zip.py index 962195a9..51ee5648 100644 --- a/lib/importlib_resources/tests/zip.py +++ b/lib/importlib_resources/tests/zip.py @@ -2,31 +2,25 @@ Generate zip test data files. """ -import contextlib -import os -import pathlib import zipfile import zipp -def make_zip_file(src, dst): +def make_zip_file(tree, dst): """ - Zip the files in src into a new zipfile at dst. + Zip the files in tree into a new zipfile at dst. """ with zipfile.ZipFile(dst, 'w') as zf: - for src_path, rel in walk(src): - dst_name = src.name / pathlib.PurePosixPath(rel.as_posix()) - zf.write(src_path, dst_name) + for name, contents in walk(tree): + zf.writestr(name, contents) zipp.CompleteDirs.inject(zf) return dst -def walk(datapath): - for dirpath, dirnames, filenames in os.walk(datapath): - with contextlib.suppress(ValueError): - dirnames.remove('__pycache__') - for filename in filenames: - res = pathlib.Path(dirpath) / filename - rel = res.relative_to(datapath) - yield res, rel +def walk(tree, prefix=''): + for name, contents in tree.items(): + if isinstance(contents, dict): + yield from walk(contents, prefix=f'{prefix}{name}/') + else: + yield f'{prefix}{name}', contents diff --git a/lib/plexapi/audio.py b/lib/plexapi/audio.py index 8f84f3be..686073a3 100644 --- a/lib/plexapi/audio.py +++ b/lib/plexapi/audio.py @@ -193,6 +193,7 @@ class Artist( similar (List<:class:`~plexapi.media.Similar`>): List of similar objects. styles (List<:class:`~plexapi.media.Style`>): List of style objects. theme (str): URL to theme resource (/library/metadata//theme/). + ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object. """ TAG = 'Directory' TYPE = 'artist' @@ -213,6 +214,7 @@ class Artist( self.similar = self.findItems(data, media.Similar) self.styles = self.findItems(data, media.Style) self.theme = data.attrib.get('theme') + self.ultraBlurColors = self.findItem(data, media.UltraBlurColors) def __iter__(self): for album in self.albums(): @@ -281,6 +283,21 @@ class Artist( filepaths += track.download(_savepath, keep_original_name, **kwargs) return filepaths + def popularTracks(self): + """ Returns a list of :class:`~plexapi.audio.Track` popular tracks by the artist. """ + filters = { + 'album.subformat!': 'Compilation,Live', + 'artist.id': self.ratingKey, + 'group': 'title', + 'ratingCount>>': 0, + } + return self.section().search( + libtype='track', + filters=filters, + sort='ratingCount:desc', + limit=100 + ) + def station(self): """ Returns a :class:`~plexapi.playlist.Playlist` artist radio station or `None`. """ key = f'{self.key}?includeStations=1' @@ -325,6 +342,7 @@ class Album( studio (str): Studio that released the album. styles (List<:class:`~plexapi.media.Style`>): List of style objects. subformats (List<:class:`~plexapi.media.Subformat`>): List of subformat objects. + ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object. viewedLeafCount (int): Number of items marked as played in the album view. year (int): Year the album was released. """ @@ -354,6 +372,7 @@ class Album( self.studio = data.attrib.get('studio') self.styles = self.findItems(data, media.Style) self.subformats = self.findItems(data, media.Subformat) + self.ultraBlurColors = self.findItem(data, media.UltraBlurColors) self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.year = utils.cast(int, data.attrib.get('year')) diff --git a/lib/plexapi/base.py b/lib/plexapi/base.py index 26b103b9..a7fa82ee 100644 --- a/lib/plexapi/base.py +++ b/lib/plexapi/base.py @@ -3,7 +3,7 @@ import re from typing import TYPE_CHECKING, Generic, Iterable, List, Optional, TypeVar, Union import weakref from functools import cached_property -from urllib.parse import urlencode +from urllib.parse import parse_qsl, urlencode, urlparse from xml.etree import ElementTree from xml.etree.ElementTree import Element @@ -391,10 +391,9 @@ class PlexObject: Parameters: key (string, optional): Override the key to reload. - **kwargs (dict): A dictionary of XML include parameters to exclude or override. - All parameters are included by default with the option to override each parameter - or disable each parameter individually by setting it to False or 0. + **kwargs (dict): A dictionary of XML include parameters to include/exclude or override. See :class:`~plexapi.base.PlexPartialObject` for all the available include parameters. + Set parameter to True to include and False to exclude. Example: @@ -402,20 +401,28 @@ class PlexObject: from plexapi.server import PlexServer plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx') - movie = plex.library.section('Movies').get('Cars') - # Partial reload of the movie without the `checkFiles` parameter. - # Excluding `checkFiles` will prevent the Plex server from reading the - # file to check if the file still exists and is accessible. - # The movie object will remain as a partial object. - movie.reload(checkFiles=False) + # Search results are partial objects. + movie = plex.library.section('Movies').get('Cars') movie.isPartialObject() # Returns True - # Full reload of the movie with all include parameters. + # Partial reload of the movie without a default include parameter. + # The movie object will remain as a partial object. + movie.reload(includeMarkers=False) + movie.isPartialObject() # Returns True + + # Full reload of the movie with all default include parameters. # The movie object will be a full object. movie.reload() movie.isFullObject() # Returns True + # Full reload of the movie with all default and extra include parameter. + # Including `checkFiles` will tell the Plex server to check if the file + # still exists and is accessible. + # The movie object will be a full object. + movie.reload(checkFiles=True) + movie.isFullObject() # Returns True + """ return self._reload(key=key, **kwargs) @@ -505,25 +512,25 @@ class PlexPartialObject(PlexObject): automatically and update itself. """ _INCLUDES = { - 'checkFiles': 1, - 'includeAllConcerts': 1, + 'checkFiles': 0, + 'includeAllConcerts': 0, 'includeBandwidths': 1, 'includeChapters': 1, - 'includeChildren': 1, - 'includeConcerts': 1, - 'includeExternalMedia': 1, - 'includeExtras': 1, + 'includeChildren': 0, + 'includeConcerts': 0, + 'includeExternalMedia': 0, + 'includeExtras': 0, 'includeFields': 'thumbBlurHash,artBlurHash', 'includeGeolocation': 1, 'includeLoudnessRamps': 1, 'includeMarkers': 1, - 'includeOnDeck': 1, - 'includePopularLeaves': 1, - 'includePreferences': 1, - 'includeRelated': 1, - 'includeRelatedCount': 1, - 'includeReviews': 1, - 'includeStations': 1, + 'includeOnDeck': 0, + 'includePopularLeaves': 0, + 'includePreferences': 0, + 'includeRelated': 0, + 'includeRelatedCount': 0, + 'includeReviews': 0, + 'includeStations': 0, } _EXCLUDES = { 'excludeElements': ( @@ -592,7 +599,11 @@ class PlexPartialObject(PlexObject): search result for a movie often only contain a portion of the attributes a full object (main url) for that movie would contain. """ - return not self.key or (self._details_key or self.key) == self._initpath + parsed_key = urlparse(self._details_key or self.key) + parsed_initpath = urlparse(self._initpath) + query_key = set(parse_qsl(parsed_key.query)) + query_init = set(parse_qsl(parsed_initpath.query)) + return not self.key or (parsed_key.path == parsed_initpath.path and query_key <= query_init) def isPartialObject(self): """ Returns True if this is not a full object. """ diff --git a/lib/plexapi/client.py b/lib/plexapi/client.py index 76513e79..3d89e3dc 100644 --- a/lib/plexapi/client.py +++ b/lib/plexapi/client.py @@ -197,7 +197,7 @@ class PlexClient(PlexObject): raise NotFound(message) else: raise BadRequest(message) - data = response.text.encode('utf8') + data = utils.cleanXMLString(response.text).encode('utf8') return ElementTree.fromstring(data) if data.strip() else None def sendCommand(self, command, proxy=None, **params): diff --git a/lib/plexapi/collection.py b/lib/plexapi/collection.py index d71ddf2f..1c3ba3f7 100644 --- a/lib/plexapi/collection.py +++ b/lib/plexapi/collection.py @@ -60,6 +60,7 @@ class Collection( title (str): Name of the collection. titleSort (str): Title to use when sorting (defaults to title). type (str): 'collection' + ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object. updatedAt (datetime): Datetime the collection was updated. userRating (float): Rating of the collection (0.0 - 10.0) equaling (0 stars - 5 stars). """ @@ -102,6 +103,7 @@ class Collection( self.title = data.attrib.get('title') self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') + self.ultraBlurColors = self.findItem(data, media.UltraBlurColors) self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) self.userRating = utils.cast(float, data.attrib.get('userRating')) self._items = None # cache for self.items diff --git a/lib/plexapi/const.py b/lib/plexapi/const.py index b0fe7e7d..130555ad 100644 --- a/lib/plexapi/const.py +++ b/lib/plexapi/const.py @@ -4,6 +4,6 @@ # Library version MAJOR_VERSION = 4 MINOR_VERSION = 15 -PATCH_VERSION = 15 +PATCH_VERSION = 16 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" diff --git a/lib/plexapi/library.py b/lib/plexapi/library.py index f1bf5375..6913b829 100644 --- a/lib/plexapi/library.py +++ b/lib/plexapi/library.py @@ -2823,7 +2823,8 @@ class FilteringType(PlexObject): additionalFields.extend([ ('duration', 'integer', 'Duration'), ('viewOffset', 'integer', 'View Offset'), - ('label', 'tag', 'Label') + ('label', 'tag', 'Label'), + ('ratingCount', 'integer', 'Rating Count'), ]) elif self.type == 'collection': additionalFields.extend([ diff --git a/lib/plexapi/media.py b/lib/plexapi/media.py index 56126dcb..2f76d722 100644 --- a/lib/plexapi/media.py +++ b/lib/plexapi/media.py @@ -106,12 +106,16 @@ class MediaPart(PlexObject): Attributes: TAG (str): 'Part' accessible (bool): True if the file is accessible. + Requires reloading the media with ``checkFiles=True``. + Refer to :func:`~plexapi.base.PlexObject.reload`. audioProfile (str): The audio profile of the file. container (str): The container type of the file (ex: avi). decision (str): Unknown. deepAnalysisVersion (int): The Plex deep analysis version for the file. duration (int): The duration of the file in milliseconds. exists (bool): True if the file exists. + Requires reloading the media with ``checkFiles=True``. + Refer to :func:`~plexapi.base.PlexObject.reload`. file (str): The path to this file on disk (ex: /media/Movies/Cars (2006)/Cars (2006).mkv) has64bitOffsets (bool): True if the file has 64 bit offsets. hasThumbnail (bool): True if the file (track) has an embedded thumbnail. @@ -999,6 +1003,28 @@ class Review(PlexObject): self.text = data.attrib.get('text') +@utils.registerPlexObject +class UltraBlurColors(PlexObject): + """ Represents a single UltraBlurColors media tag. + + Attributes: + TAG (str): 'UltraBlurColors' + bottomLeft (str): The bottom left hex color. + bottomRight (str): The bottom right hex color. + topLeft (str): The top left hex color. + topRight (str): The top right hex color. + """ + TAG = 'UltraBlurColors' + + def _loadData(self, data): + """ Load attribute values from Plex XML response. """ + self._data = data + self.bottomLeft = data.attrib.get('bottomLeft') + self.bottomRight = data.attrib.get('bottomRight') + self.topLeft = data.attrib.get('topLeft') + self.topRight = data.attrib.get('topRight') + + class BaseResource(PlexObject): """ Base class for all Art, Poster, and Theme objects. diff --git a/lib/plexapi/mixins.py b/lib/plexapi/mixins.py index 8571ba63..bdf4607e 100644 --- a/lib/plexapi/mixins.py +++ b/lib/plexapi/mixins.py @@ -14,8 +14,8 @@ class AdvancedSettingsMixin: def preferences(self): """ Returns a list of :class:`~plexapi.settings.Preferences` objects. """ - data = self._server.query(self._details_key) - return self.findItems(data, settings.Preferences, rtag='Preferences') + key = f'{self.key}?includePreferences=1' + return self.fetchItems(key, cls=settings.Preferences, rtag='Preferences') def preference(self, pref): """ Returns a :class:`~plexapi.settings.Preferences` object for the specified pref. @@ -240,8 +240,7 @@ class UnmatchMatchMixin: params['agent'] = utils.getAgentIdentifier(self.section(), agent) key = key + '?' + urlencode(params) - data = self._server.query(key, method=self._server._session.get) - return self.findItems(data, initpath=key) + return self.fetchItems(key, cls=media.SearchResult) def fixMatch(self, searchResult=None, auto=False, agent=None): """ Use match result to update show metadata. @@ -278,8 +277,8 @@ class ExtrasMixin: def extras(self): """ Returns a list of :class:`~plexapi.video.Extra` objects. """ from plexapi.video import Extra - data = self._server.query(self._details_key) - return self.findItems(data, Extra, rtag='Extras') + key = f'{self.key}/extras' + return self.fetchItems(key, cls=Extra) class HubsMixin: @@ -289,8 +288,7 @@ class HubsMixin: """ Returns a list of :class:`~plexapi.library.Hub` objects. """ from plexapi.library import Hub key = f'{self.key}/related' - data = self._server.query(key) - return self.findItems(data, Hub) + return self.fetchItems(key, cls=Hub) class PlayedUnplayedMixin: diff --git a/lib/plexapi/myplex.py b/lib/plexapi/myplex.py index bc40583e..24e32e6b 100644 --- a/lib/plexapi/myplex.py +++ b/lib/plexapi/myplex.py @@ -250,7 +250,7 @@ class MyPlexAccount(PlexObject): return response.json() elif 'text/plain' in response.headers.get('Content-Type', ''): return response.text.strip() - data = response.text.encode('utf8') + data = utils.cleanXMLString(response.text).encode('utf8') return ElementTree.fromstring(data) if data.strip() else None def ping(self): diff --git a/lib/plexapi/server.py b/lib/plexapi/server.py index f39a423f..8cd110d8 100644 --- a/lib/plexapi/server.py +++ b/lib/plexapi/server.py @@ -768,7 +768,7 @@ class PlexServer(PlexObject): raise NotFound(message) else: raise BadRequest(message) - data = response.text.encode('utf8') + data = utils.cleanXMLString(response.text).encode('utf8') return ElementTree.fromstring(data) if data.strip() else None def search(self, query, mediatype=None, limit=None, sectionId=None): diff --git a/lib/plexapi/utils.py b/lib/plexapi/utils.py index bb128532..549afc5b 100644 --- a/lib/plexapi/utils.py +++ b/lib/plexapi/utils.py @@ -6,6 +6,7 @@ import logging import os import re import string +import sys import time import unicodedata import warnings @@ -673,3 +674,45 @@ def openOrRead(file): def sha1hash(guid): """ Return the SHA1 hash of a guid. """ return sha1(guid.encode('utf-8')).hexdigest() + + +# https://stackoverflow.com/a/64570125 +_illegal_XML_characters = [ + (0x00, 0x08), + (0x0B, 0x0C), + (0x0E, 0x1F), + (0x7F, 0x84), + (0x86, 0x9F), + (0xFDD0, 0xFDDF), + (0xFFFE, 0xFFFF), +] +if sys.maxunicode >= 0x10000: # not narrow build + _illegal_XML_characters.extend( + [ + (0x1FFFE, 0x1FFFF), + (0x2FFFE, 0x2FFFF), + (0x3FFFE, 0x3FFFF), + (0x4FFFE, 0x4FFFF), + (0x5FFFE, 0x5FFFF), + (0x6FFFE, 0x6FFFF), + (0x7FFFE, 0x7FFFF), + (0x8FFFE, 0x8FFFF), + (0x9FFFE, 0x9FFFF), + (0xAFFFE, 0xAFFFF), + (0xBFFFE, 0xBFFFF), + (0xCFFFE, 0xCFFFF), + (0xDFFFE, 0xDFFFF), + (0xEFFFE, 0xEFFFF), + (0xFFFFE, 0xFFFFF), + (0x10FFFE, 0x10FFFF), + ] + ) +_illegal_XML_ranges = [ + fr'{chr(low)}-{chr(high)}' + for (low, high) in _illegal_XML_characters +] +_illegal_XML_re = re.compile(fr'[{"".join(_illegal_XML_ranges)}]') + + +def cleanXMLString(s): + return _illegal_XML_re.sub('', s) diff --git a/lib/plexapi/video.py b/lib/plexapi/video.py index 609f57f6..15755415 100644 --- a/lib/plexapi/video.py +++ b/lib/plexapi/video.py @@ -375,6 +375,7 @@ class Movie( studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment). tagline (str): Movie tag line (Back 2 Work; Who says men can't change?). theme (str): URL to theme resource (/library/metadata//theme/). + ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object. useOriginalTitle (int): Setting that indicates if the original title is used for the movie (-1 = Library default, 0 = No, 1 = Yes). viewOffset (int): View offset in milliseconds. @@ -420,6 +421,7 @@ class Movie( self.studio = data.attrib.get('studio') self.tagline = data.attrib.get('tagline') self.theme = data.attrib.get('theme') + self.ultraBlurColors = self.findItem(data, media.UltraBlurColors) self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.writers = self.findItems(data, media.Writer) @@ -456,8 +458,8 @@ class Movie( def reviews(self): """ Returns a list of :class:`~plexapi.media.Review` objects. """ - data = self._server.query(self._details_key) - return self.findItems(data, media.Review, rtag='Video') + key = f'{self.key}?includeReviews=1' + return self.fetchItems(key, cls=media.Review, rtag='Video') def editions(self): """ Returns a list of :class:`~plexapi.video.Movie` objects @@ -543,6 +545,7 @@ class Show( (-1 = Account default, 0 = Manually selected, 1 = Shown with foreign audio, 2 = Always enabled). tagline (str): Show tag line. theme (str): URL to theme resource (/library/metadata//theme/). + ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object. useOriginalTitle (int): Setting that indicates if the original title is used for the show (-1 = Library default, 0 = No, 1 = Yes). viewedLeafCount (int): Number of items marked as played in the show view. @@ -592,6 +595,7 @@ class Show( self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1')) self.tagline = data.attrib.get('tagline') self.theme = data.attrib.get('theme') + self.ultraBlurColors = self.findItem(data, media.UltraBlurColors) self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1')) self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.year = utils.cast(int, data.attrib.get('year')) @@ -614,8 +618,8 @@ class Show( """ Returns show's On Deck :class:`~plexapi.video.Video` object or `None`. If show is unwatched, return will likely be the first episode. """ - data = self._server.query(self._details_key) - return next(iter(self.findItems(data, rtag='OnDeck')), None) + key = f'{self.key}?includeOnDeck=1' + return next(iter(self.fetchItems(key, cls=Episode, rtag='OnDeck')), None) def season(self, title=None, season=None): """ Returns the season with the specified title or number. @@ -735,6 +739,7 @@ class Season( subtitleLanguage (str): Setting that indicates the preferred subtitle language. subtitleMode (int): Setting that indicates the auto-select subtitle mode. (-1 = Series default, 0 = Manually selected, 1 = Shown with foreign audio, 2 = Always enabled). + ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object. viewedLeafCount (int): Number of items marked as played in the season view. year (int): Year the season was released. """ @@ -766,6 +771,7 @@ class Season( self.ratings = self.findItems(data, media.Rating) self.subtitleLanguage = data.attrib.get('subtitleLanguage', '') self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1')) + self.ultraBlurColors = self.findItem(data, media.UltraBlurColors) self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.year = utils.cast(int, data.attrib.get('year')) @@ -796,8 +802,8 @@ class Season( """ Returns season's On Deck :class:`~plexapi.video.Video` object or `None`. Will only return a match if the show's On Deck episode is in this season. """ - data = self._server.query(self._details_key) - return next(iter(self.findItems(data, rtag='OnDeck')), None) + key = f'{self.key}?includeOnDeck=1' + return next(iter(self.fetchItems(key, cls=Episode, rtag='OnDeck')), None) def episode(self, title=None, episode=None): """ Returns the episode with the given title or number. @@ -914,6 +920,7 @@ class Episode( skipParent (bool): True if the show's seasons are set to hidden. sourceURI (str): Remote server URI (server:///com.plexapp.plugins.library) (remote playlist item only). + ultraBlurColors (:class:`~plexapi.media.UltraBlurColors`): Ultra blur color object. viewOffset (int): View offset in milliseconds. writers (List<:class:`~plexapi.media.Writer`>): List of writers objects. year (int): Year the episode was released. @@ -958,6 +965,7 @@ class Episode( self.roles = self.findItems(data, media.Role) self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0')) self.sourceURI = data.attrib.get('source') # remote playlist item + self.ultraBlurColors = self.findItem(data, media.UltraBlurColors) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.writers = self.findItems(data, media.Writer) self.year = utils.cast(int, data.attrib.get('year')) diff --git a/lib/pyparsing/__init__.py b/lib/pyparsing/__init__.py index 79d8153c..a440cfbe 100644 --- a/lib/pyparsing/__init__.py +++ b/lib/pyparsing/__init__.py @@ -120,8 +120,8 @@ class version_info(NamedTuple): return f"{__name__}.{type(self).__name__}({', '.join('{}={!r}'.format(*nv) for nv in zip(self._fields, self))})" -__version_info__ = version_info(3, 1, 2, "final", 1) -__version_time__ = "06 Mar 2024 07:08 UTC" +__version_info__ = version_info(3, 1, 4, "final", 1) +__version_time__ = "25 Aug 2024 14:40 UTC" __version__ = __version_info__.__version__ __versionTime__ = __version_time__ __author__ = "Paul McGuire " @@ -143,7 +143,7 @@ from .common import ( _builtin_exprs as common_builtin_exprs, ) -# define backward compat synonyms +# Compatibility synonyms if "pyparsing_unicode" not in globals(): pyparsing_unicode = unicode # type: ignore[misc] if "pyparsing_common" not in globals(): diff --git a/lib/pyparsing/actions.py b/lib/pyparsing/actions.py index ce51b395..1d2dce99 100644 --- a/lib/pyparsing/actions.py +++ b/lib/pyparsing/actions.py @@ -196,7 +196,7 @@ def with_class(classname, namespace=""): return with_attribute(**{classattr: classname}) -# pre-PEP8 compatibility symbols +# Compatibility synonyms # fmt: off replaceWith = replaced_by_pep8("replaceWith", replace_with) removeQuotes = replaced_by_pep8("removeQuotes", remove_quotes) diff --git a/lib/pyparsing/common.py b/lib/pyparsing/common.py index 74faa460..649aad00 100644 --- a/lib/pyparsing/common.py +++ b/lib/pyparsing/common.py @@ -418,20 +418,15 @@ class pyparsing_common: # fmt: on # pre-PEP8 compatibility names - convertToInteger = convert_to_integer - """Deprecated - use :class:`convert_to_integer`""" - convertToFloat = convert_to_float - """Deprecated - use :class:`convert_to_float`""" - convertToDate = convert_to_date - """Deprecated - use :class:`convert_to_date`""" - convertToDatetime = convert_to_datetime - """Deprecated - use :class:`convert_to_datetime`""" - stripHTMLTags = strip_html_tags - """Deprecated - use :class:`strip_html_tags`""" - upcaseTokens = upcase_tokens - """Deprecated - use :class:`upcase_tokens`""" - downcaseTokens = downcase_tokens - """Deprecated - use :class:`downcase_tokens`""" + # fmt: off + convertToInteger = staticmethod(replaced_by_pep8("convertToInteger", convert_to_integer)) + convertToFloat = staticmethod(replaced_by_pep8("convertToFloat", convert_to_float)) + convertToDate = staticmethod(replaced_by_pep8("convertToDate", convert_to_date)) + convertToDatetime = staticmethod(replaced_by_pep8("convertToDatetime", convert_to_datetime)) + stripHTMLTags = staticmethod(replaced_by_pep8("stripHTMLTags", strip_html_tags)) + upcaseTokens = staticmethod(replaced_by_pep8("upcaseTokens", upcase_tokens)) + downcaseTokens = staticmethod(replaced_by_pep8("downcaseTokens", downcase_tokens)) + # fmt: on _builtin_exprs = [ diff --git a/lib/pyparsing/core.py b/lib/pyparsing/core.py index b19d1221..cbe73c98 100644 --- a/lib/pyparsing/core.py +++ b/lib/pyparsing/core.py @@ -53,6 +53,11 @@ from .unicode import pyparsing_unicode _MAX_INT = sys.maxsize str_type: Tuple[type, ...] = (str, bytes) +if sys.version_info >= (3, 7): + _RePattern = re.Pattern +else: + _RePattern = typing.Pattern + # # Copyright (c) 2003-2022 Paul T. McGuire # @@ -218,19 +223,11 @@ if _should_enable_warnings( # build list of single arg builtins, that can be used as parse actions +# fmt: off _single_arg_builtins = { - sum, - len, - sorted, - reversed, - list, - tuple, - set, - any, - all, - min, - max, + sum, len, sorted, reversed, list, tuple, set, any, all, min, max } +# fmt: on _generatorType = types.GeneratorType ParseImplReturnType = Tuple[int, Any] @@ -255,13 +252,13 @@ DebugSuccessAction = Callable[ DebugExceptionAction = Callable[[str, int, "ParserElement", Exception, bool], None] -alphas = string.ascii_uppercase + string.ascii_lowercase -identchars = pyparsing_unicode.Latin1.identchars -identbodychars = pyparsing_unicode.Latin1.identbodychars -nums = "0123456789" -hexnums = nums + "ABCDEFabcdef" -alphanums = alphas + nums -printables = "".join([c for c in string.printable if c not in string.whitespace]) +alphas: str = string.ascii_uppercase + string.ascii_lowercase +identchars: str = pyparsing_unicode.Latin1.identchars +identbodychars: str = pyparsing_unicode.Latin1.identbodychars +nums: str = "0123456789" +hexnums: str = nums + "ABCDEFabcdef" +alphanums: str = alphas + nums +printables: str = "".join([c for c in string.printable if c not in string.whitespace]) _trim_arity_call_line: traceback.StackSummary = None # type: ignore[assignment] @@ -280,7 +277,7 @@ def _trim_arity(func, max_limit=3): # user's parse action 'func', so that we don't incur call penalty at parse time # fmt: off - LINE_DIFF = 7 + LINE_DIFF = 9 # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!! _trim_arity_call_line = (_trim_arity_call_line or traceback.extract_stack(limit=2)[-1]) @@ -288,6 +285,8 @@ def _trim_arity(func, max_limit=3): def wrapper(*args): nonlocal found_arity, limit + if found_arity: + return func(*args[limit:]) while 1: try: ret = func(*args[limit:]) @@ -476,7 +475,7 @@ class ParserElement(ABC): self.streamlined = False # optimize exception handling for subclasses that don't advance parse index self.mayIndexError = True - self.errmsg = "" + self.errmsg: Union[str, None] = "" # mark results names as modal (report only last) or cumulative (list all) self.modalResults = True # custom debug actions @@ -582,15 +581,15 @@ class ParserElement(ABC): listAllMatches = listAllMatches or list_all_matches return self._setResultsName(name, listAllMatches) - def _setResultsName(self, name, listAllMatches=False): + def _setResultsName(self, name, list_all_matches=False) -> "ParserElement": if name is None: return self newself = self.copy() if name.endswith("*"): name = name[:-1] - listAllMatches = True + list_all_matches = True newself.resultsName = name - newself.modalResults = not listAllMatches + newself.modalResults = not list_all_matches return newself def set_break(self, break_flag: bool = True) -> "ParserElement": @@ -602,12 +601,12 @@ class ParserElement(ABC): if break_flag: _parseMethod = self._parse - def breaker(instring, loc, doActions=True, callPreParse=True): + def breaker(instring, loc, do_actions=True, callPreParse=True): import pdb # this call to pdb.set_trace() is intentional, not a checkin error pdb.set_trace() - return _parseMethod(instring, loc, doActions, callPreParse) + return _parseMethod(instring, loc, do_actions, callPreParse) breaker._originalParseMethod = _parseMethod # type: ignore [attr-defined] self._parse = breaker # type: ignore [assignment] @@ -615,7 +614,7 @@ class ParserElement(ABC): self._parse = self._parse._originalParseMethod # type: ignore [attr-defined, assignment] return self - def set_parse_action(self, *fns: ParseAction, **kwargs) -> "ParserElement": + def set_parse_action(self, *fns: ParseAction, **kwargs: Any) -> "ParserElement": """ Define one or more actions to perform when successfully matching parse element definition. @@ -691,19 +690,19 @@ class ParserElement(ABC): ''') """ if list(fns) == [None]: - self.parseAction = [] + self.parseAction.clear() return self if not all(callable(fn) for fn in fns): raise TypeError("parse actions must be callable") - self.parseAction = [_trim_arity(fn) for fn in fns] + self.parseAction[:] = [_trim_arity(fn) for fn in fns] self.callDuringTry = kwargs.get( "call_during_try", kwargs.get("callDuringTry", False) ) return self - def add_parse_action(self, *fns: ParseAction, **kwargs) -> "ParserElement": + def add_parse_action(self, *fns: ParseAction, **kwargs: Any) -> "ParserElement": """ Add one or more parse actions to expression's list of parse actions. See :class:`set_parse_action`. @@ -715,7 +714,7 @@ class ParserElement(ABC): ) return self - def add_condition(self, *fns: ParseCondition, **kwargs) -> "ParserElement": + def add_condition(self, *fns: ParseCondition, **kwargs: Any) -> "ParserElement": """Add a boolean predicate function to expression's list of parse actions. See :class:`set_parse_action` for function call signatures. Unlike ``set_parse_action``, functions passed to ``add_condition`` need to return boolean success/fail of the condition. @@ -801,7 +800,7 @@ class ParserElement(ABC): return loc - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: return loc, [] def postParse(self, instring, loc, tokenlist): @@ -809,10 +808,10 @@ class ParserElement(ABC): # @profile def _parseNoCache( - self, instring, loc, doActions=True, callPreParse=True + self, instring, loc, do_actions=True, callPreParse=True ) -> Tuple[int, ParseResults]: TRY, MATCH, FAIL = 0, 1, 2 - debugging = self.debug # and doActions) + debugging = self.debug # and do_actions) len_instring = len(instring) if debugging or self.failAction: @@ -827,11 +826,11 @@ class ParserElement(ABC): self.debugActions.debug_try(instring, tokens_start, self, False) if self.mayIndexError or pre_loc >= len_instring: try: - loc, tokens = self.parseImpl(instring, pre_loc, doActions) + loc, tokens = self.parseImpl(instring, pre_loc, do_actions) except IndexError: raise ParseException(instring, len_instring, self.errmsg, self) else: - loc, tokens = self.parseImpl(instring, pre_loc, doActions) + loc, tokens = self.parseImpl(instring, pre_loc, do_actions) except Exception as err: # print("Exception raised:", err) if self.debugActions.debug_fail: @@ -849,18 +848,18 @@ class ParserElement(ABC): tokens_start = pre_loc if self.mayIndexError or pre_loc >= len_instring: try: - loc, tokens = self.parseImpl(instring, pre_loc, doActions) + loc, tokens = self.parseImpl(instring, pre_loc, do_actions) except IndexError: raise ParseException(instring, len_instring, self.errmsg, self) else: - loc, tokens = self.parseImpl(instring, pre_loc, doActions) + loc, tokens = self.parseImpl(instring, pre_loc, do_actions) tokens = self.postParse(instring, loc, tokens) ret_tokens = ParseResults( tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults ) - if self.parseAction and (doActions or self.callDuringTry): + if self.parseAction and (do_actions or self.callDuringTry): if debugging: try: for fn in self.parseAction: @@ -919,7 +918,7 @@ class ParserElement(ABC): do_actions: bool = False, ) -> int: try: - return self._parse(instring, loc, doActions=do_actions)[0] + return self._parse(instring, loc, do_actions=do_actions)[0] except ParseFatalException: if raise_fatal: raise @@ -960,18 +959,18 @@ class ParserElement(ABC): # this method gets repeatedly called during backtracking with the same arguments - # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression def _parseCache( - self, instring, loc, doActions=True, callPreParse=True + self, instring, loc, do_actions=True, callPreParse=True ) -> Tuple[int, ParseResults]: HIT, MISS = 0, 1 TRY, MATCH, FAIL = 0, 1, 2 - lookup = (self, instring, loc, callPreParse, doActions) + lookup = (self, instring, loc, callPreParse, do_actions) with ParserElement.packrat_cache_lock: cache = ParserElement.packrat_cache value = cache.get(lookup) if value is cache.not_in_cache: ParserElement.packrat_cache_stats[MISS] += 1 try: - value = self._parseNoCache(instring, loc, doActions, callPreParse) + value = self._parseNoCache(instring, loc, do_actions, callPreParse) except ParseBaseException as pe: # cache a copy of the exception, without the traceback cache.set(lookup, pe.__class__(*pe.args)) @@ -1190,7 +1189,7 @@ class ParserElement(ABC): loc, tokens = self._parse(instring, 0) if parseAll: loc = self.preParse(instring, loc) - se = Empty() + StringEnd() + se = Empty() + StringEnd().set_debug(False) se._parse(instring, loc) except ParseBaseException as exc: if ParserElement.verbose_stacktrace: @@ -1887,9 +1886,14 @@ class ParserElement(ABC): Child classes must define this method, which defines how the ``default_name`` is set. """ - def set_name(self, name: str) -> "ParserElement": + def set_name(self, name: typing.Optional[str]) -> "ParserElement": """ - Define name for this expression, makes debugging and exception messages clearer. + Define name for this expression, makes debugging and exception messages clearer. If + `__diag__.enable_debug_on_named_expressions` is set to True, setting a name will also + enable debug for this expression. + + If `name` is None, clears any custom name for this expression, and clears the + debug flag is it was enabled via `__diag__.enable_debug_on_named_expressions`. Example:: @@ -1900,9 +1904,11 @@ class ParserElement(ABC): integer.parse_string("ABC") # -> Exception: Expected integer (at char 0), (line:1, col:1) """ self.customName = name - self.errmsg = f"Expected {self.name}" + self.errmsg = f"Expected {str(self)}" + if __diag__.enable_debug_on_named_expressions: - self.set_debug() + self.set_debug(name is not None) + return self @property @@ -1910,6 +1916,10 @@ class ParserElement(ABC): # This will use a user-defined name if available, but otherwise defaults back to the auto-generated name return self.customName if self.customName is not None else self.default_name + @name.setter + def name(self, new_name) -> None: + self.set_name(new_name) + def __str__(self) -> str: return self.name @@ -2014,7 +2024,9 @@ class ParserElement(ABC): full_dump: bool = True, print_results: bool = True, failure_tests: bool = False, - post_parse: typing.Optional[Callable[[str, ParseResults], str]] = None, + post_parse: typing.Optional[ + Callable[[str, ParseResults], typing.Optional[str]] + ] = None, file: typing.Optional[TextIO] = None, with_line_numbers: bool = False, *, @@ -2022,7 +2034,9 @@ class ParserElement(ABC): fullDump: bool = True, printResults: bool = True, failureTests: bool = False, - postParse: typing.Optional[Callable[[str, ParseResults], str]] = None, + postParse: typing.Optional[ + Callable[[str, ParseResults], typing.Optional[str]] + ] = None, ) -> Tuple[bool, List[Tuple[str, Union[ParseResults, Exception]]]]: """ Execute the parse expression on a series of test strings, showing each @@ -2159,7 +2173,7 @@ class ParserElement(ABC): f"{nlstr}{nlstr.join(comments) if comments else ''}", pyparsing_test.with_line_numbers(t) if with_line_numbers else t, ] - comments = [] + comments.clear() try: # convert newline marks to actual newlines, and strip leading BOM if present t = NL.transform_string(t.lstrip(BOM)) @@ -2264,10 +2278,15 @@ class ParserElement(ABC): # Compatibility synonyms # fmt: off - inlineLiteralsUsing = replaced_by_pep8("inlineLiteralsUsing", inline_literals_using) - setDefaultWhitespaceChars = replaced_by_pep8( + inlineLiteralsUsing = staticmethod(replaced_by_pep8("inlineLiteralsUsing", inline_literals_using)) + setDefaultWhitespaceChars = staticmethod(replaced_by_pep8( "setDefaultWhitespaceChars", set_default_whitespace_chars - ) + )) + disableMemoization = staticmethod(replaced_by_pep8("disableMemoization", disable_memoization)) + enableLeftRecursion = staticmethod(replaced_by_pep8("enableLeftRecursion", enable_left_recursion)) + enablePackrat = staticmethod(replaced_by_pep8("enablePackrat", enable_packrat)) + resetCache = staticmethod(replaced_by_pep8("resetCache", reset_cache)) + setResultsName = replaced_by_pep8("setResultsName", set_results_name) setBreak = replaced_by_pep8("setBreak", set_break) setParseAction = replaced_by_pep8("setParseAction", set_parse_action) @@ -2275,8 +2294,6 @@ class ParserElement(ABC): addCondition = replaced_by_pep8("addCondition", add_condition) setFailAction = replaced_by_pep8("setFailAction", set_fail_action) tryParse = replaced_by_pep8("tryParse", try_parse) - enableLeftRecursion = replaced_by_pep8("enableLeftRecursion", enable_left_recursion) - enablePackrat = replaced_by_pep8("enablePackrat", enable_packrat) parseString = replaced_by_pep8("parseString", parse_string) scanString = replaced_by_pep8("scanString", scan_string) transformString = replaced_by_pep8("transformString", transform_string) @@ -2290,8 +2307,7 @@ class ParserElement(ABC): setName = replaced_by_pep8("setName", set_name) parseFile = replaced_by_pep8("parseFile", parse_file) runTests = replaced_by_pep8("runTests", run_tests) - canParseNext = can_parse_next - resetCache = reset_cache + canParseNext = replaced_by_pep8("canParseNext", can_parse_next) defaultName = default_name # fmt: on @@ -2331,7 +2347,7 @@ class _PendingSkip(ParserElement): def __repr__(self): return self.defaultName - def parseImpl(self, *args): + def parseImpl(self, *args) -> ParseImplReturnType: raise Exception( "use of `...` expression without following SkipTo target expression" ) @@ -2360,7 +2376,7 @@ class NoMatch(Token): self.mayIndexError = False self.errmsg = "Unmatchable token" - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: raise ParseException(instring, loc, self.errmsg, self) @@ -2409,7 +2425,7 @@ class Literal(Token): def _generateDefaultName(self) -> str: return repr(self.match) - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if instring[loc] == self.firstMatchChar and instring.startswith( self.match, loc ): @@ -2430,12 +2446,12 @@ class Empty(Literal): def _generateDefaultName(self) -> str: return "Empty" - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: return loc, [] class _SingleCharLiteral(Literal): - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if instring[loc] == self.firstMatchChar: return loc + 1, self.match raise ParseException(instring, loc, self.errmsg, self) @@ -2505,7 +2521,7 @@ class Keyword(Token): def _generateDefaultName(self) -> str: return repr(self.match) - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: errmsg = self.errmsg errloc = loc if self.caseless: @@ -2556,7 +2572,10 @@ class Keyword(Token): """ Keyword.DEFAULT_KEYWORD_CHARS = chars - setDefaultKeywordChars = set_default_keyword_chars + # Compatibility synonyms + setDefaultKeywordChars = staticmethod( + replaced_by_pep8("setDefaultKeywordChars", set_default_keyword_chars) + ) class CaselessLiteral(Literal): @@ -2580,7 +2599,7 @@ class CaselessLiteral(Literal): self.returnString = match_string self.errmsg = f"Expected {self.name}" - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if instring[loc : loc + self.matchLen].upper() == self.match: return loc + self.matchLen, self.returnString raise ParseException(instring, loc, self.errmsg, self) @@ -2666,7 +2685,7 @@ class CloseMatch(Token): def _generateDefaultName(self) -> str: return f"{type(self).__name__}:{self.match_string!r}" - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: start = loc instrlen = len(instring) maxloc = start + len(self.match_string) @@ -2911,36 +2930,36 @@ class Word(Token): return base + f"{{{self.minLen},{self.maxLen}}}" return base - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if instring[loc] not in self.initChars: raise ParseException(instring, loc, self.errmsg, self) start = loc loc += 1 instrlen = len(instring) - bodychars = self.bodyChars + body_chars: set[str] = self.bodyChars maxloc = start + self.maxLen maxloc = min(maxloc, instrlen) - while loc < maxloc and instring[loc] in bodychars: + while loc < maxloc and instring[loc] in body_chars: loc += 1 - throwException = False + throw_exception = False if loc - start < self.minLen: - throwException = True - elif self.maxSpecified and loc < instrlen and instring[loc] in bodychars: - throwException = True + throw_exception = True + elif self.maxSpecified and loc < instrlen and instring[loc] in body_chars: + throw_exception = True elif self.asKeyword and ( - (start > 0 and instring[start - 1] in bodychars) - or (loc < instrlen and instring[loc] in bodychars) + (start > 0 and instring[start - 1] in body_chars) + or (loc < instrlen and instring[loc] in body_chars) ): - throwException = True + throw_exception = True - if throwException: + if throw_exception: raise ParseException(instring, loc, self.errmsg, self) return loc, instring[start:loc] - def parseImpl_regex(self, instring, loc, doActions=True): + def parseImpl_regex(self, instring, loc, do_actions=True) -> ParseImplReturnType: result = self.re_match(instring, loc) if not result: raise ParseException(instring, loc, self.errmsg, self) @@ -3043,7 +3062,7 @@ class Regex(Token): self.parseImpl = self.parseImplAsMatch # type: ignore [assignment] @cached_property - def re(self): + def re(self) -> _RePattern: if self._re: return self._re @@ -3053,17 +3072,18 @@ class Regex(Token): raise ValueError(f"invalid pattern ({self.pattern!r}) passed to Regex") @cached_property - def re_match(self): + def re_match(self) -> Callable[[str], Any]: return self.re.match @cached_property - def mayReturnEmpty(self): + def mayReturnEmpty(self) -> bool: return self.re_match("") is not None def _generateDefaultName(self) -> str: - return "Re:({})".format(repr(self.pattern).replace("\\\\", "\\")) + unescaped = self.pattern.replace("\\\\", "\\") + return f"Re:({unescaped!r})" - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: result = self.re_match(instring, loc) if not result: raise ParseException(instring, loc, self.errmsg, self) @@ -3077,7 +3097,7 @@ class Regex(Token): return loc, ret - def parseImplAsGroupList(self, instring, loc, doActions=True): + def parseImplAsGroupList(self, instring, loc, do_actions=True): result = self.re_match(instring, loc) if not result: raise ParseException(instring, loc, self.errmsg, self) @@ -3086,7 +3106,7 @@ class Regex(Token): ret = result.groups() return loc, ret - def parseImplAsMatch(self, instring, loc, doActions=True): + def parseImplAsMatch(self, instring, loc, do_actions=True): result = self.re_match(instring, loc) if not result: raise ParseException(instring, loc, self.errmsg, self) @@ -3223,7 +3243,7 @@ class QuotedString(Token): # fmt: off # build up re pattern for the content between the quote delimiters - inner_pattern = [] + inner_pattern: List[str] = [] if esc_quote: inner_pattern.append(rf"(?:{re.escape(esc_quote)})") @@ -3245,12 +3265,12 @@ class QuotedString(Token): self.re_flags |= re.MULTILINE | re.DOTALL inner_pattern.append( rf"(?:[^{_escape_regex_range_chars(self.end_quote_char[0])}" - rf"{(_escape_regex_range_chars(esc_char) if self.has_esc_char else '')}])" + rf"{(_escape_regex_range_chars(self.esc_char) if self.has_esc_char else '')}])" ) else: inner_pattern.append( rf"(?:[^{_escape_regex_range_chars(self.end_quote_char[0])}\n\r" - rf"{(_escape_regex_range_chars(esc_char) if self.has_esc_char else '')}])" + rf"{(_escape_regex_range_chars(self.esc_char) if self.has_esc_char else '')}])" ) self.pattern = "".join( @@ -3298,7 +3318,7 @@ class QuotedString(Token): return f"quoted string, starting with {self.quote_char} ending with {self.end_quote_char}" - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: # check first character of opening quote to see if that is a match # before doing the more complicated regex match result = ( @@ -3412,7 +3432,7 @@ class CharsNotIn(Token): else: return f"!W:({self.notChars})" - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: notchars = self.notCharsSet if instring[loc] in notchars: raise ParseException(instring, loc, self.errmsg, self) @@ -3490,7 +3510,7 @@ class White(Token): def _generateDefaultName(self) -> str: return "".join(White.whiteStrs[c] for c in self.matchWhite) - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if instring[loc] not in self.matchWhite: raise ParseException(instring, loc, self.errmsg, self) start = loc @@ -3538,7 +3558,7 @@ class GoToColumn(PositionToken): return loc - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: thiscol = col(loc, instring) if thiscol > self.col: raise ParseException(instring, loc, "Text not in expected column", self) @@ -3576,7 +3596,7 @@ class LineStart(PositionToken): self.orig_whiteChars = set() | self.whiteChars self.whiteChars.discard("\n") self.skipper = Empty().set_whitespace_chars(self.whiteChars) - self.errmsg = "Expected start of line" + self.set_name("start of line") def preParse(self, instring: str, loc: int) -> int: if loc == 0: @@ -3590,7 +3610,7 @@ class LineStart(PositionToken): return ret - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if col(loc, instring) == 1: return loc, [] raise ParseException(instring, loc, self.errmsg, self) @@ -3605,9 +3625,9 @@ class LineEnd(PositionToken): super().__init__() self.whiteChars.discard("\n") self.set_whitespace_chars(self.whiteChars, copy_defaults=False) - self.errmsg = "Expected end of line" + self.set_name("end of line") - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if loc < len(instring): if instring[loc] == "\n": return loc + 1, "\n" @@ -3626,9 +3646,9 @@ class StringStart(PositionToken): def __init__(self): super().__init__() - self.errmsg = "Expected start of text" + self.set_name("start of text") - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: # see if entire string up to here is just whitespace and ignoreables if loc != 0 and loc != self.preParse(instring, 0): raise ParseException(instring, loc, self.errmsg, self) @@ -3643,9 +3663,9 @@ class StringEnd(PositionToken): def __init__(self): super().__init__() - self.errmsg = "Expected end of text" + self.set_name("end of text") - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if loc < len(instring): raise ParseException(instring, loc, self.errmsg, self) if loc == len(instring): @@ -3670,9 +3690,9 @@ class WordStart(PositionToken): wordChars = word_chars if wordChars == printables else wordChars super().__init__() self.wordChars = set(wordChars) - self.errmsg = "Not at the start of a word" + self.set_name("start of a word") - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if loc != 0: if ( instring[loc - 1] in self.wordChars @@ -3696,9 +3716,9 @@ class WordEnd(PositionToken): super().__init__() self.wordChars = set(wordChars) self.skipWhitespace = False - self.errmsg = "Not at the end of a word" + self.set_name("end of a word") - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: instrlen = len(instring) if instrlen > 0 and loc < instrlen: if ( @@ -3709,6 +3729,47 @@ class WordEnd(PositionToken): return loc, [] +class Tag(Token): + """ + A meta-element for inserting a named result into the parsed + tokens that may be checked later in a parse action or while + processing the parsed results. Accepts an optional tag value, + defaulting to `True`. + + Example:: + + end_punc = "." | ("!" + Tag("enthusiastic"))) + greeting = "Hello," + Word(alphas) + end_punc + + result = greeting.parse_string("Hello, World.") + print(result.dump()) + + result = greeting.parse_string("Hello, World!") + print(result.dump()) + + prints:: + + ['Hello,', 'World', '.'] + + ['Hello,', 'World', '!'] + - enthusiastic: True + """ + def __init__(self, tag_name: str, value: Any = True): + super().__init__() + self.mayReturnEmpty = True + self.mayIndexError = False + self.leave_whitespace() + self.tag_name = tag_name + self.tag_value = value + self.add_parse_action(self._add_tag) + + def _add_tag(self, tokens: ParseResults): + tokens[self.tag_name] = self.tag_value + + def _generateDefaultName(self) -> str: + return f"{type(self).__name__}:{self.tag_name}={self.tag_value!r}" + + class ParseExpression(ParserElement): """Abstract subclass of ParserElement, for combining and post-processing parsed tokens. @@ -3846,13 +3907,13 @@ class ParseExpression(ParserElement): ret.exprs = [e.copy() for e in self.exprs] return ret - def _setResultsName(self, name, listAllMatches=False): + def _setResultsName(self, name, list_all_matches=False) -> ParserElement: if not ( __diag__.warn_ungrouped_named_tokens_in_collection and Diagnostics.warn_ungrouped_named_tokens_in_collection not in self.suppress_warnings_ ): - return super()._setResultsName(name, listAllMatches) + return super()._setResultsName(name, list_all_matches) for e in self.exprs: if ( @@ -3871,7 +3932,7 @@ class ParseExpression(ParserElement): warnings.warn(warning, stacklevel=3) break - return super()._setResultsName(name, listAllMatches) + return super()._setResultsName(name, list_all_matches) # Compatibility synonyms # fmt: off @@ -3911,7 +3972,7 @@ class And(ParseExpression): ): exprs: List[ParserElement] = list(exprs_arg) if exprs and Ellipsis in exprs: - tmp = [] + tmp: List[ParserElement] = [] for i, expr in enumerate(exprs): if expr is not Ellipsis: tmp.append(expr) @@ -3991,11 +4052,11 @@ class And(ParseExpression): self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs) return self - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True): # pass False as callPreParse arg to _parse for first element, since we already # pre-parsed the string as part of our And pre-parsing loc, resultlist = self.exprs[0]._parse( - instring, loc, doActions, callPreParse=False + instring, loc, do_actions, callPreParse=False ) errorStop = False for e in self.exprs[1:]: @@ -4005,7 +4066,7 @@ class And(ParseExpression): continue if errorStop: try: - loc, exprtokens = e._parse(instring, loc, doActions) + loc, exprtokens = e._parse(instring, loc, do_actions) except ParseSyntaxException: raise except ParseBaseException as pe: @@ -4016,7 +4077,7 @@ class And(ParseExpression): instring, len(instring), self.errmsg, self ) else: - loc, exprtokens = e._parse(instring, loc, doActions) + loc, exprtokens = e._parse(instring, loc, do_actions) resultlist += exprtokens return loc, resultlist @@ -4080,11 +4141,11 @@ class Or(ParseExpression): self.saveAsList = False return self - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: maxExcLoc = -1 maxException = None - matches = [] - fatals = [] + matches: List[Tuple[int, ParserElement]] = [] + fatals: List[ParseFatalException] = [] if all(e.callPreparse for e in self.exprs): loc = self.preParse(instring, loc) for e in self.exprs: @@ -4117,11 +4178,11 @@ class Or(ParseExpression): # might change whether or how much they match of the input. matches.sort(key=itemgetter(0), reverse=True) - if not doActions: + if not do_actions: # no further conditions or parse actions to change the selection of # alternative, so the first match will be the best match best_expr = matches[0][1] - return best_expr._parse(instring, loc, doActions) + return best_expr._parse(instring, loc, do_actions) longest = -1, None for loc1, expr1 in matches: @@ -4130,7 +4191,7 @@ class Or(ParseExpression): return longest try: - loc2, toks = expr1._parse(instring, loc, doActions) + loc2, toks = expr1._parse(instring, loc, do_actions) except ParseException as err: err.__traceback__ = None if err.loc > maxExcLoc: @@ -4173,7 +4234,7 @@ class Or(ParseExpression): def _generateDefaultName(self) -> str: return f"{{{' ^ '.join(str(e) for e in self.exprs)}}}" - def _setResultsName(self, name, listAllMatches=False): + def _setResultsName(self, name, list_all_matches=False) -> ParserElement: if ( __diag__.warn_multiple_tokens_in_named_alternation and Diagnostics.warn_multiple_tokens_in_named_alternation @@ -4194,7 +4255,7 @@ class Or(ParseExpression): ) warnings.warn(warning, stacklevel=3) - return super()._setResultsName(name, listAllMatches) + return super()._setResultsName(name, list_all_matches) class MatchFirst(ParseExpression): @@ -4239,13 +4300,13 @@ class MatchFirst(ParseExpression): self.mayReturnEmpty = True return self - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: maxExcLoc = -1 maxException = None for e in self.exprs: try: - return e._parse(instring, loc, doActions) + return e._parse(instring, loc, do_actions) except ParseFatalException as pfe: pfe.__traceback__ = None pfe.parser_element = e @@ -4280,7 +4341,7 @@ class MatchFirst(ParseExpression): def _generateDefaultName(self) -> str: return f"{{{' | '.join(str(e) for e in self.exprs)}}}" - def _setResultsName(self, name, listAllMatches=False): + def _setResultsName(self, name, list_all_matches=False) -> ParserElement: if ( __diag__.warn_multiple_tokens_in_named_alternation and Diagnostics.warn_multiple_tokens_in_named_alternation @@ -4301,7 +4362,7 @@ class MatchFirst(ParseExpression): ) warnings.warn(warning, stacklevel=3) - return super()._setResultsName(name, listAllMatches) + return super()._setResultsName(name, list_all_matches) class Each(ParseExpression): @@ -4387,7 +4448,7 @@ class Each(ParseExpression): self.mayReturnEmpty = True return self - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if self.initExprGroups: self.opt1map = dict( (id(e.expr), e) for e in self.exprs if isinstance(e, Opt) @@ -4419,11 +4480,11 @@ class Each(ParseExpression): tmpReqd = self.required[:] tmpOpt = self.optionals[:] multis = self.multioptionals[:] - matchOrder = [] + matchOrder: List[ParserElement] = [] keepMatching = True - failed = [] - fatals = [] + failed: List[ParserElement] = [] + fatals: List[ParseFatalException] = [] while keepMatching: tmpExprs = tmpReqd + tmpOpt + multis failed.clear() @@ -4469,7 +4530,7 @@ class Each(ParseExpression): total_results = ParseResults([]) for e in matchOrder: - loc, results = e._parse(instring, loc, doActions) + loc, results = e._parse(instring, loc, do_actions) total_results += results return loc, total_results @@ -4509,12 +4570,14 @@ class ParseElementEnhance(ParserElement): def recurse(self) -> List[ParserElement]: return [self.expr] if self.expr is not None else [] - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True): if self.expr is None: raise ParseException(instring, loc, "No expression defined", self) try: - return self.expr._parse(instring, loc, doActions, callPreParse=False) + return self.expr._parse(instring, loc, do_actions, callPreParse=False) + except ParseSyntaxException: + raise except ParseBaseException as pbe: if not isinstance(self, Forward) or self.customName is not None: if self.errmsg: @@ -4611,14 +4674,14 @@ class IndentedBlock(ParseElementEnhance): self._grouped = grouped self.parent_anchor = 1 - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: # advance parse position to non-whitespace by using an Empty() # this should be the column to be used for all subsequent indented lines anchor_loc = Empty().preParse(instring, loc) # see if self.expr matches at the current location - if not it will raise an exception # and no further work is necessary - self.expr.try_parse(instring, anchor_loc, do_actions=doActions) + self.expr.try_parse(instring, anchor_loc, do_actions=do_actions) indent_col = col(anchor_loc, instring) peer_detect_expr = self._Indent(indent_col) @@ -4643,7 +4706,7 @@ class IndentedBlock(ParseElementEnhance): else: wrapper = lambda expr: expr return (wrapper(block) + Optional(trailing_undent)).parseImpl( - instring, anchor_loc, doActions + instring, anchor_loc, do_actions ) @@ -4662,10 +4725,10 @@ class AtStringStart(ParseElementEnhance): super().__init__(expr) self.callPreparse = False - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if loc != 0: raise ParseException(instring, loc, "not found at string start") - return super().parseImpl(instring, loc, doActions) + return super().parseImpl(instring, loc, do_actions) class AtLineStart(ParseElementEnhance): @@ -4695,10 +4758,10 @@ class AtLineStart(ParseElementEnhance): super().__init__(expr) self.callPreparse = False - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if col(loc, instring) != 1: raise ParseException(instring, loc, "not found at line start") - return super().parseImpl(instring, loc, doActions) + return super().parseImpl(instring, loc, do_actions) class FollowedBy(ParseElementEnhance): @@ -4728,10 +4791,10 @@ class FollowedBy(ParseElementEnhance): super().__init__(expr) self.mayReturnEmpty = True - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: # by using self._expr.parse and deleting the contents of the returned ParseResults list # we keep any named results that were defined in the FollowedBy expression - _, ret = self.expr._parse(instring, loc, doActions=doActions) + _, ret = self.expr._parse(instring, loc, do_actions=do_actions) del ret[:] return loc, ret @@ -4793,7 +4856,7 @@ class PrecededBy(ParseElementEnhance): self.skipWhitespace = False self.parseAction.append(lambda s, l, t: t.__delitem__(slice(None, None))) - def parseImpl(self, instring, loc=0, doActions=True): + def parseImpl(self, instring, loc=0, do_actions=True) -> ParseImplReturnType: if self.exact: if loc < self.retreat: raise ParseException(instring, loc, self.errmsg) @@ -4848,9 +4911,9 @@ class Located(ParseElementEnhance): """ - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: start = loc - loc, tokens = self.expr._parse(instring, start, doActions, callPreParse=False) + loc, tokens = self.expr._parse(instring, start, do_actions, callPreParse=False) ret_tokens = ParseResults([start, tokens, loc]) ret_tokens["locn_start"] = start ret_tokens["value"] = tokens @@ -4896,8 +4959,8 @@ class NotAny(ParseElementEnhance): self.mayReturnEmpty = True self.errmsg = f"Found unwanted token, {self.expr}" - def parseImpl(self, instring, loc, doActions=True): - if self.expr.can_parse_next(instring, loc, do_actions=doActions): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: + if self.expr.can_parse_next(instring, loc, do_actions=do_actions): raise ParseException(instring, loc, self.errmsg, self) return loc, [] @@ -4927,7 +4990,7 @@ class _MultipleMatch(ParseElementEnhance): self.not_ender = ~ender if ender is not None else None return self - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: self_expr_parse = self.expr._parse self_skip_ignorables = self._skipIgnorables check_ender = self.not_ender is not None @@ -4938,7 +5001,7 @@ class _MultipleMatch(ParseElementEnhance): # if so, fail) if check_ender: try_not_ender(instring, loc) - loc, tokens = self_expr_parse(instring, loc, doActions) + loc, tokens = self_expr_parse(instring, loc, do_actions) try: hasIgnoreExprs = not not self.ignoreExprs while 1: @@ -4948,14 +5011,14 @@ class _MultipleMatch(ParseElementEnhance): preloc = self_skip_ignorables(instring, loc) else: preloc = loc - loc, tmptokens = self_expr_parse(instring, preloc, doActions) + loc, tmptokens = self_expr_parse(instring, preloc, do_actions) tokens += tmptokens except (ParseException, IndexError): pass return loc, tokens - def _setResultsName(self, name, listAllMatches=False): + def _setResultsName(self, name, list_all_matches=False) -> ParserElement: if ( __diag__.warn_ungrouped_named_tokens_in_collection and Diagnostics.warn_ungrouped_named_tokens_in_collection @@ -4978,7 +5041,7 @@ class _MultipleMatch(ParseElementEnhance): warnings.warn(warning, stacklevel=3) break - return super()._setResultsName(name, listAllMatches) + return super()._setResultsName(name, list_all_matches) class OneOrMore(_MultipleMatch): @@ -5037,9 +5100,9 @@ class ZeroOrMore(_MultipleMatch): super().__init__(expr, stopOn=stopOn or stop_on) self.mayReturnEmpty = True - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: try: - return super().parseImpl(instring, loc, doActions) + return super().parseImpl(instring, loc, do_actions) except (ParseException, IndexError): return loc, ParseResults([], name=self.resultsName) @@ -5170,10 +5233,10 @@ class Opt(ParseElementEnhance): self.defaultValue = default self.mayReturnEmpty = True - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: self_expr = self.expr try: - loc, tokens = self_expr._parse(instring, loc, doActions, callPreParse=False) + loc, tokens = self_expr._parse(instring, loc, do_actions, callPreParse=False) except (ParseException, IndexError): default_value = self.defaultValue if default_value is not self.__optionalNotMatched: @@ -5279,7 +5342,7 @@ class SkipTo(ParseElementEnhance): self.failOn = self._literalStringClass(failOn) else: self.failOn = failOn - self.errmsg = "No match found for " + str(self.expr) + self.errmsg = f"No match found for {self.expr}" self.ignorer = Empty().leave_whitespace() self._update_ignorer() @@ -5295,7 +5358,7 @@ class SkipTo(ParseElementEnhance): super().ignore(expr) self._update_ignorer() - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True): startloc = loc instrlen = len(instring) self_expr_parse = self.expr._parse @@ -5325,7 +5388,7 @@ class SkipTo(ParseElementEnhance): prev_tmploc = tmploc try: - self_expr_parse(instring, tmploc, doActions=False, callPreParse=False) + self_expr_parse(instring, tmploc, do_actions=False, callPreParse=False) except (ParseException, IndexError): # no match, advance loc in string tmploc += 1 @@ -5343,7 +5406,7 @@ class SkipTo(ParseElementEnhance): skipresult = ParseResults(skiptext) if self.includeMatch: - loc, mat = self_expr_parse(instring, loc, doActions, callPreParse=False) + loc, mat = self_expr_parse(instring, loc, do_actions, callPreParse=False) skipresult += mat return loc, skipresult @@ -5440,7 +5503,7 @@ class Forward(ParseElementEnhance): lineno=self.caller_frame.lineno, ) - def parseImpl(self, instring, loc, doActions=True): + def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType: if ( self.expr is None and __diag__.warn_on_parse_using_empty_Forward @@ -5466,7 +5529,7 @@ class Forward(ParseElementEnhance): stacklevel=stacklevel, ) if not ParserElement._left_recursion_enabled: - return super().parseImpl(instring, loc, doActions) + return super().parseImpl(instring, loc, do_actions) # ## Bounded Recursion algorithm ## # Recursion only needs to be processed at ``Forward`` elements, since they are # the only ones that can actually refer to themselves. The general idea is @@ -5484,13 +5547,13 @@ class Forward(ParseElementEnhance): # # There is a complication since we not only *parse* but also *transform* via # actions: We do not want to run the actions too often while expanding. Thus, - # we expand using `doActions=False` and only run `doActions=True` if the next + # we expand using `do_actions=False` and only run `do_actions=True` if the next # recursion level is acceptable. with ParserElement.recursion_lock: memo = ParserElement.recursion_memos try: # we are parsing at a specific recursion expansion - use it as-is - prev_loc, prev_result = memo[loc, self, doActions] + prev_loc, prev_result = memo[loc, self, do_actions] if isinstance(prev_result, Exception): raise prev_result return prev_loc, prev_result.copy() @@ -5498,14 +5561,14 @@ class Forward(ParseElementEnhance): act_key = (loc, self, True) peek_key = (loc, self, False) # we are searching for the best recursion expansion - keep on improving - # both `doActions` cases must be tracked separately here! + # both `do_actions` cases must be tracked separately here! prev_loc, prev_peek = memo[peek_key] = ( loc - 1, ParseException( instring, loc, "Forward recursion without base case", self ), ) - if doActions: + if do_actions: memo[act_key] = memo[peek_key] while True: try: @@ -5517,8 +5580,8 @@ class Forward(ParseElementEnhance): new_loc, new_peek = prev_loc, prev_peek # the match did not get better: we are done if new_loc <= prev_loc: - if doActions: - # replace the match for doActions=False as well, + if do_actions: + # replace the match for do_actions=False as well, # in case the action did backtrack prev_loc, prev_result = memo[peek_key] = memo[act_key] del memo[peek_key], memo[act_key] @@ -5526,7 +5589,7 @@ class Forward(ParseElementEnhance): del memo[peek_key] return prev_loc, prev_peek.copy() # the match did get better: see if we can improve further - if doActions: + if do_actions: try: memo[act_key] = super().parseImpl(instring, loc, True) except ParseException as e: @@ -5586,7 +5649,7 @@ class Forward(ParseElementEnhance): ret <<= self return ret - def _setResultsName(self, name, list_all_matches=False): + def _setResultsName(self, name, list_all_matches=False) -> ParserElement: # fmt: off if ( __diag__.warn_name_set_on_empty_Forward @@ -5892,7 +5955,9 @@ def trace_parse_action(f: ParseAction) -> ParseAction: try: ret = f(*paArgs) except Exception as exc: - sys.stderr.write(f"< {% endif %} + {{ body | safe }} {% for diagram in diagrams %}
      @@ -89,7 +90,7 @@ class AnnotatedItem(railroad.Group): """ def __init__(self, label: str, item): - super().__init__(item=item, label="[{}]".format(label) if label else label) + super().__init__(item=item, label=f"[{label}]") class EditablePartial(Generic[T]): @@ -145,7 +146,7 @@ def railroad_to_html(diagrams: List[NamedDiagram], embed=False, **kwargs) -> str continue io = StringIO() try: - css = kwargs.get('css') + css = kwargs.get("css") diagram.diagram.writeStandalone(io.write, css=css) except AttributeError: diagram.diagram.writeSvg(io.write) @@ -425,9 +426,11 @@ def _apply_diagram_item_enhancements(fn): element_results_name = element.resultsName if element_results_name: # add "*" to indicate if this is a "list all results" name - element_results_name += "" if element.modalResults else "*" + modal_tag = "" if element.modalResults else "*" ret = EditablePartial.from_call( - railroad.Group, item=ret, label=element_results_name + railroad.Group, + item=ret, + label=f"{repr(element_results_name)}{modal_tag}", ) return ret @@ -534,7 +537,7 @@ def _to_diagram_element( # (all will have the same name, and resultsName) if not exprs: return None - if len(set((e.name, e.resultsName) for e in exprs)) == 1: + if len(set((e.name, e.resultsName) for e in exprs)) == 1 and len(exprs) > 2: ret = EditablePartial.from_call( railroad.OneOrMore, item="", repeat=str(len(exprs)) ) @@ -563,7 +566,7 @@ def _to_diagram_element( if show_groups: ret = EditablePartial.from_call(AnnotatedItem, label="", item="") else: - ret = EditablePartial.from_call(railroad.Group, label="", item="") + ret = EditablePartial.from_call(railroad.Sequence, items=[]) elif isinstance(element, pyparsing.TokenConverter): label = type(element).__name__.lower() if label == "tokenconverter": @@ -573,8 +576,36 @@ def _to_diagram_element( elif isinstance(element, pyparsing.Opt): ret = EditablePartial.from_call(railroad.Optional, item="") elif isinstance(element, pyparsing.OneOrMore): - ret = EditablePartial.from_call(railroad.OneOrMore, item="") + if element.not_ender is not None: + args = [ + parent, + lookup, + vertical, + index, + name_hint, + show_results_names, + show_groups, + ] + return _to_diagram_element( + (~element.not_ender.expr + element.expr)[1, ...].set_name(element.name), + *args, + ) + ret = EditablePartial.from_call(railroad.OneOrMore, item=None) elif isinstance(element, pyparsing.ZeroOrMore): + if element.not_ender is not None: + args = [ + parent, + lookup, + vertical, + index, + name_hint, + show_results_names, + show_groups, + ] + return _to_diagram_element( + (~element.not_ender.expr + element.expr)[...].set_name(element.name), + *args, + ) ret = EditablePartial.from_call(railroad.ZeroOrMore, item="") elif isinstance(element, pyparsing.Group): ret = EditablePartial.from_call( diff --git a/lib/pyparsing/exceptions.py b/lib/pyparsing/exceptions.py index 1aaea56f..8db34f19 100644 --- a/lib/pyparsing/exceptions.py +++ b/lib/pyparsing/exceptions.py @@ -85,7 +85,7 @@ class ParseBaseException(Exception): ret = [] if isinstance(exc, ParseBaseException): ret.append(exc.line) - ret.append(" " * (exc.column - 1) + "^") + ret.append(f"{' ' * (exc.column - 1)}^") ret.append(f"{type(exc).__name__}: {exc}") if depth <= 0: @@ -245,6 +245,7 @@ class ParseBaseException(Exception): """ return self.explain_exception(self, depth) + # Compatibility synonyms # fmt: off markInputline = replaced_by_pep8("markInputline", mark_input_line) # fmt: on diff --git a/lib/pyparsing/helpers.py b/lib/pyparsing/helpers.py index dcfdb8fe..d5d14a08 100644 --- a/lib/pyparsing/helpers.py +++ b/lib/pyparsing/helpers.py @@ -782,9 +782,12 @@ def infix_notation( # if lpar and rpar are not suppressed, wrap in group if not (isinstance(lpar, Suppress) and isinstance(rpar, Suppress)): - lastExpr = base_expr | Group(lpar + ret + rpar) + lastExpr = base_expr | Group(lpar + ret + rpar).set_name( + f"nested_{base_expr.name}" + ) else: - lastExpr = base_expr | (lpar + ret + rpar) + lastExpr = base_expr | (lpar + ret + rpar).set_name(f"nested_{base_expr.name}") + root_expr = lastExpr arity: int rightLeftAssoc: opAssoc @@ -855,6 +858,7 @@ def infix_notation( thisExpr <<= (matchExpr | lastExpr).setName(term_name) lastExpr = thisExpr ret <<= lastExpr + root_expr.set_name("base_expr") return ret @@ -1049,7 +1053,7 @@ def delimited_list( ) -# pre-PEP8 compatible names +# Compatibility synonyms # fmt: off opAssoc = OpAssoc anyOpenTag = any_open_tag diff --git a/lib/pyparsing/results.py b/lib/pyparsing/results.py index 3e5fe208..3bb7c948 100644 --- a/lib/pyparsing/results.py +++ b/lib/pyparsing/results.py @@ -4,12 +4,14 @@ from collections.abc import ( Mapping, MutableSequence, Iterator, - Sequence, - Container, + Iterable, ) import pprint from typing import Tuple, Any, Dict, Set, List +from .util import replaced_by_pep8 + + str_type: Tuple[type, ...] = (str, bytes) _generator_type = type((_ for _ in ())) @@ -573,20 +575,20 @@ class ParseResults: # replace values with copies if they are of known mutable types for i, obj in enumerate(self._toklist): if isinstance(obj, ParseResults): - self._toklist[i] = obj.deepcopy() + ret._toklist[i] = obj.deepcopy() elif isinstance(obj, (str, bytes)): pass elif isinstance(obj, MutableMapping): - self._toklist[i] = dest = type(obj)() + ret._toklist[i] = dest = type(obj)() for k, v in obj.items(): dest[k] = v.deepcopy() if isinstance(v, ParseResults) else v - elif isinstance(obj, Container): - self._toklist[i] = type(obj)( + elif isinstance(obj, Iterable): + ret._toklist[i] = type(obj)( v.deepcopy() if isinstance(v, ParseResults) else v for v in obj ) return ret - def get_name(self): + def get_name(self) -> str: r""" Returns the results name for this token expression. Useful when several different expressions might match at a particular location. diff --git a/lib/pyparsing/unicode.py b/lib/pyparsing/unicode.py index 426b8b23..0e3e0657 100644 --- a/lib/pyparsing/unicode.py +++ b/lib/pyparsing/unicode.py @@ -53,51 +53,51 @@ class unicode_set: _ranges: UnicodeRangeList = [] @_lazyclassproperty - def _chars_for_ranges(cls): - ret = [] + def _chars_for_ranges(cls) -> List[str]: + ret: List[int] = [] for cc in cls.__mro__: if cc is unicode_set: break for rr in getattr(cc, "_ranges", ()): ret.extend(range(rr[0], rr[-1] + 1)) - return [chr(c) for c in sorted(set(ret))] + return sorted(chr(c) for c in set(ret)) @_lazyclassproperty - def printables(cls): + def printables(cls) -> str: """all non-whitespace characters in this range""" return "".join(filterfalse(str.isspace, cls._chars_for_ranges)) @_lazyclassproperty - def alphas(cls): + def alphas(cls) -> str: """all alphabetic characters in this range""" return "".join(filter(str.isalpha, cls._chars_for_ranges)) @_lazyclassproperty - def nums(cls): + def nums(cls) -> str: """all numeric digit characters in this range""" return "".join(filter(str.isdigit, cls._chars_for_ranges)) @_lazyclassproperty - def alphanums(cls): + def alphanums(cls) -> str: """all alphanumeric characters in this range""" return cls.alphas + cls.nums @_lazyclassproperty - def identchars(cls): + def identchars(cls) -> str: """all characters in this range that are valid identifier characters, plus underscore '_'""" return "".join( sorted( - set( - "".join(filter(str.isidentifier, cls._chars_for_ranges)) - + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzªµº" - + "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ" - + "_" + set(filter(str.isidentifier, cls._chars_for_ranges)) + | set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzªµº" + "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ" + "_" ) ) ) @_lazyclassproperty - def identbodychars(cls): + def identbodychars(cls) -> str: """ all characters in this range that are valid identifier body characters, plus the digits 0-9, and · (Unicode MIDDLE DOT) @@ -105,7 +105,9 @@ class unicode_set: identifier_chars = set( c for c in cls._chars_for_ranges if ("_" + c).isidentifier() ) - return "".join(sorted(identifier_chars | set(cls.identchars + "0123456789·"))) + return "".join( + sorted(identifier_chars | set(cls.identchars) | set("0123456789·")) + ) @_lazyclassproperty def identifier(cls): diff --git a/lib/pyparsing/util.py b/lib/pyparsing/util.py index 4ae018a9..94837fea 100644 --- a/lib/pyparsing/util.py +++ b/lib/pyparsing/util.py @@ -246,7 +246,7 @@ def replaced_by_pep8(compat_name: str, fn: C) -> C: # (Presence of 'self' arg in signature is used by explain_exception() methods, so we take # some extra steps to add it if present in decorated function.) - if "self" == list(inspect.signature(fn).parameters)[0]: + if ["self"] == list(inspect.signature(fn).parameters)[:1]: @wraps(fn) def _inner(self, *args, **kwargs): diff --git a/lib/pytz/__init__.py b/lib/pytz/__init__.py index 2355f8b4..96409e2d 100644 --- a/lib/pytz/__init__.py +++ b/lib/pytz/__init__.py @@ -22,8 +22,8 @@ from pytz.tzfile import build_tzinfo # The IANA (nee Olson) database is updated several times a year. -OLSON_VERSION = '2024a' -VERSION = '2024.1' # pip compatible version number. +OLSON_VERSION = '2024b' +VERSION = '2024.2' # pip compatible version number. __version__ = VERSION OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling @@ -1340,7 +1340,6 @@ common_timezones = \ 'Asia/Bishkek', 'Asia/Brunei', 'Asia/Chita', - 'Asia/Choibalsan', 'Asia/Colombo', 'Asia/Damascus', 'Asia/Dhaka', diff --git a/lib/pytz/zoneinfo/Africa/Blantyre b/lib/pytz/zoneinfo/Africa/Blantyre index 52753c0f..65f043f1 100644 Binary files a/lib/pytz/zoneinfo/Africa/Blantyre and b/lib/pytz/zoneinfo/Africa/Blantyre differ diff --git a/lib/pytz/zoneinfo/Africa/Bujumbura b/lib/pytz/zoneinfo/Africa/Bujumbura index 52753c0f..65f043f1 100644 Binary files a/lib/pytz/zoneinfo/Africa/Bujumbura and b/lib/pytz/zoneinfo/Africa/Bujumbura differ diff --git a/lib/pytz/zoneinfo/Africa/Gaborone b/lib/pytz/zoneinfo/Africa/Gaborone index 52753c0f..65f043f1 100644 Binary files a/lib/pytz/zoneinfo/Africa/Gaborone and b/lib/pytz/zoneinfo/Africa/Gaborone differ diff --git a/lib/pytz/zoneinfo/Africa/Harare b/lib/pytz/zoneinfo/Africa/Harare index 52753c0f..65f043f1 100644 Binary files a/lib/pytz/zoneinfo/Africa/Harare and b/lib/pytz/zoneinfo/Africa/Harare differ diff --git a/lib/pytz/zoneinfo/Africa/Kigali b/lib/pytz/zoneinfo/Africa/Kigali index 52753c0f..65f043f1 100644 Binary files a/lib/pytz/zoneinfo/Africa/Kigali and b/lib/pytz/zoneinfo/Africa/Kigali differ diff --git a/lib/pytz/zoneinfo/Africa/Lubumbashi b/lib/pytz/zoneinfo/Africa/Lubumbashi index 52753c0f..65f043f1 100644 Binary files a/lib/pytz/zoneinfo/Africa/Lubumbashi and b/lib/pytz/zoneinfo/Africa/Lubumbashi differ diff --git a/lib/pytz/zoneinfo/Africa/Lusaka b/lib/pytz/zoneinfo/Africa/Lusaka index 52753c0f..65f043f1 100644 Binary files a/lib/pytz/zoneinfo/Africa/Lusaka and b/lib/pytz/zoneinfo/Africa/Lusaka differ diff --git a/lib/pytz/zoneinfo/Africa/Maputo b/lib/pytz/zoneinfo/Africa/Maputo index 52753c0f..65f043f1 100644 Binary files a/lib/pytz/zoneinfo/Africa/Maputo and b/lib/pytz/zoneinfo/Africa/Maputo differ diff --git a/lib/pytz/zoneinfo/America/Bahia_Banderas b/lib/pytz/zoneinfo/America/Bahia_Banderas index ae4a8a75..3a493e3d 100644 Binary files a/lib/pytz/zoneinfo/America/Bahia_Banderas and b/lib/pytz/zoneinfo/America/Bahia_Banderas differ diff --git a/lib/pytz/zoneinfo/America/Cancun b/lib/pytz/zoneinfo/America/Cancun index e7acbff1..0f7771e8 100644 Binary files a/lib/pytz/zoneinfo/America/Cancun and b/lib/pytz/zoneinfo/America/Cancun differ diff --git a/lib/pytz/zoneinfo/America/Chihuahua b/lib/pytz/zoneinfo/America/Chihuahua index e0910396..667a2191 100644 Binary files a/lib/pytz/zoneinfo/America/Chihuahua and b/lib/pytz/zoneinfo/America/Chihuahua differ diff --git a/lib/pytz/zoneinfo/America/Ciudad_Juarez b/lib/pytz/zoneinfo/America/Ciudad_Juarez index eb1e5396..29af5982 100644 Binary files a/lib/pytz/zoneinfo/America/Ciudad_Juarez and b/lib/pytz/zoneinfo/America/Ciudad_Juarez differ diff --git a/lib/pytz/zoneinfo/America/Ensenada b/lib/pytz/zoneinfo/America/Ensenada index 63dfdf48..0fe73912 100644 Binary files a/lib/pytz/zoneinfo/America/Ensenada and b/lib/pytz/zoneinfo/America/Ensenada differ diff --git a/lib/pytz/zoneinfo/America/Hermosillo b/lib/pytz/zoneinfo/America/Hermosillo index 86bd1a20..441fe3f2 100644 Binary files a/lib/pytz/zoneinfo/America/Hermosillo and b/lib/pytz/zoneinfo/America/Hermosillo differ diff --git a/lib/pytz/zoneinfo/America/Mazatlan b/lib/pytz/zoneinfo/America/Mazatlan index 06fa2274..386616a5 100644 Binary files a/lib/pytz/zoneinfo/America/Mazatlan and b/lib/pytz/zoneinfo/America/Mazatlan differ diff --git a/lib/pytz/zoneinfo/America/Merida b/lib/pytz/zoneinfo/America/Merida index 17654cb5..c4b9b4e8 100644 Binary files a/lib/pytz/zoneinfo/America/Merida and b/lib/pytz/zoneinfo/America/Merida differ diff --git a/lib/pytz/zoneinfo/America/Mexico_City b/lib/pytz/zoneinfo/America/Mexico_City index 68176daa..ad70cf3e 100644 Binary files a/lib/pytz/zoneinfo/America/Mexico_City and b/lib/pytz/zoneinfo/America/Mexico_City differ diff --git a/lib/pytz/zoneinfo/America/Monterrey b/lib/pytz/zoneinfo/America/Monterrey index 5eb723c8..2d7993a0 100644 Binary files a/lib/pytz/zoneinfo/America/Monterrey and b/lib/pytz/zoneinfo/America/Monterrey differ diff --git a/lib/pytz/zoneinfo/America/Ojinaga b/lib/pytz/zoneinfo/America/Ojinaga index f97946d1..45118a4f 100644 Binary files a/lib/pytz/zoneinfo/America/Ojinaga and b/lib/pytz/zoneinfo/America/Ojinaga differ diff --git a/lib/pytz/zoneinfo/America/Santa_Isabel b/lib/pytz/zoneinfo/America/Santa_Isabel index 63dfdf48..0fe73912 100644 Binary files a/lib/pytz/zoneinfo/America/Santa_Isabel and b/lib/pytz/zoneinfo/America/Santa_Isabel differ diff --git a/lib/pytz/zoneinfo/America/Tijuana b/lib/pytz/zoneinfo/America/Tijuana index 63dfdf48..0fe73912 100644 Binary files a/lib/pytz/zoneinfo/America/Tijuana and b/lib/pytz/zoneinfo/America/Tijuana differ diff --git a/lib/pytz/zoneinfo/Asia/Choibalsan b/lib/pytz/zoneinfo/Asia/Choibalsan index c5f4bb0b..2aa5cc4b 100644 Binary files a/lib/pytz/zoneinfo/Asia/Choibalsan and b/lib/pytz/zoneinfo/Asia/Choibalsan differ diff --git a/lib/pytz/zoneinfo/Asia/Dili b/lib/pytz/zoneinfo/Asia/Dili index c1af113a..4614e4fc 100644 Binary files a/lib/pytz/zoneinfo/Asia/Dili and b/lib/pytz/zoneinfo/Asia/Dili differ diff --git a/lib/pytz/zoneinfo/Atlantic/Azores b/lib/pytz/zoneinfo/Atlantic/Azores index 10232ab3..dd2c235b 100644 Binary files a/lib/pytz/zoneinfo/Atlantic/Azores and b/lib/pytz/zoneinfo/Atlantic/Azores differ diff --git a/lib/pytz/zoneinfo/Atlantic/Madeira b/lib/pytz/zoneinfo/Atlantic/Madeira index 7ddcd883..6725a0ff 100644 Binary files a/lib/pytz/zoneinfo/Atlantic/Madeira and b/lib/pytz/zoneinfo/Atlantic/Madeira differ diff --git a/lib/pytz/zoneinfo/CET b/lib/pytz/zoneinfo/CET index 122e9342..40d7124e 100644 Binary files a/lib/pytz/zoneinfo/CET and b/lib/pytz/zoneinfo/CET differ diff --git a/lib/pytz/zoneinfo/CST6CDT b/lib/pytz/zoneinfo/CST6CDT index ca67929f..c6981a06 100644 Binary files a/lib/pytz/zoneinfo/CST6CDT and b/lib/pytz/zoneinfo/CST6CDT differ diff --git a/lib/pytz/zoneinfo/EET b/lib/pytz/zoneinfo/EET index cbdb71dd..9f3a0678 100644 Binary files a/lib/pytz/zoneinfo/EET and b/lib/pytz/zoneinfo/EET differ diff --git a/lib/pytz/zoneinfo/EST b/lib/pytz/zoneinfo/EST index 21ebc00b..9964b9a3 100644 Binary files a/lib/pytz/zoneinfo/EST and b/lib/pytz/zoneinfo/EST differ diff --git a/lib/pytz/zoneinfo/EST5EDT b/lib/pytz/zoneinfo/EST5EDT index 9bce5007..a8b9ab19 100644 Binary files a/lib/pytz/zoneinfo/EST5EDT and b/lib/pytz/zoneinfo/EST5EDT differ diff --git a/lib/pytz/zoneinfo/Europe/Lisbon b/lib/pytz/zoneinfo/Europe/Lisbon index 55f01930..616de167 100644 Binary files a/lib/pytz/zoneinfo/Europe/Lisbon and b/lib/pytz/zoneinfo/Europe/Lisbon differ diff --git a/lib/pytz/zoneinfo/HST b/lib/pytz/zoneinfo/HST index cccd45eb..c7cd0601 100644 Binary files a/lib/pytz/zoneinfo/HST and b/lib/pytz/zoneinfo/HST differ diff --git a/lib/pytz/zoneinfo/MET b/lib/pytz/zoneinfo/MET index 4a826bb1..40d7124e 100644 Binary files a/lib/pytz/zoneinfo/MET and b/lib/pytz/zoneinfo/MET differ diff --git a/lib/pytz/zoneinfo/MST b/lib/pytz/zoneinfo/MST index c93a58ee..ab37e845 100644 Binary files a/lib/pytz/zoneinfo/MST and b/lib/pytz/zoneinfo/MST differ diff --git a/lib/pytz/zoneinfo/MST7MDT b/lib/pytz/zoneinfo/MST7MDT index 4506a6e1..abb2b974 100644 Binary files a/lib/pytz/zoneinfo/MST7MDT and b/lib/pytz/zoneinfo/MST7MDT differ diff --git a/lib/pytz/zoneinfo/Mexico/BajaNorte b/lib/pytz/zoneinfo/Mexico/BajaNorte index 63dfdf48..0fe73912 100644 Binary files a/lib/pytz/zoneinfo/Mexico/BajaNorte and b/lib/pytz/zoneinfo/Mexico/BajaNorte differ diff --git a/lib/pytz/zoneinfo/Mexico/BajaSur b/lib/pytz/zoneinfo/Mexico/BajaSur index 06fa2274..386616a5 100644 Binary files a/lib/pytz/zoneinfo/Mexico/BajaSur and b/lib/pytz/zoneinfo/Mexico/BajaSur differ diff --git a/lib/pytz/zoneinfo/Mexico/General b/lib/pytz/zoneinfo/Mexico/General index 68176daa..ad70cf3e 100644 Binary files a/lib/pytz/zoneinfo/Mexico/General and b/lib/pytz/zoneinfo/Mexico/General differ diff --git a/lib/pytz/zoneinfo/PST8PDT b/lib/pytz/zoneinfo/PST8PDT index 99d246ba..610e7af5 100644 Binary files a/lib/pytz/zoneinfo/PST8PDT and b/lib/pytz/zoneinfo/PST8PDT differ diff --git a/lib/pytz/zoneinfo/Portugal b/lib/pytz/zoneinfo/Portugal index 55f01930..616de167 100644 Binary files a/lib/pytz/zoneinfo/Portugal and b/lib/pytz/zoneinfo/Portugal differ diff --git a/lib/pytz/zoneinfo/WET b/lib/pytz/zoneinfo/WET index c27390b5..616de167 100644 Binary files a/lib/pytz/zoneinfo/WET and b/lib/pytz/zoneinfo/WET differ diff --git a/lib/pytz/zoneinfo/leapseconds b/lib/pytz/zoneinfo/leapseconds index ce150bfe..6c715cb2 100644 --- a/lib/pytz/zoneinfo/leapseconds +++ b/lib/pytz/zoneinfo/leapseconds @@ -69,11 +69,11 @@ Leap 2016 Dec 31 23:59:60 + S # Any additional leap seconds will come after this. # This Expires line is commented out for now, # so that pre-2020a zic implementations do not reject this file. -#Expires 2024 Dec 28 00:00:00 +#Expires 2025 Jun 28 00:00:00 # POSIX timestamps for the data in this file: -#updated 1704708379 (2024-01-08 10:06:19 UTC) -#expires 1735344000 (2024-12-28 00:00:00 UTC) +#updated 1720104763 (2024-07-04 14:52:43 UTC) +#expires 1751068800 (2025-06-28 00:00:00 UTC) # Updated through IERS Bulletin C (https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat) -# File expires on 28 December 2024 +# File expires on 28 June 2025 diff --git a/lib/pytz/zoneinfo/tzdata.zi b/lib/pytz/zoneinfo/tzdata.zi index b5a03be7..b89326a7 100644 --- a/lib/pytz/zoneinfo/tzdata.zi +++ b/lib/pytz/zoneinfo/tzdata.zi @@ -1324,14 +1324,10 @@ R O 1961 1964 - May lastSu 1s 1 S R O 1962 1964 - S lastSu 1s 0 - R p 1916 o - Jun 17 23 1 S R p 1916 o - N 1 1 0 - -R p 1917 o - F 28 23s 1 S -R p 1917 1921 - O 14 23s 0 - -R p 1918 o - Mar 1 23s 1 S -R p 1919 o - F 28 23s 1 S -R p 1920 o - F 29 23s 1 S -R p 1921 o - F 28 23s 1 S +R p 1917 1921 - Mar 1 0 1 S +R p 1917 1921 - O 14 24 0 - R p 1924 o - Ap 16 23s 1 S -R p 1924 o - O 14 23s 0 - +R p 1924 o - O 4 23s 0 - R p 1926 o - Ap 17 23s 1 S R p 1926 1929 - O Sa>=1 23s 0 - R p 1927 o - Ap 9 23s 1 S @@ -1349,8 +1345,9 @@ R p 1938 o - Mar 26 23s 1 S R p 1939 o - Ap 15 23s 1 S R p 1939 o - N 18 23s 0 - R p 1940 o - F 24 23s 1 S -R p 1940 1941 - O 5 23s 0 - +R p 1940 o - O 7 23s 0 - R p 1941 o - Ap 5 23s 1 S +R p 1941 o - O 5 23s 0 - R p 1942 1945 - Mar Sa>=8 23s 1 S R p 1942 o - Ap 25 22s 2 M R p 1942 o - Au 15 22s 1 S @@ -1360,16 +1357,16 @@ R p 1943 1945 - Au Sa>=25 22s 1 S R p 1944 1945 - Ap Sa>=21 22s 2 M R p 1946 o - Ap Sa>=1 23s 1 S R p 1946 o - O Sa>=1 23s 0 - -R p 1947 1965 - Ap Su>=1 2s 1 S +R p 1947 1966 - Ap Su>=1 2s 1 S R p 1947 1965 - O Su>=1 2s 0 - -R p 1977 o - Mar 27 0s 1 S -R p 1977 o - S 25 0s 0 - -R p 1978 1979 - Ap Su>=1 0s 1 S -R p 1978 o - O 1 0s 0 - -R p 1979 1982 - S lastSu 1s 0 - -R p 1980 o - Mar lastSu 0s 1 S -R p 1981 1982 - Mar lastSu 1s 1 S -R p 1983 o - Mar lastSu 2s 1 S +R p 1976 o - S lastSu 1 0 - +R p 1977 o - Mar lastSu 0s 1 S +R p 1977 o - S lastSu 0s 0 - +R p 1978 1980 - Ap Su>=1 1s 1 S +R p 1978 o - O 1 1s 0 - +R p 1979 1980 - S lastSu 1s 0 - +R p 1981 1986 - Mar lastSu 0s 1 S +R p 1981 1985 - S lastSu 0s 0 - R z 1932 o - May 21 0s 1 S R z 1932 1939 - O Su>=1 0s 0 - R z 1933 1939 - Ap Su>=2 0s 1 S @@ -1728,7 +1725,7 @@ R Y 1972 2006 - O lastSu 2 0 S R Y 1987 2006 - Ap Su>=1 2 1 D R Yu 1965 o - Ap lastSu 0 2 DD R Yu 1965 o - O lastSu 2 0 S -R m 1931 o - May 1 23 1 D +R m 1931 o - April 30 0 1 D R m 1931 o - O 1 0 0 S R m 1939 o - F 5 0 1 D R m 1939 o - Jun 25 0 0 S @@ -2096,15 +2093,15 @@ Z Africa/Algiers 0:12:12 - LMT 1891 Mar 16 0 d WE%sT 1981 May 1 - CET Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u --1 - -01 1975 +-1 - %z 1975 0 - GMT Z Africa/Cairo 2:5:9 - LMT 1900 O 2 K EE%sT Z Africa/Casablanca -0:30:20 - LMT 1913 O 26 -0 M +00/+01 1984 Mar 16 -1 - +01 1986 -0 M +00/+01 2018 O 28 3 -1 M +01/+00 +0 M %z 1984 Mar 16 +1 - %z 1986 +0 M %z 2018 O 28 3 +1 M %z Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u 0 - WET 1918 May 6 23 0 1 WEST 1918 O 7 23 @@ -2115,9 +2112,9 @@ Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u 1 - CET 1986 1 E CE%sT Z Africa/El_Aaiun -0:52:48 - LMT 1934 --1 - -01 1976 Ap 14 -0 M +00/+01 2018 O 28 3 -1 M +01/+00 +-1 - %z 1976 Ap 14 +0 M %z 2018 O 28 3 +1 M %z Z Africa/Johannesburg 1:52 - LMT 1892 F 8 1:30 - SAST 1903 Mar 2 SA SAST @@ -2132,19 +2129,19 @@ Z Africa/Khartoum 2:10:8 - LMT 1931 Z Africa/Lagos 0:13:35 - LMT 1905 Jul 0 - GMT 1908 Jul 0:13:35 - LMT 1914 -0:30 - +0030 1919 S +0:30 - %z 1919 S 1 - WAT -Z Africa/Maputo 2:10:20 - LMT 1903 Mar +Z Africa/Maputo 2:10:18 - LMT 1909 2 - CAT Z Africa/Monrovia -0:43:8 - LMT 1882 -0:43:8 - MMT 1919 Mar -0:44:30 - MMT 1972 Ja 7 0 - GMT Z Africa/Nairobi 2:27:16 - LMT 1908 May -2:30 - +0230 1928 Jun 30 24 +2:30 - %z 1928 Jun 30 24 3 - EAT 1930 Ja 4 24 -2:30 - +0230 1936 D 31 24 -2:45 - +0245 1942 Jul 31 24 +2:30 - %z 1936 D 31 24 +2:45 - %z 1942 Jul 31 24 3 - EAT Z Africa/Ndjamena 1:0:12 - LMT 1912 1 - WAT 1979 O 14 @@ -2168,7 +2165,7 @@ Z Africa/Tunis 0:40:44 - LMT 1881 May 12 0:9:21 - PMT 1911 Mar 11 1 n CE%sT Z Africa/Windhoek 1:8:24 - LMT 1892 F 8 -1:30 - +0130 1903 Mar +1:30 - %z 1903 Mar 2 - SAST 1942 S 20 2 2 1 SAST 1943 Mar 21 2 2 - SAST 1990 Mar 21 @@ -2191,167 +2188,166 @@ Z America/Anchorage 14:0:24 - LMT 1867 O 19 14:31:37 -9 u Y%sT 1983 N 30 -9 u AK%sT Z America/Araguaina -3:12:48 - LMT 1914 --3 B -03/-02 1990 S 17 --3 - -03 1995 S 14 --3 B -03/-02 2003 S 24 --3 - -03 2012 O 21 --3 B -03/-02 2013 S --3 - -03 +-3 B %z 1990 S 17 +-3 - %z 1995 S 14 +-3 B %z 2003 S 24 +-3 - %z 2012 O 21 +-3 B %z 2013 S +-3 - %z Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 A -03/-02 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 A %z Z America/Argentina/Catamarca -4:23:8 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar 3 --4 - -04 1991 O 20 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 Jun --4 - -04 2004 Jun 20 --3 A -03/-02 2008 O 18 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar 3 +-4 - %z 1991 O 20 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 Jun +-4 - %z 2004 Jun 20 +-3 A %z 2008 O 18 +-3 - %z Z America/Argentina/Cordoba -4:16:48 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar 3 --4 - -04 1991 O 20 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 A -03/-02 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar 3 +-4 - %z 1991 O 20 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 A %z Z America/Argentina/Jujuy -4:21:12 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1990 Mar 4 --4 - -04 1990 O 28 --4 1 -03 1991 Mar 17 --4 - -04 1991 O 6 --3 1 -02 1992 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 A -03/-02 2008 O 18 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1990 Mar 4 +-4 - %z 1990 O 28 +-4 1 %z 1991 Mar 17 +-4 - %z 1991 O 6 +-3 1 %z 1992 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 A %z 2008 O 18 +-3 - %z Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar --4 - -04 1991 May 7 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 Jun --4 - -04 2004 Jun 20 --3 A -03/-02 2008 O 18 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar +-4 - %z 1991 May 7 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 Jun +-4 - %z 2004 Jun 20 +-3 A %z 2008 O 18 +-3 - %z Z America/Argentina/Mendoza -4:35:16 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1990 Mar 4 --4 - -04 1990 O 15 --4 1 -03 1991 Mar --4 - -04 1991 O 15 --4 1 -03 1992 Mar --4 - -04 1992 O 18 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 May 23 --4 - -04 2004 S 26 --3 A -03/-02 2008 O 18 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1990 Mar 4 +-4 - %z 1990 O 15 +-4 1 %z 1991 Mar +-4 - %z 1991 O 15 +-4 1 %z 1992 Mar +-4 - %z 1992 O 18 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 May 23 +-4 - %z 2004 S 26 +-3 A %z 2008 O 18 +-3 - %z Z America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 Jun --4 - -04 2004 Jun 20 --3 A -03/-02 2008 O 18 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 Jun +-4 - %z 2004 Jun 20 +-3 A %z 2008 O 18 +-3 - %z Z America/Argentina/Salta -4:21:40 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar 3 --4 - -04 1991 O 20 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 A -03/-02 2008 O 18 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar 3 +-4 - %z 1991 O 20 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 A %z 2008 O 18 +-3 - %z Z America/Argentina/San_Juan -4:34:4 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar --4 - -04 1991 May 7 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 May 31 --4 - -04 2004 Jul 25 --3 A -03/-02 2008 O 18 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar +-4 - %z 1991 May 7 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 May 31 +-4 - %z 2004 Jul 25 +-3 A %z 2008 O 18 +-3 - %z Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1990 --3 1 -02 1990 Mar 14 --4 - -04 1990 O 15 --4 1 -03 1991 Mar --4 - -04 1991 Jun --3 - -03 1999 O 3 --4 1 -03 2000 Mar 3 --3 - -03 2004 May 31 --4 - -04 2004 Jul 25 --3 A -03/-02 2008 Ja 21 --4 Sa -04/-03 2009 O 11 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1990 +-3 1 %z 1990 Mar 14 +-4 - %z 1990 O 15 +-4 1 %z 1991 Mar +-4 - %z 1991 Jun +-3 - %z 1999 O 3 +-4 1 %z 2000 Mar 3 +-3 - %z 2004 May 31 +-4 - %z 2004 Jul 25 +-3 A %z 2008 Ja 21 +-4 Sa %z 2009 O 11 +-3 - %z Z America/Argentina/Tucuman -4:20:52 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar 3 --4 - -04 1991 O 20 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 Jun --4 - -04 2004 Jun 13 --3 A -03/-02 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar 3 +-4 - %z 1991 O 20 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 Jun +-4 - %z 2004 Jun 13 +-3 A %z Z America/Argentina/Ushuaia -4:33:12 - LMT 1894 O 31 -4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 May 30 --4 - -04 2004 Jun 20 --3 A -03/-02 2008 O 18 --3 - -03 +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 May 30 +-4 - %z 2004 Jun 20 +-3 A %z 2008 O 18 +-3 - %z Z America/Asuncion -3:50:40 - LMT 1890 -3:50:40 - AMT 1931 O 10 --4 - -04 1972 O --3 - -03 1974 Ap --4 y -04/-03 +-4 - %z 1972 O +-3 - %z 1974 Ap +-4 y %z Z America/Bahia -2:34:4 - LMT 1914 --3 B -03/-02 2003 S 24 --3 - -03 2011 O 16 --3 B -03/-02 2012 O 21 --3 - -03 +-3 B %z 2003 S 24 +-3 - %z 2011 O 16 +-3 B %z 2012 O 21 +-3 - %z Z America/Bahia_Banderas -7:1 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 +-7 - MST 1927 Jun 10 -6 - CST 1930 N 15 -7 m M%sT 1932 Ap -6 - CST 1942 Ap 24 --7 - MST 1949 Ja 14 --8 - PST 1970 +-7 - MST 1970 -7 m M%sT 2010 Ap 4 2 -6 m C%sT Z America/Barbados -3:58:29 - LMT 1911 Au 28 @@ -2359,18 +2355,18 @@ Z America/Barbados -3:58:29 - LMT 1911 Au 28 -4 BB AST/-0330 1945 -4 BB A%sT Z America/Belem -3:13:56 - LMT 1914 --3 B -03/-02 1988 S 12 --3 - -03 +-3 B %z 1988 S 12 +-3 - %z Z America/Belize -5:52:48 - LMT 1912 Ap -6 BZ %s Z America/Boa_Vista -4:2:40 - LMT 1914 --4 B -04/-03 1988 S 12 --4 - -04 1999 S 30 --4 B -04/-03 2000 O 15 --4 - -04 +-4 B %z 1988 S 12 +-4 - %z 1999 S 30 +-4 B %z 2000 O 15 +-4 - %z Z America/Bogota -4:56:16 - LMT 1884 Mar 13 -4:56:16 - BMT 1914 N 23 --5 CO -05/-04 +-5 CO %z Z America/Boise -7:44:49 - LMT 1883 N 18 20u -8 u P%sT 1923 May 13 2 -7 u M%sT 1974 @@ -2383,21 +2379,23 @@ Z America/Cambridge_Bay 0 - -00 1920 -6 - CST 2001 Ap 1 3 -7 C M%sT Z America/Campo_Grande -3:38:28 - LMT 1914 --4 B -04/-03 +-4 B %z Z America/Cancun -5:47:4 - LMT 1922 Ja 1 6u --6 - CST 1981 D 23 +-6 - CST 1981 D 26 2 +-5 - EST 1983 Ja 4 +-6 m C%sT 1997 O 26 2 -5 m E%sT 1998 Au 2 2 -6 m C%sT 2015 F 1 2 -5 - EST Z America/Caracas -4:27:44 - LMT 1890 -4:27:40 - CMT 1912 F 12 --4:30 - -0430 1965 --4 - -04 2007 D 9 3 --4:30 - -0430 2016 May 1 2:30 --4 - -04 +-4:30 - %z 1965 +-4 - %z 2007 D 9 3 +-4:30 - %z 2016 May 1 2:30 +-4 - %z Z America/Cayenne -3:29:20 - LMT 1911 Jul --4 - -04 1967 O --3 - -03 +-4 - %z 1967 O +-3 - %z Z America/Chicago -5:50:36 - LMT 1883 N 18 18u -6 u C%sT 1920 -6 Ch C%sT 1936 Mar 1 2 @@ -2407,7 +2405,7 @@ Z America/Chicago -5:50:36 - LMT 1883 N 18 18u -6 Ch C%sT 1967 -6 u C%sT Z America/Chihuahua -7:4:20 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 +-7 - MST 1927 Jun 10 -6 - CST 1930 N 15 -7 m M%sT 1932 Ap -6 - CST 1996 @@ -2416,7 +2414,7 @@ Z America/Chihuahua -7:4:20 - LMT 1922 Ja 1 7u -7 m M%sT 2022 O 30 2 -6 - CST Z America/Ciudad_Juarez -7:5:56 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 +-7 - MST 1927 Jun 10 -6 - CST 1930 N 15 -7 m M%sT 1932 Ap -6 - CST 1996 @@ -2430,12 +2428,12 @@ Z America/Costa_Rica -5:36:13 - LMT 1890 -5:36:13 - SJMT 1921 Ja 15 -6 CR C%sT Z America/Cuiaba -3:44:20 - LMT 1914 --4 B -04/-03 2003 S 24 --4 - -04 2004 O --4 B -04/-03 +-4 B %z 2003 S 24 +-4 - %z 2004 O +-4 B %z Z America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28 --3 - -03 1980 Ap 6 2 --3 E -03/-02 1996 +-3 - %z 1980 Ap 6 2 +-3 E %z 1996 0 - GMT Z America/Dawson -9:17:40 - LMT 1900 Au 20 -9 Y Y%sT 1965 @@ -2467,12 +2465,12 @@ Z America/Edmonton -7:33:52 - LMT 1906 S -7 Ed M%sT 1987 -7 C M%sT Z America/Eirunepe -4:39:28 - LMT 1914 --5 B -05/-04 1988 S 12 --5 - -05 1993 S 28 --5 B -05/-04 1994 S 22 --5 - -05 2008 Jun 24 --4 - -04 2013 N 10 --5 - -05 +-5 B %z 1988 S 12 +-5 - %z 1993 S 28 +-5 B %z 1994 S 22 +-5 - %z 2008 Jun 24 +-4 - %z 2013 N 10 +-5 - %z Z America/El_Salvador -5:56:48 - LMT 1921 -6 SV C%sT Z America/Fort_Nelson -8:10:47 - LMT 1884 @@ -2482,12 +2480,12 @@ Z America/Fort_Nelson -8:10:47 - LMT 1884 -8 C P%sT 2015 Mar 8 2 -7 - MST Z America/Fortaleza -2:34 - LMT 1914 --3 B -03/-02 1990 S 17 --3 - -03 1999 S 30 --3 B -03/-02 2000 O 22 --3 - -03 2001 S 13 --3 B -03/-02 2002 O --3 - -03 +-3 B %z 1990 S 17 +-3 - %z 1999 S 30 +-3 B %z 2000 O 22 +-3 - %z 2001 S 13 +-3 B %z 2002 O +-3 - %z Z America/Glace_Bay -3:59:48 - LMT 1902 Jun 15 -4 C A%sT 1953 -4 H A%sT 1954 @@ -2514,12 +2512,12 @@ Z America/Guatemala -6:2:4 - LMT 1918 O 5 -6 GT C%sT Z America/Guayaquil -5:19:20 - LMT 1890 -5:14 - QMT 1931 --5 EC -05/-04 +-5 EC %z Z America/Guyana -3:52:39 - LMT 1911 Au --4 - -04 1915 Mar --3:45 - -0345 1975 Au --3 - -03 1992 Mar 29 1 --4 - -04 +-4 - %z 1915 Mar +-3:45 - %z 1975 Au +-3 - %z 1992 Mar 29 1 +-4 - %z Z America/Halifax -4:14:24 - LMT 1902 Jun 15 -4 H A%sT 1918 -4 C A%sT 1919 @@ -2531,12 +2529,11 @@ Z America/Havana -5:29:28 - LMT 1890 -5:29:36 - HMT 1925 Jul 19 12 -5 Q C%sT Z America/Hermosillo -7:23:52 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 +-7 - MST 1927 Jun 10 -6 - CST 1930 N 15 -7 m M%sT 1932 Ap -6 - CST 1942 Ap 24 --7 - MST 1949 Ja 14 --8 - PST 1970 +-7 - MST 1996 -7 m M%sT 1999 -7 - MST Z America/Indiana/Indianapolis -5:44:38 - LMT 1883 N 18 18u @@ -2644,23 +2641,23 @@ Z America/Kentucky/Monticello -5:39:24 - LMT 1883 N 18 18u Z America/La_Paz -4:32:36 - LMT 1890 -4:32:36 - CMT 1931 O 15 -4:32:36 1 BST 1932 Mar 21 --4 - -04 +-4 - %z Z America/Lima -5:8:12 - LMT 1890 -5:8:36 - LMT 1908 Jul 28 --5 PE -05/-04 +-5 PE %z Z America/Los_Angeles -7:52:58 - LMT 1883 N 18 20u -8 u P%sT 1946 -8 CA P%sT 1967 -8 u P%sT Z America/Maceio -2:22:52 - LMT 1914 --3 B -03/-02 1990 S 17 --3 - -03 1995 O 13 --3 B -03/-02 1996 S 4 --3 - -03 1999 S 30 --3 B -03/-02 2000 O 22 --3 - -03 2001 S 13 --3 B -03/-02 2002 O --3 - -03 +-3 B %z 1990 S 17 +-3 - %z 1995 O 13 +-3 B %z 1996 S 4 +-3 - %z 1999 S 30 +-3 B %z 2000 O 22 +-3 - %z 2001 S 13 +-3 B %z 2002 O +-3 - %z Z America/Managua -5:45:8 - LMT 1890 -5:45:12 - MMT 1934 Jun 23 -6 - CST 1973 May @@ -2671,10 +2668,10 @@ Z America/Managua -5:45:8 - LMT 1890 -5 - EST 1997 -6 NI C%sT Z America/Manaus -4:0:4 - LMT 1914 --4 B -04/-03 1988 S 12 --4 - -04 1993 S 28 --4 B -04/-03 1994 S 22 --4 - -04 +-4 B %z 1988 S 12 +-4 - %z 1993 S 28 +-4 B %z 1994 S 22 +-4 - %z Z America/Martinique -4:4:20 - LMT 1890 -4:4:20 - FFMT 1911 May -4 - AST 1980 Ap 6 @@ -2686,12 +2683,11 @@ Z America/Matamoros -6:30 - LMT 1922 Ja 1 6u -6 m C%sT 2010 -6 u C%sT Z America/Mazatlan -7:5:40 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 +-7 - MST 1927 Jun 10 -6 - CST 1930 N 15 -7 m M%sT 1932 Ap -6 - CST 1942 Ap 24 --7 - MST 1949 Ja 14 --8 - PST 1970 +-7 - MST 1970 -7 m M%sT Z America/Menominee -5:50:27 - LMT 1885 S 18 12 -6 u C%sT 1946 @@ -2699,8 +2695,8 @@ Z America/Menominee -5:50:27 - LMT 1885 S 18 12 -5 - EST 1973 Ap 29 2 -6 u C%sT Z America/Merida -5:58:28 - LMT 1922 Ja 1 6u --6 - CST 1981 D 23 --5 - EST 1982 D 2 +-6 - CST 1981 D 26 2 +-5 - EST 1982 N 2 2 -6 m C%sT Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55 -8:46:18 - LMT 1900 Au 20 12 @@ -2713,7 +2709,7 @@ Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55 -8 - PST 2019 Ja 20 2 -9 u AK%sT Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 +-7 - MST 1927 Jun 10 -6 - CST 1930 N 15 -7 m M%sT 1932 Ap -6 m C%sT 2001 S 30 2 @@ -2721,8 +2717,8 @@ Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 7u -6 m C%sT Z America/Miquelon -3:44:40 - LMT 1911 Jun 15 -4 - AST 1980 May --3 - -03 1987 --3 C -03/-02 +-3 - %z 1987 +-3 C %z Z America/Moncton -4:19:8 - LMT 1883 D 9 -5 - EST 1902 Jun 15 -4 C A%sT 1933 @@ -2733,20 +2729,23 @@ Z America/Moncton -4:19:8 - LMT 1883 D 9 -4 o A%sT 2007 -4 C A%sT Z America/Monterrey -6:41:16 - LMT 1922 Ja 1 6u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap -6 - CST 1988 -6 u C%sT 1989 -6 m C%sT Z America/Montevideo -3:44:51 - LMT 1908 Jun 10 -3:44:51 - MMT 1920 May --4 - -04 1923 O --3:30 U -0330/-03 1942 D 14 --3 U -03/-0230 1960 --3 U -03/-02 1968 --3 U -03/-0230 1970 --3 U -03/-02 1974 --3 U -03/-0130 1974 Mar 10 --3 U -03/-0230 1974 D 22 --3 U -03/-02 +-4 - %z 1923 O +-3:30 U %z 1942 D 14 +-3 U %z 1960 +-3 U %z 1968 +-3 U %z 1970 +-3 U %z 1974 +-3 U %z 1974 Mar 10 +-3 U %z 1974 D 22 +-3 U %z Z America/New_York -4:56:2 - LMT 1883 N 18 17u -5 u E%sT 1920 -5 NY E%sT 1942 @@ -2763,12 +2762,12 @@ Z America/Nome 12:58:22 - LMT 1867 O 19 13:29:35 -9 u Y%sT 1983 N 30 -9 u AK%sT Z America/Noronha -2:9:40 - LMT 1914 --2 B -02/-01 1990 S 17 --2 - -02 1999 S 30 --2 B -02/-01 2000 O 15 --2 - -02 2001 S 13 --2 B -02/-01 2002 O --2 - -02 +-2 B %z 1990 S 17 +-2 - %z 1999 S 30 +-2 B %z 2000 O 15 +-2 - %z 2001 S 13 +-2 B %z 2002 O +-2 - %z Z America/North_Dakota/Beulah -6:47:7 - LMT 1883 N 18 19u -7 u M%sT 2010 N 7 2 -6 u C%sT @@ -2779,12 +2778,12 @@ Z America/North_Dakota/New_Salem -6:45:39 - LMT 1883 N 18 19u -7 u M%sT 2003 O 26 2 -6 u C%sT Z America/Nuuk -3:26:56 - LMT 1916 Jul 28 --3 - -03 1980 Ap 6 2 --3 E -03/-02 2023 Mar 26 1u --2 - -02 2023 O 29 1u --2 E -02/-01 +-3 - %z 1980 Ap 6 2 +-3 E %z 2023 Mar 26 1u +-2 - %z 2023 O 29 1u +-2 E %z Z America/Ojinaga -6:57:40 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 +-7 - MST 1927 Jun 10 -6 - CST 1930 N 15 -7 m M%sT 1932 Ap -6 - CST 1996 @@ -2800,8 +2799,8 @@ Z America/Panama -5:18:8 - LMT 1890 Z America/Paramaribo -3:40:40 - LMT 1911 -3:40:52 - PMT 1935 -3:40:36 - PMT 1945 O --3:30 - -0330 1984 O --3 - -03 +-3:30 - %z 1984 O +-3 - %z Z America/Phoenix -7:28:18 - LMT 1883 N 18 19u -7 u M%sT 1944 Ja 1 0:1 -7 - MST 1944 Ap 1 0:1 @@ -2813,37 +2812,37 @@ Z America/Port-au-Prince -4:49:20 - LMT 1890 -4:49 - PPMT 1917 Ja 24 12 -5 HT E%sT Z America/Porto_Velho -4:15:36 - LMT 1914 --4 B -04/-03 1988 S 12 --4 - -04 +-4 B %z 1988 S 12 +-4 - %z Z America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12 -4 - AST 1942 May 3 -4 u A%sT 1946 -4 - AST Z America/Punta_Arenas -4:43:40 - LMT 1890 -4:42:45 - SMT 1910 Ja 10 --5 - -05 1916 Jul +-5 - %z 1916 Jul -4:42:45 - SMT 1918 S 10 --4 - -04 1919 Jul +-4 - %z 1919 Jul -4:42:45 - SMT 1927 S --5 x -05/-04 1932 S --4 - -04 1942 Jun --5 - -05 1942 Au --4 - -04 1946 Au 28 24 --5 1 -04 1947 Mar 31 24 --5 - -05 1947 May 21 23 --4 x -04/-03 2016 D 4 --3 - -03 +-5 x %z 1932 S +-4 - %z 1942 Jun +-5 - %z 1942 Au +-4 - %z 1946 Au 28 24 +-5 1 %z 1947 Mar 31 24 +-5 - %z 1947 May 21 23 +-4 x %z 2016 D 4 +-3 - %z Z America/Rankin_Inlet 0 - -00 1957 -6 Y C%sT 2000 O 29 2 -5 - EST 2001 Ap 1 3 -6 C C%sT Z America/Recife -2:19:36 - LMT 1914 --3 B -03/-02 1990 S 17 --3 - -03 1999 S 30 --3 B -03/-02 2000 O 15 --3 - -03 2001 S 13 --3 B -03/-02 2002 O --3 - -03 +-3 B %z 1990 S 17 +-3 - %z 1999 S 30 +-3 B %z 2000 O 15 +-3 - %z 2001 S 13 +-3 B %z 2002 O +-3 - %z Z America/Regina -6:58:36 - LMT 1905 S -7 r M%sT 1960 Ap lastSu 2 -6 - CST @@ -2854,28 +2853,28 @@ Z America/Resolute 0 - -00 1947 Au 31 -5 - EST 2007 Mar 11 3 -6 C C%sT Z America/Rio_Branco -4:31:12 - LMT 1914 --5 B -05/-04 1988 S 12 --5 - -05 2008 Jun 24 --4 - -04 2013 N 10 --5 - -05 +-5 B %z 1988 S 12 +-5 - %z 2008 Jun 24 +-4 - %z 2013 N 10 +-5 - %z Z America/Santarem -3:38:48 - LMT 1914 --4 B -04/-03 1988 S 12 --4 - -04 2008 Jun 24 --3 - -03 +-4 B %z 1988 S 12 +-4 - %z 2008 Jun 24 +-3 - %z Z America/Santiago -4:42:45 - LMT 1890 -4:42:45 - SMT 1910 Ja 10 --5 - -05 1916 Jul +-5 - %z 1916 Jul -4:42:45 - SMT 1918 S 10 --4 - -04 1919 Jul +-4 - %z 1919 Jul -4:42:45 - SMT 1927 S --5 x -05/-04 1932 S --4 - -04 1942 Jun --5 - -05 1942 Au --4 - -04 1946 Jul 14 24 --4 1 -03 1946 Au 28 24 --5 1 -04 1947 Mar 31 24 --5 - -05 1947 May 21 23 --4 x -04/-03 +-5 x %z 1932 S +-4 - %z 1942 Jun +-5 - %z 1942 Au +-4 - %z 1946 Jul 14 24 +-4 1 %z 1946 Au 28 24 +-5 1 %z 1947 Mar 31 24 +-5 - %z 1947 May 21 23 +-4 x %z Z America/Santo_Domingo -4:39:36 - LMT 1890 -4:40 - SDMT 1933 Ap 1 12 -5 DO %s 1974 O 27 @@ -2883,14 +2882,14 @@ Z America/Santo_Domingo -4:39:36 - LMT 1890 -5 u E%sT 2000 D 3 1 -4 - AST Z America/Sao_Paulo -3:6:28 - LMT 1914 --3 B -03/-02 1963 O 23 --3 1 -02 1964 --3 B -03/-02 +-3 B %z 1963 O 23 +-3 1 %z 1964 +-3 B %z Z America/Scoresbysund -1:27:52 - LMT 1916 Jul 28 --2 - -02 1980 Ap 6 2 --2 c -02/-01 1981 Mar 29 --1 E -01/+00 2024 Mar 31 --2 E -02/-01 +-2 - %z 1980 Ap 6 2 +-2 c %z 1981 Mar 29 +-1 E %z 2024 Mar 31 +-2 E %z Z America/Sitka 14:58:47 - LMT 1867 O 19 15:30 -9:1:13 - LMT 1900 Au 20 12 -8 - PST 1942 @@ -2918,15 +2917,21 @@ Z America/Thule -4:35:8 - LMT 1916 Jul 28 -4 Th A%sT Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 7u -7 - MST 1924 --8 - PST 1927 Jun 10 23 +-8 - PST 1927 Jun 10 -7 - MST 1930 N 15 -8 - PST 1931 Ap -8 1 PDT 1931 S 30 -8 - PST 1942 Ap 24 -8 1 PWT 1945 Au 14 23u --8 1 PPT 1945 N 12 +-8 1 PPT 1945 N 15 -8 - PST 1948 Ap 5 -8 1 PDT 1949 Ja 14 +-8 - PST 1950 May +-8 1 PDT 1950 S 24 +-8 - PST 1951 Ap 29 2 +-8 1 PDT 1951 S 30 2 +-8 - PST 1952 Ap 27 2 +-8 1 PDT 1952 S 28 2 -8 - PST 1954 -8 CA P%sT 1961 -8 - PST 1976 @@ -2961,31 +2966,31 @@ Z America/Yakutat 14:41:5 - LMT 1867 O 19 15:12:18 -9 u Y%sT 1983 N 30 -9 u AK%sT Z Antarctica/Casey 0 - -00 1969 -8 - +08 2009 O 18 2 -11 - +11 2010 Mar 5 2 -8 - +08 2011 O 28 2 -11 - +11 2012 F 21 17u -8 - +08 2016 O 22 -11 - +11 2018 Mar 11 4 -8 - +08 2018 O 7 4 -11 - +11 2019 Mar 17 3 -8 - +08 2019 O 4 3 -11 - +11 2020 Mar 8 3 -8 - +08 2020 O 4 0:1 -11 - +11 2021 Mar 14 -8 - +08 2021 O 3 0:1 -11 - +11 2022 Mar 13 -8 - +08 2022 O 2 0:1 -11 - +11 2023 Mar 9 3 -8 - +08 +8 - %z 2009 O 18 2 +11 - %z 2010 Mar 5 2 +8 - %z 2011 O 28 2 +11 - %z 2012 F 21 17u +8 - %z 2016 O 22 +11 - %z 2018 Mar 11 4 +8 - %z 2018 O 7 4 +11 - %z 2019 Mar 17 3 +8 - %z 2019 O 4 3 +11 - %z 2020 Mar 8 3 +8 - %z 2020 O 4 0:1 +11 - %z 2021 Mar 14 +8 - %z 2021 O 3 0:1 +11 - %z 2022 Mar 13 +8 - %z 2022 O 2 0:1 +11 - %z 2023 Mar 9 3 +8 - %z Z Antarctica/Davis 0 - -00 1957 Ja 13 -7 - +07 1964 N +7 - %z 1964 N 0 - -00 1969 F -7 - +07 2009 O 18 2 -5 - +05 2010 Mar 10 20u -7 - +07 2011 O 28 2 -5 - +05 2012 F 21 20u -7 - +07 +7 - %z 2009 O 18 2 +5 - %z 2010 Mar 10 20u +7 - %z 2011 O 28 2 +5 - %z 2012 F 21 20u +7 - %z Z Antarctica/Macquarie 0 - -00 1899 N 10 - AEST 1916 O 1 2 10 1 AEDT 1917 F @@ -2996,151 +3001,146 @@ Z Antarctica/Macquarie 0 - -00 1899 N 10 1 AEDT 2011 10 AT AE%sT Z Antarctica/Mawson 0 - -00 1954 F 13 -6 - +06 2009 O 18 2 -5 - +05 +6 - %z 2009 O 18 2 +5 - %z Z Antarctica/Palmer 0 - -00 1965 --4 A -04/-03 1969 O 5 --3 A -03/-02 1982 May --4 x -04/-03 2016 D 4 --3 - -03 +-4 A %z 1969 O 5 +-3 A %z 1982 May +-4 x %z 2016 D 4 +-3 - %z Z Antarctica/Rothera 0 - -00 1976 D --3 - -03 +-3 - %z Z Antarctica/Troll 0 - -00 2005 F 12 0 Tr %s Z Antarctica/Vostok 0 - -00 1957 D 16 -7 - +07 1994 F +7 - %z 1994 F 0 - -00 1994 N -7 - +07 2023 D 18 2 -5 - +05 +7 - %z 2023 D 18 2 +5 - %z Z Asia/Almaty 5:7:48 - LMT 1924 May 2 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2s -5 R +05/+06 1992 Ja 19 2s -6 R +06/+07 2004 O 31 2s -6 - +06 2024 Mar -5 - +05 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2s +5 R %z 1992 Ja 19 2s +6 R %z 2004 O 31 2s +6 - %z 2024 Mar +5 - %z Z Asia/Amman 2:23:44 - LMT 1931 2 J EE%sT 2022 O 28 0s -3 - +03 +3 - %z Z Asia/Anadyr 11:49:56 - LMT 1924 May 2 -12 - +12 1930 Jun 21 -13 R +13/+14 1982 Ap 1 0s -12 R +12/+13 1991 Mar 31 2s -11 R +11/+12 1992 Ja 19 2s -12 R +12/+13 2010 Mar 28 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 +12 - %z 1930 Jun 21 +13 R %z 1982 Ap 1 0s +12 R %z 1991 Mar 31 2s +11 R %z 1992 Ja 19 2s +12 R %z 2010 Mar 28 2s +11 R %z 2011 Mar 27 2s +12 - %z Z Asia/Aqtau 3:21:4 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 1994 S 25 2s -4 R +04/+05 2004 O 31 2s -5 - +05 +4 - %z 1930 Jun 21 +5 - %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 1994 S 25 2s +4 R %z 2004 O 31 2s +5 - %z Z Asia/Aqtobe 3:48:40 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 2004 O 31 2s -5 - +05 +4 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 2004 O 31 2s +5 - %z Z Asia/Ashgabat 3:53:32 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 R +05/+06 1991 Mar 31 2 -4 R +04/+05 1992 Ja 19 2 -5 - +05 +4 - %z 1930 Jun 21 +5 R %z 1991 Mar 31 2 +4 R %z 1992 Ja 19 2 +5 - %z Z Asia/Atyrau 3:27:44 - LMT 1924 May 2 -3 - +03 1930 Jun 21 -5 - +05 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 1999 Mar 28 2s -4 R +04/+05 2004 O 31 2s -5 - +05 +3 - %z 1930 Jun 21 +5 - %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 1999 Mar 28 2s +4 R %z 2004 O 31 2s +5 - %z Z Asia/Baghdad 2:57:40 - LMT 1890 2:57:36 - BMT 1918 -3 - +03 1982 May -3 IQ +03/+04 +3 - %z 1982 May +3 IQ %z Z Asia/Baku 3:19:24 - LMT 1924 May 2 -3 - +03 1957 Mar -4 R +04/+05 1991 Mar 31 2s -3 R +03/+04 1992 S lastSu 2s -4 - +04 1996 -4 E +04/+05 1997 -4 AZ +04/+05 +3 - %z 1957 Mar +4 R %z 1991 Mar 31 2s +3 R %z 1992 S lastSu 2s +4 - %z 1996 +4 E %z 1997 +4 AZ %z Z Asia/Bangkok 6:42:4 - LMT 1880 6:42:4 - BMT 1920 Ap -7 - +07 +7 - %z Z Asia/Barnaul 5:35 - LMT 1919 D 10 -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 1995 May 28 -6 R +06/+07 2011 Mar 27 2s -7 - +07 2014 O 26 2s -6 - +06 2016 Mar 27 2s -7 - +07 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 1995 May 28 +6 R %z 2011 Mar 27 2s +7 - %z 2014 O 26 2s +6 - %z 2016 Mar 27 2s +7 - %z Z Asia/Beirut 2:22 - LMT 1880 2 l EE%sT Z Asia/Bishkek 4:58:24 - LMT 1924 May 2 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2s -5 R +05/+06 1991 Au 31 2 -5 KG +05/+06 2005 Au 12 -6 - +06 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2s +5 R %z 1991 Au 31 2 +5 KG %z 2005 Au 12 +6 - %z Z Asia/Chita 7:33:52 - LMT 1919 D 15 -8 - +08 1930 Jun 21 -9 R +09/+10 1991 Mar 31 2s -8 R +08/+09 1992 Ja 19 2s -9 R +09/+10 2011 Mar 27 2s -10 - +10 2014 O 26 2s -8 - +08 2016 Mar 27 2 -9 - +09 -Z Asia/Choibalsan 7:38 - LMT 1905 Au -7 - +07 1978 -8 - +08 1983 Ap -9 X +09/+10 2008 Mar 31 -8 X +08/+09 +8 - %z 1930 Jun 21 +9 R %z 1991 Mar 31 2s +8 R %z 1992 Ja 19 2s +9 R %z 2011 Mar 27 2s +10 - %z 2014 O 26 2s +8 - %z 2016 Mar 27 2 +9 - %z Z Asia/Colombo 5:19:24 - LMT 1880 5:19:32 - MMT 1906 -5:30 - +0530 1942 Ja 5 -5:30 0:30 +06 1942 S -5:30 1 +0630 1945 O 16 2 -5:30 - +0530 1996 May 25 -6:30 - +0630 1996 O 26 0:30 -6 - +06 2006 Ap 15 0:30 -5:30 - +0530 +5:30 - %z 1942 Ja 5 +5:30 0:30 %z 1942 S +5:30 1 %z 1945 O 16 2 +5:30 - %z 1996 May 25 +6:30 - %z 1996 O 26 0:30 +6 - %z 2006 Ap 15 0:30 +5:30 - %z Z Asia/Damascus 2:25:12 - LMT 1920 2 S EE%sT 2022 O 28 -3 - +03 +3 - %z Z Asia/Dhaka 6:1:40 - LMT 1890 5:53:20 - HMT 1941 O -6:30 - +0630 1942 May 15 -5:30 - +0530 1942 S -6:30 - +0630 1951 S 30 -6 - +06 2009 -6 BD +06/+07 -Z Asia/Dili 8:22:20 - LMT 1912 -8 - +08 1942 F 21 23 -9 - +09 1976 May 3 -8 - +08 2000 S 17 -9 - +09 +6:30 - %z 1942 May 15 +5:30 - %z 1942 S +6:30 - %z 1951 S 30 +6 - %z 2009 +6 BD %z +Z Asia/Dili 8:22:20 - LMT 1911 D 31 16u +8 - %z 1942 F 21 23 +9 - %z 1976 May 3 +8 - %z 2000 S 17 +9 - %z Z Asia/Dubai 3:41:12 - LMT 1920 -4 - +04 +4 - %z Z Asia/Dushanbe 4:35:12 - LMT 1924 May 2 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2s -5 1 +06 1991 S 9 2s -5 - +05 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2s +5 1 %z 1991 S 9 2s +5 - %z Z Asia/Famagusta 2:15:48 - LMT 1921 N 14 2 CY EE%sT 1998 S 2 E EE%sT 2016 S 8 -3 - +03 2017 O 29 1u +3 - %z 2017 O 29 1u 2 E EE%sT Z Asia/Gaza 2:17:52 - LMT 1900 O 2 Z EET/EEST 1948 May 15 @@ -3162,14 +3162,14 @@ Z Asia/Hebron 2:20:23 - LMT 1900 O 2 P EE%sT Z Asia/Ho_Chi_Minh 7:6:30 - LMT 1906 Jul 7:6:30 - PLMT 1911 May -7 - +07 1942 D 31 23 -8 - +08 1945 Mar 14 23 -9 - +09 1945 S 1 24 -7 - +07 1947 Ap -8 - +08 1955 Jul 1 1 -7 - +07 1959 D 31 23 -8 - +08 1975 Jun 13 -7 - +07 +7 - %z 1942 D 31 23 +8 - %z 1945 Mar 14 23 +9 - %z 1945 S 1 24 +7 - %z 1947 Ap +8 - %z 1955 Jul 1 1 +7 - %z 1959 D 31 23 +8 - %z 1975 Jun 13 +7 - %z Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 29 17u 8 - HKT 1941 Jun 15 3 8 1 HKST 1941 O 1 4 @@ -3177,96 +3177,96 @@ Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 29 17u 9 - JST 1945 N 18 2 8 HK HK%sT Z Asia/Hovd 6:6:36 - LMT 1905 Au -6 - +06 1978 -7 X +07/+08 +6 - %z 1978 +7 X %z Z Asia/Irkutsk 6:57:5 - LMT 1880 6:57:5 - IMT 1920 Ja 25 -7 - +07 1930 Jun 21 -8 R +08/+09 1991 Mar 31 2s -7 R +07/+08 1992 Ja 19 2s -8 R +08/+09 2011 Mar 27 2s -9 - +09 2014 O 26 2s -8 - +08 +7 - %z 1930 Jun 21 +8 R %z 1991 Mar 31 2s +7 R %z 1992 Ja 19 2s +8 R %z 2011 Mar 27 2s +9 - %z 2014 O 26 2s +8 - %z Z Asia/Jakarta 7:7:12 - LMT 1867 Au 10 7:7:12 - BMT 1923 D 31 16:40u -7:20 - +0720 1932 N -7:30 - +0730 1942 Mar 23 -9 - +09 1945 S 23 -7:30 - +0730 1948 May -8 - +08 1950 May -7:30 - +0730 1964 +7:20 - %z 1932 N +7:30 - %z 1942 Mar 23 +9 - %z 1945 S 23 +7:30 - %z 1948 May +8 - %z 1950 May +7:30 - %z 1964 7 - WIB Z Asia/Jayapura 9:22:48 - LMT 1932 N -9 - +09 1944 S -9:30 - +0930 1964 +9 - %z 1944 S +9:30 - %z 1964 9 - WIT Z Asia/Jerusalem 2:20:54 - LMT 1880 2:20:40 - JMT 1918 2 Z I%sT Z Asia/Kabul 4:36:48 - LMT 1890 -4 - +04 1945 -4:30 - +0430 +4 - %z 1945 +4:30 - %z Z Asia/Kamchatka 10:34:36 - LMT 1922 N 10 -11 - +11 1930 Jun 21 -12 R +12/+13 1991 Mar 31 2s -11 R +11/+12 1992 Ja 19 2s -12 R +12/+13 2010 Mar 28 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 +11 - %z 1930 Jun 21 +12 R %z 1991 Mar 31 2s +11 R %z 1992 Ja 19 2s +12 R %z 2010 Mar 28 2s +11 R %z 2011 Mar 27 2s +12 - %z Z Asia/Karachi 4:28:12 - LMT 1907 -5:30 - +0530 1942 S -5:30 1 +0630 1945 O 15 -5:30 - +0530 1951 S 30 -5 - +05 1971 Mar 26 +5:30 - %z 1942 S +5:30 1 %z 1945 O 15 +5:30 - %z 1951 S 30 +5 - %z 1971 Mar 26 5 PK PK%sT Z Asia/Kathmandu 5:41:16 - LMT 1920 -5:30 - +0530 1986 -5:45 - +0545 +5:30 - %z 1986 +5:45 - %z Z Asia/Khandyga 9:2:13 - LMT 1919 D 15 -8 - +08 1930 Jun 21 -9 R +09/+10 1991 Mar 31 2s -8 R +08/+09 1992 Ja 19 2s -9 R +09/+10 2004 -10 R +10/+11 2011 Mar 27 2s -11 - +11 2011 S 13 0s -10 - +10 2014 O 26 2s -9 - +09 +8 - %z 1930 Jun 21 +9 R %z 1991 Mar 31 2s +8 R %z 1992 Ja 19 2s +9 R %z 2004 +10 R %z 2011 Mar 27 2s +11 - %z 2011 S 13 0s +10 - %z 2014 O 26 2s +9 - %z Z Asia/Kolkata 5:53:28 - LMT 1854 Jun 28 5:53:20 - HMT 1870 5:21:10 - MMT 1906 5:30 - IST 1941 O -5:30 1 +0630 1942 May 15 +5:30 1 %z 1942 May 15 5:30 - IST 1942 S -5:30 1 +0630 1945 O 15 +5:30 1 %z 1945 O 15 5:30 - IST Z Asia/Krasnoyarsk 6:11:26 - LMT 1920 Ja 6 -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 2011 Mar 27 2s -8 - +08 2014 O 26 2s -7 - +07 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 2011 Mar 27 2s +8 - %z 2014 O 26 2s +7 - %z Z Asia/Kuching 7:21:20 - LMT 1926 Mar -7:30 - +0730 1933 -8 NB +08/+0820 1942 F 16 -9 - +09 1945 S 12 -8 - +08 +7:30 - %z 1933 +8 NB %z 1942 F 16 +9 - %z 1945 S 12 +8 - %z Z Asia/Macau 7:34:10 - LMT 1904 O 30 8 - CST 1941 D 21 23 -9 _ +09/+10 1945 S 30 24 +9 _ %z 1945 S 30 24 8 _ C%sT Z Asia/Magadan 10:3:12 - LMT 1924 May 2 -10 - +10 1930 Jun 21 -11 R +11/+12 1991 Mar 31 2s -10 R +10/+11 1992 Ja 19 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 2014 O 26 2s -10 - +10 2016 Ap 24 2s -11 - +11 +10 - %z 1930 Jun 21 +11 R %z 1991 Mar 31 2s +10 R %z 1992 Ja 19 2s +11 R %z 2011 Mar 27 2s +12 - %z 2014 O 26 2s +10 - %z 2016 Ap 24 2s +11 - %z Z Asia/Makassar 7:57:36 - LMT 1920 7:57:36 - MMT 1932 N -8 - +08 1942 F 9 -9 - +09 1945 S 23 +8 - %z 1942 F 9 +9 - %z 1945 S 23 8 - WITA Z Asia/Manila -15:56 - LMT 1844 D 31 8:4 - LMT 1899 May 11 @@ -3277,45 +3277,45 @@ Z Asia/Nicosia 2:13:28 - LMT 1921 N 14 2 CY EE%sT 1998 S 2 E EE%sT Z Asia/Novokuznetsk 5:48:48 - LMT 1924 May -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 2010 Mar 28 2s -6 R +06/+07 2011 Mar 27 2s -7 - +07 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 2010 Mar 28 2s +6 R %z 2011 Mar 27 2s +7 - %z Z Asia/Novosibirsk 5:31:40 - LMT 1919 D 14 6 -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 1993 May 23 -6 R +06/+07 2011 Mar 27 2s -7 - +07 2014 O 26 2s -6 - +06 2016 Jul 24 2s -7 - +07 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 1993 May 23 +6 R %z 2011 Mar 27 2s +7 - %z 2014 O 26 2s +6 - %z 2016 Jul 24 2s +7 - %z Z Asia/Omsk 4:53:30 - LMT 1919 N 14 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2s -5 R +05/+06 1992 Ja 19 2s -6 R +06/+07 2011 Mar 27 2s -7 - +07 2014 O 26 2s -6 - +06 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2s +5 R %z 1992 Ja 19 2s +6 R %z 2011 Mar 27 2s +7 - %z 2014 O 26 2s +6 - %z Z Asia/Oral 3:25:24 - LMT 1924 May 2 -3 - +03 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1989 Mar 26 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 1992 Mar 29 2s -4 R +04/+05 2004 O 31 2s -5 - +05 +3 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1989 Mar 26 2s +4 R %z 1992 Ja 19 2s +5 R %z 1992 Mar 29 2s +4 R %z 2004 O 31 2s +5 - %z Z Asia/Pontianak 7:17:20 - LMT 1908 May 7:17:20 - PMT 1932 N -7:30 - +0730 1942 Ja 29 -9 - +09 1945 S 23 -7:30 - +0730 1948 May -8 - +08 1950 May -7:30 - +0730 1964 +7:30 - %z 1942 Ja 29 +9 - %z 1945 S 23 +7:30 - %z 1948 May +8 - %z 1950 May +7:30 - %z 1964 8 - WITA 1988 7 - WIB Z Asia/Pyongyang 8:23 - LMT 1908 Ap @@ -3325,48 +3325,48 @@ Z Asia/Pyongyang 8:23 - LMT 1908 Ap 8:30 - KST 2018 May 4 23:30 9 - KST Z Asia/Qatar 3:26:8 - LMT 1920 -4 - +04 1972 Jun -3 - +03 +4 - %z 1972 Jun +3 - %z Z Asia/Qostanay 4:14:28 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 2004 O 31 2s -6 - +06 2024 Mar -5 - +05 +4 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 2004 O 31 2s +6 - %z 2024 Mar +5 - %z Z Asia/Qyzylorda 4:21:52 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1991 S 29 2s -5 R +05/+06 1992 Ja 19 2s -6 R +06/+07 1992 Mar 29 2s -5 R +05/+06 2004 O 31 2s -6 - +06 2018 D 21 -5 - +05 +4 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1991 S 29 2s +5 R %z 1992 Ja 19 2s +6 R %z 1992 Mar 29 2s +5 R %z 2004 O 31 2s +6 - %z 2018 D 21 +5 - %z Z Asia/Riyadh 3:6:52 - LMT 1947 Mar 14 -3 - +03 +3 - %z Z Asia/Sakhalin 9:30:48 - LMT 1905 Au 23 -9 - +09 1945 Au 25 -11 R +11/+12 1991 Mar 31 2s -10 R +10/+11 1992 Ja 19 2s -11 R +11/+12 1997 Mar lastSu 2s -10 R +10/+11 2011 Mar 27 2s -11 - +11 2014 O 26 2s -10 - +10 2016 Mar 27 2s -11 - +11 +9 - %z 1945 Au 25 +11 R %z 1991 Mar 31 2s +10 R %z 1992 Ja 19 2s +11 R %z 1997 Mar lastSu 2s +10 R %z 2011 Mar 27 2s +11 - %z 2014 O 26 2s +10 - %z 2016 Mar 27 2s +11 - %z Z Asia/Samarkand 4:27:53 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1992 -5 - +05 +4 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1992 +5 - %z Z Asia/Seoul 8:27:52 - LMT 1908 Ap 8:30 - KST 1912 9 - JST 1945 S 8 @@ -3378,161 +3378,147 @@ Z Asia/Shanghai 8:5:43 - LMT 1901 8 CN C%sT Z Asia/Singapore 6:55:25 - LMT 1901 6:55:25 - SMT 1905 Jun -7 - +07 1933 -7 0:20 +0720 1936 -7:20 - +0720 1941 S -7:30 - +0730 1942 F 16 -9 - +09 1945 S 12 -7:30 - +0730 1981 D 31 16u -8 - +08 +7 - %z 1933 +7 0:20 %z 1936 +7:20 - %z 1941 S +7:30 - %z 1942 F 16 +9 - %z 1945 S 12 +7:30 - %z 1981 D 31 16u +8 - %z Z Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2 -10 - +10 1930 Jun 21 -11 R +11/+12 1991 Mar 31 2s -10 R +10/+11 1992 Ja 19 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 2014 O 26 2s -11 - +11 +10 - %z 1930 Jun 21 +11 R %z 1991 Mar 31 2s +10 R %z 1992 Ja 19 2s +11 R %z 2011 Mar 27 2s +12 - %z 2014 O 26 2s +11 - %z Z Asia/Taipei 8:6 - LMT 1896 8 - CST 1937 O 9 - JST 1945 S 21 1 8 f C%sT Z Asia/Tashkent 4:37:11 - LMT 1924 May 2 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2 -5 R +05/+06 1992 -5 - +05 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2 +5 R %z 1992 +5 - %z Z Asia/Tbilisi 2:59:11 - LMT 1880 2:59:11 - TBMT 1924 May 2 -3 - +03 1957 Mar -4 R +04/+05 1991 Mar 31 2s -3 R +03/+04 1992 -3 e +03/+04 1994 S lastSu -4 e +04/+05 1996 O lastSu -4 1 +05 1997 Mar lastSu -4 e +04/+05 2004 Jun 27 -3 R +03/+04 2005 Mar lastSu 2 -4 - +04 +3 - %z 1957 Mar +4 R %z 1991 Mar 31 2s +3 R %z 1992 +3 e %z 1994 S lastSu +4 e %z 1996 O lastSu +4 1 %z 1997 Mar lastSu +4 e %z 2004 Jun 27 +3 R %z 2005 Mar lastSu 2 +4 - %z Z Asia/Tehran 3:25:44 - LMT 1916 3:25:44 - TMT 1935 Jun 13 -3:30 i +0330/+0430 1977 O 20 24 -4 i +04/+05 1979 -3:30 i +0330/+0430 +3:30 i %z 1977 O 20 24 +4 i %z 1979 +3:30 i %z Z Asia/Thimphu 5:58:36 - LMT 1947 Au 15 -5:30 - +0530 1987 O -6 - +06 +5:30 - %z 1987 O +6 - %z Z Asia/Tokyo 9:18:59 - LMT 1887 D 31 15u 9 JP J%sT Z Asia/Tomsk 5:39:51 - LMT 1919 D 22 -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 2002 May 1 3 -6 R +06/+07 2011 Mar 27 2s -7 - +07 2014 O 26 2s -6 - +06 2016 May 29 2s -7 - +07 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 2002 May 1 3 +6 R %z 2011 Mar 27 2s +7 - %z 2014 O 26 2s +6 - %z 2016 May 29 2s +7 - %z Z Asia/Ulaanbaatar 7:7:32 - LMT 1905 Au -7 - +07 1978 -8 X +08/+09 +7 - %z 1978 +8 X %z Z Asia/Urumqi 5:50:20 - LMT 1928 -6 - +06 +6 - %z Z Asia/Ust-Nera 9:32:54 - LMT 1919 D 15 -8 - +08 1930 Jun 21 -9 R +09/+10 1981 Ap -11 R +11/+12 1991 Mar 31 2s -10 R +10/+11 1992 Ja 19 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 2011 S 13 0s -11 - +11 2014 O 26 2s -10 - +10 +8 - %z 1930 Jun 21 +9 R %z 1981 Ap +11 R %z 1991 Mar 31 2s +10 R %z 1992 Ja 19 2s +11 R %z 2011 Mar 27 2s +12 - %z 2011 S 13 0s +11 - %z 2014 O 26 2s +10 - %z Z Asia/Vladivostok 8:47:31 - LMT 1922 N 15 -9 - +09 1930 Jun 21 -10 R +10/+11 1991 Mar 31 2s -9 R +09/+10 1992 Ja 19 2s -10 R +10/+11 2011 Mar 27 2s -11 - +11 2014 O 26 2s -10 - +10 +9 - %z 1930 Jun 21 +10 R %z 1991 Mar 31 2s +9 R %z 1992 Ja 19 2s +10 R %z 2011 Mar 27 2s +11 - %z 2014 O 26 2s +10 - %z Z Asia/Yakutsk 8:38:58 - LMT 1919 D 15 -8 - +08 1930 Jun 21 -9 R +09/+10 1991 Mar 31 2s -8 R +08/+09 1992 Ja 19 2s -9 R +09/+10 2011 Mar 27 2s -10 - +10 2014 O 26 2s -9 - +09 +8 - %z 1930 Jun 21 +9 R %z 1991 Mar 31 2s +8 R %z 1992 Ja 19 2s +9 R %z 2011 Mar 27 2s +10 - %z 2014 O 26 2s +9 - %z Z Asia/Yangon 6:24:47 - LMT 1880 6:24:47 - RMT 1920 -6:30 - +0630 1942 May -9 - +09 1945 May 3 -6:30 - +0630 +6:30 - %z 1942 May +9 - %z 1945 May 3 +6:30 - %z Z Asia/Yekaterinburg 4:2:33 - LMT 1916 Jul 3 3:45:5 - PMT 1919 Jul 15 4 -4 - +04 1930 Jun 21 -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 2011 Mar 27 2s -6 - +06 2014 O 26 2s -5 - +05 +4 - %z 1930 Jun 21 +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 2011 Mar 27 2s +6 - %z 2014 O 26 2s +5 - %z Z Asia/Yerevan 2:58 - LMT 1924 May 2 -3 - +03 1957 Mar -4 R +04/+05 1991 Mar 31 2s -3 R +03/+04 1995 S 24 2s -4 - +04 1997 -4 R +04/+05 2011 -4 AM +04/+05 +3 - %z 1957 Mar +4 R %z 1991 Mar 31 2s +3 R %z 1995 S 24 2s +4 - %z 1997 +4 R %z 2011 +4 AM %z Z Atlantic/Azores -1:42:40 - LMT 1884 -1:54:32 - HMT 1912 Ja 1 2u --2 p -02/-01 1942 Ap 25 22s --2 p +00 1942 Au 15 22s --2 p -02/-01 1943 Ap 17 22s --2 p +00 1943 Au 28 22s --2 p -02/-01 1944 Ap 22 22s --2 p +00 1944 Au 26 22s --2 p -02/-01 1945 Ap 21 22s --2 p +00 1945 Au 25 22s --2 p -02/-01 1966 Ap 3 2 --1 p -01/+00 1983 S 25 1s --1 W- -01/+00 1992 S 27 1s -0 E WE%sT 1993 Mar 28 1u --1 E -01/+00 +-2 p %z 1966 O 2 2s +-1 - %z 1982 Mar 28 0s +-1 p %z 1986 +-1 E %z 1992 D 27 1s +0 E WE%sT 1993 Jun 17 1u +-1 E %z Z Atlantic/Bermuda -4:19:18 - LMT 1890 -4:19:18 Be BMT/BST 1930 Ja 1 2 -4 Be A%sT 1974 Ap 28 2 -4 C A%sT 1976 -4 u A%sT Z Atlantic/Canary -1:1:36 - LMT 1922 Mar --1 - -01 1946 S 30 1 +-1 - %z 1946 S 30 1 0 - WET 1980 Ap 6 0s 0 1 WEST 1980 S 28 1u 0 E WE%sT Z Atlantic/Cape_Verde -1:34:4 - LMT 1912 Ja 1 2u --2 - -02 1942 S --2 1 -01 1945 O 15 --2 - -02 1975 N 25 2 --1 - -01 +-2 - %z 1942 S +-2 1 %z 1945 O 15 +-2 - %z 1975 N 25 2 +-1 - %z Z Atlantic/Faroe -0:27:4 - LMT 1908 Ja 11 0 - WET 1981 0 E WE%sT Z Atlantic/Madeira -1:7:36 - LMT 1884 -1:7:36 - FMT 1912 Ja 1 1u --1 p -01/+00 1942 Ap 25 22s --1 p +01 1942 Au 15 22s --1 p -01/+00 1943 Ap 17 22s --1 p +01 1943 Au 28 22s --1 p -01/+00 1944 Ap 22 22s --1 p +01 1944 Au 26 22s --1 p -01/+00 1945 Ap 21 22s --1 p +01 1945 Au 25 22s --1 p -01/+00 1966 Ap 3 2 -0 p WE%sT 1983 S 25 1s +-1 p %z 1966 O 2 2s +0 - WET 1982 Ap 4 +0 p WE%sT 1986 Jul 31 0 E WE%sT Z Atlantic/South_Georgia -2:26:8 - LMT 1890 --2 - -02 +-2 - %z Z Atlantic/Stanley -3:51:24 - LMT 1890 -3:51:24 - SMT 1912 Mar 12 --4 FK -04/-03 1983 May --3 FK -03/-02 1985 S 15 --4 FK -04/-03 2010 S 5 2 --3 - -03 +-4 FK %z 1983 May +-3 FK %z 1985 S 15 +-4 FK %z 2010 S 5 2 +-3 - %z Z Australia/Adelaide 9:14:20 - LMT 1895 F 9 - ACST 1899 May 9:30 AU AC%sT 1971 @@ -3550,8 +3536,8 @@ Z Australia/Darwin 8:43:20 - LMT 1895 F 9 - ACST 1899 May 9:30 AU AC%sT Z Australia/Eucla 8:35:28 - LMT 1895 D -8:45 AU +0845/+0945 1943 Jul -8:45 AW +0845/+0945 +8:45 AU %z 1943 Jul +8:45 AW %z Z Australia/Hobart 9:49:16 - LMT 1895 S 10 AT AE%sT 1919 O 24 10 AU AE%sT 1967 @@ -3562,8 +3548,8 @@ Z Australia/Lindeman 9:55:56 - LMT 1895 10 Ho AE%sT Z Australia/Lord_Howe 10:36:20 - LMT 1895 F 10 - AEST 1981 Mar -10:30 LH +1030/+1130 1985 Jul -10:30 LH +1030/+11 +10:30 LH %z 1985 Jul +10:30 LH %z Z Australia/Melbourne 9:39:52 - LMT 1895 F 10 AU AE%sT 1971 10 AV AE%sT @@ -3573,52 +3559,47 @@ Z Australia/Perth 7:43:24 - LMT 1895 D Z Australia/Sydney 10:4:52 - LMT 1895 F 10 AU AE%sT 1971 10 AN AE%sT -Z CET 1 c CE%sT -Z CST6CDT -6 u C%sT -Z EET 2 E EE%sT -Z EST -5 - EST -Z EST5EDT -5 u E%sT Z Etc/GMT 0 - GMT -Z Etc/GMT+1 -1 - -01 -Z Etc/GMT+10 -10 - -10 -Z Etc/GMT+11 -11 - -11 -Z Etc/GMT+12 -12 - -12 -Z Etc/GMT+2 -2 - -02 -Z Etc/GMT+3 -3 - -03 -Z Etc/GMT+4 -4 - -04 -Z Etc/GMT+5 -5 - -05 -Z Etc/GMT+6 -6 - -06 -Z Etc/GMT+7 -7 - -07 -Z Etc/GMT+8 -8 - -08 -Z Etc/GMT+9 -9 - -09 -Z Etc/GMT-1 1 - +01 -Z Etc/GMT-10 10 - +10 -Z Etc/GMT-11 11 - +11 -Z Etc/GMT-12 12 - +12 -Z Etc/GMT-13 13 - +13 -Z Etc/GMT-14 14 - +14 -Z Etc/GMT-2 2 - +02 -Z Etc/GMT-3 3 - +03 -Z Etc/GMT-4 4 - +04 -Z Etc/GMT-5 5 - +05 -Z Etc/GMT-6 6 - +06 -Z Etc/GMT-7 7 - +07 -Z Etc/GMT-8 8 - +08 -Z Etc/GMT-9 9 - +09 +Z Etc/GMT+1 -1 - %z +Z Etc/GMT+10 -10 - %z +Z Etc/GMT+11 -11 - %z +Z Etc/GMT+12 -12 - %z +Z Etc/GMT+2 -2 - %z +Z Etc/GMT+3 -3 - %z +Z Etc/GMT+4 -4 - %z +Z Etc/GMT+5 -5 - %z +Z Etc/GMT+6 -6 - %z +Z Etc/GMT+7 -7 - %z +Z Etc/GMT+8 -8 - %z +Z Etc/GMT+9 -9 - %z +Z Etc/GMT-1 1 - %z +Z Etc/GMT-10 10 - %z +Z Etc/GMT-11 11 - %z +Z Etc/GMT-12 12 - %z +Z Etc/GMT-13 13 - %z +Z Etc/GMT-14 14 - %z +Z Etc/GMT-2 2 - %z +Z Etc/GMT-3 3 - %z +Z Etc/GMT-4 4 - %z +Z Etc/GMT-5 5 - %z +Z Etc/GMT-6 6 - %z +Z Etc/GMT-7 7 - %z +Z Etc/GMT-8 8 - %z +Z Etc/GMT-9 9 - %z Z Etc/UTC 0 - UTC Z Europe/Andorra 0:6:4 - LMT 1901 0 - WET 1946 S 30 1 - CET 1985 Mar 31 2 1 E CE%sT Z Europe/Astrakhan 3:12:12 - LMT 1924 May -3 - +03 1930 Jun 21 -4 R +04/+05 1989 Mar 26 2s -3 R +03/+04 1991 Mar 31 2s -4 - +04 1992 Mar 29 2s -3 R +03/+04 2011 Mar 27 2s -4 - +04 2014 O 26 2s -3 - +03 2016 Mar 27 2s -4 - +04 +3 - %z 1930 Jun 21 +4 R %z 1989 Mar 26 2s +3 R %z 1991 Mar 31 2s +4 - %z 1992 Mar 29 2s +3 R %z 2011 Mar 27 2s +4 - %z 2014 O 26 2s +3 - %z 2016 Mar 27 2s +4 - %z Z Europe/Athens 1:34:52 - LMT 1895 S 14 1:34:52 - AMT 1916 Jul 28 0:1 2 g EE%sT 1941 Ap 30 @@ -3691,7 +3672,7 @@ Z Europe/Helsinki 1:39:49 - LMT 1878 May 31 Z Europe/Istanbul 1:55:52 - LMT 1880 1:56:56 - IMT 1910 O 2 T EE%sT 1978 Jun 29 -3 T +03/+04 1984 N 1 2 +3 T %z 1984 N 1 2 2 T EE%sT 2007 2 E EE%sT 2011 Mar 27 1u 2 - EET 2011 Mar 28 1u @@ -3700,19 +3681,19 @@ Z Europe/Istanbul 1:55:52 - LMT 1880 2 E EE%sT 2015 O 25 1u 2 1 EEST 2015 N 8 1u 2 E EE%sT 2016 S 7 -3 - +03 +3 - %z Z Europe/Kaliningrad 1:22 - LMT 1893 Ap 1 c CE%sT 1945 Ap 10 2 O EE%sT 1946 Ap 7 3 R MSK/MSD 1989 Mar 26 2s 2 R EE%sT 2011 Mar 27 2s -3 - +03 2014 O 26 2s +3 - %z 2014 O 26 2s 2 - EET Z Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0u -3 - +03 1930 Jun 21 -4 R +04/+05 1989 Mar 26 2s +3 - %z 1930 Jun 21 +4 R %z 1989 Mar 26 2s 3 R MSK/MSD 1991 Mar 31 2s -4 - +04 1992 Mar 29 2s +4 - %z 1992 Mar 29 2s 3 R MSK/MSD 2011 Mar 27 2s 4 - MSK 2014 O 26 2s 3 - MSK @@ -3727,10 +3708,10 @@ Z Europe/Kyiv 2:2:4 - LMT 1880 2 E EE%sT Z Europe/Lisbon -0:36:45 - LMT 1884 -0:36:45 - LMT 1912 Ja 1 0u -0 p WE%sT 1966 Ap 3 2 +0 p WE%sT 1966 O 2 2s 1 - CET 1976 S 26 1 -0 p WE%sT 1983 S 25 1s -0 W- WE%sT 1992 S 27 1s +0 p WE%sT 1986 +0 E WE%sT 1992 S 27 1u 1 E CE%sT 1996 Mar 31 1u 0 E WE%sT Z Europe/London -0:1:15 - LMT 1847 D @@ -3754,7 +3735,7 @@ Z Europe/Minsk 1:50:16 - LMT 1880 3 R MSK/MSD 1990 3 - MSK 1991 Mar 31 2s 2 R EE%sT 2011 Mar 27 2s -3 - +03 +3 - %z Z Europe/Moscow 2:30:17 - LMT 1880 2:30:17 - MMT 1916 Jul 3 2:31:19 R %s 1919 Jul 1 0u @@ -3802,24 +3783,24 @@ Z Europe/Rome 0:49:56 - LMT 1866 D 12 1 I CE%sT 1980 1 E CE%sT Z Europe/Samara 3:20:20 - LMT 1919 Jul 1 0u -3 - +03 1930 Jun 21 -4 - +04 1935 Ja 27 -4 R +04/+05 1989 Mar 26 2s -3 R +03/+04 1991 Mar 31 2s -2 R +02/+03 1991 S 29 2s -3 - +03 1991 O 20 3 -4 R +04/+05 2010 Mar 28 2s -3 R +03/+04 2011 Mar 27 2s -4 - +04 +3 - %z 1930 Jun 21 +4 - %z 1935 Ja 27 +4 R %z 1989 Mar 26 2s +3 R %z 1991 Mar 31 2s +2 R %z 1991 S 29 2s +3 - %z 1991 O 20 3 +4 R %z 2010 Mar 28 2s +3 R %z 2011 Mar 27 2s +4 - %z Z Europe/Saratov 3:4:18 - LMT 1919 Jul 1 0u -3 - +03 1930 Jun 21 -4 R +04/+05 1988 Mar 27 2s -3 R +03/+04 1991 Mar 31 2s -4 - +04 1992 Mar 29 2s -3 R +03/+04 2011 Mar 27 2s -4 - +04 2014 O 26 2s -3 - +03 2016 D 4 2s -4 - +04 +3 - %z 1930 Jun 21 +4 R %z 1988 Mar 27 2s +3 R %z 1991 Mar 31 2s +4 - %z 1992 Mar 29 2s +3 R %z 2011 Mar 27 2s +4 - %z 2014 O 26 2s +3 - %z 2016 D 4 2s +4 - %z Z Europe/Simferopol 2:16:24 - LMT 1880 2:16 - SMT 1924 May 2 2 - EET 1930 Jun 21 @@ -3863,14 +3844,14 @@ Z Europe/Tirane 1:19:20 - LMT 1914 1 q CE%sT 1984 Jul 1 E CE%sT Z Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0u -3 - +03 1930 Jun 21 -4 R +04/+05 1989 Mar 26 2s -3 R +03/+04 1991 Mar 31 2s -2 R +02/+03 1992 Ja 19 2s -3 R +03/+04 2011 Mar 27 2s -4 - +04 2014 O 26 2s -3 - +03 2016 Mar 27 2s -4 - +04 +3 - %z 1930 Jun 21 +4 R %z 1989 Mar 26 2s +3 R %z 1991 Mar 31 2s +2 R %z 1992 Ja 19 2s +3 R %z 2011 Mar 27 2s +4 - %z 2014 O 26 2s +3 - %z 2016 Mar 27 2s +4 - %z Z Europe/Vienna 1:5:21 - LMT 1893 Ap 1 c CE%sT 1920 1 a CE%sT 1940 Ap 1 2s @@ -3895,15 +3876,15 @@ Z Europe/Vilnius 1:41:16 - LMT 1880 2 - EET 2003 2 E EE%sT Z Europe/Volgograd 2:57:40 - LMT 1920 Ja 3 -3 - +03 1930 Jun 21 -4 - +04 1961 N 11 -4 R +04/+05 1988 Mar 27 2s +3 - %z 1930 Jun 21 +4 - %z 1961 N 11 +4 R %z 1988 Mar 27 2s 3 R MSK/MSD 1991 Mar 31 2s -4 - +04 1992 Mar 29 2s +4 - %z 1992 Mar 29 2s 3 R MSK/MSD 2011 Mar 27 2s 4 - MSK 2014 O 26 2s 3 - MSK 2018 O 28 2s -4 - +04 2020 D 27 2s +4 - %z 2020 D 27 2s 3 - MSK Z Europe/Warsaw 1:24 - LMT 1880 1:24 - WMT 1915 Au 5 @@ -3919,58 +3900,53 @@ Z Europe/Zurich 0:34:8 - LMT 1853 Jul 16 1 CH CE%sT 1981 1 E CE%sT Z Factory 0 - -00 -Z HST -10 - HST Z Indian/Chagos 4:49:40 - LMT 1907 -5 - +05 1996 -6 - +06 +5 - %z 1996 +6 - %z Z Indian/Maldives 4:54 - LMT 1880 4:54 - MMT 1960 -5 - +05 +5 - %z Z Indian/Mauritius 3:50 - LMT 1907 -4 MU +04/+05 -Z MET 1 c ME%sT -Z MST -7 - MST -Z MST7MDT -7 u M%sT -Z PST8PDT -8 u P%sT +4 MU %z Z Pacific/Apia 12:33:4 - LMT 1892 Jul 5 -11:26:56 - LMT 1911 --11:30 - -1130 1950 --11 WS -11/-10 2011 D 29 24 -13 WS +13/+14 +-11:30 - %z 1950 +-11 WS %z 2011 D 29 24 +13 WS %z Z Pacific/Auckland 11:39:4 - LMT 1868 N 2 11:30 NZ NZ%sT 1946 12 NZ NZ%sT Z Pacific/Bougainville 10:22:16 - LMT 1880 9:48:32 - PMMT 1895 -10 - +10 1942 Jul -9 - +09 1945 Au 21 -10 - +10 2014 D 28 2 -11 - +11 +10 - %z 1942 Jul +9 - %z 1945 Au 21 +10 - %z 2014 D 28 2 +11 - %z Z Pacific/Chatham 12:13:48 - LMT 1868 N 2 -12:15 - +1215 1946 -12:45 k +1245/+1345 +12:15 - %z 1946 +12:45 k %z Z Pacific/Easter -7:17:28 - LMT 1890 -7:17:28 - EMT 1932 S --7 x -07/-06 1982 Mar 14 3u --6 x -06/-05 +-7 x %z 1982 Mar 14 3u +-6 x %z Z Pacific/Efate 11:13:16 - LMT 1912 Ja 13 -11 VU +11/+12 +11 VU %z Z Pacific/Fakaofo -11:24:56 - LMT 1901 --11 - -11 2011 D 30 -13 - +13 +-11 - %z 2011 D 30 +13 - %z Z Pacific/Fiji 11:55:44 - LMT 1915 O 26 -12 FJ +12/+13 +12 FJ %z Z Pacific/Galapagos -5:58:24 - LMT 1931 --5 - -05 1986 --6 EC -06/-05 +-5 - %z 1986 +-6 EC %z Z Pacific/Gambier -8:59:48 - LMT 1912 O --9 - -09 +-9 - %z Z Pacific/Guadalcanal 10:39:48 - LMT 1912 O -11 - +11 +11 - %z Z Pacific/Guam -14:21 - LMT 1844 D 31 9:39 - LMT 1901 10 - GST 1941 D 10 -9 - +09 1944 Jul 31 +9 - %z 1944 Jul 31 10 Gu G%sT 2000 D 23 10 - ChST Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12 @@ -3979,74 +3955,73 @@ Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12 -10:30 u H%sT 1947 Jun 8 2 -10 - HST Z Pacific/Kanton 0 - -00 1937 Au 31 --12 - -12 1979 O --11 - -11 1994 D 31 -13 - +13 +-12 - %z 1979 O +-11 - %z 1994 D 31 +13 - %z Z Pacific/Kiritimati -10:29:20 - LMT 1901 --10:40 - -1040 1979 O --10 - -10 1994 D 31 -14 - +14 +-10:40 - %z 1979 O +-10 - %z 1994 D 31 +14 - %z Z Pacific/Kosrae -13:8:4 - LMT 1844 D 31 10:51:56 - LMT 1901 -11 - +11 1914 O -9 - +09 1919 F -11 - +11 1937 -10 - +10 1941 Ap -9 - +09 1945 Au -11 - +11 1969 O -12 - +12 1999 -11 - +11 +11 - %z 1914 O +9 - %z 1919 F +11 - %z 1937 +10 - %z 1941 Ap +9 - %z 1945 Au +11 - %z 1969 O +12 - %z 1999 +11 - %z Z Pacific/Kwajalein 11:9:20 - LMT 1901 -11 - +11 1937 -10 - +10 1941 Ap -9 - +09 1944 F 6 -11 - +11 1969 O --12 - -12 1993 Au 20 24 -12 - +12 +11 - %z 1937 +10 - %z 1941 Ap +9 - %z 1944 F 6 +11 - %z 1969 O +-12 - %z 1993 Au 20 24 +12 - %z Z Pacific/Marquesas -9:18 - LMT 1912 O --9:30 - -0930 +-9:30 - %z Z Pacific/Nauru 11:7:40 - LMT 1921 Ja 15 -11:30 - +1130 1942 Au 29 -9 - +09 1945 S 8 -11:30 - +1130 1979 F 10 2 -12 - +12 +11:30 - %z 1942 Au 29 +9 - %z 1945 S 8 +11:30 - %z 1979 F 10 2 +12 - %z Z Pacific/Niue -11:19:40 - LMT 1952 O 16 --11:20 - -1120 1964 Jul --11 - -11 +-11:20 - %z 1964 Jul +-11 - %z Z Pacific/Norfolk 11:11:52 - LMT 1901 -11:12 - +1112 1951 -11:30 - +1130 1974 O 27 2s -11:30 1 +1230 1975 Mar 2 2s -11:30 - +1130 2015 O 4 2s -11 - +11 2019 Jul -11 AN +11/+12 +11:12 - %z 1951 +11:30 - %z 1974 O 27 2s +11:30 1 %z 1975 Mar 2 2s +11:30 - %z 2015 O 4 2s +11 - %z 2019 Jul +11 AN %z Z Pacific/Noumea 11:5:48 - LMT 1912 Ja 13 -11 NC +11/+12 +11 NC %z Z Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5 -11:22:48 - LMT 1911 -11 - SST Z Pacific/Palau -15:2:4 - LMT 1844 D 31 8:57:56 - LMT 1901 -9 - +09 +9 - %z Z Pacific/Pitcairn -8:40:20 - LMT 1901 --8:30 - -0830 1998 Ap 27 --8 - -08 +-8:30 - %z 1998 Ap 27 +-8 - %z Z Pacific/Port_Moresby 9:48:40 - LMT 1880 9:48:32 - PMMT 1895 -10 - +10 +10 - %z Z Pacific/Rarotonga 13:20:56 - LMT 1899 D 26 -10:39:4 - LMT 1952 O 16 --10:30 - -1030 1978 N 12 --10 CK -10/-0930 +-10:30 - %z 1978 N 12 +-10 CK %z Z Pacific/Tahiti -9:58:16 - LMT 1912 O --10 - -10 +-10 - %z Z Pacific/Tarawa 11:32:4 - LMT 1901 -12 - +12 +12 - %z Z Pacific/Tongatapu 12:19:12 - LMT 1945 S 10 -12:20 - +1220 1961 -13 - +13 1999 -13 TO +13/+14 -Z WET 0 E WE%sT +12:20 - %z 1961 +13 - %z 1999 +13 TO %z L Etc/GMT GMT L Australia/Sydney Australia/ACT L Australia/Lord_Howe Australia/LHI @@ -4062,6 +4037,8 @@ L America/Rio_Branco Brazil/Acre L America/Noronha Brazil/DeNoronha L America/Sao_Paulo Brazil/East L America/Manaus Brazil/West +L Europe/Brussels CET +L America/Chicago CST6CDT L America/Halifax Canada/Atlantic L America/Winnipeg Canada/Central L America/Toronto Canada/Eastern @@ -4073,6 +4050,9 @@ L America/Whitehorse Canada/Yukon L America/Santiago Chile/Continental L Pacific/Easter Chile/EasterIsland L America/Havana Cuba +L Europe/Athens EET +L America/Panama EST +L America/New_York EST5EDT L Africa/Cairo Egypt L Europe/Dublin Eire L Etc/GMT Etc/GMT+0 @@ -4096,6 +4076,9 @@ L America/Jamaica Jamaica L Asia/Tokyo Japan L Pacific/Kwajalein Kwajalein L Africa/Tripoli Libya +L Europe/Brussels MET +L America/Phoenix MST +L America/Denver MST7MDT L America/Tijuana Mexico/BajaNorte L America/Mazatlan Mexico/BajaSur L America/Mexico_City Mexico/General @@ -4259,6 +4242,7 @@ L America/Denver America/Shiprock L America/Toronto America/Thunder_Bay L America/Edmonton America/Yellowknife L Pacific/Auckland Antarctica/South_Pole +L Asia/Ulaanbaatar Asia/Choibalsan L Asia/Shanghai Asia/Chongqing L Asia/Shanghai Asia/Harbin L Asia/Urumqi Asia/Kashgar @@ -4273,6 +4257,7 @@ L Europe/Kyiv Europe/Zaporozhye L Pacific/Kanton Pacific/Enderbury L Pacific/Honolulu Pacific/Johnston L Pacific/Port_Moresby Pacific/Yap +L Europe/Lisbon WET L Africa/Nairobi Africa/Asmera L America/Nuuk America/Godthab L Asia/Ashgabat Asia/Ashkhabad @@ -4290,5 +4275,7 @@ L Asia/Ulaanbaatar Asia/Ulan_Bator L Atlantic/Faroe Atlantic/Faeroe L Europe/Kyiv Europe/Kiev L Asia/Nicosia Europe/Nicosia +L Pacific/Honolulu HST +L America/Los_Angeles PST8PDT L Pacific/Guadalcanal Pacific/Ponape L Pacific/Port_Moresby Pacific/Truk diff --git a/lib/pytz/zoneinfo/zone.tab b/lib/pytz/zoneinfo/zone.tab index 3fa9306a..bfc0b593 100644 --- a/lib/pytz/zoneinfo/zone.tab +++ b/lib/pytz/zoneinfo/zone.tab @@ -264,8 +264,7 @@ MK +4159+02126 Europe/Skopje ML +1239-00800 Africa/Bamako MM +1647+09610 Asia/Yangon MN +4755+10653 Asia/Ulaanbaatar most of Mongolia -MN +4801+09139 Asia/Hovd Bayan-Olgiy, Govi-Altai, Hovd, Uvs, Zavkhan -MN +4804+11430 Asia/Choibalsan Dornod, Sukhbaatar +MN +4801+09139 Asia/Hovd Bayan-Olgii, Hovd, Uvs MO +221150+1133230 Asia/Macau MP +1512+14545 Pacific/Saipan MQ +1436-06105 America/Martinique diff --git a/lib/pytz/zoneinfo/zone1970.tab b/lib/pytz/zoneinfo/zone1970.tab index abd94897..7726f39a 100644 --- a/lib/pytz/zoneinfo/zone1970.tab +++ b/lib/pytz/zoneinfo/zone1970.tab @@ -209,8 +209,7 @@ MD +4700+02850 Europe/Chisinau MH +0905+16720 Pacific/Kwajalein Kwajalein MM,CC +1647+09610 Asia/Yangon MN +4755+10653 Asia/Ulaanbaatar most of Mongolia -MN +4801+09139 Asia/Hovd Bayan-Ölgii, Govi-Altai, Hovd, Uvs, Zavkhan -MN +4804+11430 Asia/Choibalsan Dornod, Sükhbaatar +MN +4801+09139 Asia/Hovd Bayan-Ölgii, Hovd, Uvs MO +221150+1133230 Asia/Macau MQ +1436-06105 America/Martinique MT +3554+01431 Europe/Malta diff --git a/lib/pytz/zoneinfo/zonenow.tab b/lib/pytz/zoneinfo/zonenow.tab index b6f29109..01f536b3 100644 --- a/lib/pytz/zoneinfo/zonenow.tab +++ b/lib/pytz/zoneinfo/zonenow.tab @@ -5,7 +5,7 @@ # From Paul Eggert (2023-12-18): # This file contains a table where each row stands for a timezone # where civil timestamps are predicted to agree from now on. -# This file is like zone1970.tab (see zone1970.tab's coments), +# This file is like zone1970.tab (see zone1970.tab's comments), # but with the following changes: # # 1. Each timezone corresponds to a set of clocks that are planned @@ -123,8 +123,6 @@ XX +1455-02331 Atlantic/Cape_Verde Cape Verde # # -01/+00 (EU DST) XX +3744-02540 Atlantic/Azores Azores -# -01/+00 (EU DST) until 2024-03-31; then -02/-01 (EU DST) -XX +7029-02158 America/Scoresbysund Ittoqqortoormiit # # +00 - GMT XX +0519-00402 Africa/Abidjan far western Africa; Iceland ("GMT") @@ -199,7 +197,7 @@ XX +2518+05518 Asia/Dubai Russia; Caucasus; Persian Gulf; Seychelles; Réunion XX +3431+06912 Asia/Kabul Afghanistan # # +05 -XX +4120+06918 Asia/Tashkent Russia; west Kazakhstan; Tajikistan; Turkmenistan; Uzbekistan; Maldives +XX +4120+06918 Asia/Tashkent Russia; Kazakhstan; Tajikistan; Turkmenistan; Uzbekistan; Maldives # # +05 - PKT XX +2452+06703 Asia/Karachi Pakistan ("PKT") @@ -215,8 +213,6 @@ XX +2743+08519 Asia/Kathmandu Nepal # # +06 XX +2343+09025 Asia/Dhaka Russia; Kyrgyzstan; Bhutan; Bangladesh; Chagos -# +06 until 2024-03-01; then +05 -XX +4315+07657 Asia/Almaty Kazakhstan (except western areas) # # +06:30 XX +1647+09610 Asia/Yangon Myanmar; Cocos diff --git a/lib/simplejson/__init__.py b/lib/simplejson/__init__.py index 2d1900d8..d3ff141d 100644 --- a/lib/simplejson/__init__.py +++ b/lib/simplejson/__init__.py @@ -118,7 +118,7 @@ Serializing multiple objects to JSON lines (newline-delimited JSON):: """ from __future__ import absolute_import -__version__ = '3.19.2' +__version__ = '3.19.3' __all__ = [ 'dump', 'dumps', 'load', 'loads', 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', diff --git a/lib/zipp/__init__.py b/lib/zipp/__init__.py index d65297b8..031d9d4f 100644 --- a/lib/zipp/__init__.py +++ b/lib/zipp/__init__.py @@ -1,16 +1,27 @@ +""" +A Path-like interface for zipfiles. + +This codebase is shared between zipfile.Path in the stdlib +and zipp in PyPI. See +https://github.com/python/importlib_metadata/wiki/Development-Methodology +for more detail. +""" + +import functools import io -import posixpath -import zipfile import itertools -import contextlib import pathlib +import posixpath import re import stat import sys +import zipfile from .compat.py310 import text_encoding from .glob import Translator +from ._functools import save_method_args + __all__ = ['Path'] @@ -37,7 +48,7 @@ def _parents(path): def _ancestry(path): """ Given a path with elements separated by - posixpath.sep, generate all elements of that path + posixpath.sep, generate all elements of that path. >>> list(_ancestry('b/d')) ['b/d', 'b'] @@ -49,9 +60,14 @@ def _ancestry(path): ['b'] >>> list(_ancestry('')) [] + + Multiple separators are treated like a single. + + >>> list(_ancestry('//b//d///f//')) + ['//b//d///f', '//b//d', '//b'] """ path = path.rstrip(posixpath.sep) - while path and path != posixpath.sep: + while path.rstrip(posixpath.sep): yield path path, tail = posixpath.split(path) @@ -73,82 +89,19 @@ class InitializedState: Mix-in to save the initialization state for pickling. """ + @save_method_args def __init__(self, *args, **kwargs): - self.__args = args - self.__kwargs = kwargs super().__init__(*args, **kwargs) def __getstate__(self): - return self.__args, self.__kwargs + return self._saved___init__.args, self._saved___init__.kwargs def __setstate__(self, state): args, kwargs = state super().__init__(*args, **kwargs) -class SanitizedNames: - """ - ZipFile mix-in to ensure names are sanitized. - """ - - def namelist(self): - return list(map(self._sanitize, super().namelist())) - - @staticmethod - def _sanitize(name): - r""" - Ensure a relative path with posix separators and no dot names. - - Modeled after - https://github.com/python/cpython/blob/bcc1be39cb1d04ad9fc0bd1b9193d3972835a57c/Lib/zipfile/__init__.py#L1799-L1813 - but provides consistent cross-platform behavior. - - >>> san = SanitizedNames._sanitize - >>> san('/foo/bar') - 'foo/bar' - >>> san('//foo.txt') - 'foo.txt' - >>> san('foo/.././bar.txt') - 'foo/bar.txt' - >>> san('foo../.bar.txt') - 'foo../.bar.txt' - >>> san('\\foo\\bar.txt') - 'foo/bar.txt' - >>> san('D:\\foo.txt') - 'D/foo.txt' - >>> san('\\\\server\\share\\file.txt') - 'server/share/file.txt' - >>> san('\\\\?\\GLOBALROOT\\Volume3') - '?/GLOBALROOT/Volume3' - >>> san('\\\\.\\PhysicalDrive1\\root') - 'PhysicalDrive1/root' - - Retain any trailing slash. - >>> san('abc/') - 'abc/' - - Raises a ValueError if the result is empty. - >>> san('../..') - Traceback (most recent call last): - ... - ValueError: Empty filename - """ - - def allowed(part): - return part and part not in {'..', '.'} - - # Remove the drive letter. - # Don't use ntpath.splitdrive, because that also strips UNC paths - bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE) - clean = bare.replace('\\', '/') - parts = clean.split('/') - joined = '/'.join(filter(allowed, parts)) - if not joined: - raise ValueError("Empty filename") - return joined + '/' * name.endswith('/') - - -class CompleteDirs(InitializedState, SanitizedNames, zipfile.ZipFile): +class CompleteDirs(InitializedState, zipfile.ZipFile): """ A ZipFile subclass that ensures that implied directories are always included in the namelist. @@ -230,16 +183,18 @@ class FastLookup(CompleteDirs): """ def namelist(self): - with contextlib.suppress(AttributeError): - return self.__names - self.__names = super().namelist() - return self.__names + return self._namelist + + @functools.cached_property + def _namelist(self): + return super().namelist() def _name_set(self): - with contextlib.suppress(AttributeError): - return self.__lookup - self.__lookup = super()._name_set() - return self.__lookup + return self._name_set_prop + + @functools.cached_property + def _name_set_prop(self): + return super()._name_set() def _extract_text_encoding(encoding=None, *args, **kwargs): @@ -329,7 +284,7 @@ class Path: >>> str(path.parent) 'mem' - If the zipfile has no filename, such attributes are not + If the zipfile has no filename, such attributes are not valid and accessing them will raise an Exception. >>> zf.filename = None @@ -388,7 +343,7 @@ class Path: if self.is_dir(): raise IsADirectoryError(self) zip_mode = mode[0] - if not self.exists() and zip_mode == 'r': + if zip_mode == 'r' and not self.exists(): raise FileNotFoundError(self) stream = self.root.open(self.at, zip_mode, pwd=pwd) if 'b' in mode: @@ -470,8 +425,7 @@ class Path: prefix = re.escape(self.at) tr = Translator(seps='/') matches = re.compile(prefix + tr.translate(pattern)).fullmatch - names = (data.filename for data in self.root.filelist) - return map(self._next, filter(matches, names)) + return map(self._next, filter(matches, self.root.namelist())) def rglob(self, pattern): return self.glob(f'**/{pattern}') diff --git a/lib/zipp/_functools.py b/lib/zipp/_functools.py new file mode 100644 index 00000000..f75ae2b0 --- /dev/null +++ b/lib/zipp/_functools.py @@ -0,0 +1,20 @@ +import collections +import functools + + +# from jaraco.functools 4.0.2 +def save_method_args(method): + """ + Wrap a method such that when it is called, the args and kwargs are + saved on the method. + """ + args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') + + @functools.wraps(method) + def wrapper(self, /, *args, **kwargs): + attr_name = '_saved_' + method.__name__ + attr = args_and_kwargs(args, kwargs) + setattr(self, attr_name, attr) + return method(self, *args, **kwargs) + + return wrapper diff --git a/lib/zipp/compat/overlay.py b/lib/zipp/compat/overlay.py new file mode 100644 index 00000000..5a97ee7c --- /dev/null +++ b/lib/zipp/compat/overlay.py @@ -0,0 +1,37 @@ +""" +Expose zipp.Path as .zipfile.Path. + +Includes everything else in ``zipfile`` to match future usage. Just +use: + +>>> from zipp.compat.overlay import zipfile + +in place of ``import zipfile``. + +Relative imports are supported too. + +>>> from zipp.compat.overlay.zipfile import ZipInfo + +The ``zipfile`` object added to ``sys.modules`` needs to be +hashable (#126). + +>>> _ = hash(sys.modules['zipp.compat.overlay.zipfile']) +""" + +import importlib +import sys +import types + +import zipp + + +class HashableNamespace(types.SimpleNamespace): + def __hash__(self): + return hash(tuple(vars(self))) + + +zipfile = HashableNamespace(**vars(importlib.import_module('zipfile'))) +zipfile.Path = zipp.Path +zipfile._path = zipp + +sys.modules[__name__ + '.zipfile'] = zipfile # type: ignore[assignment] diff --git a/lib/zipp/compat/py310.py b/lib/zipp/compat/py310.py index d5ca53e0..e1e7ec22 100644 --- a/lib/zipp/compat/py310.py +++ b/lib/zipp/compat/py310.py @@ -1,5 +1,5 @@ -import sys import io +import sys def _text_encoding(encoding, stacklevel=2, /): # pragma: no cover @@ -7,5 +7,7 @@ def _text_encoding(encoding, stacklevel=2, /): # pragma: no cover text_encoding = ( - io.text_encoding if sys.version_info > (3, 10) else _text_encoding # type: ignore + io.text_encoding # type: ignore[unused-ignore, attr-defined] + if sys.version_info > (3, 10) + else _text_encoding ) diff --git a/lib/zipp/glob.py b/lib/zipp/glob.py index 69c41d77..4ed74cc4 100644 --- a/lib/zipp/glob.py +++ b/lib/zipp/glob.py @@ -1,7 +1,6 @@ import os import re - _default_seps = os.sep + str(os.altsep) * bool(os.altsep) @@ -28,7 +27,7 @@ class Translator: """ Given a glob pattern, produce a regex that matches it. """ - return self.extend(self.translate_core(pattern)) + return self.extend(self.match_dirs(self.translate_core(pattern))) def extend(self, pattern): r""" @@ -41,6 +40,14 @@ class Translator: """ return rf'(?s:{pattern})\Z' + def match_dirs(self, pattern): + """ + Ensure that zipfile.Path directory names are matched. + + zipfile.Path directory names always end in a slash. + """ + return rf'{pattern}[/]?' + def translate_core(self, pattern): r""" Given a glob pattern, produce a regex that matches it. diff --git a/package/requirements-package.txt b/package/requirements-package.txt index 9ac6fba1..7e3c7a1c 100644 --- a/package/requirements-package.txt +++ b/package/requirements-package.txt @@ -1,7 +1,7 @@ apscheduler==3.10.1 cryptography==43.0.0 -importlib-metadata==8.2.0 -importlib-resources==6.4.0 +importlib-metadata==8.5.0 +importlib-resources==6.4.5 pyinstaller==6.8.0 pyopenssl==24.2.1 diff --git a/plexpy/helpers.py b/plexpy/helpers.py index 014f1359..fdb79830 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -24,8 +24,6 @@ from cloudinary.utils import cloudinary_url from collections import OrderedDict from datetime import date, datetime, timezone from functools import reduce, wraps -import hashlib -import imghdr from itertools import groupby from future.moves.itertools import islice, zip_longest from ipaddress import ip_address, ip_network, IPv4Address @@ -272,7 +270,8 @@ def human_duration(ms, sig='dhm', units='ms', return_seconds=300000): if return_seconds and ms < return_seconds: sig = 'dhms' - ms = ms * factors[units] + r = factors[sig[-1]] + ms = round(ms * factors[units] / r) * r d, h = divmod(ms, factors['d']) h, m = divmod(h, factors['h']) @@ -942,39 +941,6 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100, return url -def cache_image(url, image=None): - """ - Saves an image to the cache directory. - If no image is provided, tries to return the image from the cache directory. - """ - # Create image directory if it doesn't exist - imgdir = os.path.join(plexpy.CONFIG.CACHE_DIR, 'images/') - if not os.path.exists(imgdir): - logger.debug("Tautulli Helpers :: Creating image cache directory at %s" % imgdir) - os.makedirs(imgdir) - - # Create a hash of the url to use as the filename - imghash = hashlib.md5(url).hexdigest() - imagefile = os.path.join(imgdir, imghash) - - # If an image is provided, save it to the cache directory - if image: - try: - with open(imagefile, 'wb') as cache_file: - cache_file.write(image) - except IOError as e: - logger.error("Tautulli Helpers :: Failed to cache image %s: %s" % (imagefile, e)) - - # Try to return the image from the cache directory - if os.path.isfile(imagefile): - imagetype = 'image/' + imghdr.what(os.path.abspath(imagefile)) - else: - imagefile = None - imagetype = 'image/jpeg' - - return imagefile, imagetype - - def build_datatables_json(kwargs, dt_columns, default_sort_col=None): """ Builds datatables json data diff --git a/plexpy/newsletter_handler.py b/plexpy/newsletter_handler.py index 69d646d9..05cb52fa 100644 --- a/plexpy/newsletter_handler.py +++ b/plexpy/newsletter_handler.py @@ -17,6 +17,7 @@ from io import open import os +import shlex from apscheduler.triggers.cron import CronTrigger import email.utils @@ -58,25 +59,36 @@ def schedule_newsletters(newsletter_id=None): def schedule_newsletter_job(newsletter_job_id, name='', func=None, remove_job=False, args=None, cron=None): - # apscheduler day_of_week uses 0-6 = mon-sun if cron: - cron = cron.split(' ') - cron[4] = str((int(cron[4]) - 1) % 7) if cron[4].isdigit() else cron[4] - cron = ' '.join(cron) + values = shlex.split(cron) + # apscheduler day_of_week uses 0-6 = mon-sun + values[4] = str((int(values[4]) - 1) % 7) if values[4].isdigit() else values[4] if NEWSLETTER_SCHED.get_job(newsletter_job_id): if remove_job: NEWSLETTER_SCHED.remove_job(newsletter_job_id) logger.info("Tautulli NewsletterHandler :: Removed scheduled newsletter: %s" % name) else: - NEWSLETTER_SCHED.reschedule_job( - newsletter_job_id, args=args, trigger=CronTrigger.from_crontab(cron)) - logger.info("Tautulli NewsletterHandler :: Re-scheduled newsletter: %s" % name) + try: + NEWSLETTER_SCHED.reschedule_job( + newsletter_job_id, args=args, trigger=CronTrigger( + minute=values[0], hour=values[1], day=values[2], month=values[3], day_of_week=values[4] + ) + ) + logger.info("Tautulli NewsletterHandler :: Re-scheduled newsletter: %s" % name) + except ValueError as e: + logger.error("Tautulli NewsletterHandler :: Failed to re-schedule newsletter: %s" % e) elif not remove_job: - NEWSLETTER_SCHED.add_job( - func, args=args, id=newsletter_job_id, trigger=CronTrigger.from_crontab(cron), - misfire_grace_time=None) - logger.info("Tautulli NewsletterHandler :: Scheduled newsletter: %s" % name) + try: + NEWSLETTER_SCHED.add_job( + func, args=args, id=newsletter_job_id, trigger=CronTrigger( + minute=values[0], hour=values[1], day=values[2], month=values[3], day_of_week=values[4] + ), + misfire_grace_time=None + ) + logger.info("Tautulli NewsletterHandler :: Scheduled newsletter: %s" % name) + except ValueError as e: + logger.error("Tautulli NewsletterHandler :: Failed to schedule newsletter: %s" % e) def notify(newsletter_id=None, notify_action=None, **kwargs): diff --git a/plexpy/newsletters.py b/plexpy/newsletters.py index 7641decf..b49077a2 100644 --- a/plexpy/newsletters.py +++ b/plexpy/newsletters.py @@ -586,6 +586,8 @@ class Newsletter(object): return parameters def _build_params(self): + from plexpy.notification_handler import CustomArrow + date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT) if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL: @@ -595,8 +597,8 @@ class Newsletter(object): parameters = { 'server_name': helpers.pms_name(), - 'start_date': self.start_date.format(date_format), - 'end_date': self.end_date.format(date_format), + 'start_date': CustomArrow(self.start_date, date_format), + 'end_date': CustomArrow(self.end_date, date_format), 'current_year': self.start_date.year, 'current_month': self.start_date.month, 'current_day': self.start_date.day, diff --git a/plexpy/version.py b/plexpy/version.py index feccd9cc..08fcf9e3 100644 --- a/plexpy/version.py +++ b/plexpy/version.py @@ -16,4 +16,4 @@ # along with Tautulli. If not, see . PLEXPY_BRANCH = "master" -PLEXPY_RELEASE_VERSION = "v2.14.4" \ No newline at end of file +PLEXPY_RELEASE_VERSION = "v2.14.6" \ No newline at end of file diff --git a/plexpy/versioncheck.py b/plexpy/versioncheck.py index 2c161dd3..9f546594 100644 --- a/plexpy/versioncheck.py +++ b/plexpy/versioncheck.py @@ -281,7 +281,7 @@ def check_github(scheduler=False, notify=False, use_cache=False): 'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND}) - elif scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and \ + if scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and \ not plexpy.DOCKER and not plexpy.SNAP and not plexpy.FROZEN: logger.info('Running automatic update.') plexpy.shutdown(restart=True, update=True) diff --git a/requirements.txt b/requirements.txt index 67f60dd1..9834d3ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ arrow==1.3.0 backports.zoneinfo==0.2.1;python_version<"3.9" beautifulsoup4==4.12.3 bleach==6.1.0 -certifi==2024.7.4 +certifi==2024.8.30 cheroot==10.0.1 cherrypy==18.10.0 cloudinary==1.41.0 @@ -16,8 +16,8 @@ gntp==1.0.3 html5lib==1.1 httpagentparser==1.9.5 idna==3.10 -importlib-metadata==8.2.0 -importlib-resources==6.4.0 +importlib-metadata==8.5.0 +importlib-resources==6.4.5 git+https://github.com/Tautulli/ipwhois.git@master#egg=ipwhois IPy==1.01 Mako==1.3.5 @@ -26,18 +26,18 @@ musicbrainzngs==0.7.1 packaging==24.1 paho-mqtt==2.1.0 platformdirs==4.2.2 -plexapi==4.15.15 +plexapi==4.15.16 portend==3.2.0 profilehooks==1.12.0 PyJWT==2.9.0 -pyparsing==3.1.2 +pyparsing==3.1.4 python-dateutil==2.9.0.post0 python-twitter==3.5 -pytz==2024.1 +pytz==2024.2 requests==2.32.3 requests-oauthlib==2.0.0 rumps==0.4.0; platform_system == "Darwin" -simplejson==3.19.2 +simplejson==3.19.3 six==1.16.0 tempora==5.7.0 tokenize-rt==6.0.0 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 6f76e8cc..35c22479 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -26,14 +26,14 @@ parts: stage-packages: - python3 - python3-openssl - - python3-pycryptodome + - python3-cryptography - python3-setuptools - python3-pkg-resources build-packages: - git - python3 - python3-openssl - - python3-pycryptodome + - python3-cryptography - python3-setuptools - python3-pkg-resources override-pull: |