Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Doc/library/cmdlinelibs.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. _cmdlinelibs:

********************************
Command Line Interface Libraries
Command-line interface libraries
********************************

The modules described in this chapter assist with implementing
Expand All @@ -19,3 +19,4 @@ Here's an overview:
curses.rst
curses.ascii.rst
curses.panel.rst
cmd.rst
19 changes: 7 additions & 12 deletions Doc/library/frameworks.rst
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
:orphan:

.. _frameworks:

******************
Program Frameworks
Program frameworks
******************

The modules described in this chapter are frameworks that will largely dictate
the structure of your program. Currently the modules described here are all
oriented toward writing command-line interfaces.

The full list of modules described in this chapter is:


.. toctree::
This chapter is no longer maintained, and the modules it contained have been moved to their respective topical documentation.

turtle.rst
cmd.rst
shlex.rst
* :mod:`cmd` — :doc:`Command Line Interface Libraries <./cmdlinelibs>`
* :mod:`shlex` — :doc:`Unix Specific Services <./unix>`
* :mod:`turtle` — :doc:`Graphical User Interfaces with Tk <./tk>`
11 changes: 10 additions & 1 deletion Doc/library/http.cookies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ The character set, :data:`string.ascii_letters`, :data:`string.digits` and
in a cookie name (as :attr:`~Morsel.key`).

.. versionchanged:: 3.3
Allowed ':' as a valid cookie name character.
Allowed '``:``' as a valid cookie name character.

.. versionchanged:: next
Allowed '``"``' as a valid cookie value character.

.. note::

Expand Down Expand Up @@ -314,3 +316,10 @@ The following example demonstrates how to use the :mod:`http.cookies` module.
>>> print(C)
Set-Cookie: number=7
Set-Cookie: string=seven
>>> import json
>>> C = cookies.SimpleCookie()
>>> C.load(f'cookies=7; mixins="{json.dumps({"chips": "dark chocolate"})}"; state=gooey')
>>> print(C)
Set-Cookie: cookies=7
Set-Cookie: mixins="{"chips": "dark chocolate"}"
Set-Cookie: state=gooey
1 change: 0 additions & 1 deletion Doc/library/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ the `Python Package Index <https://pypi.org>`_.
internet.rst
mm.rst
i18n.rst
frameworks.rst
tk.rst
development.rst
debug.rst
Expand Down
3 changes: 2 additions & 1 deletion Doc/library/tk.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. _tkinter:

*********************************
Graphical User Interfaces with Tk
Graphical user interfaces with Tk
*********************************

