From 71292bbc114295a41a30769f65d9b76daabc79af Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Sat, 1 Nov 2025 13:24:52 +1000 Subject: [PATCH 01/13] include patch from pypi/warehouse PR 18741 by nlhkabu --- warehouse/static/sass/base/_forms.scss | 4 + warehouse/static/sass/blocks/_button.scss | 15 ++ .../static/sass/blocks/_filter-wheels.scss | 56 ++++ warehouse/static/sass/blocks/_form-group.scss | 36 +++ warehouse/static/sass/blocks/_table.scss | 23 +- warehouse/static/sass/warehouse.scss | 1 + .../templates/manage/project/release.html | 2 +- warehouse/templates/packaging/detail.html | 242 ++++++++++++------ 8 files changed, 299 insertions(+), 80 deletions(-) create mode 100644 warehouse/static/sass/blocks/_filter-wheels.scss diff --git a/warehouse/static/sass/base/_forms.scss b/warehouse/static/sass/base/_forms.scss index ef0d1fb075b1..5a542619f0dc 100644 --- a/warehouse/static/sass/base/_forms.scss +++ b/warehouse/static/sass/base/_forms.scss @@ -48,6 +48,10 @@ } } +select { + height: 40px; +} + input[type="checkbox"] { &:focus, &:hover, diff --git a/warehouse/static/sass/blocks/_button.scss b/warehouse/static/sass/blocks/_button.scss index a0d3859a3784..e8bbbc8d7383 100644 --- a/warehouse/static/sass/blocks/_button.scss +++ b/warehouse/static/sass/blocks/_button.scss @@ -13,6 +13,7 @@ - primary: Makes button bright blue. - danger: Makes button red. - warning: Makes button brown. + - link: styles button like a link (removes all padding, border, etc.) - disabled: Styles for when the button cannot be clicked. - switch-to-desktop: Switch to desktop button found in site footer. - switch-to-mobile: Switch to mobile button found in site header. @@ -126,6 +127,20 @@ } } + &--link { + padding: 0; + background-color: transparent; + border: 0; + color: $text-color; + display: inline-block; + + &:focus, + &:hover, + &:active { + color: $primary-color; + } + } + &[disabled], &--disabled { cursor: not-allowed; diff --git a/warehouse/static/sass/blocks/_filter-wheels.scss b/warehouse/static/sass/blocks/_filter-wheels.scss new file mode 100644 index 000000000000..014f77f92895 --- /dev/null +++ b/warehouse/static/sass/blocks/_filter-wheels.scss @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + Search and filter wheels + +
+ +
+ Filters here +
+
+*/ + +.filter-wheels { + display: flex; + gap: $spacing-unit / 2; + justify-content: space-between; + + @media only screen and (max-width: $tablet) { + flex-wrap: wrap; + } + + &__search { + flex-grow: 1; + + .form-group input { + height: 40px; + } + + @media only screen and (max-width: $tablet) { + width: 100%; + } + } + + &__filters { + flex-shrink: 0; + flex: 0 1 auto; + display: flex; + gap: $spacing-unit / 3; + align-items: flex-end; + + div { + flex: 0 1 auto; + } + + @media only screen and (max-width: $tablet) { + width: 100%; + } + + @media only screen and (max-width: $small-tablet) { + display: block; + } + } +} diff --git a/warehouse/static/sass/blocks/_form-group.scss b/warehouse/static/sass/blocks/_form-group.scss index ce936206cc8e..768c374cfa25 100644 --- a/warehouse/static/sass/blocks/_form-group.scss +++ b/warehouse/static/sass/blocks/_form-group.scss @@ -12,6 +12,10 @@

When a field is not editable, you can use me instead

Some help text here

+ + + Modifiers: + - flex-width: makes fields inside the group flexible width (instead of 350px wide) */ .form-group { @@ -126,6 +130,38 @@ } } + &--flex-width { + max-width: unset; + :where( + input:not([type]), + select, + textarea, + [type="color"], + [type="date"], + [type="datetime"], + [type="datetime-local"], + [type="email"], + [type="month"], + [type="month"], + [type="number"], + [type="password"], + [type="search"], + [type="tel"], + [type="text"], + [type="time"], + [type="url"], + [type="week"] + ).form-group__field, + select.form-group__field { + width: 100%; + max-width: 100%; + } + + select.form-group__field { + padding-right: $spacing-unit; + } + } + // Specific cases for input validation using `pattern` attribute /* stylelint-disable-next-line selector-id-pattern -- Form sets name */ diff --git a/warehouse/static/sass/blocks/_table.scss b/warehouse/static/sass/blocks/_table.scss index 10df7f4217ef..bfab883edf76 100644 --- a/warehouse/static/sass/blocks/_table.scss +++ b/warehouse/static/sass/blocks/_table.scss @@ -31,8 +31,9 @@ Modifiers: - information: table for displaying table data - ideally with row level headings - downloads: specific styles for downloads table on project detail page + - files: specific styles for files table on project detail page - releases: specific styles for releases table on manage project page - - files: specific styles for files table on releases tab + - manage-files: specific styles for files table on the releases tab - history: specific styles for project journals - hashes: specific styles for the hashes table on an individual file - collaborators: specific styles for managing a project's collaborators @@ -59,6 +60,7 @@ tbody tr td:last-child { display: block; width: 100%; + max-width: 100%; text-align: left; border-bottom: 0; padding: 2px 0; @@ -169,8 +171,8 @@ &--information { th { - width: 1%; - white-space: nowrap; + width: 1%; + white-space: nowrap; } td, @@ -197,6 +199,19 @@ } } + &--files { + margin-bottom: $half-spacing-unit; + + td.file-name { + word-wrap: break-word; + max-width: 250px; + } + + @media only screen and (max-width: $desktop) { + @include mobile-friendly-table; + } + } + &--releases { word-wrap: break-word; margin-bottom: $spacing-unit; @@ -207,7 +222,7 @@ } } - &--files, + &--manage-files, &--history { margin-top: $half-spacing-unit; diff --git a/warehouse/static/sass/warehouse.scss b/warehouse/static/sass/warehouse.scss index 7c3200981c1e..684e91ada890 100644 --- a/warehouse/static/sass/warehouse.scss +++ b/warehouse/static/sass/warehouse.scss @@ -96,6 +96,7 @@ @import "blocks/project-description"; /*rtl:end:ignore*/ @import "blocks/files"; +@import "blocks/filter-wheels"; @import "blocks/radio-toggle-form"; @import "blocks/release"; @import "blocks/release-timeline"; diff --git a/warehouse/templates/manage/project/release.html b/warehouse/templates/manage/project/release.html index 6f3277ed9997..ec37bfbf1c0e 100644 --- a/warehouse/templates/manage/project/release.html +++ b/warehouse/templates/manage/project/release.html @@ -32,7 +32,7 @@

{% if files %} - +
diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html index b29903e561b7..332f89b920ba 100644 --- a/warehouse/templates/packaging/detail.html +++ b/warehouse/templates/packaging/detail.html @@ -70,28 +70,118 @@

pip install{{ index_url }} {{ release.project.name }}{{ project_version }} {%- endif -%} {%- endmacro -%} -{%- macro file_table(files) -%} + +{%- macro sdists_table(files) -%} +

{% trans version=release.version, project_name=project.name %}Files for release {{ version }} of {{ project_name }}{% endtrans %}
+ + + + + + + + {% for file in files %} -
-
- -
-
+
+ + + + + + + {% endfor %} +
{% trans name=release.project.name, release=release.version %}Source distribution for {{ name }} {{ version }}{% endtrans %}
{% trans %}File{% endtrans %}{% trans %}Uploaded{% endtrans %}{% trans %}Size{% endtrans %}{% trans %}Type{% endtrans %}{% trans %}Details{% endtrans %}
+ {% trans %}File{% endtrans %} +   {{ file.filename }} - ({{ file.size|filesizeformat() if file.size else 0|filesizeformat() }} + + {% trans %}Uploaded{% endtrans %} + {% trans upload_time=humanize(file.upload_time) %}Uploaded {{ upload_time }}{% endtrans %} + + {% trans %}Size{% endtrans %} + {{ file.size|filesizeformat() if file.size else 0|filesizeformat() }} + + {% trans %}Type{% endtrans %} + {% trans %}Source{% endtrans %} + + {% trans %}View details{% endtrans %} {%- trans -%}view details{%- endtrans -%}) -

- Uploaded {{ humanize(file.upload_time) }} - {% for tag in file.pretty_wheel_tags %}{{ tag }}{% endfor %} -

+ data-project-tabs-target="tab" + data-action="project-tabs#onTabClick"> + {% trans %}Details{% endtrans %} + +
+{%- endmacro -%} + +{%- macro bdists_table(files) -%} + + + + + + + + + + {% for file in files %} + + + + + + - + {% endfor %} +
{% trans name=release.project.name, release=release.version %}Table of built distributions (wheels) for {{ name }} {{ release }}{% endtrans %}
{% trans %}File{% endtrans %}{% trans %}Interpreter{% endtrans %}{% trans %}ABI{% endtrans %}{% trans %}Platform{% endtrans %}{% trans %}Details{% endtrans %}
+ {% trans %}File{% endtrans %} +   + {{ file.filename }} +
+ + {{ file.size|filesizeformat() if file.size else 0|filesizeformat() }}.  + {% trans upload_time=humanize(file.upload_time) %}Uploaded {{ upload_time }}{% endtrans %}. + +
+ {% trans %}Interpreter{% endtrans %} + {% for key, value in file.wheel_filters.items() %} + {% if key == 'interpreters' %} + {% for interpreter in value %} + {{ interpreter }}
+ {% endfor %} + {% endif %} + {% endfor %} +
+ {% trans %}ABI{% endtrans %} + {% for key, value in file.wheel_filters.items() %} + {% if key == 'abis' %} + {% for abi in value %} + {{ abi }}
+ {% endfor %} + {% endif %} + {% endfor %} +
+ {% trans %}Platform{% endtrans %} + {% for key, value in file.wheel_filters.items() %} + {% if key == 'platforms' %} + {% for platform in value %} + {{ platform }}
+ {% endfor %} + {% endif %} + {% endfor %} +
+ {% trans %}View details{% endtrans %} + + {% trans %}Details{% endtrans %} + +
{%- endmacro -%} + {% macro filter_select(name, title, selected) %} {% endmacro %} + {% block title %}{{ release.project.name }}{% endblock %} {% block description %}{{ release.summary }}{% endblock %} {% block additional_rss -%} @@ -441,90 +532,91 @@

data-controller="filter-list">

{% trans %}Download files{% endtrans %}

- {% trans href='https://packaging.python.org/tutorials/installing-packages/', title=gettext('External link') %}Download the file for your platform. If you're not sure which to choose, learn more about installing packages.{% endtrans %} + {% trans href='https://packaging.python.org/en/latest/discussions/package-formats/#package-formats', title=gettext('External link') %}For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.{% endtrans %}

{% trans count=sdists|length %} - Source Distribution + Source distribution (sdist) {% pluralize %} - Source Distributions + Source distributions (sdists) {% endtrans %}

{% if sdists %} - {{ file_table(sdists) }} + {{ sdists_table(sdists) }} {% else %} -
-
- -
-
- {% trans %}No source distribution files available for this release.{% endtrans %} - {% trans href='https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives', title=gettext('External link') %}See tutorial on generating distribution archives.{% endtrans %} -
+
+   + {% trans %}No source distribution files available for this release.{% endtrans %}  + {% trans href='https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives', title=gettext('External link') %}See tutorial on generating distribution archives.{% endtrans %}
{% endif %} + {% if bdists %} + {% set bdist_count = bdists|length %}

- {% trans count=bdists|length %} - Built Distribution + {% trans count=bdist_count %} + Built distribution (wheel) {% pluralize %} - Built Distributions + Built distributions (wheels) {% endtrans %}

- -

- {% trans href='https://packaging.python.org/en/latest/specifications/binary-distribution-format/', title=gettext('External link') %}If you're not sure about the file name format, learn more about wheel file names.{% endtrans %} -

