From 3195c2a9827a02da70938ee67c1aa60e2e9afaef Mon Sep 17 00:00:00 2001
From: Michael Thamm Sourced from urllib3's
releases. urllib3
is raising ~$40,000 USD to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects please consider contributing
financially to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul. Thank you for your support. Addressed high-severity security issues. Impact was limited to
specific use cases detailed in the accompanying advisories; overall user
exposure was estimated to be marginal. Decompression-bomb safeguards of the streaming API were bypassed: See GHSA-mf9v-mfxr-j63j for details. HTTP pools created using
Sourced from urllib3's
changelog. Addressed high-severity security issues.
Impact was limited to specific use cases detailed in the accompanying
advisories; overall user exposure was estimated to be marginal. Decompression-bomb safeguards of the streaming API were bypassed: See HTTP pools created using
... (truncated) Sourced from cryptography's
changelog. 46.0.7 - 2026-04-07 .. _v46-0-6: 46.0.6 - 2026-03-25Release notes
2.7.0
🚀 urllib3 is fundraising for HTTP/2 support
Security
HTTPResponse.drain_conn() was called after the
response had been read and decompressed partially. (Reported by @Cycloctane)HTTPResponse.read(amt=N) or
HTTPResponse.stream(amt=N) call when the response was
decompressed using the official Brotli library. (Reported by
@kimkou2024)ProxyManager.connection_from_url did not strip sensitive
headers specified in Retry.remove_headers_on_redirect when
redirecting to a different host. (GHSA-qccp-gfcp-xxvc reported by @christos-spearbit)Deprecations and Removals
FutureWarning instead of
DeprecationWarning for better visibility of existing
deprecation notices. Rescheduled the removal of deprecated features to
version 3.0. (urllib3/urllib3#3763)Bugfixes
HTTPResponse.read(amt=None) was
ignoring decompressed data buffered from previous partial reads. (urllib3/urllib3#3636)HTTPResponse.read() could cache only
part of the response after a partial read when
cache_content=True. (urllib3/urllib3#4967)HTTPResponse.stream() and
HTTPResponse.read_chunked() to handle amt=0.
(urllib3/urllib3#3793)_TYPE_BODY type alias to include missing
Iterable[str], matching the documented and runtime behavior
of chunked request bodies. (urllib3/urllib3#3798)LocationParseError when paths resembling
schemeless URIs were passed to
HTTPConnectionPool.urlopen(). (urllib3/urllib3#3352)BaseHTTPResponse.readinto() type annotation to
accept memoryview in addition to bytearray,
matching the io.RawIOBase.readinto contract and enabling
use with io.BufferedReader without type errors. (urllib3/urllib3#3764)Changelog
2.7.0 (2026-05-07)
Security
HTTPResponse.drain_conn() was called after the
response had been
read and decompressed partially.HTTPResponse.read(amt=N) or
HTTPResponse.stream(amt=N) call when the response was
decompressed
using the official Brotli
<https://pypi.org/project/brotli/>__ library.GHSA-mf9v-mfxr-j63j
<https://github.com/urllib3/urllib3/security/advisories/GHSA-mf9v-mfxr-j63j>__
for details.ProxyManager.connection_from_url did not strip
sensitive headers specified in
Retry.remove_headers_on_redirect when
redirecting to a different host.
(GHSA-qccp-gfcp-xxvc
<https://github.com/urllib3/urllib3/security/advisories/GHSA-qccp-gfcp-xxvc>__)Deprecations and Removals
FutureWarning instead of
DeprecationWarning for better
visibility of existing deprecation notices. Rescheduled the removal of
deprecated features to version 3.0.
([#3763](https://github.com/urllib3/urllib3/issues/3763)
<https://github.com/urllib3/urllib3/issues/3763>__)[#3720](https://github.com/urllib3/urllib3/issues/3720)
<https://github.com/urllib3/urllib3/issues/3720>__)[#4979](https://github.com/urllib3/urllib3/issues/4979)
<https://github.com/urllib3/urllib3/issues/4979>__)[#3777](https://github.com/urllib3/urllib3/issues/3777)
<https://github.com/urllib3/urllib3/issues/3777>__)Bugfixes
HTTPResponse.read(amt=None) was
ignoring decompressed
data buffered from previous partial reads.
([#3636](https://github.com/urllib3/urllib3/issues/3636)
<https://github.com/urllib3/urllib3/issues/3636>__)HTTPResponse.read() could cache only
part of the
response after a partial read when cache_content=True.Commits
9a950b9
Release 2.7.05ec0de4
Merge commit from fork2bdcc44
Merge commit from forkf45b0df
Fix a misleading example for ProxyManager (#4970)577193c
Switch to nightly PyPy3.11 in CI for now (#4984)e90af45
Avoid infinite loop in HTTPResponse.read_chunked when
amt=0 (#4974)67ed74f
Bump dev dependencies (#4972)3abd481
Upgrade mypy to version 1.20.2 (#4978)2b8725d
Drop support for EOL PyPy3.10 (#4979)2944b2a
Upgrade setup-chrome and setup-firefox to fix
warnings (#4973)
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show Changelog
* **SECURITY ISSUE**: Fixed an issue where non-contiguous buffers could
be
passed to APIs that accept Python buffers, which could lead to buffer
overflow. **CVE-2026-39892**
* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL
3.5.6.
.. _v46-0-5:
{rel_links}
')) if desc := entry['description']: content.append(_rst_lines(desc)) + if tags := entry['tags']: + content.append(_tags_rst(tags, tag_descriptions)) return _rst_table_indent('\n'.join(content)) @@ -283,15 +374,18 @@ def _rel_links(entry: _InterfaceLibEntry) -> str: if not (name := entry['rel_name']): return '' if not (main_url := entry['rel_url_charmhub']): - return _html_no_spellcheck_span(name) + return f'{name}' main_link = _html_link(name, main_url) if not (schema_url := entry['rel_url_schema']): - return main_link + return f'{main_link}' schema_link = _html_link('schema', schema_url) - return f'{main_link} ({schema_link})' + return f'{main_link} ({schema_link})' -def _general_description(entry: _GeneralLibEntry) -> str: +def _general_description( + entry: _GeneralLibEntry, + tag_descriptions: dict[str, str], +) -> str: substrates = ('machine', 'K8s') sortkeys = [ *('0' if entry[s] else '1' for s in substrates), @@ -300,13 +394,28 @@ def _general_description(entry: _GeneralLibEntry) -> str: str(_KIND_SORTKEYS[entry['kind']]), ] content = [_rst_raw_html(_html_hidden_span(''.join(sortkeys)))] - if firstline := ' '.join(_EMOJIS.get(s, '') + s for s in substrates if entry[s]): - content.append(_rst_lines(firstline)) if desc := entry['description']: content.append(_rst_lines(desc)) + substrate_parts = [ + _html_tag_tooltip(f'{_EMOJIS.get(s, "")}{s}', _SUBSTRATE_TOOLTIPS.get(s)) + for s in substrates + if entry[s] + ] + tag_parts = [_html_tag_tooltip(f'#{t}', tag_descriptions.get(t)) for t in entry['tags']] + if substrate_parts or tag_parts: + joined = ' '.join(substrate_parts + tag_parts) + content.append(_rst_raw_html(f'{joined}')) return _rst_table_indent('\n'.join(content)) +def _tags_rst(tags: list[str], tag_descriptions: dict[str, str]) -> str: + """Return RST raw HTML for a tags line, or empty string if no tags.""" + assert tags + tag_htmls = [_html_tag_tooltip(f'#{t}', tag_descriptions.get(t)) for t in tags] + joined = ' '.join(tag_htmls) + return _rst_raw_html(f'{joined}') + + ####### # rst # ####### @@ -369,7 +478,11 @@ def _html_link(text: str, url: str) -> str: return f'{text}' -def _html_no_spellcheck_span(text: object) -> str: - e = ElementTree.Element('span', attrib={'class': 'no-spellcheck'}) - e.text = str(text) +def _html_tag_tooltip(tag_text: str, tooltip: str | None) -> str: + e = ElementTree.Element('a', attrib={'class': 'tag-div no-spellcheck', 'href': '#'}) + e.text = tag_text + if tooltip is not None: + child = ElementTree.Element('span', attrib={'class': 'tag-tooltip'}) + child.text = tooltip + e.append(child) return ElementTree.tostring(e, encoding='unicode') diff --git a/.docs/extensions/test_generate_tables.py b/.docs/extensions/test_generate_tables.py index 730cee056..acc9ae7eb 100644 --- a/.docs/extensions/test_generate_tables.py +++ b/.docs/extensions/test_generate_tables.py @@ -40,6 +40,65 @@ def test_status_key_table(): assert table is not None +def test_tags_key_table_with_tags(): + entries: list[generate_tables._LibEntry] = [ + { + 'name': 'lib1', + 'status': '', + 'url': '', + 'docs': '', + 'src': '', + 'kind': '', + 'description': '', + 'tags': ['security', 'data'], + }, + { + 'name': 'lib2', + 'status': '', + 'url': '', + 'docs': '', + 'src': '', + 'kind': '', + 'description': '', + 'tags': ['security'], + }, + ] + tag_descriptions = {'security': 'Security stuff', 'data': 'Data stuff'} + rst = generate_tables._get_tags_key_table_dropdown( + entries, tag_descriptions, column='description' + ) + assert rst # non-empty + dropdown_contents = '\n'.join(rst.split('\n')[1:]) + html_content = rst_to_html(dropdown_contents) + table = ElementTree.fromstring(html.unescape(html_content)).find('.//table') + assert table is not None + rows = table.findall('.//tr') + # header + 2 unique tags (data, security) + assert len(rows) == 3 + # Tags in the key should be clickable elements + tag_links = table.findall('.//a[@class="tag-div no-spellcheck"]') + assert len(tag_links) == 2 + tag_texts = sorted(link.text or '' for link in tag_links) + assert tag_texts == ['#data', '#security'] + + +def test_tags_key_table_no_tags(): + entries: list[generate_tables._LibEntry] = [ + { + 'name': 'lib1', + 'status': '', + 'url': '', + 'docs': '', + 'src': '', + 'kind': '', + 'description': '', + 'tags': [], + }, + ] + rst = generate_tables._get_tags_key_table_dropdown(entries, {}, column='description') + assert rst == '' + + @pytest.mark.parametrize('rows', ([('r1c1', 'r1c2'), ('r2c1', 'r2c2')],)) def test_rst_rows(rows: list[tuple[str, ...]]): rst = generate_tables._rst_rows(rows) @@ -80,3 +139,24 @@ def test_html_link(text: str, url: str): a = ElementTree.fromstring(html_content) assert a.attrib['href'] == url assert a.text == text + + +@pytest.mark.parametrize( + ('tag_text', 'tooltip'), + [('#security', 'TLS certificate management'), ('#data', None)], +) +def test_html_tag_tooltip(tag_text: str, tooltip: str | None): + html_content = generate_tables._html_tag_tooltip(tag_text, tooltip) + a = ElementTree.fromstring(html_content) + assert a.tag == 'a' + assert a.attrib['href'] == '#' + assert 'tag-div' in a.attrib['class'] + assert 'no-spellcheck' in a.attrib['class'] + assert a.text == tag_text + children = list(a) + if tooltip is not None: + assert len(children) == 1 + assert 'tag-tooltip' in children[0].attrib['class'] + assert children[0].text == tooltip + else: + assert len(children) == 0 diff --git a/.docs/how-to/migrate.md b/.docs/how-to/migrate.md index 3df154cdb..cdd2dfb43 100644 --- a/.docs/how-to/migrate.md +++ b/.docs/how-to/migrate.md @@ -279,6 +279,17 @@ docs └── tutorial.{md,rst} ``` +## Update the library metadata + +The {ref}`interface library listingbefore
after
'
+ result = ids._resolve_images(raw, cooked)
+ assert result == ''
+
+
+def test_resolve_images_no_images_is_noop():
+ raw = 'plain text with no images'
+ assert ids._resolve_images(raw, '') == raw
+
+
+# --- main ---
+
+
+def test_main_writes_resolved_markdown(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch):
+ out = tmp_path / 'docs' / 'explanation' / 'page.md'
+
+ def fake_fetch(topic_id: int):
+ assert topic_id == 15539
+ return {
+ 'post_stream': {
+ 'posts': [
+ {
+ 'raw': '# Title\n\n',
+ 'cooked': '
',
+ },
+ ],
+ },
+ }
+
+ monkeypatch.setattr(ids, '_fetch', fake_fetch)
+ monkeypatch.setattr(
+ 'sys.argv',
+ ['import_discourse_docs.py', 'https://discourse.charmhub.io/t/slug/15539', str(out)],
+ )
+
+ ids.main()
+
+ assert out.read_text() == '# Title\n\n'
diff --git a/.template/{{ cookiecutter.project_slug }}/docs/explanation/a-{{ cookiecutter.__dist_pkg }}-explanation.md b/.template/{{ cookiecutter.project_slug }}/docs/explanation/a-{{ cookiecutter.__dist_pkg }}-explanation.md
new file mode 100644
index 000000000..f67af7a2d
--- /dev/null
+++ b/.template/{{ cookiecutter.project_slug }}/docs/explanation/a-{{ cookiecutter.__dist_pkg }}-explanation.md
@@ -0,0 +1,17 @@
+---
+myst:
+ html_meta:
+ description: ...
+---
+
+# {{ cookiecutter.__import_pkg }} explanation
+
+Explanations provide background, context, details, and design rationale. They help users understand why and how the library works the way it does, rather than how to use it.
+
+Good explanation topics include: design philosophy, how the library relates to the broader Juju ecosystem, and complicated implementation details that are important for advanced users.
+
+If the design is straightforward enough to cover in the package docstring, prefer that over a separate explanation page.
+
+Delete this file (and the `explanation/` directory) if no explanations are needed.
+
+See also: {ref}`how-to-add-library-docs`
diff --git a/.template/{{ cookiecutter.project_slug }}/docs/how-to/a-{{ cookiecutter.__dist_pkg }}-how-to.md b/.template/{{ cookiecutter.project_slug }}/docs/how-to/a-{{ cookiecutter.__dist_pkg }}-how-to.md
new file mode 100644
index 000000000..90d4f4b09
--- /dev/null
+++ b/.template/{{ cookiecutter.project_slug }}/docs/how-to/a-{{ cookiecutter.__dist_pkg }}-how-to.md
@@ -0,0 +1,17 @@
+---
+myst:
+ html_meta:
+ description: ...
+---
+
+# How to {{ cookiecutter.__import_pkg }}
+
+How-to guides are goal-oriented: each one answers "how do I do X?" for a user who already understands the basics. They are not tutorials (no narrative walkthrough) and not explanations (no background theory).
+
+Common how-to topics include: configuring advanced options, migrating from a previous version, integrating with another library, or handling a specific edge case.
+
+If the procedure is short enough to fit in a docstring, prefer that over a separate how-to page.
+
+Delete this file (and the `how-to/` directory) if no how-to guides are needed.
+
+See also: {ref}`how-to-add-library-docs`
diff --git a/.template/{{ cookiecutter.project_slug }}/docs/tutorial.md b/.template/{{ cookiecutter.project_slug }}/docs/tutorial.md
new file mode 100644
index 000000000..0b6bfb52b
--- /dev/null
+++ b/.template/{{ cookiecutter.project_slug }}/docs/tutorial.md
@@ -0,0 +1,15 @@
+---
+myst:
+ html_meta:
+ description: ...
+---
+
+# {{ cookiecutter.__import_pkg }}
+
+Quick-start information (installation, basic usage, a short example) belongs in the package docstring in `__init__.py` — that's where most users look first, and it appears in the generated API reference.
+
+Only add a tutorial if the library needs a longer, step-by-step walkthrough that would be too large for the docstring. For example, if your library requires deployment of multiple charms in a complicated configuration.
+
+Delete this file if a tutorial is not needed for your library.
+
+See also: {ref}`how-to-add-library-docs`
diff --git a/AGENTS.md b/AGENTS.md
index 684cc9926..6654b5080 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -230,6 +230,15 @@ Read more: {ref}`how-to-customize-integration-tests`
Read more: {ref}`charm-libs-charmhub-hosted`, {ref}`Charmcraft | Manage libraries