.. index::
Expand Down Expand Up @@ -39,6 +39,7 @@ alternative `GUI frameworks and tools <https://wiki.python.org/moin/GuiProgrammi
tkinter.dnd.rst
tkinter.ttk.rst
idle.rst
turtle.rst

.. Other sections I have in mind are
Tkinter internals
Expand Down
3 changes: 2 additions & 1 deletion Doc/library/unix.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. _unix:

**********************
Unix Specific Services
Unix-specific services
**********************

The modules described in this chapter provide interfaces to features that are
Expand All @@ -11,6 +11,7 @@ of it. Here's an overview:

.. toctree::

shlex.rst
posix.rst
pwd.rst
grp.rst
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ http.client
(Contributed by Alexander Enrique Urieles Nieto in :gh:`131724`.)


http.cookies
------------

* Allow '``"``' double quotes in cookie values.
(Contributed by Nick Burns and Senthil Kumaran in :gh:`92936`.)


math
----

Expand Down
38 changes: 14 additions & 24 deletions Lib/importlib/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import zipfile
import operator
import textwrap
import warnings
import functools
import itertools
import posixpath
Expand All @@ -21,7 +20,7 @@
from . import _meta
from ._collections import FreezableDefaultDict, Pair
from ._functools import method_cache, pass_none
from ._itertools import always_iterable, unique_everseen
from ._itertools import always_iterable, bucket, unique_everseen
from ._meta import PackageMetadata, SimplePath

from contextlib import suppress
Expand All @@ -35,6 +34,7 @@
'DistributionFinder',
'PackageMetadata',
'PackageNotFoundError',
'SimplePath',
'distribution',
'distributions',
'entry_points',
Expand Down Expand Up @@ -329,27 +329,7 @@ def __repr__(self) -> str:
return f'<FileHash mode: {self.mode} value: {self.value}>'


class DeprecatedNonAbstract:
# Required until Python 3.14
def __new__(cls, *args, **kwargs):
all_names = {
name for subclass in inspect.getmro(cls) for name in vars(subclass)
}
abstract = {
name
for name in all_names
if getattr(getattr(cls, name), '__isabstractmethod__', False)
}
if abstract:
warnings.warn(
f"Unimplemented abstract methods {abstract}",
DeprecationWarning,
stacklevel=2,
)
return super().__new__(cls)


class Distribution(DeprecatedNonAbstract):
class Distribution(metaclass=abc.ABCMeta):
"""
An abstract Python distribution package.

Expand Down Expand Up @@ -404,7 +384,7 @@ def from_name(cls, name: str) -> Distribution:
if not name:
raise ValueError("A distribution name is required.")
try:
return next(iter(cls.discover(name=name)))
return next(iter(cls._prefer_valid(cls.discover(name=name))))
except StopIteration:
raise PackageNotFoundError(name)

Expand All @@ -428,6 +408,16 @@ def discover(
resolver(context) for resolver in cls._discover_resolvers()
)

@staticmethod
def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]:
"""
Prefer (move to the front) distributions that have metadata.

Ref python/importlib_resources#489.
"""
buckets = bucket(dists, lambda dist: bool(dist.metadata))
return itertools.chain(buckets[True], buckets[False])

@staticmethod
def at(path: str | os.PathLike[str]) -> Distribution:
"""Return a Distribution for the indicated metadata path.
Expand Down
22 changes: 8 additions & 14 deletions Lib/importlib/metadata/_adapters.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import functools
import warnings
import re
import textwrap
import email.message

from ._text import FoldedCase


# Do not remove prior to 2024-01-01 or Python 3.14
_warn = functools.partial(
warnings.warn,
"Implicit None on return values is deprecated and will raise KeyErrors.",
DeprecationWarning,
stacklevel=2,
)


class Message(email.message.Message):
multiple_use_keys = set(
map(
Expand Down Expand Up @@ -52,12 +41,17 @@ def __iter__(self):

def __getitem__(self, item):
"""
Warn users that a ``KeyError`` can be expected when a
missing key is supplied. Ref python/importlib_metadata#371.
Override parent behavior to typical dict behavior.

``email.message.Message`` will emit None values for missing
keys. Typical mappings, including this ``Message``, will raise
a key error for missing keys.

Ref python/importlib_metadata#371.
"""
res = super().__getitem__(item)
if res is None:
_warn()
raise KeyError(item)
return res

def _repair_headers(self):
Expand Down
98 changes: 98 additions & 0 deletions Lib/importlib/metadata/_itertools.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import defaultdict, deque
from itertools import filterfalse


Expand Down Expand Up @@ -71,3 +72,100 @@ def always_iterable(obj, base_type=(str, bytes)):
return iter(obj)
except TypeError:
return iter((obj,))


# Copied from more_itertools 10.3
class bucket:
"""Wrap *iterable* and return an object that buckets the iterable into
child iterables based on a *key* function.

>>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3']
>>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character
>>> sorted(list(s)) # Get the keys
['a', 'b', 'c']
>>> a_iterable = s['a']
>>> next(a_iterable)
'a1'
>>> next(a_iterable)
'a2'
>>> list(s['b'])
['b1', 'b2', 'b3']

The original iterable will be advanced and its items will be cached until
they are used by the child iterables. This may require significant storage.

By default, attempting to select a bucket to which no items belong will
exhaust the iterable and cache all values.
If you specify a *validator* function, selected buckets will instead be
checked against it.

>>> from itertools import count
>>> it = count(1, 2) # Infinite sequence of odd numbers
>>> key = lambda x: x % 10 # Bucket by last digit
>>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only
>>> s = bucket(it, key=key, validator=validator)
>>> 2 in s
False
>>> list(s[2])
[]

"""

def __init__(self, iterable, key, validator=None):
self._it = iter(iterable)
self._key = key
self._cache = defaultdict(deque)
self._validator = validator or (lambda x: True)

def __contains__(self, value):
if not self._validator(value):
return False

try:
item = next(self[value])
except StopIteration:
return False
else:
self._cache[value].appendleft(item)

return True

def _get_values(self, value):
"""
Helper to yield items from the parent iterator that match *value*.
Items that don't match are stored in the local cache as they
are encountered.
"""
while True:
# If we've cached some items that match the target value, emit
# the first one and evict it from the cache.
if self._cache[value]:
yield self._cache[value].popleft()
# Otherwise we need to advance the parent iterator to search for
# a matching item, caching the rest.
else:
while True:
try:
item = next(self._it)
except StopIteration:
return
item_value = self._key(item)
if item_value == value:
yield item
break
elif self._validator(item_value):
self._cache[item_value].append(item)

def __iter__(self):
for item in self._it:
item_value = self._key(item)
if self._validator(item_value):
self._cache[item_value].append(item)

yield from self._cache.keys()

def __getitem__(self, value):
if not self._validator(value):
return iter(())

return self._get_values(value)
17 changes: 4 additions & 13 deletions Lib/test/test_importlib/metadata/test_api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import re
import textwrap
import unittest
import warnings
import importlib
import contextlib

from . import fixtures
from importlib.metadata import (
Expand All @@ -18,13 +16,6 @@
)


@contextlib.contextmanager
def suppress_known_deprecation():
with warnings.catch_warnings(record=True) as ctx:
warnings.simplefilter('default', category=DeprecationWarning)
yield ctx


class APITests(
fixtures.EggInfoPkg,
fixtures.EggInfoPkgPipInstalledNoToplevel,
Expand Down Expand Up @@ -153,13 +144,13 @@ def test_metadata_for_this_package(self):
classifiers = md.get_all('Classifier')
assert 'Topic :: Software Development :: Libraries' in classifiers

def test_missing_key_legacy(self):
def test_missing_key(self):
"""
Requesting a missing key will still return None, but warn.
Requesting a missing key raises KeyError.
"""
md = metadata('distinfo-pkg')
with suppress_known_deprecation():
assert md['does-not-exist'] is None
with self.assertRaises(KeyError):
md['does-not-exist']

def test_get_key(self):
"""
Expand Down
Loading
Loading