+ {% if bdist_count > 3 %} - + data-clipboard-tooltip-value="{% trans %}Copy link to filters{% endtrans %}"> + + {% trans %}Copy link to filters{% endtrans %} + + + {{ filter_select('interpreters', 'Interpreter', wheel_filters_params) }} +
+
{{ filter_select('abis', 'ABI', wheel_filters_params) }}
+
{{ filter_select('platforms', 'Platform', wheel_filters_params) }}
+ + + {% endif %} + + {{ bdists_table(bdists) }} + + {% if bdist_count > 3 %} -
- -
-
{{ filter_select('interpreters', 'Interpreter', wheel_filters_params) }}
-
{{ filter_select('abis', 'ABI', wheel_filters_params) }}
-
{{ filter_select('platforms', 'Platform', wheel_filters_params) }}
-
-
- {{ file_table(bdists) }} + {% endif %} + {% endif %} {# Tabs: file details #} From 3919fa40349de94ff2b20dd9cf857fbffa1e3d04 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Sat, 1 Nov 2025 13:25:53 +1000 Subject: [PATCH 02/13] improve wheel tag parsing for file download filters --- tests/unit/utils/test_wheel.py | 259 ++++++++++++++++----------------- warehouse/utils/wheel.py | 199 +++++++++++++++++++++++++ 2 files changed, 328 insertions(+), 130 deletions(-) diff --git a/tests/unit/utils/test_wheel.py b/tests/unit/utils/test_wheel.py index 5f1d5a00ebbb..ae0f9db1e601 100644 --- a/tests/unit/utils/test_wheel.py +++ b/tests/unit/utils/test_wheel.py @@ -5,138 +5,137 @@ from warehouse.utils import wheel +def _build(**kwargs): + grouped_labels = { + "interpreter": {}, + "abi": {}, + "platform": {}, + "other": {}, + } + for key, value in kwargs.items(): + if key.startswith('interp_'): + grouped_labels['interpreter'][key.removeprefix('interp_')] = value + elif key.startswith('abi_'): + grouped_labels['abi'][key.removeprefix('abi_')] = value + elif key.startswith('plat_'): + grouped_labels['platform'][key.removeprefix('plat_')] = value + elif key.startswith('other_'): + grouped_labels['other'][key.removeprefix('other_')] = value + else: + raise ValueError(f"Unknown item {key}={value}") + return grouped_labels + + @pytest.mark.parametrize( ("filename", "expected_tags"), [ - ("cryptography-42.0.5.tar.gz", ["Source"]), - ("Pillow-2.5.0-py3.4-win-amd64.egg", ["Egg"]), - ("Pillow-2.5.0-py3.4-win32.egg", ["Egg"]), - ( - "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", - ["PyPy", "Windows x86-64"], - ), - ( - "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", - ["PyPy", "manylinux: glibc 2.28+ x86-64"], - ), - ( - "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", - ["CPython 3.7+", "musllinux: musl 1.2+ x86-64"], - ), - ( - "cryptography-42.0.5-cp37-abi3-macosx_10_5_intel.whl", - ["CPython 3.7+", "macOS 10.5+ Intel (x86-64, i386)"], - ), - ( - "cryptography-42.0.5-cp37-abi3-macosx_10_5_fat.whl", - ["CPython 3.7+", "macOS 10.5+ fat (i386, PPC)"], - ), - ( - "cryptography-42.0.5-cp37-abi3-macosx_10_5_fat3.whl", - ["CPython 3.7+", "macOS 10.5+ fat3 (x86-64, i386, PPC)"], - ), - ( - "cryptography-42.0.5-cp37-abi3-macosx_10_5_fat64.whl", - ["CPython 3.7+", "macOS 10.5+ fat64 (x86-64, PPC64)"], - ), - ( - "cryptography-42.0.5-cp37-abi3-macosx_10_5_universal.whl", - ["CPython 3.7+", "macOS 10.5+ universal (x86-64, i386, PPC64, PPC)"], - ), - ( - "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", - ["CPython 3.7+", "macOS 10.12+ universal2 (ARM64, x86-64)"], - ), - ( - "cryptography-42.0.5-cp313-cp313-android_27_armeabi_v7a.whl", - ["Android API level 27+ ARM EABI v7a", "CPython 3.13"], - ), - ( - "cryptography-42.0.5-cp313-cp313-android_27_arm64_v8a.whl", - ["Android API level 27+ ARM64 v8a", "CPython 3.13"], - ), - ( - "cryptography-42.0.5-cp313-cp313-android_27_x86.whl", - ["Android API level 27+ x86", "CPython 3.13"], - ), - ( - "cryptography-42.0.5-cp313-cp313-android_27_x86_64.whl", - ["Android API level 27+ x86-64", "CPython 3.13"], - ), - ( - "cryptography-42.0.5-cp313-abi3-android_16_armeabi_v7a.whl", - ["Android API level 16+ ARM EABI v7a", "CPython 3.13+"], - ), - ( - "cryptography-42.0.5-cp313-cp313-iOS_15_6_arm64_iphoneos.whl", - ["CPython 3.13", "iOS 15.6+ ARM64 Device"], - ), - ( - "cryptography-42.0.5-cp313-cp313-iOS_15_6_arm64_iphonesimulator.whl", - ["CPython 3.13", "iOS 15.6+ ARM64 Simulator"], - ), - ( - "cryptography-42.0.5-cp313-cp313-iOS_15_6_x86_64_iphonesimulator.whl", - ["CPython 3.13", "iOS 15.6+ x86-64 Simulator"], - ), - ( - "cryptography-42.0.5-cp313-abi3-iOS_13_0_arm64_iphoneos.whl", - ["CPython 3.13+", "iOS 13.0+ ARM64 Device"], - ), - ( - "cryptography-42.0.5-cp313-abi3-iOS_13_0_arm64_iphonesimulator.whl", - ["CPython 3.13+", "iOS 13.0+ ARM64 Simulator"], - ), - ( - "pgf-1.0-pp27-pypy_73-manylinux2010_x86_64.whl", - ["PyPy", "manylinux: glibc 2.12+ x86-64"], - ), - ("pdfcomparator-0_2_0-py2-none-any.whl", []), - ( - "mclbn256-0.6.0-py3-abi3-macosx_12_0_arm64.whl", - ["Python 3", "macOS 12.0+ ARM64"], - ), - ( - "pep272_encryption-0.4-py2.pp35.pp36.pp37.pp38.pp39-none-any.whl", - ["PyPy", "Python 2"], - ), - ( - "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", - ["Python 3", "musllinux: musl 1.2+ ARMv7l"], - ), - ( - "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", - ["CPython 3.12", "musllinux: musl 1.1+ x86-64"], - ), - ( - "numpy-1.26.4-lolinterpreter-lolabi-musllinux_1_1_x86_64.whl", - ["lolinterpreter", "musllinux: musl 1.1+ x86-64"], - ), - ( - ( - "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64." - "manylinux2014_aarch64.whl" - ), - ["PyPy", "manylinux: glibc 2.17+ ARM64"], - ), - ("numpy-1.13.1-cp36-none-win_amd64.whl", ["CPython 3.6", "Windows x86-64"]), - ("cryptography-38.0.2-cp36-abi3-win32.whl", ["CPython 3.6+", "Windows x86"]), - ( - "plato_learn-0.4.7-py36.py37.py38.py39-none-any.whl", - [ - "Python 3.6", - "Python 3.7", - "Python 3.8", - "Python 3.9", - ], - ), - ("juriscraper-1.1.11-py27-none-any.whl", ["Python 2.7"]), - ("OZI-0.0.291-py312-none-any.whl", ["Python 3.12"]), - ("foo-0.0.0-ip27-none-any.whl", ["IronPython 2.7"]), - ("foo-0.0.0-jy38-none-any.whl", ["Jython 3.8"]), - ("foo-0.0.0-garbage-none-any.whl", ["garbage"]), - ("foo-0.0.0-69-none-any.whl", []), + ("cryptography-42.0.5.tar.gz", + _build(other_source="Source")), + ("Pillow-2.5.0-py3.4-win-amd64.egg", + _build(other_egg="Egg")), + ("Pillow-2.5.0-py3.4-win32.egg", + _build(other_egg="Egg")), + ("cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", + _build(interp_pp310="PyPy 310", abi_pypy310_pp73="PyPy 310 pp73", plat_win_amd64="Windows x86-64")), + ("cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", + _build(interp_pp310="PyPy 310", abi_pypy310_pp73="PyPy 310 pp73", plat_manylinux_2_28_x86_64="linux glibc 2.28+ x86-64")), + ("cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", + _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_musllinux_1_2_x86_64="linux musl 1.2+ x86-64")), + ("cryptography-42.0.5-cp37-abi3-macosx_10_5_intel.whl", + _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_intel="macOS 10.5+ Intel (x86-64, i386)")), + ("cryptography-42.0.5-cp37-abi3-macosx_10_5_fat.whl", + _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_fat="macOS 10.5+ fat (i386, PPC)")), + ("cryptography-42.0.5-cp37-abi3-macosx_10_5_fat3.whl", + _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_fat3="macOS 10.5+ fat3 (x86-64, i386, PPC)")), + ("cryptography-42.0.5-cp37-abi3-macosx_10_5_fat64.whl", + _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_fat64="macOS 10.5+ fat64 (x86-64, PPC64)")), + ("cryptography-42.0.5-cp37-abi3-macosx_10_5_universal.whl", + _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_universal="macOS 10.5+ universal (x86-64, i386, PPC64, PPC)")), + ("cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", + _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_12_universal2="macOS 10.12+ universal2 (ARM64, x86-64)")), + ("cryptography-42.0.5-cp313-cp313-android_27_armeabi_v7a.whl", + _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_android_27_armeabi_v7a="Android API level 27+ ARM EABI v7a")), + ("cryptography-42.0.5-cp313-cp313-android_27_arm64_v8a.whl", + _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_android_27_arm64_v8a="Android API level 27+ ARM64 v8a")), + ("cryptography-42.0.5-cp313-cp313-android_27_x86.whl", + _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_android_27_x86="Android API level 27+ x86")), + ("cryptography-42.0.5-cp313-cp313-android_27_x86_64.whl", + _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_android_27_x86_64="Android API level 27+ x86-64")), + ("cryptography-42.0.5-cp313-abi3-android_16_armeabi_v7a.whl", + _build(interp_cp313="CPython 3.13", abi_abi3="CPython abi3", plat_android_16_armeabi_v7a="Android API level 16+ ARM EABI v7a")), + ("cryptography-42.0.5-cp313-cp313-iOS_15_6_arm64_iphoneos.whl", + _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_ios_15_6_arm64_iphoneos="iOS 15.6+ ARM64 Device")), + ("cryptography-42.0.5-cp313-cp313-iOS_15_6_arm64_iphonesimulator.whl", + _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_ios_15_6_arm64_iphonesimulator="iOS 15.6+ ARM64 Simulator")), + ("cryptography-42.0.5-cp313-cp313-iOS_15_6_x86_64_iphonesimulator.whl", + _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_ios_15_6_x86_64_iphonesimulator="iOS 15.6+ x86-64 Simulator")), + ("cryptography-42.0.5-cp313-abi3-iOS_13_0_arm64_iphoneos.whl", + _build(interp_cp313="CPython 3.13", abi_abi3="CPython abi3", plat_ios_13_0_arm64_iphoneos="iOS 13.0+ ARM64 Device")), + ("cryptography-42.0.5-cp313-abi3-iOS_13_0_arm64_iphonesimulator.whl", + _build(interp_cp313="CPython 3.13", abi_abi3="CPython abi3", plat_ios_13_0_arm64_iphonesimulator="iOS 13.0+ ARM64 Simulator")), + ("pgf-1.0-pp27-pypy_73-manylinux2010_x86_64.whl", + _build(interp_pp27="PyPy 27", abi_pypy_73="PyPy 73", plat_manylinux2010_x86_64="linux glibc 2.12+ x86-64")), + # Cannot parse 'pdfcomparator-0_2_0-py2-none-any.whl' - invalid version? + ("pdfcomparator-0_2_0-py2-none-any.whl", + # _build(interp_py2="Python 2", abi_none="(none)", plat_any="(any)")), + _build()), + ("mclbn256-0.6.0-py3-abi3-macosx_12_0_arm64.whl", + _build(interp_py3="Python 3", abi_abi3="CPython abi3", plat_macosx_12_0_arm64="macOS 12.0+ ARM64")), + ("pep272_encryption-0.4-py2.pp35.pp36.pp37.pp38.pp39-none-any.whl", + _build(interp_py2="Python 2", interp_pp35="PyPy 35", interp_pp36="PyPy 36", interp_pp37="PyPy 37", interp_pp38="PyPy 38", interp_pp39="PyPy 39", + abi_none="(none)", plat_any="(any)")), + ("ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", + _build(interp_py3="Python 3", abi_none="(none)", plat_musllinux_1_2_armv7l="linux musl 1.2+ ARMv7l")), + ("numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", + _build(interp_cp312="CPython 3.12", abi_cp312="CPython 3.12", plat_musllinux_1_1_x86_64="linux musl 1.1+ x86-64")), + ("numpy-1.26.4-lol_interpreter-lol_abi-lol_platform.whl", + _build(interp_lol_interpreter="lol interpreter", abi_lol_abi="lol abi", plat_lol_platform="lol platform")), + ("pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + _build(interp_pp39="PyPy 39", abi_pypy39_pp73="PyPy 39 pp73", plat_manylinux_2_17_aarch64="linux glibc 2.17+ ARM64", + plat_manylinux2014_aarch64="linux glibc 2.17+ ARM64")), + ("numpy-1.13.1-cp36-none-win_amd64.whl", + _build(interp_cp36="CPython 3.6", abi_none="(none)", plat_win_amd64="Windows x86-64")), + ("cryptography-38.0.2-cp36-abi3-win32.whl", + _build(interp_cp36="CPython 3.6", abi_abi3="CPython abi3", plat_win32="Windows x86")), + ("plato_learn-0.4.7-py36.py37.py38.py39-none-any.whl", + _build(interp_py36="Python 3.6", interp_py37="Python 3.7", interp_py38="Python 3.8", interp_py39="Python 3.9", + abi_none="(none)", plat_any="(any)")), + ("juriscraper-1.1.11-py27-none-any.whl", + _build(interp_py27="Python 2.7", abi_none="(none)", plat_any="(any)")), + ("OZI-0.0.291-py312-none-any.whl", + _build(interp_py312="Python 3.12", abi_none="(none)", plat_any="(any)")), + ("foo-0.0.0-ip27-none-any.whl", + _build(interp_ip27="IronPython 2.7", abi_none="(none)", plat_any="(any)")), + ("foo-0.0.0-jy38-none-any.whl", + _build(interp_jy38="Jython 3.8", abi_none="(none)", plat_any="(any)")), + ("foo-0.0.0-garbage-none-any.whl", + _build(interp_garbage="garbage", abi_none="(none)", plat_any="(any)")), + ("foo-0.0.0-69-none-any.whl", + _build(interp_69="69", abi_none="(none)", plat_any="(any)")), + ("aiohttp-3.13.2-cp314-cp314udmtz-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", + _build(interp_cp314="CPython 3.14", + abi_cp314udmtz="CPython 3.14 debug free-threading pymalloc wide-unicode z", + plat_manylinux_2_31_riscv64="linux glibc 2.31+ RISC-V 64", + plat_manylinux_2_39_riscv64="linux glibc 2.39+ RISC-V 64")), + ("aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", + _build(interp_cp314="CPython 3.14", + abi_cp314t="CPython 3.14 free-threading", + plat_manylinux2014_s390x="linux glibc 2.17+ IBM System/390x", + plat_manylinux_2_17_s390x="linux glibc 2.17+ IBM System/390x", + plat_manylinux_2_28_s390x="linux glibc 2.28+ IBM System/390x")), + ("aiohttp-3.13.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", + _build(interp_cp39="CPython 3.9", + abi_cp39="CPython 3.9", + plat_manylinux2014_ppc64le="linux glibc 2.17+ PowerPC 64-le", + plat_manylinux_2_17_ppc64le="linux glibc 2.17+ PowerPC 64-le", + plat_manylinux_2_28_ppc64le="linux glibc 2.28+ PowerPC 64-le")), + ("numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", + _build(interp_pp311="PyPy 311", + abi_pypy311_pp73="PyPy 311 pp73", + plat_manylinux_2_27_aarch64="linux glibc 2.27+ ARM64", + plat_manylinux_2_28_aarch64="linux glibc 2.28+ ARM64")) ], ) -def test_wheel_to_pretty_tags(filename, expected_tags): - assert wheel.filename_to_pretty_tags(filename) == expected_tags +def test_wheel_to_groups_labels(filename, expected_tags): + # assert wheel.filename_to_pretty_tags(filename) == expected_tags + assert wheel.filename_to_grouped_labels(filename) == expected_tags diff --git a/warehouse/utils/wheel.py b/warehouse/utils/wheel.py index db8065ab9a17..9bee41a3aa41 100644 --- a/warehouse/utils/wheel.py +++ b/warehouse/utils/wheel.py @@ -153,3 +153,202 @@ def tags_to_filters(tags: set[packaging.tags.Tag]) -> dict[str, list[str]]: "abis": sorted(abis), "platforms": sorted(platforms), } + + +# Map known Python tags, ABI tags, Platform tags to labels. +_PLATFORM_MAP = { + "win": [(re.compile(r"^win_(.*?)$"), lambda m: f"Windows {_norm_arch(m.group(1))}")], + "win32": [(re.compile(r"^win32$"), lambda m: "Windows x86")], + "manylinux": [( + re.compile(r"^manylinux_(\d+)_(\d+)_(.*?)$"), + lambda m: f"linux glibc {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))}" + + )], + "manylinux2014": [( + re.compile(r"^manylinux2014_(.*?)$"), + lambda m: f"linux glibc 2.17+ {_norm_arch(m.group(1))}", + )], + "manylinux2010": [( + re.compile(r"^manylinux2010_(.*?)$"), + lambda m: f"linux glibc 2.12+ {_norm_arch(m.group(1))}", + )], + "manylinux1": [( + re.compile(r"^manylinux1_(.*?)$"), + lambda m: f"linux glibc 2.5+ {_norm_arch(m.group(1))}", + )], + "musllinux": [( + re.compile(r"^musllinux_(\d+)_(\d+)_(.*?)$"), + lambda m: f"linux musl {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))}" + )], + "macosx": [( + re.compile(r"^macosx_(\d+)_(\d+)_(.*?)$"), + lambda m: f"macOS {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))}", + )], + "ios": [( + re.compile(r"^ios_(\d+)_(\d+)_(.*?)_iphoneos$"), + lambda m: f"iOS {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))} Device", # noqa: E501 + ), + ( + re.compile(r"^ios_(\d+)_(\d+)_(.*?)_iphonesimulator$"), + lambda m: f"iOS {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))} Simulator", # noqa: E501 + )], + "android": + [( + re.compile(r"^android_(\d+)_(.*?)$"), + lambda m: f"Android API level {m.group(1)}+ {_norm_arch(m.group(2))}", + )], +} +_ARCH_MAP = { + "amd64": "x86-64", + "aarch64": "ARM64", + "armeabi_v7a": "ARM EABI v7a", + "arm64_v8a": "ARM64 v8a", + "x86_64": "x86-64", + "intel": "Intel (x86-64, i386)", + "fat": "fat (i386, PPC)", + "fat3": "fat3 (x86-64, i386, PPC)", + "fat64": "fat64 (x86-64, PPC64)", + "universal": "universal (x86-64, i386, PPC64, PPC)", + "universal2": "universal2 (ARM64, x86-64)", + "arm64": "ARM64", + "armv7l": "ARMv7l", + "i686": "x86-32", + "ppc64": "PowerPC 64-be", + "ppc64le": "PowerPC 64-le", + "s390x": "IBM System/390x", + "riscv64": "RISC-V 64", +} +_CPYTHON_SUFFIX_MAP = { + "d": "debug", + "m": "pymalloc", + "t": "free-threading", + "u": "wide-unicode", +} + + +def _norm_arch(a: str) -> str: + return _ARCH_MAP.get(a, a) + +def _norm_str(s: str) -> str: + return (s or "").replace('_', ' ').strip() + +def _implementation_to_label(raw: str) -> str: + if raw.startswith("pypy"): + version = _norm_str(raw.removeprefix("pypy")) + return f"PyPy {version}" + elif raw.startswith("py"): + major, minor = raw[2:3], raw[3:] + return f"Python {major}{'.' if minor else ''}{minor}" + elif raw.startswith("cp"): + version, suffixes = _format_cpython(raw.removeprefix("cp")) + return f"CPython {version} {suffixes}".strip() + elif raw.startswith("pp"): + version = _norm_str(raw.removeprefix("pp")) + return f"PyPy {version}" + elif raw.startswith("ip"): + major, minor = raw[2:3], raw[3:] + return f"IronPython {major}{'.' if minor else ''}{minor}" + elif raw.startswith("jy"): + major, minor = raw[2:3], raw[3:] + version = f"{major}{'.' if minor else ''}{minor}" + return f"Jython {version}" + else: + # Unknown format. Normalise and return it. + return _norm_str(raw) + + +def _format_cpython(s: str) -> tuple[str, str]: + suffixes = [] + raw = (s or "").strip() + while raw[-1].isalpha(): + last_char = raw[-1] + name = _CPYTHON_SUFFIX_MAP.get(last_char) + if not name: + # Unknown CPython abi suffix. Just include it. + name = last_char + suffixes.append(name) + raw = raw[0:-1] + version = _format_version(raw) + return version, ' '.join(sorted(suffixes)) + + +def _interpreter_to_label(tag: packaging.tags.Tag) -> str: + return _implementation_to_label(tag.interpreter) + + +def _abi_to_label(tag: packaging.tags.Tag) -> str: + if tag.abi == "none": + return "(none)" + elif tag.abi == "abi3": + # NOTE: CPython abi3 should have a CPython interpreter. + # if not tag.interpreter.startswith("cp"): + # A non- CPython interpreter with CPython abi3. + # Should this be possible? + # pass + return "CPython abi3" + elif tag.abi.startswith("cp"): + return _implementation_to_label(tag.abi) + elif tag.abi.startswith("pypy"): + return _implementation_to_label(tag.abi) + elif tag.abi.startswith("pp"): + return _implementation_to_label(tag.abi) + elif tag.abi.startswith("ip"): + return _implementation_to_label(tag.abi) + elif tag.abi.startswith("jy"): + return _implementation_to_label(tag.abi) + else: + # Unknown abi. Just return it. + return _norm_str(tag.abi) + + +def _platform_to_label(tag: packaging.tags.Tag) -> str: + if tag.platform == 'any': + return "(any)" + + value = tag.platform + key = value.split('_', maxsplit=1)[0] if '_' in value else value + + patterns = _PLATFORM_MAP.get(key, []) + for (prefix_re, tmpl) in patterns: + if match := prefix_re.match(value): + return tmpl(match) + + # Unknown platform. Just return it + return _norm_str(value) + + +def _add_group_label(container: dict, group: str, value: str, label: str) -> None: + if value not in container[group]: + container[group][value] = label + elif container[group][value] != label: + # A value that is already present, with a different label. + # This looks odd. Is this possible? + # Use the most recently seen label. + container[group][value] = label + + +def filename_to_grouped_labels(filename: str) -> dict[str, dict]: + grouped_labels = { + "interpreter": {}, + "abi": {}, + "platform": {}, + "other": {}, + } + + if filename.endswith(".egg"): + grouped_labels['other']['egg'] = "Egg" + return grouped_labels + elif not filename.endswith(".whl"): + grouped_labels['other']['source'] = "Source" + return grouped_labels + + tags = filename_to_tags(filename) + for tag in tags: + _add_group_label(grouped_labels, "interpreter", tag.interpreter, _interpreter_to_label(tag)) + _add_group_label(grouped_labels, "abi", tag.abi, _abi_to_label(tag)) + _add_group_label(grouped_labels, "platform", tag.platform, _platform_to_label(tag)) + return grouped_labels + + +def combine_grouped_labels(*args) -> dict[str, dict]: + pass From bb94c8b42bb2af270c71f5899d1497809faeac27 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Sat, 1 Nov 2025 15:04:25 +1000 Subject: [PATCH 03/13] use new filename to wheel tag label functions --- tests/unit/packaging/test_views.py | 8 +- warehouse/packaging/models.py | 2 +- warehouse/packaging/views.py | 4 +- warehouse/utils/wheel.py | 198 +++++++---------------------- 4 files changed, 51 insertions(+), 161 deletions(-) diff --git a/tests/unit/packaging/test_views.py b/tests/unit/packaging/test_views.py index 1a18503a2e54..04dfe3c68ffb 100644 --- a/tests/unit/packaging/test_views.py +++ b/tests/unit/packaging/test_views.py @@ -215,7 +215,7 @@ def test_detail_rendered(self, db_request): "maintainers": sorted(users, key=lambda u: u.username.lower()), "license": None, "PEP740AttestationViewer": views.PEP740AttestationViewer, - "wheel_filters_all": {"interpreters": [], "abis": [], "platforms": []}, + "wheel_filters_all": {"interpreter": {}, "abi":{}, "platform": {}, "other":{}}, "wheel_filters_params": { "filename": "", "interpreters": "", @@ -249,9 +249,9 @@ def test_detail_renders_files_natural_sort(self, db_request): assert result["files"] == sorted_files assert [file.wheel_filters for file in result["files"]] == [ - {"interpreters": ["cp310"], "abis": ["none"], "platforms": ["any"]}, - {"interpreters": ["cp39"], "abis": ["none"], "platforms": ["any"]}, - {"interpreters": ["cp27"], "abis": ["none"], "platforms": ["any"]}, + {"interpreter": {"cp310": "CPython 3.10"}, "abi": {"none": "(none)"}, "platform": {"any": "(any)"}, "other":{}}, + {"interpreter": {"cp39": "CPython 3.9"}, "abi": {"none": "(none)"}, "platform": {"any": "(any)"}, "other":{}}, + {"interpreter": {"cp27": "CPython 2.7"}, "abi": {"none": "(none)"}, "platform": {"any": "(any)"}, "other":{}}, ] def test_license_from_classifier(self, db_request): diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py index 3f1b209e0722..a8c7abbe6b85 100644 --- a/warehouse/packaging/models.py +++ b/warehouse/packaging/models.py @@ -1038,7 +1038,7 @@ def pretty_wheel_tags(self) -> list[str]: @property def wheel_filters(self): - return wheel.filename_to_filters(self.filename) + return wheel.filename_to_grouped_labels(self.filename) class Filename(db.ModelBase): diff --git a/warehouse/packaging/views.py b/warehouse/packaging/views.py index 30859ec2db70..4b0c03dd64ef 100644 --- a/warehouse/packaging/views.py +++ b/warehouse/packaging/views.py @@ -268,7 +268,9 @@ def release_detail(release, request): ) # Collect all the available bdist details to enable building filters. - wheel_filters_all = wheel.filenames_to_filters([bdist.filename for bdist in bdists]) + wheel_filters_all = wheel.filenames_to_grouped_labels( + [bdist.filename for bdist in bdists] + ) # Get the querystring to load any pre-set parameters wheel_filters_params = { diff --git a/warehouse/utils/wheel.py b/warehouse/utils/wheel.py index 9bee41a3aa41..0a858db15e4c 100644 --- a/warehouse/utils/wheel.py +++ b/warehouse/utils/wheel.py @@ -5,156 +5,6 @@ import packaging.tags import packaging.utils -# import sentry_sdk - -_PLATFORMS = [ - (re.compile(r"^win_(.*?)$"), lambda m: f"Windows {_normalize_arch(m.group(1))}"), - (re.compile(r"^win32$"), lambda m: "Windows x86"), - ( - re.compile(r"^manylinux2010_(.*?)$"), - lambda m: f"manylinux: glibc 2.12+ {_normalize_arch(m.group(1))}", - ), - ( - re.compile(r"^manylinux_(\d+)_(\d+)_(.*?)$"), - lambda m: ( - f"manylinux: glibc " - f"{m.group(1)}.{m.group(2)}+ {_normalize_arch(m.group(3))}" - ), - ), - ( - re.compile(r"^musllinux_(\d+)_(\d+)_(.*?)$"), - lambda m: ( - f"musllinux: musl {m.group(1)}.{m.group(2)}+ {_normalize_arch(m.group(3))}" - ), - ), - ( - re.compile(r"^macosx_(\d+)_(\d+)_(.*?)$"), - lambda m: f"macOS {m.group(1)}.{m.group(2)}+ {_normalize_arch(m.group(3))}", - ), - ( - re.compile(r"^android_(\d+)_(.*?)$"), - lambda m: f"Android API level {m.group(1)}+ {_normalize_arch(m.group(2))}", - ), - ( - re.compile(r"^ios_(\d+)_(\d+)_(.*?)_iphoneos$"), - lambda m: f"iOS {m.group(1)}.{m.group(2)}+ {_normalize_arch(m.group(3))} Device", # noqa: E501 - ), - ( - re.compile(r"^ios_(\d+)_(\d+)_(.*?)_iphonesimulator$"), - lambda m: f"iOS {m.group(1)}.{m.group(2)}+ {_normalize_arch(m.group(3))} Simulator", # noqa: E501 - ), -] - -_ARCHS = { - "amd64": "x86-64", - "aarch64": "ARM64", - "armeabi_v7a": "ARM EABI v7a", - "arm64_v8a": "ARM64 v8a", - "x86_64": "x86-64", - "intel": "Intel (x86-64, i386)", - "fat": "fat (i386, PPC)", - "fat3": "fat3 (x86-64, i386, PPC)", - "fat64": "fat64 (x86-64, PPC64)", - "universal": "universal (x86-64, i386, PPC64, PPC)", - "universal2": "universal2 (ARM64, x86-64)", - "arm64": "ARM64", - "armv7l": "ARMv7l", -} - - -def _normalize_arch(a: str) -> str: - return _ARCHS.get(a, a) - - -def _format_version(s: str) -> str: - return f"{s[0]}.{s[1:]}" - - -def filename_to_tags(filename: str) -> set[packaging.tags.Tag]: - """Parse a wheel file name to extract the tags.""" - try: - _, _, _, tags = packaging.utils.parse_wheel_filename(filename) - return set(tags) - except packaging.utils.InvalidWheelFilename: - return set() - - -def filename_to_pretty_tags(filename: str) -> list[str]: - if filename.endswith(".egg"): - return ["Egg"] - elif not filename.endswith(".whl"): - return ["Source"] - - tags = filename_to_tags(filename) - - pretty_tags = set() - for tag in tags: - if tag.platform != "any": - for prefix_re, tmpl in _PLATFORMS: - if match := prefix_re.match(tag.platform): - pretty_tags.add(tmpl(match)) - - if len(tag.interpreter) < 3 or not tag.interpreter[:2].isalpha(): - # This tag doesn't fit our format, give up - pass - elif tag.interpreter.startswith("pp"): - # PyPy tags are a disaster, give up. - pretty_tags.add("PyPy") - elif tag.interpreter.startswith("py"): - major, minor = tag.interpreter[2:3], tag.interpreter[3:] - pretty_tags.add(f"Python {major}{'.' if minor else ''}{minor}") - elif tag.interpreter.startswith("ip"): - major, minor = tag.interpreter[2:3], tag.interpreter[3:] - pretty_tags.add(f"IronPython {major}{'.' if minor else ''}{minor}") - elif tag.interpreter.startswith("jy"): - major, minor = tag.interpreter[2:3], tag.interpreter[3:] - pretty_tags.add(f"Jython {major}{'.' if minor else ''}{minor}") - elif tag.abi == "abi3": - assert tag.interpreter.startswith("cp") - version = _format_version(tag.interpreter.removeprefix("cp")) - pretty_tags.add(f"CPython {version}+") - elif tag.abi.startswith("cp"): - version = _format_version(tag.abi.removeprefix("cp")) - pretty_tags.add(f"CPython {version}") - elif tag.interpreter.startswith("cp"): - version = _format_version(tag.interpreter.removeprefix("cp")) - pretty_tags.add(f"CPython {version}") - else: - # There's a lot of cruft from over the years. If we can't identify - # the interpreter tag, just add it directly. - pretty_tags.add(tag.interpreter) - - return sorted(pretty_tags) - - -def filenames_to_filters(filenames: list[str]) -> dict[str, list[str]]: - tags = set() - for filename in filenames: - tags.update(filename_to_tags(filename)) - return tags_to_filters(tags) - - -def filename_to_filters(filename: str) -> dict[str, list[str]]: - tags = filename_to_tags(filename) - return tags_to_filters(tags) - - -def tags_to_filters(tags: set[packaging.tags.Tag]) -> dict[str, list[str]]: - interpreters = set() - abis = set() - platforms = set() - for tag in tags or []: - interpreters.add(tag.interpreter) - abis.add(tag.abi) - platforms.add(tag.platform) - - return { - "interpreters": sorted(interpreters), - "abis": sorted(abis), - "platforms": sorted(platforms), - } - - # Map known Python tags, ABI tags, Platform tags to labels. _PLATFORM_MAP = { "win": [(re.compile(r"^win_(.*?)$"), lambda m: f"Windows {_norm_arch(m.group(1))}")], @@ -226,12 +76,18 @@ def tags_to_filters(tags: set[packaging.tags.Tag]) -> dict[str, list[str]]: } +def _format_version(s: str) -> str: + return f"{s[0]}.{s[1:]}" + + def _norm_arch(a: str) -> str: return _ARCH_MAP.get(a, a) + def _norm_str(s: str) -> str: return (s or "").replace('_', ' ').strip() + def _implementation_to_label(raw: str) -> str: if raw.startswith("pypy"): version = _norm_str(raw.removeprefix("pypy")) @@ -282,9 +138,9 @@ def _abi_to_label(tag: packaging.tags.Tag) -> str: elif tag.abi == "abi3": # NOTE: CPython abi3 should have a CPython interpreter. # if not tag.interpreter.startswith("cp"): - # A non- CPython interpreter with CPython abi3. - # Should this be possible? - # pass + # A non- CPython interpreter with CPython abi3. + # Should this be possible? + # pass return "CPython abi3" elif tag.abi.startswith("cp"): return _implementation_to_label(tag.abi) @@ -327,6 +183,24 @@ def _add_group_label(container: dict, group: str, value: str, label: str) -> Non container[group][value] = label +def filename_to_tags(filename: str) -> set[packaging.tags.Tag]: + """Parse a wheel file name to extract the tags.""" + try: + _, _, _, tags = packaging.utils.parse_wheel_filename(filename) + return set(tags) + except packaging.utils.InvalidWheelFilename: + return set() + + +def filename_to_pretty_tags(filename: str) -> list[str]: + grouped_labels = filename_to_grouped_labels(filename) + pretty_tags = set() + for kind, kind_items in grouped_labels.items(): + for value, label in kind_items.items(): + pretty_tags.add(label) + return sorted(pretty_tags) + + def filename_to_grouped_labels(filename: str) -> dict[str, dict]: grouped_labels = { "interpreter": {}, @@ -350,5 +224,19 @@ def filename_to_grouped_labels(filename: str) -> dict[str, dict]: return grouped_labels -def combine_grouped_labels(*args) -> dict[str, dict]: - pass +def filenames_to_grouped_labels(filenames: list[str]) -> dict[str, dict]: + grouped_labels = { + "interpreter": {}, + "abi": {}, + "platform": {}, + "other": {}, + } + for filename in filenames: + grouped = filename_to_grouped_labels(filename) + for kind, kind_items in grouped.items(): + if kind not in grouped_labels: + grouped_labels[kind] = {} + for value, label in kind_items.items(): + if value not in grouped_labels[kind]: + grouped_labels[kind][value] = label + return grouped_labels From 82362f7f205902ca4f9a3e504ba043810a878540 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Sat, 1 Nov 2025 17:14:51 +1000 Subject: [PATCH 04/13] update js filter controller to allow exact or includes match Format files. --- tests/unit/packaging/test_views.py | 28 +- tests/unit/utils/test_wheel.py | 421 +++++++++++++----- .../controllers/filter_list_controller.js | 96 ++-- warehouse/templates/packaging/detail.html | 274 ++++++------ warehouse/utils/wheel.py | 107 +++-- 5 files changed, 588 insertions(+), 338 deletions(-) diff --git a/tests/unit/packaging/test_views.py b/tests/unit/packaging/test_views.py index 04dfe3c68ffb..d63b3d14baab 100644 --- a/tests/unit/packaging/test_views.py +++ b/tests/unit/packaging/test_views.py @@ -215,7 +215,12 @@ def test_detail_rendered(self, db_request): "maintainers": sorted(users, key=lambda u: u.username.lower()), "license": None, "PEP740AttestationViewer": views.PEP740AttestationViewer, - "wheel_filters_all": {"interpreter": {}, "abi":{}, "platform": {}, "other":{}}, + "wheel_filters_all": { + "interpreter": {}, + "abi": {}, + "platform": {}, + "other": {}, + }, "wheel_filters_params": { "filename": "", "interpreters": "", @@ -249,9 +254,24 @@ def test_detail_renders_files_natural_sort(self, db_request): assert result["files"] == sorted_files assert [file.wheel_filters for file in result["files"]] == [ - {"interpreter": {"cp310": "CPython 3.10"}, "abi": {"none": "(none)"}, "platform": {"any": "(any)"}, "other":{}}, - {"interpreter": {"cp39": "CPython 3.9"}, "abi": {"none": "(none)"}, "platform": {"any": "(any)"}, "other":{}}, - {"interpreter": {"cp27": "CPython 2.7"}, "abi": {"none": "(none)"}, "platform": {"any": "(any)"}, "other":{}}, + { + "interpreter": {"cp310": "CPython 3.10"}, + "abi": {"none": "(none)"}, + "platform": {"any": "(any)"}, + "other": {}, + }, + { + "interpreter": {"cp39": "CPython 3.9"}, + "abi": {"none": "(none)"}, + "platform": {"any": "(any)"}, + "other": {}, + }, + { + "interpreter": {"cp27": "CPython 2.7"}, + "abi": {"none": "(none)"}, + "platform": {"any": "(any)"}, + "other": {}, + }, ] def test_license_from_classifier(self, db_request): diff --git a/tests/unit/utils/test_wheel.py b/tests/unit/utils/test_wheel.py index ae0f9db1e601..eba283fe6954 100644 --- a/tests/unit/utils/test_wheel.py +++ b/tests/unit/utils/test_wheel.py @@ -13,14 +13,14 @@ def _build(**kwargs): "other": {}, } for key, value in kwargs.items(): - if key.startswith('interp_'): - grouped_labels['interpreter'][key.removeprefix('interp_')] = value - elif key.startswith('abi_'): - grouped_labels['abi'][key.removeprefix('abi_')] = value - elif key.startswith('plat_'): - grouped_labels['platform'][key.removeprefix('plat_')] = value - elif key.startswith('other_'): - grouped_labels['other'][key.removeprefix('other_')] = value + if key.startswith("interp_"): + grouped_labels["interpreter"][key.removeprefix("interp_")] = value + elif key.startswith("abi_"): + grouped_labels["abi"][key.removeprefix("abi_")] = value + elif key.startswith("plat_"): + grouped_labels["platform"][key.removeprefix("plat_")] = value + elif key.startswith("other_"): + grouped_labels["other"][key.removeprefix("other_")] = value else: raise ValueError(f"Unknown item {key}={value}") return grouped_labels @@ -29,113 +29,326 @@ def _build(**kwargs): @pytest.mark.parametrize( ("filename", "expected_tags"), [ - ("cryptography-42.0.5.tar.gz", - _build(other_source="Source")), - ("Pillow-2.5.0-py3.4-win-amd64.egg", - _build(other_egg="Egg")), - ("Pillow-2.5.0-py3.4-win32.egg", - _build(other_egg="Egg")), - ("cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", - _build(interp_pp310="PyPy 310", abi_pypy310_pp73="PyPy 310 pp73", plat_win_amd64="Windows x86-64")), - ("cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", - _build(interp_pp310="PyPy 310", abi_pypy310_pp73="PyPy 310 pp73", plat_manylinux_2_28_x86_64="linux glibc 2.28+ x86-64")), - ("cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", - _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_musllinux_1_2_x86_64="linux musl 1.2+ x86-64")), - ("cryptography-42.0.5-cp37-abi3-macosx_10_5_intel.whl", - _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_intel="macOS 10.5+ Intel (x86-64, i386)")), - ("cryptography-42.0.5-cp37-abi3-macosx_10_5_fat.whl", - _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_fat="macOS 10.5+ fat (i386, PPC)")), - ("cryptography-42.0.5-cp37-abi3-macosx_10_5_fat3.whl", - _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_fat3="macOS 10.5+ fat3 (x86-64, i386, PPC)")), - ("cryptography-42.0.5-cp37-abi3-macosx_10_5_fat64.whl", - _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_fat64="macOS 10.5+ fat64 (x86-64, PPC64)")), - ("cryptography-42.0.5-cp37-abi3-macosx_10_5_universal.whl", - _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_5_universal="macOS 10.5+ universal (x86-64, i386, PPC64, PPC)")), - ("cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", - _build(interp_cp37="CPython 3.7", abi_abi3="CPython abi3", plat_macosx_10_12_universal2="macOS 10.12+ universal2 (ARM64, x86-64)")), - ("cryptography-42.0.5-cp313-cp313-android_27_armeabi_v7a.whl", - _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_android_27_armeabi_v7a="Android API level 27+ ARM EABI v7a")), - ("cryptography-42.0.5-cp313-cp313-android_27_arm64_v8a.whl", - _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_android_27_arm64_v8a="Android API level 27+ ARM64 v8a")), - ("cryptography-42.0.5-cp313-cp313-android_27_x86.whl", - _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_android_27_x86="Android API level 27+ x86")), - ("cryptography-42.0.5-cp313-cp313-android_27_x86_64.whl", - _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_android_27_x86_64="Android API level 27+ x86-64")), - ("cryptography-42.0.5-cp313-abi3-android_16_armeabi_v7a.whl", - _build(interp_cp313="CPython 3.13", abi_abi3="CPython abi3", plat_android_16_armeabi_v7a="Android API level 16+ ARM EABI v7a")), - ("cryptography-42.0.5-cp313-cp313-iOS_15_6_arm64_iphoneos.whl", - _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_ios_15_6_arm64_iphoneos="iOS 15.6+ ARM64 Device")), - ("cryptography-42.0.5-cp313-cp313-iOS_15_6_arm64_iphonesimulator.whl", - _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_ios_15_6_arm64_iphonesimulator="iOS 15.6+ ARM64 Simulator")), - ("cryptography-42.0.5-cp313-cp313-iOS_15_6_x86_64_iphonesimulator.whl", - _build(interp_cp313="CPython 3.13", abi_cp313="CPython 3.13", plat_ios_15_6_x86_64_iphonesimulator="iOS 15.6+ x86-64 Simulator")), - ("cryptography-42.0.5-cp313-abi3-iOS_13_0_arm64_iphoneos.whl", - _build(interp_cp313="CPython 3.13", abi_abi3="CPython abi3", plat_ios_13_0_arm64_iphoneos="iOS 13.0+ ARM64 Device")), - ("cryptography-42.0.5-cp313-abi3-iOS_13_0_arm64_iphonesimulator.whl", - _build(interp_cp313="CPython 3.13", abi_abi3="CPython abi3", plat_ios_13_0_arm64_iphonesimulator="iOS 13.0+ ARM64 Simulator")), - ("pgf-1.0-pp27-pypy_73-manylinux2010_x86_64.whl", - _build(interp_pp27="PyPy 27", abi_pypy_73="PyPy 73", plat_manylinux2010_x86_64="linux glibc 2.12+ x86-64")), + ("cryptography-42.0.5.tar.gz", _build(other_source="Source")), + ("Pillow-2.5.0-py3.4-win-amd64.egg", _build(other_egg="Egg")), + ("Pillow-2.5.0-py3.4-win32.egg", _build(other_egg="Egg")), + ( + "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", + _build( + interp_pp310="PyPy 310", + abi_pypy310_pp73="PyPy 310 pp73", + plat_win_amd64="Windows x86-64", + ), + ), + ( + "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", + _build( + interp_pp310="PyPy 310", + abi_pypy310_pp73="PyPy 310 pp73", + plat_manylinux_2_28_x86_64="linux glibc 2.28+ x86-64", + ), + ), + ( + "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", + _build( + interp_cp37="CPython 3.7", + abi_abi3="CPython abi3", + plat_musllinux_1_2_x86_64="linux musl 1.2+ x86-64", + ), + ), + ( + "cryptography-42.0.5-cp37-abi3-macosx_10_5_intel.whl", + _build( + interp_cp37="CPython 3.7", + abi_abi3="CPython abi3", + plat_macosx_10_5_intel="macOS 10.5+ Intel (x86-64, i386)", + ), + ), + ( + "cryptography-42.0.5-cp37-abi3-macosx_10_5_fat.whl", + _build( + interp_cp37="CPython 3.7", + abi_abi3="CPython abi3", + plat_macosx_10_5_fat="macOS 10.5+ fat (i386, PPC)", + ), + ), + ( + "cryptography-42.0.5-cp37-abi3-macosx_10_5_fat3.whl", + _build( + interp_cp37="CPython 3.7", + abi_abi3="CPython abi3", + plat_macosx_10_5_fat3="macOS 10.5+ fat3 (x86-64, i386, PPC)", + ), + ), + ( + "cryptography-42.0.5-cp37-abi3-macosx_10_5_fat64.whl", + _build( + interp_cp37="CPython 3.7", + abi_abi3="CPython abi3", + plat_macosx_10_5_fat64="macOS 10.5+ fat64 (x86-64, PPC64)", + ), + ), + ( + "cryptography-42.0.5-cp37-abi3-macosx_10_5_universal.whl", + _build( + interp_cp37="CPython 3.7", + abi_abi3="CPython abi3", + plat_macosx_10_5_universal="macOS 10.5+ " + "universal (x86-64, i386, PPC64, PPC)", + ), + ), + ( + "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", + _build( + interp_cp37="CPython 3.7", + abi_abi3="CPython abi3", + plat_macosx_10_12_universal2="macOS 10.12+ universal2 (ARM64, x86-64)", + ), + ), + ( + "cryptography-42.0.5-cp313-cp313-android_27_armeabi_v7a.whl", + _build( + interp_cp313="CPython 3.13", + abi_cp313="CPython 3.13", + plat_android_27_armeabi_v7a="Android API level 27+ ARM EABI v7a", + ), + ), + ( + "cryptography-42.0.5-cp313-cp313-android_27_arm64_v8a.whl", + _build( + interp_cp313="CPython 3.13", + abi_cp313="CPython 3.13", + plat_android_27_arm64_v8a="Android API level 27+ ARM64 v8a", + ), + ), + ( + "cryptography-42.0.5-cp313-cp313-android_27_x86.whl", + _build( + interp_cp313="CPython 3.13", + abi_cp313="CPython 3.13", + plat_android_27_x86="Android API level 27+ x86", + ), + ), + ( + "cryptography-42.0.5-cp313-cp313-android_27_x86_64.whl", + _build( + interp_cp313="CPython 3.13", + abi_cp313="CPython 3.13", + plat_android_27_x86_64="Android API level 27+ x86-64", + ), + ), + ( + "cryptography-42.0.5-cp313-abi3-android_16_armeabi_v7a.whl", + _build( + interp_cp313="CPython 3.13", + abi_abi3="CPython abi3", + plat_android_16_armeabi_v7a="Android API level 16+ ARM EABI v7a", + ), + ), + ( + "cryptography-42.0.5-cp313-cp313-iOS_15_6_arm64_iphoneos.whl", + _build( + interp_cp313="CPython 3.13", + abi_cp313="CPython 3.13", + plat_ios_15_6_arm64_iphoneos="iOS 15.6+ ARM64 Device", + ), + ), + ( + "cryptography-42.0.5-cp313-cp313-iOS_15_6_arm64_iphonesimulator.whl", + _build( + interp_cp313="CPython 3.13", + abi_cp313="CPython 3.13", + plat_ios_15_6_arm64_iphonesimulator="iOS 15.6+ ARM64 Simulator", + ), + ), + ( + "cryptography-42.0.5-cp313-cp313-iOS_15_6_x86_64_iphonesimulator.whl", + _build( + interp_cp313="CPython 3.13", + abi_cp313="CPython 3.13", + plat_ios_15_6_x86_64_iphonesimulator="iOS 15.6+ x86-64 Simulator", + ), + ), + ( + "cryptography-42.0.5-cp313-abi3-iOS_13_0_arm64_iphoneos.whl", + _build( + interp_cp313="CPython 3.13", + abi_abi3="CPython abi3", + plat_ios_13_0_arm64_iphoneos="iOS 13.0+ ARM64 Device", + ), + ), + ( + "cryptography-42.0.5-cp313-abi3-iOS_13_0_arm64_iphonesimulator.whl", + _build( + interp_cp313="CPython 3.13", + abi_abi3="CPython abi3", + plat_ios_13_0_arm64_iphonesimulator="iOS 13.0+ ARM64 Simulator", + ), + ), + ( + "pgf-1.0-pp27-pypy_73-manylinux2010_x86_64.whl", + _build( + interp_pp27="PyPy 27", + abi_pypy_73="PyPy 73", + plat_manylinux2010_x86_64="linux glibc 2.12+ x86-64", + ), + ), # Cannot parse 'pdfcomparator-0_2_0-py2-none-any.whl' - invalid version? - ("pdfcomparator-0_2_0-py2-none-any.whl", - # _build(interp_py2="Python 2", abi_none="(none)", plat_any="(any)")), - _build()), - ("mclbn256-0.6.0-py3-abi3-macosx_12_0_arm64.whl", - _build(interp_py3="Python 3", abi_abi3="CPython abi3", plat_macosx_12_0_arm64="macOS 12.0+ ARM64")), - ("pep272_encryption-0.4-py2.pp35.pp36.pp37.pp38.pp39-none-any.whl", - _build(interp_py2="Python 2", interp_pp35="PyPy 35", interp_pp36="PyPy 36", interp_pp37="PyPy 37", interp_pp38="PyPy 38", interp_pp39="PyPy 39", - abi_none="(none)", plat_any="(any)")), - ("ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", - _build(interp_py3="Python 3", abi_none="(none)", plat_musllinux_1_2_armv7l="linux musl 1.2+ ARMv7l")), - ("numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", - _build(interp_cp312="CPython 3.12", abi_cp312="CPython 3.12", plat_musllinux_1_1_x86_64="linux musl 1.1+ x86-64")), - ("numpy-1.26.4-lol_interpreter-lol_abi-lol_platform.whl", - _build(interp_lol_interpreter="lol interpreter", abi_lol_abi="lol abi", plat_lol_platform="lol platform")), - ("pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - _build(interp_pp39="PyPy 39", abi_pypy39_pp73="PyPy 39 pp73", plat_manylinux_2_17_aarch64="linux glibc 2.17+ ARM64", - plat_manylinux2014_aarch64="linux glibc 2.17+ ARM64")), - ("numpy-1.13.1-cp36-none-win_amd64.whl", - _build(interp_cp36="CPython 3.6", abi_none="(none)", plat_win_amd64="Windows x86-64")), - ("cryptography-38.0.2-cp36-abi3-win32.whl", - _build(interp_cp36="CPython 3.6", abi_abi3="CPython abi3", plat_win32="Windows x86")), - ("plato_learn-0.4.7-py36.py37.py38.py39-none-any.whl", - _build(interp_py36="Python 3.6", interp_py37="Python 3.7", interp_py38="Python 3.8", interp_py39="Python 3.9", - abi_none="(none)", plat_any="(any)")), - ("juriscraper-1.1.11-py27-none-any.whl", - _build(interp_py27="Python 2.7", abi_none="(none)", plat_any="(any)")), - ("OZI-0.0.291-py312-none-any.whl", - _build(interp_py312="Python 3.12", abi_none="(none)", plat_any="(any)")), - ("foo-0.0.0-ip27-none-any.whl", - _build(interp_ip27="IronPython 2.7", abi_none="(none)", plat_any="(any)")), - ("foo-0.0.0-jy38-none-any.whl", - _build(interp_jy38="Jython 3.8", abi_none="(none)", plat_any="(any)")), - ("foo-0.0.0-garbage-none-any.whl", - _build(interp_garbage="garbage", abi_none="(none)", plat_any="(any)")), - ("foo-0.0.0-69-none-any.whl", - _build(interp_69="69", abi_none="(none)", plat_any="(any)")), - ("aiohttp-3.13.2-cp314-cp314udmtz-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", - _build(interp_cp314="CPython 3.14", - abi_cp314udmtz="CPython 3.14 debug free-threading pymalloc wide-unicode z", + ( + "pdfcomparator-0_2_0-py2-none-any.whl", + # _build(interp_py2="Python 2", abi_none="(none)", plat_any="(any)")), + _build(), + ), + ( + "mclbn256-0.6.0-py3-abi3-macosx_12_0_arm64.whl", + _build( + interp_py3="Python 3", + abi_abi3="CPython abi3", + plat_macosx_12_0_arm64="macOS 12.0+ ARM64", + ), + ), + ( + "pep272_encryption-0.4-py2.pp35.pp36.pp37.pp38.pp39-none-any.whl", + _build( + interp_py2="Python 2", + interp_pp35="PyPy 35", + interp_pp36="PyPy 36", + interp_pp37="PyPy 37", + interp_pp38="PyPy 38", + interp_pp39="PyPy 39", + abi_none="(none)", + plat_any="(any)", + ), + ), + ( + "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", + _build( + interp_py3="Python 3", + abi_none="(none)", + plat_musllinux_1_2_armv7l="linux musl 1.2+ ARMv7l", + ), + ), + ( + "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", + _build( + interp_cp312="CPython 3.12", + abi_cp312="CPython 3.12", + plat_musllinux_1_1_x86_64="linux musl 1.1+ x86-64", + ), + ), + ( + "numpy-1.26.4-lol_interpreter-lol_abi-lol_platform.whl", + _build( + interp_lol_interpreter="lol interpreter", + abi_lol_abi="lol abi", + plat_lol_platform="lol platform", + ), + ), + ( + "pydantic_core-2.16.2-pp39-pypy39_pp73-" + "manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + _build( + interp_pp39="PyPy 39", + abi_pypy39_pp73="PyPy 39 pp73", + plat_manylinux_2_17_aarch64="linux glibc 2.17+ ARM64", + plat_manylinux2014_aarch64="linux glibc 2.17+ ARM64", + ), + ), + ( + "numpy-1.13.1-cp36-none-win_amd64.whl", + _build( + interp_cp36="CPython 3.6", + abi_none="(none)", + plat_win_amd64="Windows x86-64", + ), + ), + ( + "cryptography-38.0.2-cp36-abi3-win32.whl", + _build( + interp_cp36="CPython 3.6", + abi_abi3="CPython abi3", + plat_win32="Windows x86", + ), + ), + ( + "plato_learn-0.4.7-py36.py37.py38.py39-none-any.whl", + _build( + interp_py36="Python 3.6", + interp_py37="Python 3.7", + interp_py38="Python 3.8", + interp_py39="Python 3.9", + abi_none="(none)", + plat_any="(any)", + ), + ), + ( + "juriscraper-1.1.11-py27-none-any.whl", + _build(interp_py27="Python 2.7", abi_none="(none)", plat_any="(any)"), + ), + ( + "OZI-0.0.291-py312-none-any.whl", + _build(interp_py312="Python 3.12", abi_none="(none)", plat_any="(any)"), + ), + ( + "foo-0.0.0-ip27-none-any.whl", + _build(interp_ip27="IronPython 2.7", abi_none="(none)", plat_any="(any)"), + ), + ( + "foo-0.0.0-jy38-none-any.whl", + _build(interp_jy38="Jython 3.8", abi_none="(none)", plat_any="(any)"), + ), + ( + "foo-0.0.0-garbage-none-any.whl", + _build(interp_garbage="garbage", abi_none="(none)", plat_any="(any)"), + ), + ( + "foo-0.0.0-69-none-any.whl", + _build(interp_69="69", abi_none="(none)", plat_any="(any)"), + ), + ( + "aiohttp-3.13.2-cp314-cp314udmtz-" + "manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", + _build( + interp_cp314="CPython 3.14", + abi_cp314udmtz="CPython 3.14 " + "debug free-threading pymalloc wide-unicode z", plat_manylinux_2_31_riscv64="linux glibc 2.31+ RISC-V 64", - plat_manylinux_2_39_riscv64="linux glibc 2.39+ RISC-V 64")), - ("aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", - _build(interp_cp314="CPython 3.14", + plat_manylinux_2_39_riscv64="linux glibc 2.39+ RISC-V 64", + ), + ), + ( + "aiohttp-3.13.2-cp314-cp314t-" + "manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", + _build( + interp_cp314="CPython 3.14", abi_cp314t="CPython 3.14 free-threading", plat_manylinux2014_s390x="linux glibc 2.17+ IBM System/390x", plat_manylinux_2_17_s390x="linux glibc 2.17+ IBM System/390x", - plat_manylinux_2_28_s390x="linux glibc 2.28+ IBM System/390x")), - ("aiohttp-3.13.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", - _build(interp_cp39="CPython 3.9", + plat_manylinux_2_28_s390x="linux glibc 2.28+ IBM System/390x", + ), + ), + ( + "aiohttp-3.13.2-cp39-cp39-" + "manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", + _build( + interp_cp39="CPython 3.9", abi_cp39="CPython 3.9", plat_manylinux2014_ppc64le="linux glibc 2.17+ PowerPC 64-le", plat_manylinux_2_17_ppc64le="linux glibc 2.17+ PowerPC 64-le", - plat_manylinux_2_28_ppc64le="linux glibc 2.28+ PowerPC 64-le")), - ("numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", - _build(interp_pp311="PyPy 311", + plat_manylinux_2_28_ppc64le="linux glibc 2.28+ PowerPC 64-le", + ), + ), + ( + "numpy-2.3.4-pp311-pypy311_pp73-" + "manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", + _build( + interp_pp311="PyPy 311", abi_pypy311_pp73="PyPy 311 pp73", plat_manylinux_2_27_aarch64="linux glibc 2.27+ ARM64", - plat_manylinux_2_28_aarch64="linux glibc 2.28+ ARM64")) + plat_manylinux_2_28_aarch64="linux glibc 2.28+ ARM64", + ), + ), ], ) def test_wheel_to_groups_labels(filename, expected_tags): - # assert wheel.filename_to_pretty_tags(filename) == expected_tags assert wheel.filename_to_grouped_labels(filename) == expected_tags diff --git a/warehouse/static/js/warehouse/controllers/filter_list_controller.js b/warehouse/static/js/warehouse/controllers/filter_list_controller.js index ce062b0f7418..b9507c51e4a0 100644 --- a/warehouse/static/js/warehouse/controllers/filter_list_controller.js +++ b/warehouse/static/js/warehouse/controllers/filter_list_controller.js @@ -49,11 +49,9 @@ export default class extends Controller { this.itemTargets.forEach((item, index) => { total += 1; const itemData = this.mappingItemFilterData[index]; - const compareResult = this._compare(itemData, filterData); + const isShow = this._compare(itemData, filterData); // Should the item be displayed or not? - // Show if there are no filters, or if there are filters and at least one match. - const isShow = !compareResult.hasFilter || (compareResult.hasFilter && compareResult.isMatch); if (isShow) { // match: show item item.classList.remove("hidden"); @@ -78,7 +76,7 @@ export default class extends Controller { if (this.hasUrlTarget && this.urlTarget) { const searchParams = new URLSearchParams(); for (const key in filterData) { - for (const value of filterData[key]) { + for (const value of filterData[key].values) { if (value && value.trim()) { searchParams.set(key, [...searchParams.getAll(key), value]); } @@ -152,69 +150,65 @@ export default class extends Controller { const key = filterTarget.dataset.filteredSource; const value = filterTarget.value; if (!Object.hasOwn(filterData, key)) { - filterData[key] = []; + filterData[key] = {values: [], comparison: 'exact'}; + } + filterData[key].values.push(value); + + const comparison = filterTarget.dataset.comparisonType; + if (comparison){ + filterData[key].comparison = comparison; } - filterData[key].push(value); }); } return filterData; } /** - * Compare an item's tags to all filter values and find matches. + * Compare an item's data to all filter values and find matches. * @param itemData The item mapping. * @param filterData The filter mapping. - * @returns {{hasFilter: boolean, isMatch: boolean, matches: *[]}} + * @returns {boolean} * @private */ _compare(itemData, filterData) { - // The overall match will be true when, - // for every filter key that has at least one value, - // at least one item value for the same key includes any filter value. - - const isMatch = []; - const matches = []; - const misses = []; - let hasFilter = false; - for (const itemKey in itemData) { - const itemValues = itemData[itemKey]; - const filterValues = filterData[itemKey]; - - let isKeyMatch = false; - let hasKeyFilter = false; - - for (const itemValue of itemValues) { - for (const filterItemValue of filterValues) { + // if there are no filters, return true + const anyFilter = Object.values(filterData) + .some(filterInfo => { + const filterValues = filterInfo.values || [] + return filterValues.length > 0 && filterValues.some(filterValue => !!filterValue); + }); + if (!anyFilter) { + return true; + } - if (filterItemValue && !hasKeyFilter) { - // Record whether there are any filter values in any filter key. - hasFilter = true; - } + // Match using 'OR': the overall match will be true when, + // for at least one filter key, + // that has at least one value, + // at least one item value for the same key includes any filter value. + return Object.entries(filterData) + .some(filterEntry => { + // only include filter values and item values that are truthy + const filterValues = (filterEntry[1].values || []).filter(i => !!i); + const itemValues = (itemData[filterEntry[0]] || []).filter(i => !!i); + const comparison = filterEntry[1].comparison; + if (itemValues.length === 0 || filterValues.length === 0) { + // If there is at least one filter, + // and both the item and the filter have no values, + // filter out the item. + return false; + } - if (filterItemValue && !hasKeyFilter) { - // Record whether there are any filter values in *this* filter key. - hasKeyFilter = true; + return filterValues.some(filterValue => { + if (!filterValue) { + return false; } - - // There could be two types of comparisons - exact match for tags, contains for filename. - // Using: for each named group, does any item value include any filter value? - if (filterItemValue && itemValue.includes(filterItemValue)) { - isKeyMatch = true; - matches.push({"key": itemKey, "filter": filterItemValue, "item": itemValue}); + if (comparison === 'exact') { + return itemValues.some(itemValue => itemValue === filterValue); } - if (filterItemValue && !itemValue.includes(filterItemValue)) { - misses.push({"key": itemKey, "filter": filterItemValue, "item": itemValue}); + if (comparison === 'includes') { + return itemValues.some(itemValue => itemValue.includes(filterValue)); } - } - } - isMatch.push(!hasKeyFilter || (isKeyMatch && hasKeyFilter)); - } - - return { - "isMatch": isMatch.every(value => value), - "hasFilter": hasFilter, - "matches": matches, - "misses": misses, - }; + }); + }); } } diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html index 332f89b920ba..4cdbadc406e1 100644 --- a/warehouse/templates/packaging/detail.html +++ b/warehouse/templates/packaging/detail.html @@ -70,9 +70,8 @@

pip install{{ index_url }} {{ release.project.name }}{{ project_version }} {%- endif -%} {%- endmacro -%} - {%- macro sdists_table(files) -%} - +
@@ -81,38 +80,35 @@

- {% for file in files %} - - - - - - - - {% endfor %} + {% for file in files %} + + + + + + + + {% endfor %}
{% trans name=release.project.name, release=release.version %}Source distribution for {{ name }} {{ version }}{% endtrans %}
{% trans %}File{% endtrans %}{% trans %}Type{% endtrans %} {% trans %}Details{% endtrans %}
- {% trans %}File{% endtrans %} -   - {{ file.filename }} - - {% trans %}Uploaded{% endtrans %} - {% trans upload_time=humanize(file.upload_time) %}Uploaded {{ upload_time }}{% endtrans %} - - {% trans %}Size{% endtrans %} - {{ file.size|filesizeformat() if file.size else 0|filesizeformat() }} - - {% trans %}Type{% endtrans %} - {% trans %}Source{% endtrans %} - - {% trans %}View details{% endtrans %} - - {% trans %}Details{% endtrans %} - -
+ {% trans %}File{% endtrans %} +   + {{ file.filename }} + + {% trans %}Uploaded{% endtrans %} + {% trans upload_time=humanize(file.upload_time) %}Uploaded {{ upload_time }}{% endtrans %} + + {% trans %}Size{% endtrans %} + {{ file.size|filesizeformat() if file.size else 0|filesizeformat() }} + + {% trans %}Type{% endtrans %} + {% trans %}Source{% endtrans %} + + {% trans %}View details{% endtrans %} + {% trans %}Details{% endtrans %} +
{%- endmacro -%} - {%- macro bdists_table(files) -%} @@ -123,65 +119,74 @@

- {% for file in files %} - - - + + {% if 'Egg' in file.pretty_wheel_tags %} + - + {% else %} + - + - + {% if key == 'abi' %} + {% for abi_value, abi_label in value.items() %} + {{ abi_label }} + {% if not loop.last %}
{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + + + {% endif %} + {% endfor %} -
{% trans name=release.project.name, release=release.version %}Table of built distributions (wheels) for {{ name }} {{ release }}{% endtrans %}
{% trans %}Platform{% endtrans %} {% trans %}Details{% endtrans %}
- {% trans %}File{% endtrans %} -   - {{ file.filename }} -
- - {{ file.size|filesizeformat() if file.size else 0|filesizeformat() }}.  - {% trans upload_time=humanize(file.upload_time) %}Uploaded {{ upload_time }}{% endtrans %}. - -
- {% trans %}Interpreter{% endtrans %} - {% for key, value in file.wheel_filters.items() %} - {% if key == 'interpreters' %} - {% for interpreter in value %} - {{ interpreter }}
+ {% for file in files %} +
+ {% trans %}File{% endtrans %} +   + Download file +
+ + {{ file.size|filesizeformat() if file.size else 0|filesizeformat() }}.  + {% trans upload_time=humanize(file.upload_time) %}Uploaded {{ upload_time }}{% endtrans %}. + +
+ {{ file.filename }} + {% for tag in file.pretty_wheel_tags %} + {% if loop.first %} - {% endif %} + {{ tag }} + {% if not loop.last %} {% endif %} {% endfor %} - {% endif %} - {% endfor %} - - {% trans %}ABI{% endtrans %} + + {% trans %}Interpreter{% endtrans %} {% for key, value in file.wheel_filters.items() %} - {% if key == 'abis' %} - {% for abi in value %} - {{ abi }}
- {% endfor %} - {% endif %} - {% endfor %} -
- {% trans %}Platform{% endtrans %} + {% if key == 'interpreter' %} + {% for interpreter_value, interpreter_label in value.items() %} + {{ interpreter_label }} + {% if not loop.last %}
{% endif %} + {% endfor %} + {% endif %} + {% endfor %} +
+ {% trans %}ABI{% endtrans %} {% for key, value in file.wheel_filters.items() %} - {% if key == 'platforms' %} - {% for platform in value %} - {{ platform }}
- {% endfor %} - {% endif %} - {% endfor %} -
- {% trans %}View details{% endtrans %} - - {% trans %}Details{% endtrans %} - - + {% trans %}Platform{% endtrans %} + {% for key, value in file.wheel_filters.items() %} + {% if key == 'platform' %} + {% for platform_value, platform_label in value.items() %} + {{ platform_label }} + {% if not loop.last %}
{% endif %} + {% endfor %} + {% endif %} + {% endfor %} +
+ {% trans %}View details{% endtrans %} + {% trans %}Details{% endtrans %} +
+ {%- endmacro -%} - {% macro filter_select(name, title, selected) %} {% endmacro %} - {% block title %}{{ release.project.name }}{% endblock %} {% block description %}{{ release.summary }}{% endblock %} {% block additional_rss -%} @@ -550,9 +555,8 @@

{% trans href='https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives', title=gettext('External link') %}See tutorial on generating distribution archives.{% endtrans %} {% endif %} - {% if bdists %} - {% set bdist_count = bdists|length %} + {% set bdist_count = bdists|length %}

{% trans count=bdist_count %} Built distribution (wheel) @@ -561,62 +565,60 @@

{% endtrans %}

{% if bdist_count > 3 %} - -
- {% endif %} - {{ bdists_table(bdists) }} - {% if bdist_count > 3 %} - + {% endif %} - {% endif %}
{# Tabs: file details #} diff --git a/warehouse/utils/wheel.py b/warehouse/utils/wheel.py index 0a858db15e4c..6e89caf58f55 100644 --- a/warehouse/utils/wheel.py +++ b/warehouse/utils/wheel.py @@ -7,46 +7,63 @@ # Map known Python tags, ABI tags, Platform tags to labels. _PLATFORM_MAP = { - "win": [(re.compile(r"^win_(.*?)$"), lambda m: f"Windows {_norm_arch(m.group(1))}")], + "win": [ + (re.compile(r"^win_(.*?)$"), lambda m: f"Windows {_norm_arch(m.group(1))}") + ], "win32": [(re.compile(r"^win32$"), lambda m: "Windows x86")], - "manylinux": [( - re.compile(r"^manylinux_(\d+)_(\d+)_(.*?)$"), - lambda m: f"linux glibc {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))}" - - )], - "manylinux2014": [( - re.compile(r"^manylinux2014_(.*?)$"), - lambda m: f"linux glibc 2.17+ {_norm_arch(m.group(1))}", - )], - "manylinux2010": [( - re.compile(r"^manylinux2010_(.*?)$"), - lambda m: f"linux glibc 2.12+ {_norm_arch(m.group(1))}", - )], - "manylinux1": [( - re.compile(r"^manylinux1_(.*?)$"), - lambda m: f"linux glibc 2.5+ {_norm_arch(m.group(1))}", - )], - "musllinux": [( - re.compile(r"^musllinux_(\d+)_(\d+)_(.*?)$"), - lambda m: f"linux musl {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))}" - )], - "macosx": [( - re.compile(r"^macosx_(\d+)_(\d+)_(.*?)$"), - lambda m: f"macOS {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))}", - )], - "ios": [( - re.compile(r"^ios_(\d+)_(\d+)_(.*?)_iphoneos$"), - lambda m: f"iOS {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))} Device", # noqa: E501 - ), + "manylinux": [ + ( + re.compile(r"^manylinux_(\d+)_(\d+)_(.*?)$"), + lambda m: f"linux glibc {m.group(1)}.{m.group(2)}+ " + f"{_norm_arch(m.group(3))}", + ) + ], + "manylinux2014": [ + ( + re.compile(r"^manylinux2014_(.*?)$"), + lambda m: f"linux glibc 2.17+ {_norm_arch(m.group(1))}", + ) + ], + "manylinux2010": [ + ( + re.compile(r"^manylinux2010_(.*?)$"), + lambda m: f"linux glibc 2.12+ {_norm_arch(m.group(1))}", + ) + ], + "manylinux1": [ + ( + re.compile(r"^manylinux1_(.*?)$"), + lambda m: f"linux glibc 2.5+ {_norm_arch(m.group(1))}", + ) + ], + "musllinux": [ + ( + re.compile(r"^musllinux_(\d+)_(\d+)_(.*?)$"), + lambda m: f"linux musl {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))}", + ) + ], + "macosx": [ + ( + re.compile(r"^macosx_(\d+)_(\d+)_(.*?)$"), + lambda m: f"macOS {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))}", + ) + ], + "ios": [ + ( + re.compile(r"^ios_(\d+)_(\d+)_(.*?)_iphoneos$"), + lambda m: f"iOS {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))} Device", # noqa: E501 + ), ( re.compile(r"^ios_(\d+)_(\d+)_(.*?)_iphonesimulator$"), lambda m: f"iOS {m.group(1)}.{m.group(2)}+ {_norm_arch(m.group(3))} Simulator", # noqa: E501 - )], - "android": - [( + ), + ], + "android": [ + ( re.compile(r"^android_(\d+)_(.*?)$"), lambda m: f"Android API level {m.group(1)}+ {_norm_arch(m.group(2))}", - )], + ) + ], } _ARCH_MAP = { "amd64": "x86-64", @@ -85,7 +102,7 @@ def _norm_arch(a: str) -> str: def _norm_str(s: str) -> str: - return (s or "").replace('_', ' ').strip() + return (s or "").replace("_", " ").strip() def _implementation_to_label(raw: str) -> str: @@ -125,7 +142,7 @@ def _format_cpython(s: str) -> tuple[str, str]: suffixes.append(name) raw = raw[0:-1] version = _format_version(raw) - return version, ' '.join(sorted(suffixes)) + return version, " ".join(sorted(suffixes)) def _interpreter_to_label(tag: packaging.tags.Tag) -> str: @@ -158,14 +175,14 @@ def _abi_to_label(tag: packaging.tags.Tag) -> str: def _platform_to_label(tag: packaging.tags.Tag) -> str: - if tag.platform == 'any': + if tag.platform == "any": return "(any)" value = tag.platform - key = value.split('_', maxsplit=1)[0] if '_' in value else value + key = value.split("_", maxsplit=1)[0] if "_" in value else value patterns = _PLATFORM_MAP.get(key, []) - for (prefix_re, tmpl) in patterns: + for prefix_re, tmpl in patterns: if match := prefix_re.match(value): return tmpl(match) @@ -210,17 +227,21 @@ def filename_to_grouped_labels(filename: str) -> dict[str, dict]: } if filename.endswith(".egg"): - grouped_labels['other']['egg'] = "Egg" + grouped_labels["other"]["egg"] = "Egg" return grouped_labels elif not filename.endswith(".whl"): - grouped_labels['other']['source'] = "Source" + grouped_labels["other"]["source"] = "Source" return grouped_labels tags = filename_to_tags(filename) for tag in tags: - _add_group_label(grouped_labels, "interpreter", tag.interpreter, _interpreter_to_label(tag)) + _add_group_label( + grouped_labels, "interpreter", tag.interpreter, _interpreter_to_label(tag) + ) _add_group_label(grouped_labels, "abi", tag.abi, _abi_to_label(tag)) - _add_group_label(grouped_labels, "platform", tag.platform, _platform_to_label(tag)) + _add_group_label( + grouped_labels, "platform", tag.platform, _platform_to_label(tag) + ) return grouped_labels From 8a71098744839c2be3bcbfd979ac79c7bfe76717 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Sat, 1 Nov 2025 21:37:53 +1000 Subject: [PATCH 05/13] update filters controller to get and set filters url query string --- tests/unit/packaging/test_views.py | 8 +- warehouse/packaging/views.py | 9 - .../controllers/filter_list_controller.js | 183 +++++++++++++----- warehouse/templates/packaging/detail.html | 20 +- 4 files changed, 141 insertions(+), 79 deletions(-) diff --git a/tests/unit/packaging/test_views.py b/tests/unit/packaging/test_views.py index d63b3d14baab..96afc909eb44 100644 --- a/tests/unit/packaging/test_views.py +++ b/tests/unit/packaging/test_views.py @@ -220,13 +220,7 @@ def test_detail_rendered(self, db_request): "abi": {}, "platform": {}, "other": {}, - }, - "wheel_filters_params": { - "filename": "", - "interpreters": "", - "abis": "", - "platforms": "", - }, + } } def test_detail_renders_files_natural_sort(self, db_request): diff --git a/warehouse/packaging/views.py b/warehouse/packaging/views.py index 4b0c03dd64ef..52c243212b35 100644 --- a/warehouse/packaging/views.py +++ b/warehouse/packaging/views.py @@ -272,14 +272,6 @@ def release_detail(release, request): [bdist.filename for bdist in bdists] ) - # Get the querystring to load any pre-set parameters - wheel_filters_params = { - "filename": request.params.get("filename", ""), - "interpreters": request.params.get("interpreters", ""), - "abis": request.params.get("abis", ""), - "platforms": request.params.get("platforms", ""), - } - return { "project": project, "release": release, @@ -294,7 +286,6 @@ def release_detail(release, request): # Additional function to format the attestations "PEP740AttestationViewer": PEP740AttestationViewer, "wheel_filters_all": wheel_filters_all, - "wheel_filters_params": wheel_filters_params, } diff --git a/warehouse/static/js/warehouse/controllers/filter_list_controller.js b/warehouse/static/js/warehouse/controllers/filter_list_controller.js index b9507c51e4a0..38d2f748e1c4 100644 --- a/warehouse/static/js/warehouse/controllers/filter_list_controller.js +++ b/warehouse/static/js/warehouse/controllers/filter_list_controller.js @@ -29,6 +29,9 @@ export default class extends Controller { this._populateMappings(); this._initVisibility(); + const urlFilters = this._getFiltersUrlSearch(); + this._setFiltersHtmlElements(urlFilters); + this.filter(); } @@ -72,23 +75,21 @@ export default class extends Controller { total); } - // Update the direct url to this filter + // Update the current url to include the filters + const htmlElementFilters = this._getFiltersHtmlElements(); + this._setFiltersUrlSearch(htmlElementFilters); + + // Update the direct url to these filters if (this.hasUrlTarget && this.urlTarget) { - const searchParams = new URLSearchParams(); - for (const key in filterData) { - for (const value of filterData[key].values) { - if (value && value.trim()) { - searchParams.set(key, [...searchParams.getAll(key), value]); - } + const urlTargetUrl = new URL(this.urlTarget.href); + Object.entries(htmlElementFilters ?? {}).forEach(([key, value]) => { + if (value) { + urlTargetUrl.searchParams.set(key, value); + } else { + urlTargetUrl.searchParams.delete(key); } - } - - const qs = searchParams.toString(); - const baseUrl = new URL(this.urlTarget.href); - if (qs) { - baseUrl.search = "?" + qs; - } - this.urlTarget.textContent = baseUrl.toString(); + }); + this.urlTarget.textContent = urlTargetUrl.toString(); } } @@ -155,7 +156,7 @@ export default class extends Controller { filterData[key].values.push(value); const comparison = filterTarget.dataset.comparisonType; - if (comparison){ + if (comparison) { filterData[key].comparison = comparison; } }); @@ -165,50 +166,126 @@ export default class extends Controller { /** * Compare an item's data to all filter values and find matches. - * @param itemData The item mapping. - * @param filterData The filter mapping. + * Filters are processed as 'AND' - the item data must match all the filters. + * @param itemData {{[key: string]:string[]}} The item mapping. + * @param filterData {{[key: string]: {values: string[], comparison: "exact"|"includes"}}} The filter mapping. * @returns {boolean} * @private */ _compare(itemData, filterData) { - // if there are no filters, return true - const anyFilter = Object.values(filterData) - .some(filterInfo => { - const filterValues = filterInfo.values || [] - return filterValues.length > 0 && filterValues.some(filterValue => !!filterValue); - }); - if (!anyFilter) { - return true; - } + for (const [filterKey, filterInfo] of Object.entries(filterData)) { + const comparison = filterInfo.comparison; + const filterValues = Array.from(new Set((filterInfo.values ?? []).map(i => i?.toString()?.trim() ?? "").filter(i => !!i))); + const itemValues = Array.from(new Set((itemData[filterKey] ?? []).map(i => i?.toString()?.trim() ?? "").filter(i => !!i))); + + // Not a match if the item values and filter values are different lengths. + if (filterValues.length > 0 && filterValues.length !== itemValues.length) { + console.log(`_compare lengths filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); + return false; + } - // Match using 'OR': the overall match will be true when, - // for at least one filter key, - // that has at least one value, - // at least one item value for the same key includes any filter value. - return Object.entries(filterData) - .some(filterEntry => { - // only include filter values and item values that are truthy - const filterValues = (filterEntry[1].values || []).filter(i => !!i); - const itemValues = (itemData[filterEntry[0]] || []).filter(i => !!i); - const comparison = filterEntry[1].comparison; - if (itemValues.length === 0 || filterValues.length === 0) { - // If there is at least one filter, - // and both the item and the filter have no values, - // filter out the item. + // Not a match if the item values and filter values contain different values. + if (filterValues.length > 0 && comparison === 'exact') { + if (!filterValues.every(filterValue => itemValues.includes(filterValue))) { + console.log(`_compare exact filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); return false; } + } - return filterValues.some(filterValue => { - if (!filterValue) { - return false; - } - if (comparison === 'exact') { - return itemValues.some(itemValue => itemValue === filterValue); - } - if (comparison === 'includes') { - return itemValues.some(itemValue => itemValue.includes(filterValue)); - } - }); - }); + if (filterValues.length > 0 && comparison === 'includes') { + if (!filterValues.every(filterValue => itemValues.some(itemValue => itemValue.includes(filterValue)))) { + console.log(`_compare includes filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); + return false; + } + } + } + console.log(`_compare true filterData ${JSON.stringify(filterData)} itemData ${JSON.stringify(itemData)}`); + return true; + } + + /** + * Get the filters from the url query string. + * @returns {{[key: string]: string}} + * @private + */ + _getFiltersUrlSearch() { + const enabledFilterTargets = this._getAutoUpdateUrlQuerystringFilters(); + const currentSearchParams = new URLSearchParams(document.location.search); + const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + return Object.fromEntries(filterTargets.map(filterTarget => { + const key = filterTarget.dataset.filteredSource; + const value = currentSearchParams.get(key); + return [key, value]; + }).filter(([key, _value]) => enabledFilterTargets.includes(key))); + } + + /** + * Set the filters to the url query string. + * @param filters The filters to set. + * @private + */ + _setFiltersUrlSearch(filters) { + const enabledFilterTargets = this._getAutoUpdateUrlQuerystringFilters(); + const currentUrl = new URL(document.location.href); + const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + filterTargets.forEach(filterTarget => { + const key = filterTarget.dataset.filteredSource; + if (!enabledFilterTargets.includes(key)) { + return; + } + const value = filters[key] ?? null; + if (value) { + currentUrl.searchParams.set(key, value); + } else { + currentUrl.searchParams.delete(key); + } + }); + window.history.replaceState(null, "", currentUrl); + } + + /** + * Get the filters from the HTML element values. + * @returns {{[key: string]: string}} + * @private + */ + _getFiltersHtmlElements() { + const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + return Object.fromEntries(filterTargets.map(filterTarget => { + const key = filterTarget.dataset.filteredSource; + const value = filterTarget.value; + return [key, value]; + })); + } + + /** + * Set the filters to the HTML element values. + * @param filters + * @private + */ + _setFiltersHtmlElements(filters) { + const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + filterTargets.forEach(filterTarget => { + const key = filterTarget.dataset.filteredSource; + if (filters[key] !== undefined) { + filterTarget.value = filters[key] ?? ""; + } + }); + } + + /** + * Get a map of the filters and whether they participate in the automatic url querystring update. + * @returns {string[]} + * @private + */ + _getAutoUpdateUrlQuerystringFilters() { + const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + return filterTargets + .map(filterTarget => { + const key = filterTarget.dataset.filteredSource; + const value = filterTarget.dataset.autoUpdateUrlQuerystring; + return [key, value]; + }) + .filter(([_key, value]) => value === 'true') + .map(([key, _value]) => key); } } diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html index 4cdbadc406e1..b052e3d8c3c6 100644 --- a/warehouse/templates/packaging/detail.html +++ b/warehouse/templates/packaging/detail.html @@ -187,7 +187,7 @@

{% endfor %} {%- endmacro -%} -{% macro filter_select(name, title, selected) %} +{% macro filter_select(name, title) %} {% endmacro %} @@ -579,14 +579,14 @@

id="bdist-filenames-filter" name="bdist-filenames-filter" placeholder="{% trans %}Search by file name{% endtrans %}" - {% if wheel_filters_params['filename'] %}value="{{ wheel_filters_params['filename'] }}"{% endif %} autocapitalize="none" autocomplete="" spellcheck="false" data-action="filter-list#filter" data-filter-list-target="filter" data-filtered-source="filename" - data-comparison-type="includes"> + data-comparison-type="includes" + data-auto-update-url-querystring="true">
@@ -606,10 +606,10 @@

{% trans %}Copy link to filters{% endtrans %} - {{ filter_select('interpreter', 'Interpreter', wheel_filters_params) }} + {{ filter_select('interpreter', 'Interpreter') }}

-
{{ filter_select('abi', 'ABI', wheel_filters_params) }}
-
{{ filter_select('platform', 'Platform', wheel_filters_params) }}
+
{{ filter_select('abi', 'ABI') }}
+
{{ filter_select('platform', 'Platform') }}
{% endif %} From 0b48b58ae43e498399bf9973f394550678bc7ae7 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Sat, 1 Nov 2025 22:54:15 +1000 Subject: [PATCH 06/13] limit the filters based on what the user has already selected --- .../controllers/filter_list_controller.js | 109 +++++++++++++----- 1 file changed, 80 insertions(+), 29 deletions(-) diff --git a/warehouse/static/js/warehouse/controllers/filter_list_controller.js b/warehouse/static/js/warehouse/controllers/filter_list_controller.js index 38d2f748e1c4..b9e1b83dd389 100644 --- a/warehouse/static/js/warehouse/controllers/filter_list_controller.js +++ b/warehouse/static/js/warehouse/controllers/filter_list_controller.js @@ -24,11 +24,25 @@ export default class extends Controller { }; mappingItemFilterData = {}; + initialSelectOptions = {}; connect() { this._populateMappings(); this._initVisibility(); + // Capture the initial select element values, so they can be restored. + this._getFilterTargets().forEach(filterTarget => { + if (filterTarget.nodeName === 'SELECT') { + const key = filterTarget.dataset.filteredSource; + if (!this.initialSelectOptions[key]) { + this.initialSelectOptions[key] = []; + } + for (const option of filterTarget.options) { + this.initialSelectOptions[key].push([option.value, option.label]); + } + } + }); + const urlFilters = this._getFiltersUrlSearch(); this._setFiltersHtmlElements(urlFilters); @@ -49,6 +63,8 @@ export default class extends Controller { let total = 0; let shown = 0; + const groupedLabels = {}; + this.itemTargets.forEach((item, index) => { total += 1; const itemData = this.mappingItemFilterData[index]; @@ -59,6 +75,17 @@ export default class extends Controller { // match: show item item.classList.remove("hidden"); shown += 1; + // store the matched items to update the select options later + Object.entries(itemData).forEach(([key, values]) => { + if (!groupedLabels[key]) { + groupedLabels[key] = []; + } + values.forEach(value => { + if (!groupedLabels[key].includes(value)) { + groupedLabels[key].push(value); + } + }) + }); } else { // no match: hide item item.classList.add("hidden"); @@ -91,6 +118,34 @@ export default class extends Controller { }); this.urlTarget.textContent = urlTargetUrl.toString(); } + + // Update the dropdowns to reflect the currently displayed items. + const filterTargets = this._getFilterTargets(); + const selected = {}; + filterTargets.forEach(filterTarget => { + if (filterTarget.nodeName === 'SELECT') { + const key = filterTarget.dataset.filteredSource; + // Store which option is selected. + for (const selectedOption of filterTarget.selectedOptions) { + selected[key] = selectedOption.value; + } + // Remove all existing options. + for (let index = filterTarget.options.length - 1; index >= 0; index--) { + filterTarget.options.remove(index); + } + // Add the options reflecting the currently displayed items. + const valuesToKeep = groupedLabels[key] ?? []; + this.initialSelectOptions[key].forEach(option => { + const initialOptionValue = option[0]; + const initialOptionLabel = option[1]; + if (initialOptionValue === "" || valuesToKeep.includes(initialOptionValue)) { + const isSelected = selected[key] === initialOptionValue; + filterTarget.options.add(new Option(initialOptionLabel, initialOptionValue, null, isSelected)); + } + }); + } + }); + // console.log(`update dropdowns groupedLabels ${JSON.stringify(groupedLabels)} selected ${JSON.stringify(selected)}`); } /** @@ -141,26 +196,25 @@ export default class extends Controller { /** * Build a mapping of filteredSource names to array of values. - * @returns {{}} + * @returns {{[key: string]: {values: string[], comparison: "exact"|"includes"}}} * @private */ _buildFilterData() { const filterData = {}; - if (this.hasFilterTarget) { - this.filterTargets.forEach(filterTarget => { - const key = filterTarget.dataset.filteredSource; - const value = filterTarget.value; - if (!Object.hasOwn(filterData, key)) { - filterData[key] = {values: [], comparison: 'exact'}; - } - filterData[key].values.push(value); + const filterTargets = this._getFilterTargets(); + filterTargets.forEach(filterTarget => { + const key = filterTarget.dataset.filteredSource; + const value = filterTarget.value; + if (!Object.hasOwn(filterData, key)) { + filterData[key] = {values: [], comparison: 'exact'}; + } + filterData[key].values.push(value); - const comparison = filterTarget.dataset.comparisonType; - if (comparison) { - filterData[key].comparison = comparison; - } - }); - } + const comparison = filterTarget.dataset.comparisonType; + if (comparison) { + filterData[key].comparison = comparison; + } + }); return filterData; } @@ -178,31 +232,28 @@ export default class extends Controller { const filterValues = Array.from(new Set((filterInfo.values ?? []).map(i => i?.toString()?.trim() ?? "").filter(i => !!i))); const itemValues = Array.from(new Set((itemData[filterKey] ?? []).map(i => i?.toString()?.trim() ?? "").filter(i => !!i))); - // Not a match if the item values and filter values are different lengths. - if (filterValues.length > 0 && filterValues.length !== itemValues.length) { - console.log(`_compare lengths filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); - return false; - } - // Not a match if the item values and filter values contain different values. if (filterValues.length > 0 && comparison === 'exact') { if (!filterValues.every(filterValue => itemValues.includes(filterValue))) { - console.log(`_compare exact filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); + // console.log(`_compare exact filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); return false; } } if (filterValues.length > 0 && comparison === 'includes') { if (!filterValues.every(filterValue => itemValues.some(itemValue => itemValue.includes(filterValue)))) { - console.log(`_compare includes filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); + // console.log(`_compare includes filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); return false; } } } - console.log(`_compare true filterData ${JSON.stringify(filterData)} itemData ${JSON.stringify(itemData)}`); return true; } + _getFilterTargets() { + return this.hasFilterTarget ? (this.filterTargets ?? []) : []; + } + /** * Get the filters from the url query string. * @returns {{[key: string]: string}} @@ -211,7 +262,7 @@ export default class extends Controller { _getFiltersUrlSearch() { const enabledFilterTargets = this._getAutoUpdateUrlQuerystringFilters(); const currentSearchParams = new URLSearchParams(document.location.search); - const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + const filterTargets = this._getFilterTargets(); return Object.fromEntries(filterTargets.map(filterTarget => { const key = filterTarget.dataset.filteredSource; const value = currentSearchParams.get(key); @@ -227,7 +278,7 @@ export default class extends Controller { _setFiltersUrlSearch(filters) { const enabledFilterTargets = this._getAutoUpdateUrlQuerystringFilters(); const currentUrl = new URL(document.location.href); - const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + const filterTargets = this._getFilterTargets(); filterTargets.forEach(filterTarget => { const key = filterTarget.dataset.filteredSource; if (!enabledFilterTargets.includes(key)) { @@ -249,7 +300,7 @@ export default class extends Controller { * @private */ _getFiltersHtmlElements() { - const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + const filterTargets = this._getFilterTargets(); return Object.fromEntries(filterTargets.map(filterTarget => { const key = filterTarget.dataset.filteredSource; const value = filterTarget.value; @@ -263,7 +314,7 @@ export default class extends Controller { * @private */ _setFiltersHtmlElements(filters) { - const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + const filterTargets = this._getFilterTargets(); filterTargets.forEach(filterTarget => { const key = filterTarget.dataset.filteredSource; if (filters[key] !== undefined) { @@ -278,7 +329,7 @@ export default class extends Controller { * @private */ _getAutoUpdateUrlQuerystringFilters() { - const filterTargets = (this.hasFilterTarget ? (this.filterTargets ?? []) : []); + const filterTargets = this._getFilterTargets(); return filterTargets .map(filterTarget => { const key = filterTarget.dataset.filteredSource; From 4797588fd8fe517191d2966909e8a822b85767ab Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Wed, 5 Nov 2025 22:28:43 +1000 Subject: [PATCH 07/13] add link to clear filters Show special text when filters match no files. Update tests to match changes. --- tests/frontend/filter_list_controller_test.js | 134 +++++++++++++----- tests/unit/packaging/test_views.py | 2 +- tests/unit/utils/test_wheel.py | 30 ++++ .../controllers/filter_list_controller.js | 34 ++++- warehouse/templates/packaging/detail.html | 116 +++++++-------- warehouse/utils/wheel.py | 2 - 6 files changed, 215 insertions(+), 103 deletions(-) diff --git a/tests/frontend/filter_list_controller_test.js b/tests/frontend/filter_list_controller_test.js index ad9a00554a7e..309750538889 100644 --- a/tests/frontend/filter_list_controller_test.js +++ b/tests/frontend/filter_list_controller_test.js @@ -15,23 +15,57 @@ const testFixtureHTMLShowing = `

`; const testFixtureHTMLFilters = ` - - + + `; const testFixtureHTMLItems = ` -
Item 1
-
Item 2
-
Item 3
+ Show all files +
Item 1
+
Item 2
+
Item 3
`; describe("Filter list controller", () => { + const setFilterSelectValue = function(value) { + const elFilter = document.getElementById("filter-select"); + const dispatchEventSpy = jest.spyOn(elFilter, "dispatchEvent"); + + elFilter.value = value; + + // Manually trigger the 'input' event to get the MutationObserver that Stimulus uses to be updated. + // Also ensure the event has been dispatched. + const event = new Event("change"); + elFilter.dispatchEvent(event); + expect(dispatchEventSpy).toHaveBeenCalledWith(event); + return elFilter; + } + + const setFilterInputValue = function(value) { + const elFilter = document.getElementById("filter-input"); + const dispatchEventSpy = jest.spyOn(elFilter, "dispatchEvent"); + + elFilter.value = value; + + // Manually trigger the 'input' event to get the MutationObserver that Stimulus uses to be updated. + // Also ensure the event has been dispatched. + const event = new Event("input"); + elFilter.dispatchEvent(event); + expect(dispatchEventSpy).toHaveBeenCalledWith(event); + } + const clearFilters = function() { + const elUrl = document.getElementById('filter-clear'); + const dispatchEventSpy = jest.spyOn(elUrl, "dispatchEvent"); + const event = new Event("click"); + elUrl.dispatchEvent(event); + expect(dispatchEventSpy).toHaveBeenCalledWith(event); + } describe("is initialized as expected", () => { describe("makes expected elements visible", () => { let application; @@ -93,16 +127,16 @@ describe("Filter list controller", () => { expect(Object.keys(controller.mappingItemFilterData)).toHaveLength(3); expect(controller.mappingItemFilterData["0"]).toEqual({ - "contentType": ["contentType1", "contentType1a"], - "myattr": ["myattr1"], + "contentType": ["contentType1","Content Type 1", "contentType1a", "Content Type 1a"], + "myattr":["myattr1", "My Attr 1"], }); expect(controller.mappingItemFilterData["1"]).toEqual({ - "contentType": ["contentType2", "contentType2a"], - "myattr": ["myattr2"], + "contentType": ["contentType2", "Content Type 2", "contentType2a", "Content Type 2a"], + "myattr": ["myattr2", "My Attr 2"], }); expect(controller.mappingItemFilterData["2"]).toEqual({ - "contentType": ["contentType3", "contentType3a"], - "myattr": ["myattr3"], + "contentType": ["contentType3", "Content Type 3", "contentType3a", "Content Type 3a"], + "myattr": ["myattr3", "My Attr 3"], }); const elP = document.getElementById("url-update"); @@ -133,16 +167,37 @@ describe("Filter list controller", () => { it("all items begin shown", () => { const elP = document.getElementById("shown-and-total"); expect(elP.textContent).toEqual("Showing 3 of 3 files."); + expect(document.getElementsByClassName("my-item").length).toEqual(3); + + const elUrl = document.getElementById("url-update"); + expect(elUrl.href).toEqual("https://example.com/#testing"); + }); + it("shows message when all items are hidden", () => { + setFilterInputValue("lizards"); + + const elItem1 = document.getElementById("item-1"); + expect(elItem1.classList).toContainEqual("hidden"); + + const elItem2 = document.getElementById("item-2"); + expect(elItem2.classList).toContainEqual("hidden"); + + const elItem3 = document.getElementById("item-3"); + expect(elItem3.classList).toContainEqual("hidden"); + + const elP = document.getElementById("shown-and-total"); + expect(elP.textContent).toEqual("No files match the current filters. Showing 0 of 3 files."); }); }); describe("allows filtering", () => { + describe("input text filters the items", () => { let application; beforeEach(() => { document.body.innerHTML = `
+ ${testFixtureHTMLShowing} ${testFixtureHTMLFilters} ${testFixtureHTMLItems}
@@ -157,17 +212,13 @@ describe("Filter list controller", () => { }); it("the item classes are updated", () => { + // Set select to no filter + setFilterSelectValue(""); - const elFilter = document.getElementById("filter-input"); - const dispatchEventSpy = jest.spyOn(elFilter, "dispatchEvent"); - - elFilter.value = "2"; + setFilterInputValue("2"); - // Manually trigger the 'input' event to get the MutationObserver that Stimulus uses to be updated. - // Also ensure the event has been dispatched. - const event = new Event("input"); - elFilter.dispatchEvent(event); - expect(dispatchEventSpy).toHaveBeenCalledWith(event); + const elP = document.getElementById("url-update"); + expect(elP.textContent).toEqual("https://example.com/?contentType=2#testing"); const elItem1 = document.getElementById("item-1"); expect(elItem1.classList).toContainEqual("hidden"); @@ -177,9 +228,27 @@ describe("Filter list controller", () => { const elItem3 = document.getElementById("item-3"); expect(elItem3.classList).toContainEqual("hidden"); + }); + it("shows all items after clearing the filters", () => { + setFilterInputValue("lizards"); - const elP = document.getElementById("url-update"); - expect(elP.textContent).toEqual("https://example.com/?contentType=2#testing"); + const elItem1 = document.getElementById("item-1"); + expect(elItem1.classList).toContainEqual("hidden"); + + const elItem2 = document.getElementById("item-2"); + expect(elItem2.classList).toContainEqual("hidden"); + + const elItem3 = document.getElementById("item-3"); + expect(elItem3.classList).toContainEqual("hidden"); + + clearFilters(); + + const elP = document.getElementById("shown-and-total"); + expect(elP.textContent).toEqual("Showing 3 of 3 files."); + + expect(elItem1.classList).not.toContainEqual("hidden"); + expect(elItem2.classList).not.toContainEqual("hidden"); + expect(elItem3.classList).not.toContainEqual("hidden"); }); }); @@ -202,16 +271,7 @@ describe("Filter list controller", () => { }); it("the item classes are updated", () => { - const elFilter = document.getElementById("filter-select"); - const dispatchEventSpy = jest.spyOn(elFilter, "dispatchEvent"); - - elFilter.value = "myattr3"; - - // Manually trigger the 'input' event to get the MutationObserver that Stimulus uses to be updated. - // Also ensure the event has been dispatched. - const event = new Event("change"); - elFilter.dispatchEvent(event); - expect(dispatchEventSpy).toHaveBeenCalledWith(event); + setFilterSelectValue("myattr3"); const elItem1 = document.getElementById("item-1"); expect(elItem1.classList).toContainEqual("hidden"); diff --git a/tests/unit/packaging/test_views.py b/tests/unit/packaging/test_views.py index 96afc909eb44..bd6749ea8902 100644 --- a/tests/unit/packaging/test_views.py +++ b/tests/unit/packaging/test_views.py @@ -220,7 +220,7 @@ def test_detail_rendered(self, db_request): "abi": {}, "platform": {}, "other": {}, - } + }, } def test_detail_renders_files_natural_sort(self, db_request): diff --git a/tests/unit/utils/test_wheel.py b/tests/unit/utils/test_wheel.py index eba283fe6954..a82537cf0985 100644 --- a/tests/unit/utils/test_wheel.py +++ b/tests/unit/utils/test_wheel.py @@ -348,6 +348,36 @@ def _build(**kwargs): plat_manylinux_2_28_aarch64="linux glibc 2.28+ ARM64", ), ), + ( + "numpy-2.3.4-pp311-pp73_pypy311-" + "manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", + _build( + interp_pp311="PyPy 311", + abi_pp73_pypy311="PyPy 73 pypy311", + plat_manylinux_2_27_aarch64="linux glibc 2.27+ ARM64", + plat_manylinux_2_28_aarch64="linux glibc 2.28+ ARM64", + ), + ), + ( + "numpy-2.3.4-pp311-ip27-" + "manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", + _build( + interp_pp311="PyPy 311", + abi_ip27="IronPython 2.7", + plat_manylinux_2_27_aarch64="linux glibc 2.27+ ARM64", + plat_manylinux_2_28_aarch64="linux glibc 2.28+ ARM64", + ), + ), + ( + "numpy-2.3.4-pp311-jy38-" + "manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", + _build( + interp_pp311="PyPy 311", + abi_jy38="Jython 3.8", + plat_manylinux_2_27_aarch64="linux glibc 2.27+ ARM64", + plat_manylinux_2_28_aarch64="linux glibc 2.28+ ARM64", + ), + ), ], ) def test_wheel_to_groups_labels(filename, expected_tags): diff --git a/warehouse/static/js/warehouse/controllers/filter_list_controller.js b/warehouse/static/js/warehouse/controllers/filter_list_controller.js index b9e1b83dd389..be1a562e0362 100644 --- a/warehouse/static/js/warehouse/controllers/filter_list_controller.js +++ b/warehouse/static/js/warehouse/controllers/filter_list_controller.js @@ -15,7 +15,7 @@ * - data-filtered-target-[name of filter group in kebab-case e.g. content-type]='(stringify-ed JSON)' (zero or more) */ import {Controller} from "@hotwired/stimulus"; -import {ngettext} from "../utils/messages-access"; +import {gettext, ngettext} from "../utils/messages-access"; export default class extends Controller { static targets = ["item", "filter", "summary", "url"]; @@ -94,13 +94,18 @@ export default class extends Controller { // show the number of matches and the total number of items if (this.hasSummaryTarget) { - this.summaryTarget.textContent = ngettext( + let messages = []; + if (shown === 0) { + messages.push(gettext("No files match the current filters.")); + } + messages.push(ngettext( "Showing %1 of %2 file.", "Showing %1 of %2 files.", total, shown, - total); - } + total)); + this.summaryTarget.textContent = messages.join(' '); + } // Update the current url to include the filters const htmlElementFilters = this._getFiltersHtmlElements(); @@ -145,7 +150,24 @@ export default class extends Controller { }); } }); - // console.log(`update dropdowns groupedLabels ${JSON.stringify(groupedLabels)} selected ${JSON.stringify(selected)}`); + } + + /** + * Show all files by clearing the filters. + * @param event + */ + filterClear(event) { + // don't follow the url + event.preventDefault(); + + // set the html elements to no filter + const filterTargets = this._getFilterTargets(); + filterTargets.forEach(filterTarget => { + filterTarget.value = ""; + }); + + // update the list of files + this.filter(); } /** @@ -235,14 +257,12 @@ export default class extends Controller { // Not a match if the item values and filter values contain different values. if (filterValues.length > 0 && comparison === 'exact') { if (!filterValues.every(filterValue => itemValues.includes(filterValue))) { - // console.log(`_compare exact filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); return false; } } if (filterValues.length > 0 && comparison === 'includes') { if (!filterValues.every(filterValue => itemValues.some(itemValue => itemValue.includes(filterValue)))) { - // console.log(`_compare includes filterValues ${JSON.stringify(filterValues)} itemValues ${JSON.stringify(itemValues)}`); return false; } } diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html index b052e3d8c3c6..298456cd3777 100644 --- a/warehouse/templates/packaging/detail.html +++ b/warehouse/templates/packaging/detail.html @@ -120,7 +120,7 @@

{% trans %}Details{% endtrans %} {% for file in files %} - {% trans %}File{% endtrans %} @@ -132,60 +132,59 @@

{% trans upload_time=humanize(file.upload_time) %}Uploaded {{ upload_time }}{% endtrans %}. - {% if 'Egg' in file.pretty_wheel_tags %} - + {% if 'Egg' in file.pretty_wheel_tags %} + {{ file.filename }} {% for tag in file.pretty_wheel_tags %} - {% if loop.first %} - {% endif %} - {{ tag }} - {% if not loop.last %} {% endif %} + {% if loop.first %}-{% endif %} + {{ tag }} + {% if not loop.last %} {% endif %} {% endfor %} - - {% else %} - - {% trans %}Interpreter{% endtrans %} - {% for key, value in file.wheel_filters.items() %} - {% if key == 'interpreter' %} - {% for interpreter_value, interpreter_label in value.items() %} - {{ interpreter_label }} - {% if not loop.last %}
{% endif %} - {% endfor %} - {% endif %} - {% endfor %} - - - {% trans %}ABI{% endtrans %} - {% for key, value in file.wheel_filters.items() %} - {% if key == 'abi' %} - {% for abi_value, abi_label in value.items() %} - {{ abi_label }} - {% if not loop.last %}
{% endif %} - {% endfor %} - {% endif %} - {% endfor %} - - - {% trans %}Platform{% endtrans %} - {% for key, value in file.wheel_filters.items() %} - {% if key == 'platform' %} - {% for platform_value, platform_label in value.items() %} - {{ platform_label }} - {% if not loop.last %}
{% endif %} - {% endfor %} - {% endif %} - {% endfor %} - - {% endif %} + + {% else %} + + {% trans %}Interpreter{% endtrans %} + {% for key, value in file.wheel_filters.items() %} + {% if key == 'interpreter' %} + {% for interpreter_value, interpreter_label in value.items() %} + {{ interpreter_label }} + {% if not loop.last %}
{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + + + {% trans %}ABI{% endtrans %} + {% for key, value in file.wheel_filters.items() %} + {% if key == 'abi' %} + {% for abi_value, abi_label in value.items() %} + {{ abi_label }} + {% if not loop.last %}
{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + + + {% trans %}Platform{% endtrans %} + {% for key, value in file.wheel_filters.items() %} + {% if key == 'platform' %} + {% for platform_value, platform_label in value.items() %} + {{ platform_label }} + {% if not loop.last %}
{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + + {% endif %} {% trans %}View details{% endtrans %} {% trans %}Details{% endtrans %} - - - {% endfor %} - + + {% endfor %} + {%- endmacro -%} {% macro filter_select(name, title) %} @@ -609,17 +608,22 @@

{{ filter_select('interpreter', 'Interpreter') }}
{{ filter_select('abi', 'ABI') }}
-
{{ filter_select('platform', 'Platform') }}
- +
+ Show all files + + {{ filter_select('platform', 'Platform') }}
- {% endif %} - {{ bdists_table(bdists) }} - {% if bdist_count > 3 %} - - {% endif %} - {% endif %} + + +{% endif %} +{{ bdists_table(bdists) }} +{% if bdist_count > 3 %} + +{% endif %} +{% endif %} {# Tabs: file details #} {% for file in files %} diff --git a/warehouse/utils/wheel.py b/warehouse/utils/wheel.py index 6e89caf58f55..e35b8687df0a 100644 --- a/warehouse/utils/wheel.py +++ b/warehouse/utils/wheel.py @@ -255,8 +255,6 @@ def filenames_to_grouped_labels(filenames: list[str]) -> dict[str, dict]: for filename in filenames: grouped = filename_to_grouped_labels(filename) for kind, kind_items in grouped.items(): - if kind not in grouped_labels: - grouped_labels[kind] = {} for value, label in kind_items.items(): if value not in grouped_labels[kind]: grouped_labels[kind][value] = label From e35f5ed3eae404be4759c7167e0d55f5bf7a9347 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Wed, 5 Nov 2025 22:29:37 +1000 Subject: [PATCH 08/13] make translations --- warehouse/locale/messages.pot | 224 +++++++++++++++++++++------------- 1 file changed, 137 insertions(+), 87 deletions(-) diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index 20fc4b835709..f9217137d2d3 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -865,7 +865,7 @@ msgstr "" msgid "Provide an Inspector link to specific lines of code." msgstr "" -#: warehouse/packaging/views.py:346 +#: warehouse/packaging/views.py:339 msgid "Your report has been recorded. Thank you for your help." msgstr "" @@ -873,7 +873,11 @@ msgstr "" msgid "Copied" msgstr "" -#: warehouse/static/js/warehouse/controllers/filter_list_controller.js:69 +#: warehouse/static/js/warehouse/controllers/filter_list_controller.js:99 +msgid "No files match the current filters." +msgstr "" + +#: warehouse/static/js/warehouse/controllers/filter_list_controller.js:101 msgid "Showing %1 of %2 file." msgid_plural "Showing %1 of %2 files." msgstr[0] "" @@ -1000,9 +1004,8 @@ msgstr "" #: warehouse/templates/manage/project/release.html:226 #: warehouse/templates/manage/project/releases.html:137 #: warehouse/templates/manage/project/releases.html:190 -#: warehouse/templates/packaging/detail.html:444 -#: warehouse/templates/packaging/detail.html:462 -#: warehouse/templates/packaging/detail.html:478 +#: warehouse/templates/packaging/detail.html:539 +#: warehouse/templates/packaging/detail.html:554 #: warehouse/templates/pages/classifiers.html:17 #: warehouse/templates/pages/help.html:7 #: warehouse/templates/pages/help.html:441 @@ -1270,6 +1273,7 @@ msgstr "" #: warehouse/templates/base.html:291 warehouse/templates/base.html:320 #: warehouse/templates/error-base-with-search.html:19 #: warehouse/templates/index.html:44 +#: warehouse/templates/packaging/detail.html:575 msgid "Search" msgstr "" @@ -2992,8 +2996,7 @@ msgstr "" #: warehouse/templates/manage/account/recovery_codes-provision.html:53 #: warehouse/templates/manage/account/totp-provision.html:46 #: warehouse/templates/manage/unverified-account.html:183 -#: warehouse/templates/packaging/detail.html:148 -#: warehouse/templates/packaging/detail.html:498 +#: warehouse/templates/packaging/detail.html:243 #: warehouse/templates/pages/classifiers.html:42 msgid "Copy to clipboard" msgstr "" @@ -3005,7 +3008,6 @@ msgstr "" #: warehouse/templates/manage/account/recovery_codes-provision.html:54 #: warehouse/templates/manage/account/totp-provision.html:47 #: warehouse/templates/manage/unverified-account.html:184 -#: warehouse/templates/packaging/detail.html:499 #: warehouse/templates/pages/classifiers.html:43 msgid "Copy" msgstr "" @@ -5012,6 +5014,10 @@ msgstr "" #: warehouse/templates/manage/account/publishing.html:412 #: warehouse/templates/manage/project/publishing.html:369 +#: warehouse/templates/packaging/detail.html:81 +#: warehouse/templates/packaging/detail.html:106 +#: warehouse/templates/packaging/detail.html:120 +#: warehouse/templates/packaging/detail.html:183 msgid "Details" msgstr "" @@ -6472,12 +6478,12 @@ msgid "Back to projects" msgstr "" #: warehouse/templates/manage/project/manage_project_base.html:51 -#: warehouse/templates/packaging/detail.html:327 +#: warehouse/templates/packaging/detail.html:422 msgid "This project has been quarantined." msgstr "" #: warehouse/templates/manage/project/manage_project_base.html:53 -#: warehouse/templates/packaging/detail.html:329 +#: warehouse/templates/packaging/detail.html:424 msgid "" "PyPI Admins need to review this project before it can be restored. While " "in quarantine, the project is not installable by clients, and cannot be " @@ -6485,7 +6491,7 @@ msgid "" msgstr "" #: warehouse/templates/manage/project/manage_project_base.html:60 -#: warehouse/templates/packaging/detail.html:336 +#: warehouse/templates/packaging/detail.html:431 #, python-format msgid "" "Read more in the project in quarantine help " @@ -6493,7 +6499,7 @@ msgid "" msgstr "" #: warehouse/templates/manage/project/manage_project_base.html:67 -#: warehouse/templates/packaging/detail.html:343 +#: warehouse/templates/packaging/detail.html:438 msgid "This project has been archived." msgstr "" @@ -6600,6 +6606,8 @@ msgstr "" #: warehouse/templates/manage/project/release.html:40 #: warehouse/templates/manage/project/release.html:56 +#: warehouse/templates/packaging/detail.html:80 +#: warehouse/templates/packaging/detail.html:99 msgid "Type" msgstr "" @@ -7419,205 +7427,247 @@ msgstr "" msgid "%(org)s has not uploaded any projects to PyPI, yet" msgstr "" +#: warehouse/templates/packaging/detail.html:75 +#, python-format +msgid "Source distribution for %(name)s %(version)s" +msgstr "" + +#: warehouse/templates/packaging/detail.html:77 #: warehouse/templates/packaging/detail.html:86 -msgid "view details" +#: warehouse/templates/packaging/detail.html:116 +#: warehouse/templates/packaging/detail.html:126 +msgid "File" +msgstr "" + +#: warehouse/templates/packaging/detail.html:78 +#: warehouse/templates/packaging/detail.html:91 +msgid "Uploaded" +msgstr "" + +#: warehouse/templates/packaging/detail.html:79 +#: warehouse/templates/packaging/detail.html:95 +msgid "Size" +msgstr "" + +#: warehouse/templates/packaging/detail.html:92 +#: warehouse/templates/packaging/detail.html:132 +#, python-format +msgid "Uploaded %(upload_time)s" +msgstr "" + +#: warehouse/templates/packaging/detail.html:100 +msgid "Source" msgstr "" -#: warehouse/templates/packaging/detail.html:96 #: warehouse/templates/packaging/detail.html:103 +#: warehouse/templates/packaging/detail.html:180 +msgid "View details" +msgstr "" + +#: warehouse/templates/packaging/detail.html:114 +#, python-format +msgid "Table of built distributions (wheels) for %(name)s %(release)s" +msgstr "" + +#: warehouse/templates/packaging/detail.html:117 +#: warehouse/templates/packaging/detail.html:146 +msgid "Interpreter" +msgstr "" + +#: warehouse/templates/packaging/detail.html:118 +#: warehouse/templates/packaging/detail.html:157 +msgid "ABI" +msgstr "" + +#: warehouse/templates/packaging/detail.html:119 +#: warehouse/templates/packaging/detail.html:168 +msgid "Platform" +msgstr "" + +#: warehouse/templates/packaging/detail.html:190 +#: warehouse/templates/packaging/detail.html:199 #, python-format msgid "%(title)s" msgstr "" -#: warehouse/templates/packaging/detail.html:115 +#: warehouse/templates/packaging/detail.html:210 #, python-format msgid "RSS: latest releases for %(project_name)s" msgstr "" -#: warehouse/templates/packaging/detail.html:150 +#: warehouse/templates/packaging/detail.html:245 msgid "Copy PIP instructions" msgstr "" -#: warehouse/templates/packaging/detail.html:160 +#: warehouse/templates/packaging/detail.html:255 msgid "This project has been quarantined" msgstr "" -#: warehouse/templates/packaging/detail.html:166 +#: warehouse/templates/packaging/detail.html:261 msgid "This release has been yanked" msgstr "" -#: warehouse/templates/packaging/detail.html:174 +#: warehouse/templates/packaging/detail.html:269 #, python-format msgid "Stable version available (%(version)s)" msgstr "" -#: warehouse/templates/packaging/detail.html:179 +#: warehouse/templates/packaging/detail.html:274 #, python-format msgid "Newer version available (%(version)s)" msgstr "" -#: warehouse/templates/packaging/detail.html:184 +#: warehouse/templates/packaging/detail.html:279 msgid "Latest version" msgstr "" -#: warehouse/templates/packaging/detail.html:189 +#: warehouse/templates/packaging/detail.html:284 #, python-format msgid "Released: %(release_date)s" msgstr "" -#: warehouse/templates/packaging/detail.html:202 +#: warehouse/templates/packaging/detail.html:297 msgid "No project description provided" msgstr "" -#: warehouse/templates/packaging/detail.html:214 +#: warehouse/templates/packaging/detail.html:309 msgid "Navigation" msgstr "" -#: warehouse/templates/packaging/detail.html:215 -#: warehouse/templates/packaging/detail.html:267 +#: warehouse/templates/packaging/detail.html:310 +#: warehouse/templates/packaging/detail.html:362 #, python-format msgid "Navigation for %(project)s" msgstr "" -#: warehouse/templates/packaging/detail.html:224 -#: warehouse/templates/packaging/detail.html:276 +#: warehouse/templates/packaging/detail.html:319 +#: warehouse/templates/packaging/detail.html:371 msgid "Project description. Focus will be moved to the description." msgstr "" -#: warehouse/templates/packaging/detail.html:226 -#: warehouse/templates/packaging/detail.html:278 -#: warehouse/templates/packaging/detail.html:357 +#: warehouse/templates/packaging/detail.html:321 +#: warehouse/templates/packaging/detail.html:373 +#: warehouse/templates/packaging/detail.html:452 msgid "Project description" msgstr "" -#: warehouse/templates/packaging/detail.html:235 -#: warehouse/templates/packaging/detail.html:298 +#: warehouse/templates/packaging/detail.html:330 +#: warehouse/templates/packaging/detail.html:393 msgid "Release history. Focus will be moved to the history panel." msgstr "" -#: warehouse/templates/packaging/detail.html:237 -#: warehouse/templates/packaging/detail.html:300 -#: warehouse/templates/packaging/detail.html:385 +#: warehouse/templates/packaging/detail.html:332 +#: warehouse/templates/packaging/detail.html:395 +#: warehouse/templates/packaging/detail.html:480 msgid "Release history" msgstr "" -#: warehouse/templates/packaging/detail.html:247 -#: warehouse/templates/packaging/detail.html:310 +#: warehouse/templates/packaging/detail.html:342 +#: warehouse/templates/packaging/detail.html:405 msgid "Download files. Focus will be moved to the project files." msgstr "" -#: warehouse/templates/packaging/detail.html:249 -#: warehouse/templates/packaging/detail.html:312 -#: warehouse/templates/packaging/detail.html:442 +#: warehouse/templates/packaging/detail.html:344 +#: warehouse/templates/packaging/detail.html:407 +#: warehouse/templates/packaging/detail.html:537 msgid "Download files" msgstr "" -#: warehouse/templates/packaging/detail.html:262 +#: warehouse/templates/packaging/detail.html:357 msgid "Report project as malware" msgstr "" -#: warehouse/templates/packaging/detail.html:287 +#: warehouse/templates/packaging/detail.html:382 msgid "Project details. Focus will be moved to the project details." msgstr "" -#: warehouse/templates/packaging/detail.html:289 -#: warehouse/templates/packaging/detail.html:373 +#: warehouse/templates/packaging/detail.html:384 +#: warehouse/templates/packaging/detail.html:468 msgid "Project details" msgstr "" -#: warehouse/templates/packaging/detail.html:345 +#: warehouse/templates/packaging/detail.html:440 msgid "" "The maintainers of this project have marked this project as archived. No " "new releases are expected." msgstr "" -#: warehouse/templates/packaging/detail.html:353 -#: warehouse/templates/packaging/detail.html:424 +#: warehouse/templates/packaging/detail.html:448 +#: warehouse/templates/packaging/detail.html:519 msgid "Reason this release was yanked:" msgstr "" -#: warehouse/templates/packaging/detail.html:362 +#: warehouse/templates/packaging/detail.html:457 msgid "The author of this package has not provided a project description" msgstr "" -#: warehouse/templates/packaging/detail.html:387 +#: warehouse/templates/packaging/detail.html:482 msgid "Release notifications" msgstr "" -#: warehouse/templates/packaging/detail.html:388 +#: warehouse/templates/packaging/detail.html:483 msgid "RSS feed" msgstr "" -#: warehouse/templates/packaging/detail.html:399 +#: warehouse/templates/packaging/detail.html:494 msgid "This version" msgstr "" -#: warehouse/templates/packaging/detail.html:415 +#: warehouse/templates/packaging/detail.html:510 msgid "pre-release" msgstr "" -#: warehouse/templates/packaging/detail.html:418 +#: warehouse/templates/packaging/detail.html:513 msgid "yanked" msgstr "" -#: warehouse/templates/packaging/detail.html:444 +#: warehouse/templates/packaging/detail.html:539 #, python-format msgid "" -"Download the file for your platform. If you're not sure which to choose, " -"learn more about installing packages." +"For a detailed explanation of source distributions (sdists) and built " +"distributions (wheels), please see the package formats " +"documentation." msgstr "" -#: warehouse/templates/packaging/detail.html:447 -msgid "Source Distribution" -msgid_plural "Source Distributions" +#: warehouse/templates/packaging/detail.html:542 +msgid "Source distribution (sdist)" +msgid_plural "Source distributions (sdists)" msgstr[0] "" msgstr[1] "" -#: warehouse/templates/packaging/detail.html:461 +#: warehouse/templates/packaging/detail.html:553 msgid "No source distribution files available for this release." msgstr "" -#: warehouse/templates/packaging/detail.html:462 +#: warehouse/templates/packaging/detail.html:554 #, python-format msgid "" "See tutorial on generating distribution archives." msgstr "" -#: warehouse/templates/packaging/detail.html:468 -msgid "Built Distribution" -msgid_plural "Built Distributions" +#: warehouse/templates/packaging/detail.html:560 +msgid "Built distribution (wheel)" +msgid_plural "Built distributions (wheels)" msgstr[0] "" msgstr[1] "" -#: warehouse/templates/packaging/detail.html:475 -msgid "Filter files by name, interpreter, ABI, and platform." -msgstr "" - -#: warehouse/templates/packaging/detail.html:478 -#, python-format -msgid "" -"If you're not sure about the file name format, learn more about wheel file names." -msgstr "" - -#: warehouse/templates/packaging/detail.html:482 -msgid "The dropdown lists show the available interpreters, ABIs, and platforms." +#: warehouse/templates/packaging/detail.html:569 +msgid "Enable javascript to be able to filter the list of wheel files." msgstr "" -#: warehouse/templates/packaging/detail.html:485 -msgid "Enable javascript to be able to filter the list of wheel files." +#: warehouse/templates/packaging/detail.html:580 +msgid "Search by file name" msgstr "" -#: warehouse/templates/packaging/detail.html:489 -msgid "Copy a direct link to the current filters" +#: warehouse/templates/packaging/detail.html:593 +msgid "Filters" msgstr "" -#: warehouse/templates/packaging/detail.html:507 -#: warehouse/templates/packaging/detail.html:512 -msgid "File name" +#: warehouse/templates/packaging/detail.html:603 +#: warehouse/templates/packaging/detail.html:605 +msgid "Copy link to filters" msgstr "" #: warehouse/templates/packaging/submit-malware-observation.html:20 From 9f04d90705e7747bb73512647bde271e3ea3e1ab Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Wed, 5 Nov 2025 22:39:25 +1000 Subject: [PATCH 09/13] make translations --- warehouse/locale/messages.pot | 666 ++++++++++++++++++---------------- 1 file changed, 348 insertions(+), 318 deletions(-) diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index f9217137d2d3..2c10ac038c73 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -158,7 +158,7 @@ msgstr "" msgid "Successful WebAuthn assertion" msgstr "" -#: warehouse/accounts/views.py:617 warehouse/manage/views/__init__.py:851 +#: warehouse/accounts/views.py:617 warehouse/manage/views/__init__.py:859 msgid "Recovery code accepted. The supplied code cannot be used again." msgstr "" @@ -482,143 +482,143 @@ msgid "" "less." msgstr "" -#: warehouse/manage/views/__init__.py:262 +#: warehouse/manage/views/__init__.py:316 msgid "Account details updated" msgstr "" -#: warehouse/manage/views/__init__.py:293 +#: warehouse/manage/views/__init__.py:347 #, python-brace-format msgid "Email ${email_address} added - check your email for a verification link" msgstr "" -#: warehouse/manage/views/__init__.py:798 +#: warehouse/manage/views/__init__.py:806 msgid "Recovery codes already generated" msgstr "" -#: warehouse/manage/views/__init__.py:800 +#: warehouse/manage/views/__init__.py:808 msgid "Generating new recovery codes will invalidate your existing codes." msgstr "" -#: warehouse/manage/views/__init__.py:908 +#: warehouse/manage/views/__init__.py:916 msgid "Verify your email to create an API token." msgstr "" -#: warehouse/manage/views/__init__.py:1010 +#: warehouse/manage/views/__init__.py:1018 msgid "API Token does not exist." msgstr "" -#: warehouse/manage/views/__init__.py:1042 +#: warehouse/manage/views/__init__.py:1050 msgid "Invalid credentials. Try again" msgstr "" -#: warehouse/manage/views/__init__.py:1161 +#: warehouse/manage/views/__init__.py:1169 msgid "Invalid alternate repository location details" msgstr "" -#: warehouse/manage/views/__init__.py:1199 +#: warehouse/manage/views/__init__.py:1207 #, python-brace-format msgid "Added alternate repository '${name}'" msgstr "" -#: warehouse/manage/views/__init__.py:1232 -#: warehouse/manage/views/__init__.py:1493 -#: warehouse/manage/views/__init__.py:1578 -#: warehouse/manage/views/__init__.py:1679 -#: warehouse/manage/views/__init__.py:1779 +#: warehouse/manage/views/__init__.py:1240 +#: warehouse/manage/views/__init__.py:1501 +#: warehouse/manage/views/__init__.py:1586 +#: warehouse/manage/views/__init__.py:1687 +#: warehouse/manage/views/__init__.py:1787 msgid "Confirm the request" msgstr "" -#: warehouse/manage/views/__init__.py:1244 +#: warehouse/manage/views/__init__.py:1252 msgid "Invalid alternate repository id" msgstr "" -#: warehouse/manage/views/__init__.py:1255 +#: warehouse/manage/views/__init__.py:1263 msgid "Invalid alternate repository for project" msgstr "" -#: warehouse/manage/views/__init__.py:1264 +#: warehouse/manage/views/__init__.py:1272 #, python-brace-format msgid "" "Could not delete alternate repository - ${confirm} is not the same as " "${alt_repo_name}" msgstr "" -#: warehouse/manage/views/__init__.py:1294 +#: warehouse/manage/views/__init__.py:1302 #, python-brace-format msgid "Deleted alternate repository '${name}'" msgstr "" -#: warehouse/manage/views/__init__.py:1362 -#: warehouse/manage/views/__init__.py:1663 -#: warehouse/manage/views/__init__.py:1771 +#: warehouse/manage/views/__init__.py:1370 +#: warehouse/manage/views/__init__.py:1671 +#: warehouse/manage/views/__init__.py:1779 msgid "" "Project deletion temporarily disabled. See https://pypi.org/help#admin-" "intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1506 +#: warehouse/manage/views/__init__.py:1514 msgid "Could not yank release - " msgstr "" -#: warehouse/manage/views/__init__.py:1591 +#: warehouse/manage/views/__init__.py:1599 msgid "Could not un-yank release - " msgstr "" -#: warehouse/manage/views/__init__.py:1692 +#: warehouse/manage/views/__init__.py:1700 msgid "Could not delete release - " msgstr "" -#: warehouse/manage/views/__init__.py:1791 +#: warehouse/manage/views/__init__.py:1799 msgid "Could not find file" msgstr "" -#: warehouse/manage/views/__init__.py:1796 +#: warehouse/manage/views/__init__.py:1804 msgid "Could not delete file - " msgstr "" -#: warehouse/manage/views/__init__.py:1946 +#: warehouse/manage/views/__init__.py:1954 #, python-brace-format msgid "Team '${team_name}' already has ${role_name} role for project" msgstr "" -#: warehouse/manage/views/__init__.py:2053 +#: warehouse/manage/views/__init__.py:2061 #, python-brace-format msgid "User '${username}' already has ${role_name} role for project" msgstr "" -#: warehouse/manage/views/__init__.py:2120 +#: warehouse/manage/views/__init__.py:2128 #, python-brace-format msgid "${username} is now ${role} of the '${project_name}' project." msgstr "" -#: warehouse/manage/views/__init__.py:2152 +#: warehouse/manage/views/__init__.py:2160 #, python-brace-format msgid "" "User '${username}' does not have a verified primary email address and " "cannot be added as a ${role_name} for project" msgstr "" -#: warehouse/manage/views/__init__.py:2165 +#: warehouse/manage/views/__init__.py:2173 #: warehouse/manage/views/organizations.py:962 #, python-brace-format msgid "User '${username}' already has an active invite. Please try again later." msgstr "" -#: warehouse/manage/views/__init__.py:2230 +#: warehouse/manage/views/__init__.py:2238 #: warehouse/manage/views/organizations.py:1037 #, python-brace-format msgid "Invitation sent to '${username}'" msgstr "" -#: warehouse/manage/views/__init__.py:2262 +#: warehouse/manage/views/__init__.py:2270 msgid "Could not find role invitation." msgstr "" -#: warehouse/manage/views/__init__.py:2273 +#: warehouse/manage/views/__init__.py:2281 msgid "Invitation already expired." msgstr "" -#: warehouse/manage/views/__init__.py:2306 +#: warehouse/manage/views/__init__.py:2314 #: warehouse/manage/views/organizations.py:1224 #, python-brace-format msgid "Invitation revoked from '${username}'." @@ -1554,23 +1554,23 @@ msgstr "" #: warehouse/templates/manage/account.html:401 #: warehouse/templates/manage/account.html:426 #: warehouse/templates/manage/account.html:450 -#: warehouse/templates/manage/account/publishing.html:24 -#: warehouse/templates/manage/account/publishing.html:37 -#: warehouse/templates/manage/account/publishing.html:50 -#: warehouse/templates/manage/account/publishing.html:69 -#: warehouse/templates/manage/account/publishing.html:86 -#: warehouse/templates/manage/account/publishing.html:132 -#: warehouse/templates/manage/account/publishing.html:145 -#: warehouse/templates/manage/account/publishing.html:158 -#: warehouse/templates/manage/account/publishing.html:171 -#: warehouse/templates/manage/account/publishing.html:184 -#: warehouse/templates/manage/account/publishing.html:227 -#: warehouse/templates/manage/account/publishing.html:240 -#: warehouse/templates/manage/account/publishing.html:253 -#: warehouse/templates/manage/account/publishing.html:294 -#: warehouse/templates/manage/account/publishing.html:313 -#: warehouse/templates/manage/account/publishing.html:330 -#: warehouse/templates/manage/account/publishing.html:349 +#: warehouse/templates/manage/account/publishing.html:25 +#: warehouse/templates/manage/account/publishing.html:38 +#: warehouse/templates/manage/account/publishing.html:51 +#: warehouse/templates/manage/account/publishing.html:70 +#: warehouse/templates/manage/account/publishing.html:87 +#: warehouse/templates/manage/account/publishing.html:134 +#: warehouse/templates/manage/account/publishing.html:147 +#: warehouse/templates/manage/account/publishing.html:160 +#: warehouse/templates/manage/account/publishing.html:173 +#: warehouse/templates/manage/account/publishing.html:186 +#: warehouse/templates/manage/account/publishing.html:232 +#: warehouse/templates/manage/account/publishing.html:245 +#: warehouse/templates/manage/account/publishing.html:258 +#: warehouse/templates/manage/account/publishing.html:300 +#: warehouse/templates/manage/account/publishing.html:319 +#: warehouse/templates/manage/account/publishing.html:336 +#: warehouse/templates/manage/account/publishing.html:355 #: warehouse/templates/manage/account/recovery_codes-burn.html:48 #: warehouse/templates/manage/account/token.html:127 #: warehouse/templates/manage/account/token.html:152 @@ -1593,20 +1593,20 @@ msgstr "" #: warehouse/templates/manage/organizations.html:300 #: warehouse/templates/manage/organizations.html:319 #: warehouse/templates/manage/organizations.html:346 -#: warehouse/templates/manage/project/publishing.html:43 -#: warehouse/templates/manage/project/publishing.html:56 -#: warehouse/templates/manage/project/publishing.html:75 -#: warehouse/templates/manage/project/publishing.html:92 -#: warehouse/templates/manage/project/publishing.html:140 -#: warehouse/templates/manage/project/publishing.html:153 -#: warehouse/templates/manage/project/publishing.html:166 -#: warehouse/templates/manage/project/publishing.html:179 -#: warehouse/templates/manage/project/publishing.html:203 -#: warehouse/templates/manage/project/publishing.html:238 -#: warehouse/templates/manage/project/publishing.html:251 -#: warehouse/templates/manage/project/publishing.html:293 -#: warehouse/templates/manage/project/publishing.html:310 -#: warehouse/templates/manage/project/publishing.html:329 +#: warehouse/templates/manage/project/publishing.html:44 +#: warehouse/templates/manage/project/publishing.html:57 +#: warehouse/templates/manage/project/publishing.html:76 +#: warehouse/templates/manage/project/publishing.html:93 +#: warehouse/templates/manage/project/publishing.html:142 +#: warehouse/templates/manage/project/publishing.html:155 +#: warehouse/templates/manage/project/publishing.html:168 +#: warehouse/templates/manage/project/publishing.html:181 +#: warehouse/templates/manage/project/publishing.html:205 +#: warehouse/templates/manage/project/publishing.html:244 +#: warehouse/templates/manage/project/publishing.html:257 +#: warehouse/templates/manage/project/publishing.html:300 +#: warehouse/templates/manage/project/publishing.html:317 +#: warehouse/templates/manage/project/publishing.html:336 #: warehouse/templates/manage/project/roles.html:320 #: warehouse/templates/manage/project/roles.html:333 #: warehouse/templates/manage/project/roles.html:344 @@ -1949,7 +1949,7 @@ msgstr "" #: warehouse/templates/manage/project/settings.html:212 #: warehouse/templates/manage/project/settings.html:267 #: warehouse/templates/manage/project/settings.html:272 -#: warehouse/templates/manage/unverified-account.html:102 +#: warehouse/templates/manage/unverified-account.html:119 msgid "Name" msgstr "" @@ -1959,7 +1959,7 @@ msgstr "" #: warehouse/templates/accounts/register.html:89 #: warehouse/templates/manage/account.html:350 -#: warehouse/templates/manage/unverified-account.html:225 +#: warehouse/templates/manage/unverified-account.html:259 msgid "Email address" msgstr "" @@ -2908,9 +2908,9 @@ msgstr "" #: warehouse/templates/manage/project/release.html:158 #: warehouse/templates/manage/project/releases.html:186 #: warehouse/templates/manage/project/settings.html:63 -#: warehouse/templates/manage/unverified-account.html:169 -#: warehouse/templates/manage/unverified-account.html:172 -#: warehouse/templates/manage/unverified-account.html:187 +#: warehouse/templates/manage/unverified-account.html:186 +#: warehouse/templates/manage/unverified-account.html:189 +#: warehouse/templates/manage/unverified-account.html:204 #: warehouse/templates/search/results.html:185 msgid "Close" msgstr "" @@ -2995,7 +2995,7 @@ msgstr "" #: warehouse/templates/manage/account.html:230 #: warehouse/templates/manage/account/recovery_codes-provision.html:53 #: warehouse/templates/manage/account/totp-provision.html:46 -#: warehouse/templates/manage/unverified-account.html:183 +#: warehouse/templates/manage/unverified-account.html:200 #: warehouse/templates/packaging/detail.html:243 #: warehouse/templates/pages/classifiers.html:42 msgid "Copy to clipboard" @@ -3007,7 +3007,7 @@ msgstr "" #: warehouse/templates/manage/account.html:231 #: warehouse/templates/manage/account/recovery_codes-provision.html:54 #: warehouse/templates/manage/account/totp-provision.html:47 -#: warehouse/templates/manage/unverified-account.html:184 +#: warehouse/templates/manage/unverified-account.html:201 #: warehouse/templates/pages/classifiers.html:43 msgid "Copy" msgstr "" @@ -3086,8 +3086,8 @@ msgid "%(username)s has not uploaded any projects to PyPI, yet." msgstr "" #: warehouse/templates/includes/accounts/profile-public-email.html:16 -#: warehouse/templates/manage/account/publishing.html:238 -#: warehouse/templates/manage/project/publishing.html:236 +#: warehouse/templates/manage/account/publishing.html:243 +#: warehouse/templates/manage/project/publishing.html:242 msgid "Email" msgstr "" @@ -3120,7 +3120,7 @@ msgstr "" #: warehouse/templates/manage/organization/history.html:9 #: warehouse/templates/manage/project/history.html:9 #: warehouse/templates/manage/team/history.html:9 -#: warehouse/templates/manage/unverified-account.html:236 +#: warehouse/templates/manage/unverified-account.html:270 msgid "Security history" msgstr "" @@ -3347,7 +3347,7 @@ msgstr "" #: warehouse/templates/manage/project/release.html:74 #: warehouse/templates/manage/project/releases.html:53 #: warehouse/templates/manage/unverified-account.html:69 -#: warehouse/templates/manage/unverified-account.html:130 +#: warehouse/templates/manage/unverified-account.html:147 msgid "Options" msgstr "" @@ -3365,10 +3365,12 @@ msgid "Resend verification email" msgstr "" #: warehouse/templates/manage/account.html:117 +#: warehouse/templates/manage/unverified-account.html:103 msgid "Set this email address as primary" msgstr "" #: warehouse/templates/manage/account.html:119 +#: warehouse/templates/manage/unverified-account.html:105 msgid "Set as primary" msgstr "" @@ -3383,45 +3385,45 @@ msgstr "" #: warehouse/templates/manage/account.html:153 #: warehouse/templates/manage/account.html:492 #: warehouse/templates/manage/account/token.html:150 -#: warehouse/templates/manage/unverified-account.html:106 +#: warehouse/templates/manage/unverified-account.html:123 msgid "Scope" msgstr "" #: warehouse/templates/manage/account.html:155 -#: warehouse/templates/manage/unverified-account.html:108 +#: warehouse/templates/manage/unverified-account.html:125 msgid "All projects" msgstr "" #: warehouse/templates/manage/account.html:163 #: warehouse/templates/manage/account.html:493 -#: warehouse/templates/manage/unverified-account.html:116 +#: warehouse/templates/manage/unverified-account.html:133 msgid "Created" msgstr "" #: warehouse/templates/manage/account.html:167 #: warehouse/templates/manage/account.html:494 -#: warehouse/templates/manage/unverified-account.html:120 +#: warehouse/templates/manage/unverified-account.html:137 msgid "Last used" msgstr "" #: warehouse/templates/manage/account.html:168 -#: warehouse/templates/manage/unverified-account.html:121 +#: warehouse/templates/manage/unverified-account.html:138 msgid "Never" msgstr "" #: warehouse/templates/manage/account.html:176 -#: warehouse/templates/manage/unverified-account.html:129 +#: warehouse/templates/manage/unverified-account.html:146 msgid "View token options" msgstr "" #: warehouse/templates/manage/account.html:186 #: warehouse/templates/manage/account/token.html:48 -#: warehouse/templates/manage/unverified-account.html:139 +#: warehouse/templates/manage/unverified-account.html:156 msgid "Remove token" msgstr "" #: warehouse/templates/manage/account.html:192 -#: warehouse/templates/manage/unverified-account.html:145 +#: warehouse/templates/manage/unverified-account.html:162 msgid "View unique identifier" msgstr "" @@ -3429,21 +3431,21 @@ msgstr "" #: warehouse/templates/manage/account.html:201 #: warehouse/templates/manage/account/token.html:50 #: warehouse/templates/manage/account/token.html:51 -#: warehouse/templates/manage/unverified-account.html:152 -#: warehouse/templates/manage/unverified-account.html:154 +#: warehouse/templates/manage/unverified-account.html:169 +#: warehouse/templates/manage/unverified-account.html:171 msgid "Remove API token" msgstr "" #: warehouse/templates/manage/account.html:209 #: warehouse/templates/manage/account/token.html:59 -#: warehouse/templates/manage/unverified-account.html:162 +#: warehouse/templates/manage/unverified-account.html:179 msgid "" "Applications or scripts using this token will no longer have access to " "PyPI." msgstr "" #: warehouse/templates/manage/account.html:223 -#: warehouse/templates/manage/unverified-account.html:176 +#: warehouse/templates/manage/unverified-account.html:193 #, python-format msgid "Unique identifier for API token \"%(token_description)s\"" msgstr "" @@ -3508,7 +3510,7 @@ msgid "Update account" msgstr "" #: warehouse/templates/manage/account.html:336 -#: warehouse/templates/manage/unverified-account.html:211 +#: warehouse/templates/manage/unverified-account.html:245 msgid "Account emails" msgstr "" @@ -3522,12 +3524,12 @@ msgid "" msgstr "" #: warehouse/templates/manage/account.html:347 -#: warehouse/templates/manage/unverified-account.html:222 +#: warehouse/templates/manage/unverified-account.html:256 msgid "Emails associated with your account" msgstr "" #: warehouse/templates/manage/account.html:351 -#: warehouse/templates/manage/unverified-account.html:226 +#: warehouse/templates/manage/unverified-account.html:260 msgid "Status" msgstr "" @@ -3593,62 +3595,62 @@ msgstr "" #: warehouse/templates/manage/account.html:531 #: warehouse/templates/manage/account.html:719 -#: warehouse/templates/manage/unverified-account.html:242 -#: warehouse/templates/manage/unverified-account.html:424 +#: warehouse/templates/manage/unverified-account.html:276 +#: warehouse/templates/manage/unverified-account.html:458 msgid "Token scope: entire account" msgstr "" #: warehouse/templates/manage/account.html:533 #: warehouse/templates/manage/account.html:721 -#: warehouse/templates/manage/unverified-account.html:244 -#: warehouse/templates/manage/unverified-account.html:426 +#: warehouse/templates/manage/unverified-account.html:278 +#: warehouse/templates/manage/unverified-account.html:460 #, python-format msgid "Token scope: Project %(project_name)s" msgstr "" #: warehouse/templates/manage/account.html:536 -#: warehouse/templates/manage/unverified-account.html:247 +#: warehouse/templates/manage/unverified-account.html:281 #, python-format msgid "Expires: %(exp)s" msgstr "" #: warehouse/templates/manage/account.html:541 -#: warehouse/templates/manage/unverified-account.html:252 +#: warehouse/templates/manage/unverified-account.html:286 msgid "Account created" msgstr "" #: warehouse/templates/manage/account.html:543 -#: warehouse/templates/manage/unverified-account.html:254 +#: warehouse/templates/manage/unverified-account.html:288 msgid "Logged in" msgstr "" #: warehouse/templates/manage/account.html:546 -#: warehouse/templates/manage/unverified-account.html:257 +#: warehouse/templates/manage/unverified-account.html:291 msgid "Two factor method:" msgstr "" #: warehouse/templates/manage/account.html:548 #: warehouse/templates/manage/project/release.html:61 -#: warehouse/templates/manage/unverified-account.html:259 +#: warehouse/templates/manage/unverified-account.html:293 msgid "None" msgstr "" #: warehouse/templates/manage/account.html:551 #: warehouse/templates/manage/manage_base.html:84 -#: warehouse/templates/manage/unverified-account.html:262 +#: warehouse/templates/manage/unverified-account.html:296 msgid "Security device (WebAuthn)" msgstr "" #: warehouse/templates/manage/account.html:553 #: warehouse/templates/manage/manage_base.html:58 -#: warehouse/templates/manage/unverified-account.html:264 +#: warehouse/templates/manage/unverified-account.html:298 msgid "" "Authentication application (TOTP)" msgstr "" #: warehouse/templates/manage/account.html:555 -#: warehouse/templates/manage/unverified-account.html:266 +#: warehouse/templates/manage/unverified-account.html:300 msgid "Recovery code" msgstr "" @@ -3657,129 +3659,129 @@ msgid "Remembered device" msgstr "" #: warehouse/templates/manage/account.html:561 -#: warehouse/templates/manage/unverified-account.html:270 +#: warehouse/templates/manage/unverified-account.html:304 msgid "Login failed" msgstr "" #: warehouse/templates/manage/account.html:564 -#: warehouse/templates/manage/unverified-account.html:273 +#: warehouse/templates/manage/unverified-account.html:307 msgid "- Basic Auth (Upload endpoint)" msgstr "" #: warehouse/templates/manage/account.html:569 #: warehouse/templates/manage/account.html:588 #: warehouse/templates/manage/project/history.html:247 -#: warehouse/templates/manage/unverified-account.html:278 -#: warehouse/templates/manage/unverified-account.html:297 +#: warehouse/templates/manage/unverified-account.html:312 +#: warehouse/templates/manage/unverified-account.html:331 msgid "Reason:" msgstr "" #: warehouse/templates/manage/account.html:571 #: warehouse/templates/manage/account.html:590 -#: warehouse/templates/manage/unverified-account.html:280 -#: warehouse/templates/manage/unverified-account.html:299 +#: warehouse/templates/manage/unverified-account.html:314 +#: warehouse/templates/manage/unverified-account.html:333 msgid "Incorrect Password" msgstr "" #: warehouse/templates/manage/account.html:573 -#: warehouse/templates/manage/unverified-account.html:282 +#: warehouse/templates/manage/unverified-account.html:316 msgid "Invalid two factor (TOTP)" msgstr "" #: warehouse/templates/manage/account.html:575 -#: warehouse/templates/manage/unverified-account.html:284 +#: warehouse/templates/manage/unverified-account.html:318 msgid "Invalid two factor (WebAuthn)" msgstr "" #: warehouse/templates/manage/account.html:577 #: warehouse/templates/manage/account.html:579 -#: warehouse/templates/manage/unverified-account.html:286 -#: warehouse/templates/manage/unverified-account.html:288 +#: warehouse/templates/manage/unverified-account.html:320 +#: warehouse/templates/manage/unverified-account.html:322 msgid "Invalid two factor (Recovery code)" msgstr "" #: warehouse/templates/manage/account.html:585 -#: warehouse/templates/manage/unverified-account.html:294 +#: warehouse/templates/manage/unverified-account.html:328 msgid "Session reauthentication failed" msgstr "" #: warehouse/templates/manage/account.html:596 -#: warehouse/templates/manage/unverified-account.html:305 +#: warehouse/templates/manage/unverified-account.html:339 msgid "Email added to account" msgstr "" #: warehouse/templates/manage/account.html:600 -#: warehouse/templates/manage/unverified-account.html:309 +#: warehouse/templates/manage/unverified-account.html:343 msgid "Email removed from account" msgstr "" #: warehouse/templates/manage/account.html:604 -#: warehouse/templates/manage/unverified-account.html:313 +#: warehouse/templates/manage/unverified-account.html:347 msgid "Email verified" msgstr "" #: warehouse/templates/manage/account.html:608 -#: warehouse/templates/manage/unverified-account.html:317 +#: warehouse/templates/manage/unverified-account.html:351 msgid "Email reverified" msgstr "" #: warehouse/templates/manage/account.html:613 -#: warehouse/templates/manage/unverified-account.html:322 +#: warehouse/templates/manage/unverified-account.html:356 msgid "Primary email changed" msgstr "" #: warehouse/templates/manage/account.html:616 -#: warehouse/templates/manage/unverified-account.html:325 +#: warehouse/templates/manage/unverified-account.html:359 msgid "Old primary email:" msgstr "" #: warehouse/templates/manage/account.html:618 -#: warehouse/templates/manage/unverified-account.html:327 +#: warehouse/templates/manage/unverified-account.html:361 msgid "New primary email:" msgstr "" #: warehouse/templates/manage/account.html:621 -#: warehouse/templates/manage/unverified-account.html:330 +#: warehouse/templates/manage/unverified-account.html:364 msgid "Primary email set" msgstr "" #: warehouse/templates/manage/account.html:626 -#: warehouse/templates/manage/unverified-account.html:335 +#: warehouse/templates/manage/unverified-account.html:369 msgid "Email sent" msgstr "" #: warehouse/templates/manage/account.html:629 -#: warehouse/templates/manage/unverified-account.html:338 +#: warehouse/templates/manage/unverified-account.html:372 msgid "From:" msgstr "" #: warehouse/templates/manage/account.html:631 -#: warehouse/templates/manage/unverified-account.html:340 +#: warehouse/templates/manage/unverified-account.html:374 msgid "To:" msgstr "" #: warehouse/templates/manage/account.html:633 -#: warehouse/templates/manage/unverified-account.html:342 +#: warehouse/templates/manage/unverified-account.html:376 msgid "Subject:" msgstr "" #: warehouse/templates/manage/account.html:636 -#: warehouse/templates/manage/unverified-account.html:345 +#: warehouse/templates/manage/unverified-account.html:379 msgid "Password reset requested" msgstr "" #: warehouse/templates/manage/account.html:638 -#: warehouse/templates/manage/unverified-account.html:347 +#: warehouse/templates/manage/unverified-account.html:381 msgid "Password reset attempted" msgstr "" #: warehouse/templates/manage/account.html:640 -#: warehouse/templates/manage/unverified-account.html:349 +#: warehouse/templates/manage/unverified-account.html:383 msgid "Password successfully reset" msgstr "" #: warehouse/templates/manage/account.html:642 -#: warehouse/templates/manage/unverified-account.html:351 +#: warehouse/templates/manage/unverified-account.html:385 msgid "Password successfully changed" msgstr "" @@ -3790,20 +3792,20 @@ msgstr "" #: warehouse/templates/manage/account.html:647 #: warehouse/templates/manage/account.html:651 #: warehouse/templates/manage/account/token.html:168 -#: warehouse/templates/manage/unverified-account.html:354 -#: warehouse/templates/manage/unverified-account.html:358 +#: warehouse/templates/manage/unverified-account.html:388 +#: warehouse/templates/manage/unverified-account.html:392 msgid "Project:" msgstr "" #: warehouse/templates/manage/account.html:654 -#: warehouse/templates/manage/unverified-account.html:361 +#: warehouse/templates/manage/unverified-account.html:395 msgid "Two factor authentication added" msgstr "" #: warehouse/templates/manage/account.html:658 #: warehouse/templates/manage/account.html:670 -#: warehouse/templates/manage/unverified-account.html:365 -#: warehouse/templates/manage/unverified-account.html:377 +#: warehouse/templates/manage/unverified-account.html:399 +#: warehouse/templates/manage/unverified-account.html:411 msgid "" "Method: Security device (WebAuthn)" @@ -3811,22 +3813,22 @@ msgstr "" #: warehouse/templates/manage/account.html:660 #: warehouse/templates/manage/account.html:672 -#: warehouse/templates/manage/unverified-account.html:367 -#: warehouse/templates/manage/unverified-account.html:379 +#: warehouse/templates/manage/unverified-account.html:401 +#: warehouse/templates/manage/unverified-account.html:413 msgid "Device name:" msgstr "" #: warehouse/templates/manage/account.html:662 #: warehouse/templates/manage/account.html:674 -#: warehouse/templates/manage/unverified-account.html:369 -#: warehouse/templates/manage/unverified-account.html:381 +#: warehouse/templates/manage/unverified-account.html:403 +#: warehouse/templates/manage/unverified-account.html:415 msgid "" "Method: Authentication application (TOTP)" msgstr "" #: warehouse/templates/manage/account.html:666 -#: warehouse/templates/manage/unverified-account.html:373 +#: warehouse/templates/manage/unverified-account.html:407 msgid "Two factor authentication removed" msgstr "" @@ -3835,22 +3837,22 @@ msgid "Two factor device remembered" msgstr "" #: warehouse/templates/manage/account.html:680 -#: warehouse/templates/manage/unverified-account.html:385 +#: warehouse/templates/manage/unverified-account.html:419 msgid "Recovery codes generated" msgstr "" #: warehouse/templates/manage/account.html:683 -#: warehouse/templates/manage/unverified-account.html:388 +#: warehouse/templates/manage/unverified-account.html:422 msgid "Recovery codes regenerated" msgstr "" #: warehouse/templates/manage/account.html:686 -#: warehouse/templates/manage/unverified-account.html:391 +#: warehouse/templates/manage/unverified-account.html:425 msgid "Recovery code used for login" msgstr "" #: warehouse/templates/manage/account.html:689 -#: warehouse/templates/manage/unverified-account.html:394 +#: warehouse/templates/manage/unverified-account.html:428 msgid "API token added" msgstr "" @@ -3858,55 +3860,55 @@ msgstr "" #: warehouse/templates/manage/account.html:714 #: warehouse/templates/manage/project/history.html:238 #: warehouse/templates/manage/project/history.html:245 -#: warehouse/templates/manage/unverified-account.html:397 -#: warehouse/templates/manage/unverified-account.html:419 +#: warehouse/templates/manage/unverified-account.html:431 +#: warehouse/templates/manage/unverified-account.html:453 msgid "Token name:" msgstr "" #: warehouse/templates/manage/account.html:707 #: warehouse/templates/manage/project/history.html:240 -#: warehouse/templates/manage/unverified-account.html:412 +#: warehouse/templates/manage/unverified-account.html:446 msgid "API token removed" msgstr "" #: warehouse/templates/manage/account.html:709 #: warehouse/templates/manage/account.html:716 -#: warehouse/templates/manage/unverified-account.html:414 -#: warehouse/templates/manage/unverified-account.html:421 +#: warehouse/templates/manage/unverified-account.html:448 +#: warehouse/templates/manage/unverified-account.html:455 msgid "Unique identifier:" msgstr "" #: warehouse/templates/manage/account.html:711 -#: warehouse/templates/manage/unverified-account.html:416 +#: warehouse/templates/manage/unverified-account.html:450 msgid "API token automatically removed for security reasons" msgstr "" #: warehouse/templates/manage/account.html:724 -#: warehouse/templates/manage/unverified-account.html:429 +#: warehouse/templates/manage/unverified-account.html:463 #, python-format msgid "Reason: Token found at public url" msgstr "" #: warehouse/templates/manage/account.html:728 -#: warehouse/templates/manage/unverified-account.html:433 +#: warehouse/templates/manage/unverified-account.html:467 #, python-format msgid "Invited to join %(organization_name)s" msgstr "" #: warehouse/templates/manage/account.html:732 -#: warehouse/templates/manage/unverified-account.html:437 +#: warehouse/templates/manage/unverified-account.html:471 #, python-format msgid "Invitation to join %(organization_name)s declined" msgstr "" #: warehouse/templates/manage/account.html:736 -#: warehouse/templates/manage/unverified-account.html:441 +#: warehouse/templates/manage/unverified-account.html:475 #, python-format msgid "Invitation to join %(organization_name)s revoked" msgstr "" #: warehouse/templates/manage/account.html:740 -#: warehouse/templates/manage/unverified-account.html:445 +#: warehouse/templates/manage/unverified-account.html:479 #, python-format msgid "Invitation to join %(organization_name)s expired" msgstr "" @@ -3945,7 +3947,7 @@ msgid "" msgstr "" #: warehouse/templates/manage/account.html:776 -#: warehouse/templates/manage/unverified-account.html:452 +#: warehouse/templates/manage/unverified-account.html:486 #, python-format msgid "" "Events appear here as security-related actions occur on your account. If " @@ -3954,7 +3956,7 @@ msgid "" msgstr "" #: warehouse/templates/manage/account.html:781 -#: warehouse/templates/manage/unverified-account.html:457 +#: warehouse/templates/manage/unverified-account.html:491 msgid "Recent account activity" msgstr "" @@ -3962,7 +3964,7 @@ msgstr "" #: warehouse/templates/manage/organization/history.html:193 #: warehouse/templates/manage/project/history.html:333 #: warehouse/templates/manage/team/history.html:87 -#: warehouse/templates/manage/unverified-account.html:459 +#: warehouse/templates/manage/unverified-account.html:493 msgid "Event" msgstr "" @@ -3973,25 +3975,25 @@ msgstr "" #: warehouse/templates/manage/project/history.html:343 #: warehouse/templates/manage/team/history.html:88 #: warehouse/templates/manage/team/history.html:97 -#: warehouse/templates/manage/unverified-account.html:460 +#: warehouse/templates/manage/unverified-account.html:494 msgid "Time" msgstr "" #: warehouse/templates/manage/account.html:786 #: warehouse/templates/manage/organization/history.html:195 #: warehouse/templates/manage/team/history.html:89 -#: warehouse/templates/manage/unverified-account.html:461 +#: warehouse/templates/manage/unverified-account.html:495 msgid "Additional Info" msgstr "" #: warehouse/templates/manage/account.html:794 -#: warehouse/templates/manage/unverified-account.html:468 +#: warehouse/templates/manage/unverified-account.html:502 msgid "Date / time" msgstr "" #: warehouse/templates/manage/account.html:798 #: warehouse/templates/manage/organization/history.html:207 -#: warehouse/templates/manage/unverified-account.html:472 +#: warehouse/templates/manage/unverified-account.html:506 msgid "Location Info" msgstr "" @@ -3999,12 +4001,12 @@ msgstr "" #: warehouse/templates/manage/organization/history.html:210 #: warehouse/templates/manage/project/history.html:350 #: warehouse/templates/manage/team/history.html:104 -#: warehouse/templates/manage/unverified-account.html:475 +#: warehouse/templates/manage/unverified-account.html:509 msgid "Device Info" msgstr "" #: warehouse/templates/manage/account.html:809 -#: warehouse/templates/manage/unverified-account.html:483 +#: warehouse/templates/manage/unverified-account.html:517 msgid "Events will appear here as security-related actions occur on your account." msgstr "" @@ -4133,7 +4135,7 @@ msgstr "" #: warehouse/templates/manage/manage_base.html:72 #: warehouse/templates/manage/manage_base.html:91 #: warehouse/templates/manage/manage_base.html:95 -#: warehouse/templates/manage/manage_base.html:626 +#: warehouse/templates/manage/manage_base.html:617 #: warehouse/templates/manage/organization/roles.html:276 #: warehouse/templates/manage/organization/roles.html:280 #: warehouse/templates/manage/organization/roles.html:291 @@ -4311,13 +4313,28 @@ msgid "" "href=\"%(href)s\">here." msgstr "" -#: warehouse/templates/manage/manage_base.html:587 -#: warehouse/templates/manage/manage_base.html:598 -#: warehouse/templates/manage/manage_base.html:609 +#: warehouse/templates/manage/manage_base.html:579 +#: warehouse/templates/manage/manage_base.html:590 +#: warehouse/templates/manage/manage_base.html:601 msgid "Any" msgstr "" -#: warehouse/templates/manage/manage_base.html:633 +#: warehouse/templates/manage/manage_base.html:622 +msgid "Remove trusted publisher" +msgstr "" + +#: warehouse/templates/manage/manage_base.html:625 +#, python-format +msgid "" +"Removing this %(publisher_name)s trusted publisher will prevent automated" +" package uploads from this source. You can re-add it later if needed." +msgstr "" + +#: warehouse/templates/manage/manage_base.html:631 +msgid "Remove publisher" +msgstr "" + +#: warehouse/templates/manage/manage_base.html:635 #: warehouse/templates/manage/organization/history.html:109 #: warehouse/templates/manage/organization/history.html:159 #: warehouse/templates/manage/project/history.html:27 @@ -4331,7 +4348,7 @@ msgstr "" msgid "Added by:" msgstr "" -#: warehouse/templates/manage/manage_base.html:635 +#: warehouse/templates/manage/manage_base.html:637 #: warehouse/templates/manage/organization/history.html:117 #: warehouse/templates/manage/organization/history.html:164 #: warehouse/templates/manage/project/history.html:46 @@ -4343,24 +4360,24 @@ msgstr "" msgid "Removed by:" msgstr "" -#: warehouse/templates/manage/manage_base.html:637 +#: warehouse/templates/manage/manage_base.html:639 msgid "Submitted by:" msgstr "" -#: warehouse/templates/manage/manage_base.html:640 +#: warehouse/templates/manage/manage_base.html:642 #: warehouse/templates/manage/project/history.html:224 msgid "Workflow:" msgstr "" -#: warehouse/templates/manage/manage_base.html:642 +#: warehouse/templates/manage/manage_base.html:644 msgid "Specifier:" msgstr "" -#: warehouse/templates/manage/manage_base.html:644 +#: warehouse/templates/manage/manage_base.html:646 msgid "Publisher:" msgstr "" -#: warehouse/templates/manage/manage_base.html:646 +#: warehouse/templates/manage/manage_base.html:648 #: warehouse/templates/manage/project/history.html:36 #: warehouse/templates/manage/project/history.html:89 msgid "URL:" @@ -4435,11 +4452,11 @@ msgstr "" msgid "Manager" msgstr "" -#: warehouse/templates/manage/account/publishing.html:35 +#: warehouse/templates/manage/account/publishing.html:36 #: warehouse/templates/manage/organization/roles.html:49 #: warehouse/templates/manage/organization/roles.html:234 #: warehouse/templates/manage/organizations.html:91 -#: warehouse/templates/manage/project/publishing.html:41 +#: warehouse/templates/manage/project/publishing.html:42 #: warehouse/templates/manage/project/roles.html:48 #: warehouse/templates/manage/project/roles.html:86 #: warehouse/templates/manage/project/roles.html:119 @@ -4684,20 +4701,33 @@ msgstr "" msgid "Activate your account" msgstr "" -#: warehouse/templates/manage/unverified-account.html:198 +#: warehouse/templates/manage/unverified-account.html:216 msgid "" -"You must verify a primary email address before making any other changes " -"to your account." +"You must verify an existing email address and make it your primary email " +"address before making any other changes to your account." msgstr "" -#: warehouse/templates/manage/unverified-account.html:204 +#: warehouse/templates/manage/unverified-account.html:222 +#, python-format +msgid "" +"If you cannot verify any email addresses listed, please see %(help_url)s to recover access to your account." +msgstr "" + +#: warehouse/templates/manage/unverified-account.html:230 +msgid "" +"You must verify the primary email address listed below before making any " +"other changes to your account." +msgstr "" + +#: warehouse/templates/manage/unverified-account.html:236 #, python-format msgid "" -"If you cannot verify a primary email address, please see %(help_url)s." +"If you cannot verify the primary email address listed, please see %(help_url)s to recover access to your account." msgstr "" -#: warehouse/templates/manage/unverified-account.html:213 +#: warehouse/templates/manage/unverified-account.html:247 msgid "" "You must have at least one Verified email " @@ -4712,88 +4742,88 @@ msgid "" "href=\"%(href)s\">here." msgstr "" -#: warehouse/templates/manage/account/publishing.html:22 -#: warehouse/templates/manage/account/publishing.html:130 -#: warehouse/templates/manage/account/publishing.html:225 -#: warehouse/templates/manage/account/publishing.html:292 +#: warehouse/templates/manage/account/publishing.html:23 +#: warehouse/templates/manage/account/publishing.html:132 +#: warehouse/templates/manage/account/publishing.html:230 +#: warehouse/templates/manage/account/publishing.html:298 msgid "PyPI Project Name" msgstr "" -#: warehouse/templates/manage/account/publishing.html:27 -#: warehouse/templates/manage/account/publishing.html:135 -#: warehouse/templates/manage/account/publishing.html:230 -#: warehouse/templates/manage/account/publishing.html:297 +#: warehouse/templates/manage/account/publishing.html:28 +#: warehouse/templates/manage/account/publishing.html:137 +#: warehouse/templates/manage/account/publishing.html:235 +#: warehouse/templates/manage/account/publishing.html:303 msgid "project name" msgstr "" -#: warehouse/templates/manage/account/publishing.html:29 -#: warehouse/templates/manage/account/publishing.html:137 -#: warehouse/templates/manage/account/publishing.html:232 -#: warehouse/templates/manage/account/publishing.html:305 +#: warehouse/templates/manage/account/publishing.html:30 +#: warehouse/templates/manage/account/publishing.html:139 +#: warehouse/templates/manage/account/publishing.html:237 +#: warehouse/templates/manage/account/publishing.html:311 msgid "The project (on PyPI) that will be created when this publisher is used" msgstr "" -#: warehouse/templates/manage/account/publishing.html:40 -#: warehouse/templates/manage/project/publishing.html:46 +#: warehouse/templates/manage/account/publishing.html:41 +#: warehouse/templates/manage/project/publishing.html:47 msgid "owner" msgstr "" -#: warehouse/templates/manage/account/publishing.html:42 -#: warehouse/templates/manage/project/publishing.html:48 +#: warehouse/templates/manage/account/publishing.html:43 +#: warehouse/templates/manage/project/publishing.html:49 msgid "The GitHub organization name or GitHub username that owns the repository" msgstr "" -#: warehouse/templates/manage/account/publishing.html:48 -#: warehouse/templates/manage/project/publishing.html:54 +#: warehouse/templates/manage/account/publishing.html:49 +#: warehouse/templates/manage/project/publishing.html:55 msgid "Repository name" msgstr "" -#: warehouse/templates/manage/account/publishing.html:53 -#: warehouse/templates/manage/project/publishing.html:59 +#: warehouse/templates/manage/account/publishing.html:54 +#: warehouse/templates/manage/project/publishing.html:60 msgid "repository" msgstr "" -#: warehouse/templates/manage/account/publishing.html:61 -#: warehouse/templates/manage/project/publishing.html:67 +#: warehouse/templates/manage/account/publishing.html:62 +#: warehouse/templates/manage/project/publishing.html:68 msgid "The name of the GitHub repository that contains the publishing workflow" msgstr "" -#: warehouse/templates/manage/account/publishing.html:67 -#: warehouse/templates/manage/project/publishing.html:73 +#: warehouse/templates/manage/account/publishing.html:68 +#: warehouse/templates/manage/project/publishing.html:74 msgid "Workflow name" msgstr "" -#: warehouse/templates/manage/account/publishing.html:72 -#: warehouse/templates/manage/project/publishing.html:78 +#: warehouse/templates/manage/account/publishing.html:73 +#: warehouse/templates/manage/project/publishing.html:79 msgid "workflow.yml" msgstr "" -#: warehouse/templates/manage/account/publishing.html:78 -#: warehouse/templates/manage/project/publishing.html:84 +#: warehouse/templates/manage/account/publishing.html:79 +#: warehouse/templates/manage/project/publishing.html:85 msgid "" "The filename of the publishing workflow. This file should exist in the " ".github/workflows/ directory in the repository configured " "above." msgstr "" -#: warehouse/templates/manage/account/publishing.html:84 -#: warehouse/templates/manage/account/publishing.html:182 -#: warehouse/templates/manage/project/publishing.html:90 -#: warehouse/templates/manage/project/publishing.html:177 +#: warehouse/templates/manage/account/publishing.html:85 +#: warehouse/templates/manage/account/publishing.html:184 +#: warehouse/templates/manage/project/publishing.html:91 +#: warehouse/templates/manage/project/publishing.html:179 msgid "Environment name" msgstr "" -#: warehouse/templates/manage/account/publishing.html:88 -#: warehouse/templates/manage/account/publishing.html:186 -#: warehouse/templates/manage/account/publishing.html:255 -#: warehouse/templates/manage/project/publishing.html:94 -#: warehouse/templates/manage/project/publishing.html:181 -#: warehouse/templates/manage/project/publishing.html:253 +#: warehouse/templates/manage/account/publishing.html:89 +#: warehouse/templates/manage/account/publishing.html:188 +#: warehouse/templates/manage/account/publishing.html:260 +#: warehouse/templates/manage/project/publishing.html:95 +#: warehouse/templates/manage/project/publishing.html:183 +#: warehouse/templates/manage/project/publishing.html:259 msgid "(optional)" msgstr "" -#: warehouse/templates/manage/account/publishing.html:96 -#: warehouse/templates/manage/project/publishing.html:103 +#: warehouse/templates/manage/account/publishing.html:97 +#: warehouse/templates/manage/project/publishing.html:104 #, python-format msgid "" "The name of the GitHub Actions environment that " @@ -4804,89 +4834,89 @@ msgid "" "commit access who shouldn't have PyPI publishing access." msgstr "" -#: warehouse/templates/manage/account/publishing.html:110 -#: warehouse/templates/manage/account/publishing.html:205 -#: warehouse/templates/manage/account/publishing.html:272 -#: warehouse/templates/manage/account/publishing.html:364 -#: warehouse/templates/manage/project/publishing.html:117 -#: warehouse/templates/manage/project/publishing.html:215 -#: warehouse/templates/manage/project/publishing.html:270 -#: warehouse/templates/manage/project/publishing.html:344 +#: warehouse/templates/manage/account/publishing.html:111 +#: warehouse/templates/manage/account/publishing.html:209 +#: warehouse/templates/manage/account/publishing.html:277 +#: warehouse/templates/manage/account/publishing.html:370 +#: warehouse/templates/manage/project/publishing.html:118 +#: warehouse/templates/manage/project/publishing.html:220 +#: warehouse/templates/manage/project/publishing.html:276 +#: warehouse/templates/manage/project/publishing.html:351 #: warehouse/templates/manage/project/roles.html:373 #: warehouse/templates/manage/project/settings.html:322 #: warehouse/templates/manage/team/roles.html:125 msgid "Add" msgstr "" -#: warehouse/templates/manage/account/publishing.html:117 -#: warehouse/templates/manage/project/publishing.html:125 +#: warehouse/templates/manage/account/publishing.html:118 +#: warehouse/templates/manage/project/publishing.html:126 #, python-format msgid "" "Read more about GitLab CI/CD OpenID Connect support here." msgstr "" -#: warehouse/templates/manage/account/publishing.html:143 -#: warehouse/templates/manage/project/publishing.html:138 +#: warehouse/templates/manage/account/publishing.html:145 +#: warehouse/templates/manage/project/publishing.html:140 msgid "Namespace" msgstr "" -#: warehouse/templates/manage/account/publishing.html:148 -#: warehouse/templates/manage/project/publishing.html:143 +#: warehouse/templates/manage/account/publishing.html:150 +#: warehouse/templates/manage/project/publishing.html:145 msgid "namespace" msgstr "" -#: warehouse/templates/manage/account/publishing.html:150 -#: warehouse/templates/manage/project/publishing.html:145 +#: warehouse/templates/manage/account/publishing.html:152 +#: warehouse/templates/manage/project/publishing.html:147 msgid "" "The GitLab username or GitLab group/subgroup namespace that the project " "is under" msgstr "" -#: warehouse/templates/manage/account/publishing.html:156 +#: warehouse/templates/manage/account/publishing.html:158 #: warehouse/templates/manage/project/documentation.html:21 -#: warehouse/templates/manage/project/publishing.html:151 +#: warehouse/templates/manage/project/publishing.html:153 #: warehouse/templates/manage/project/release.html:139 #: warehouse/templates/pages/stats.html:65 msgid "Project name" msgstr "" -#: warehouse/templates/manage/account/publishing.html:161 -#: warehouse/templates/manage/project/publishing.html:156 +#: warehouse/templates/manage/account/publishing.html:163 +#: warehouse/templates/manage/project/publishing.html:158 msgid "project" msgstr "" -#: warehouse/templates/manage/account/publishing.html:163 -#: warehouse/templates/manage/project/publishing.html:158 +#: warehouse/templates/manage/account/publishing.html:165 +#: warehouse/templates/manage/project/publishing.html:160 msgid "The name of the GitLab project that contains the publishing workflow" msgstr "" -#: warehouse/templates/manage/account/publishing.html:169 -#: warehouse/templates/manage/project/publishing.html:164 +#: warehouse/templates/manage/account/publishing.html:171 +#: warehouse/templates/manage/project/publishing.html:166 msgid "Top-level pipeline file path" msgstr "" -#: warehouse/templates/manage/account/publishing.html:174 -#: warehouse/templates/manage/project/publishing.html:169 +#: warehouse/templates/manage/account/publishing.html:176 +#: warehouse/templates/manage/project/publishing.html:171 msgid ".gitlab-ci.yml" msgstr "" -#: warehouse/templates/manage/account/publishing.html:176 -#: warehouse/templates/manage/project/publishing.html:171 +#: warehouse/templates/manage/account/publishing.html:178 +#: warehouse/templates/manage/project/publishing.html:173 msgid "" "The file path of the top-level pipeline, relative to the project's root. " "This file should exist in the project configured above (external " "pipelines are not supported)." msgstr "" -#: warehouse/templates/manage/account/publishing.html:189 -#: warehouse/templates/manage/project/publishing.html:97 -#: warehouse/templates/manage/project/publishing.html:184 +#: warehouse/templates/manage/account/publishing.html:191 +#: warehouse/templates/manage/project/publishing.html:98 +#: warehouse/templates/manage/project/publishing.html:186 msgid "release" msgstr "" -#: warehouse/templates/manage/account/publishing.html:191 -#: warehouse/templates/manage/project/publishing.html:186 +#: warehouse/templates/manage/account/publishing.html:193 +#: warehouse/templates/manage/project/publishing.html:188 #, python-format msgid "" "The name of the GitLab CI/CD environment that " @@ -4897,35 +4927,35 @@ msgid "" "access who shouldn't have PyPI publishing access." msgstr "" -#: warehouse/templates/manage/account/publishing.html:212 -#: warehouse/templates/manage/project/publishing.html:223 +#: warehouse/templates/manage/account/publishing.html:216 +#: warehouse/templates/manage/project/publishing.html:228 #, python-format msgid "" "Read more about Google's OpenID Connect support here." msgstr "" -#: warehouse/templates/manage/account/publishing.html:243 -#: warehouse/templates/manage/project/publishing.html:241 +#: warehouse/templates/manage/account/publishing.html:248 +#: warehouse/templates/manage/project/publishing.html:247 msgid "email" msgstr "" -#: warehouse/templates/manage/account/publishing.html:245 -#: warehouse/templates/manage/project/publishing.html:243 +#: warehouse/templates/manage/account/publishing.html:250 +#: warehouse/templates/manage/project/publishing.html:249 msgid "The email address of the account or service account used to publish." msgstr "" -#: warehouse/templates/manage/account/publishing.html:251 -#: warehouse/templates/manage/project/publishing.html:249 +#: warehouse/templates/manage/account/publishing.html:256 +#: warehouse/templates/manage/project/publishing.html:255 msgid "Subject" msgstr "" -#: warehouse/templates/manage/account/publishing.html:258 -#: warehouse/templates/manage/project/publishing.html:256 +#: warehouse/templates/manage/account/publishing.html:263 +#: warehouse/templates/manage/project/publishing.html:262 msgid "subject" msgstr "" -#: warehouse/templates/manage/account/publishing.html:266 +#: warehouse/templates/manage/account/publishing.html:271 #, python-format msgid "" "The subject is the numeric ID that represents the principal making the " @@ -4933,87 +4963,87 @@ msgid "" "identity used for publishing. More details here." msgstr "" -#: warehouse/templates/manage/account/publishing.html:279 -#: warehouse/templates/manage/project/publishing.html:278 +#: warehouse/templates/manage/account/publishing.html:284 +#: warehouse/templates/manage/project/publishing.html:284 #, python-format msgid "" "Read more about ActiveState's OpenID Connect support here." msgstr "" -#: warehouse/templates/manage/account/publishing.html:311 -#: warehouse/templates/manage/project/publishing.html:291 +#: warehouse/templates/manage/account/publishing.html:317 +#: warehouse/templates/manage/project/publishing.html:298 #: warehouse/templates/organizations/profile.html:17 msgid "Organization" msgstr "" -#: warehouse/templates/manage/account/publishing.html:316 -#: warehouse/templates/manage/project/publishing.html:296 +#: warehouse/templates/manage/account/publishing.html:322 +#: warehouse/templates/manage/project/publishing.html:303 msgid "my-organization" msgstr "" -#: warehouse/templates/manage/account/publishing.html:323 -#: warehouse/templates/manage/project/publishing.html:303 +#: warehouse/templates/manage/account/publishing.html:329 +#: warehouse/templates/manage/project/publishing.html:310 msgid "The ActiveState organization name that owns the project" msgstr "" -#: warehouse/templates/manage/account/publishing.html:328 -#: warehouse/templates/manage/project/publishing.html:308 +#: warehouse/templates/manage/account/publishing.html:334 +#: warehouse/templates/manage/project/publishing.html:315 msgid "ActiveState Project name" msgstr "" -#: warehouse/templates/manage/account/publishing.html:333 -#: warehouse/templates/manage/project/publishing.html:313 +#: warehouse/templates/manage/account/publishing.html:339 +#: warehouse/templates/manage/project/publishing.html:320 msgid "my-project" msgstr "" -#: warehouse/templates/manage/account/publishing.html:341 -#: warehouse/templates/manage/project/publishing.html:321 +#: warehouse/templates/manage/account/publishing.html:347 +#: warehouse/templates/manage/project/publishing.html:328 msgid "The ActiveState project that will build your Python artifact." msgstr "" -#: warehouse/templates/manage/account/publishing.html:347 -#: warehouse/templates/manage/project/publishing.html:327 +#: warehouse/templates/manage/account/publishing.html:353 +#: warehouse/templates/manage/project/publishing.html:334 msgid "Actor Username" msgstr "" -#: warehouse/templates/manage/account/publishing.html:352 -#: warehouse/templates/manage/project/publishing.html:332 +#: warehouse/templates/manage/account/publishing.html:358 +#: warehouse/templates/manage/project/publishing.html:339 msgid "my-username" msgstr "" -#: warehouse/templates/manage/account/publishing.html:358 -#: warehouse/templates/manage/project/publishing.html:338 +#: warehouse/templates/manage/account/publishing.html:364 +#: warehouse/templates/manage/project/publishing.html:345 msgid "" "The username for the ActiveState account that will trigger the build of " "your Python artifact." msgstr "" -#: warehouse/templates/manage/account/publishing.html:373 +#: warehouse/templates/manage/account/publishing.html:379 msgid "Manage publishers" msgstr "" -#: warehouse/templates/manage/account/publishing.html:380 +#: warehouse/templates/manage/account/publishing.html:386 msgid "Project" msgstr "" -#: warehouse/templates/manage/account/publishing.html:400 +#: warehouse/templates/manage/account/publishing.html:406 msgid "" "No publishers are currently configured. Publishers for existing projects " "can be added in the publishing configuration for each individual project." msgstr "" -#: warehouse/templates/manage/account/publishing.html:410 +#: warehouse/templates/manage/account/publishing.html:416 msgid "Pending project name" msgstr "" -#: warehouse/templates/manage/account/publishing.html:411 -#: warehouse/templates/manage/project/publishing.html:368 +#: warehouse/templates/manage/account/publishing.html:417 +#: warehouse/templates/manage/project/publishing.html:375 msgid "Publisher" msgstr "" -#: warehouse/templates/manage/account/publishing.html:412 -#: warehouse/templates/manage/project/publishing.html:369 +#: warehouse/templates/manage/account/publishing.html:418 +#: warehouse/templates/manage/project/publishing.html:376 #: warehouse/templates/packaging/detail.html:81 #: warehouse/templates/packaging/detail.html:106 #: warehouse/templates/packaging/detail.html:120 @@ -5021,21 +5051,21 @@ msgstr "" msgid "Details" msgstr "" -#: warehouse/templates/manage/account/publishing.html:422 +#: warehouse/templates/manage/account/publishing.html:428 msgid "" "No pending publishers are currently configured. Publishers for projects " "that don't exist yet can be added below." msgstr "" -#: warehouse/templates/manage/account/publishing.html:429 +#: warehouse/templates/manage/account/publishing.html:435 msgid "Add a new pending publisher" msgstr "" -#: warehouse/templates/manage/account/publishing.html:431 +#: warehouse/templates/manage/account/publishing.html:437 msgid "You can use this page to register \"pending\" trusted publishers." msgstr "" -#: warehouse/templates/manage/account/publishing.html:436 +#: warehouse/templates/manage/account/publishing.html:442 #, python-format msgid "" "These publishers behave similarly to trusted publishers registered " @@ -5046,15 +5076,15 @@ msgid "" "trusted publishers here." msgstr "" -#: warehouse/templates/manage/account/publishing.html:446 +#: warehouse/templates/manage/account/publishing.html:452 msgid "" "Configuring a \"pending\" publisher for a project name does " "not reserve that name. Until the project is created, any" " other user may create it, including via their own \"pending\" publisher." msgstr "" -#: warehouse/templates/manage/account/publishing.html:481 -#: warehouse/templates/manage/project/publishing.html:412 +#: warehouse/templates/manage/account/publishing.html:487 +#: warehouse/templates/manage/project/publishing.html:419 #, python-format msgid "" "You must first enable two-factor authentication " @@ -6509,17 +6539,17 @@ msgid "" "before submitting the form." msgstr "" -#: warehouse/templates/manage/project/publishing.html:201 +#: warehouse/templates/manage/project/publishing.html:203 msgid "GitLab instance" msgstr "" -#: warehouse/templates/manage/project/publishing.html:208 +#: warehouse/templates/manage/project/publishing.html:210 msgid "" "The GitLab instance URL. Select https://gitlab.com for the " "public GitLab service, or a custom instance." msgstr "" -#: warehouse/templates/manage/project/publishing.html:264 +#: warehouse/templates/manage/project/publishing.html:270 #, python-format msgid "" "The subject is the numeric ID that represents the principal making the " @@ -6528,33 +6558,33 @@ msgid "" "here." msgstr "" -#: warehouse/templates/manage/project/publishing.html:360 +#: warehouse/templates/manage/project/publishing.html:367 msgid "Manage current publishers" msgstr "" -#: warehouse/templates/manage/project/publishing.html:364 +#: warehouse/templates/manage/project/publishing.html:371 #, python-format msgid "OpenID Connect publishers associated with %(project_name)s" msgstr "" -#: warehouse/templates/manage/project/publishing.html:378 +#: warehouse/templates/manage/project/publishing.html:385 msgid "No publishers are currently configured." msgstr "" -#: warehouse/templates/manage/project/publishing.html:382 +#: warehouse/templates/manage/project/publishing.html:389 msgid "Add a new publisher" msgstr "" -#: warehouse/templates/manage/project/publishing.html:422 -#: warehouse/templates/manage/project/publishing.html:425 +#: warehouse/templates/manage/project/publishing.html:429 +#: warehouse/templates/manage/project/publishing.html:432 msgid "Constrain environment" msgstr "" -#: warehouse/templates/manage/project/publishing.html:438 +#: warehouse/templates/manage/project/publishing.html:445 msgid "This will restrict the Trusted Publisher's environment to" msgstr "" -#: warehouse/templates/manage/project/publishing.html:441 +#: warehouse/templates/manage/project/publishing.html:448 #, python-format msgid "" "If you currently use multiple environments in your CI/CD workflows, you " @@ -6562,7 +6592,7 @@ msgid "" "register each as a Trusted Publisher." msgstr "" -#: warehouse/templates/manage/project/publishing.html:453 +#: warehouse/templates/manage/project/publishing.html:460 #, python-format msgid "" "I understand that this Trusted Publisher will only allow uploads from " @@ -6570,7 +6600,7 @@ msgid "" "environment." msgstr "" -#: warehouse/templates/manage/project/publishing.html:458 +#: warehouse/templates/manage/project/publishing.html:465 msgid "New Environment Name" msgstr "" From a12833ff686b2d6210f756d673d0e34f7d4e3266 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Wed, 5 Nov 2025 22:39:46 +1000 Subject: [PATCH 10/13] remove unused code --- tests/unit/utils/test_wheel.py | 5 +++-- warehouse/utils/wheel.py | 8 +------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/unit/utils/test_wheel.py b/tests/unit/utils/test_wheel.py index a82537cf0985..60cd0d263613 100644 --- a/tests/unit/utils/test_wheel.py +++ b/tests/unit/utils/test_wheel.py @@ -21,8 +21,9 @@ def _build(**kwargs): grouped_labels["platform"][key.removeprefix("plat_")] = value elif key.startswith("other_"): grouped_labels["other"][key.removeprefix("other_")] = value - else: - raise ValueError(f"Unknown item {key}={value}") + # for debugging + # else: + # raise ValueError(f"Unknown item {key}={value}") return grouped_labels diff --git a/warehouse/utils/wheel.py b/warehouse/utils/wheel.py index e35b8687df0a..f3f8f47a5416 100644 --- a/warehouse/utils/wheel.py +++ b/warehouse/utils/wheel.py @@ -191,13 +191,7 @@ def _platform_to_label(tag: packaging.tags.Tag) -> str: def _add_group_label(container: dict, group: str, value: str, label: str) -> None: - if value not in container[group]: - container[group][value] = label - elif container[group][value] != label: - # A value that is already present, with a different label. - # This looks odd. Is this possible? - # Use the most recently seen label. - container[group][value] = label + container[group][value] = label def filename_to_tags(filename: str) -> set[packaging.tags.Tag]: From 3c7c9adc58560bc98c4a83b710ce1df91d3914a6 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Wed, 5 Nov 2025 22:49:59 +1000 Subject: [PATCH 11/13] fix lint issues --- tests/frontend/filter_list_controller_test.js | 9 ++++----- .../controllers/filter_list_controller.js | 16 ++++++++-------- warehouse/utils/wheel.py | 8 ++++---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/frontend/filter_list_controller_test.js b/tests/frontend/filter_list_controller_test.js index 309750538889..42849c568958 100644 --- a/tests/frontend/filter_list_controller_test.js +++ b/tests/frontend/filter_list_controller_test.js @@ -45,8 +45,7 @@ describe("Filter list controller", () => { elFilter.dispatchEvent(event); expect(dispatchEventSpy).toHaveBeenCalledWith(event); return elFilter; - } - + }; const setFilterInputValue = function(value) { const elFilter = document.getElementById("filter-input"); const dispatchEventSpy = jest.spyOn(elFilter, "dispatchEvent"); @@ -58,14 +57,14 @@ describe("Filter list controller", () => { const event = new Event("input"); elFilter.dispatchEvent(event); expect(dispatchEventSpy).toHaveBeenCalledWith(event); - } + }; const clearFilters = function() { - const elUrl = document.getElementById('filter-clear'); + const elUrl = document.getElementById("filter-clear"); const dispatchEventSpy = jest.spyOn(elUrl, "dispatchEvent"); const event = new Event("click"); elUrl.dispatchEvent(event); expect(dispatchEventSpy).toHaveBeenCalledWith(event); - } + }; describe("is initialized as expected", () => { describe("makes expected elements visible", () => { let application; diff --git a/warehouse/static/js/warehouse/controllers/filter_list_controller.js b/warehouse/static/js/warehouse/controllers/filter_list_controller.js index be1a562e0362..8c899c12d080 100644 --- a/warehouse/static/js/warehouse/controllers/filter_list_controller.js +++ b/warehouse/static/js/warehouse/controllers/filter_list_controller.js @@ -32,7 +32,7 @@ export default class extends Controller { // Capture the initial select element values, so they can be restored. this._getFilterTargets().forEach(filterTarget => { - if (filterTarget.nodeName === 'SELECT') { + if (filterTarget.nodeName === "SELECT") { const key = filterTarget.dataset.filteredSource; if (!this.initialSelectOptions[key]) { this.initialSelectOptions[key] = []; @@ -84,7 +84,7 @@ export default class extends Controller { if (!groupedLabels[key].includes(value)) { groupedLabels[key].push(value); } - }) + }); }); } else { // no match: hide item @@ -104,8 +104,8 @@ export default class extends Controller { total, shown, total)); - this.summaryTarget.textContent = messages.join(' '); - } + this.summaryTarget.textContent = messages.join(" "); + } // Update the current url to include the filters const htmlElementFilters = this._getFiltersHtmlElements(); @@ -128,7 +128,7 @@ export default class extends Controller { const filterTargets = this._getFilterTargets(); const selected = {}; filterTargets.forEach(filterTarget => { - if (filterTarget.nodeName === 'SELECT') { + if (filterTarget.nodeName === "SELECT") { const key = filterTarget.dataset.filteredSource; // Store which option is selected. for (const selectedOption of filterTarget.selectedOptions) { @@ -228,7 +228,7 @@ export default class extends Controller { const key = filterTarget.dataset.filteredSource; const value = filterTarget.value; if (!Object.hasOwn(filterData, key)) { - filterData[key] = {values: [], comparison: 'exact'}; + filterData[key] = {values: [], comparison: "exact"}; } filterData[key].values.push(value); @@ -255,13 +255,13 @@ export default class extends Controller { const itemValues = Array.from(new Set((itemData[filterKey] ?? []).map(i => i?.toString()?.trim() ?? "").filter(i => !!i))); // Not a match if the item values and filter values contain different values. - if (filterValues.length > 0 && comparison === 'exact') { + if (filterValues.length > 0 && comparison === "exact") { if (!filterValues.every(filterValue => itemValues.includes(filterValue))) { return false; } } - if (filterValues.length > 0 && comparison === 'includes') { + if (filterValues.length > 0 && comparison === "includes") { if (!filterValues.every(filterValue => itemValues.some(itemValue => itemValue.includes(filterValue)))) { return false; } diff --git a/warehouse/utils/wheel.py b/warehouse/utils/wheel.py index f3f8f47a5416..b16710d3b281 100644 --- a/warehouse/utils/wheel.py +++ b/warehouse/utils/wheel.py @@ -212,8 +212,8 @@ def filename_to_pretty_tags(filename: str) -> list[str]: return sorted(pretty_tags) -def filename_to_grouped_labels(filename: str) -> dict[str, dict]: - grouped_labels = { +def filename_to_grouped_labels(filename: str) -> dict[str, dict[str, str]]: + grouped_labels: dict[str, dict[str, str]] = { "interpreter": {}, "abi": {}, "platform": {}, @@ -239,8 +239,8 @@ def filename_to_grouped_labels(filename: str) -> dict[str, dict]: return grouped_labels -def filenames_to_grouped_labels(filenames: list[str]) -> dict[str, dict]: - grouped_labels = { +def filenames_to_grouped_labels(filenames: list[str]) -> dict[str, dict[str, str]]: + grouped_labels: dict[str, dict[str, str]] = { "interpreter": {}, "abi": {}, "platform": {}, From 3635c6849555d156b061d55182c500d6936fc74f Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Wed, 5 Nov 2025 23:01:22 +1000 Subject: [PATCH 12/13] more lint fixes --- tests/unit/utils/test_wheel.py | 3 --- .../js/warehouse/controllers/filter_list_controller.js | 6 +++--- warehouse/static/sass/blocks/_filter-wheels.scss | 2 +- warehouse/static/sass/blocks/_form-group.scss | 1 + 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/unit/utils/test_wheel.py b/tests/unit/utils/test_wheel.py index 60cd0d263613..83674f5e1d6c 100644 --- a/tests/unit/utils/test_wheel.py +++ b/tests/unit/utils/test_wheel.py @@ -21,9 +21,6 @@ def _build(**kwargs): grouped_labels["platform"][key.removeprefix("plat_")] = value elif key.startswith("other_"): grouped_labels["other"][key.removeprefix("other_")] = value - # for debugging - # else: - # raise ValueError(f"Unknown item {key}={value}") return grouped_labels diff --git a/warehouse/static/js/warehouse/controllers/filter_list_controller.js b/warehouse/static/js/warehouse/controllers/filter_list_controller.js index 8c899c12d080..a6a94aaa5db3 100644 --- a/warehouse/static/js/warehouse/controllers/filter_list_controller.js +++ b/warehouse/static/js/warehouse/controllers/filter_list_controller.js @@ -287,7 +287,7 @@ export default class extends Controller { const key = filterTarget.dataset.filteredSource; const value = currentSearchParams.get(key); return [key, value]; - }).filter(([key, _value]) => enabledFilterTargets.includes(key))); + }).filter(i => enabledFilterTargets.includes(i[0]))); } /** @@ -356,7 +356,7 @@ export default class extends Controller { const value = filterTarget.dataset.autoUpdateUrlQuerystring; return [key, value]; }) - .filter(([_key, value]) => value === 'true') - .map(([key, _value]) => key); + .filter(i => i[1] === "true") + .map(i => i[0]); } } diff --git a/warehouse/static/sass/blocks/_filter-wheels.scss b/warehouse/static/sass/blocks/_filter-wheels.scss index 014f77f92895..d1100a7bec62 100644 --- a/warehouse/static/sass/blocks/_filter-wheels.scss +++ b/warehouse/static/sass/blocks/_filter-wheels.scss @@ -35,8 +35,8 @@ } &__filters { - flex-shrink: 0; flex: 0 1 auto; + flex-shrink: 0; display: flex; gap: $spacing-unit / 3; align-items: flex-end; diff --git a/warehouse/static/sass/blocks/_form-group.scss b/warehouse/static/sass/blocks/_form-group.scss index 768c374cfa25..a29c399b7f22 100644 --- a/warehouse/static/sass/blocks/_form-group.scss +++ b/warehouse/static/sass/blocks/_form-group.scss @@ -132,6 +132,7 @@ &--flex-width { max-width: unset; + :where( input:not([type]), select, From b36b2f48030d064c6226f0315ec1fde1515bca73 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Wed, 5 Nov 2025 23:21:39 +1000 Subject: [PATCH 13/13] change to satisfy the branch coverage --- tests/unit/utils/test_wheel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_wheel.py b/tests/unit/utils/test_wheel.py index 83674f5e1d6c..660d4342d913 100644 --- a/tests/unit/utils/test_wheel.py +++ b/tests/unit/utils/test_wheel.py @@ -19,7 +19,7 @@ def _build(**kwargs): grouped_labels["abi"][key.removeprefix("abi_")] = value elif key.startswith("plat_"): grouped_labels["platform"][key.removeprefix("plat_")] = value - elif key.startswith("other_"): + else: grouped_labels["other"][key.removeprefix("other_")] = value return grouped_labels