summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2023-11-02 12:57:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2023-11-02 12:57:40 +0000
commit2713d25cd1c29d62b136b43e37969d1c1a33a0d1 (patch)
tree3278e3ac8b8e916a3b729d9c68efd5fdcace4c7c
parentReleasing progress-linux version 3.0.37-1~progress7+u1. (diff)
downloadprompt-toolkit-2713d25cd1c29d62b136b43e37969d1c1a33a0d1.tar.xz
prompt-toolkit-2713d25cd1c29d62b136b43e37969d1c1a33a0d1.zip
Merging upstream version 3.0.38.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--CHANGELOG11
-rw-r--r--docs/conf.py4
-rw-r--r--src/prompt_toolkit/__init__.py2
-rw-r--r--src/prompt_toolkit/filters/base.py81
-rw-r--r--src/prompt_toolkit/output/win32.py5
-rw-r--r--src/prompt_toolkit/output/windows10.py21
-rw-r--r--tests/test_filter.py71
7 files changed, 168 insertions, 27 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 9431417..f3794a8 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,17 @@
CHANGELOG
=========
+3.0.38: 2023-02-28
+------------------
+
+Fixes:
+- Fix regression in filters. (Use of `WeakValueDictionary` caused filters to
+ not be cached).
+
+New features:
+- Use 24-bit true color now by default on Windows 10/11.
+
+
3.0.37: 2023-02-21
------------------
diff --git a/docs/conf.py b/docs/conf.py
index 42fe2ec..08b3221 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -50,9 +50,9 @@ copyright = "2014-2020, Jonathan Slenders"
# built documents.
#
# The short X.Y version.
-version = "3.0.37"
+version = "3.0.38"
# The full version, including alpha/beta/rc tags.
-release = "3.0.37"
+release = "3.0.38"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/src/prompt_toolkit/__init__.py b/src/prompt_toolkit/__init__.py
index 4f67103..bbe820b 100644
--- a/src/prompt_toolkit/__init__.py
+++ b/src/prompt_toolkit/__init__.py
@@ -27,7 +27,7 @@ from .formatted_text import ANSI, HTML
from .shortcuts import PromptSession, print_formatted_text, prompt
# Don't forget to update in `docs/conf.py`!
-__version__ = "3.0.37"
+__version__ = "3.0.38"
assert pep440.match(__version__)
diff --git a/src/prompt_toolkit/filters/base.py b/src/prompt_toolkit/filters/base.py
index 7acefe7..bb48a3f 100644
--- a/src/prompt_toolkit/filters/base.py
+++ b/src/prompt_toolkit/filters/base.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import weakref
from abc import ABCMeta, abstractmethod
from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union
@@ -16,12 +15,8 @@ class Filter(metaclass=ABCMeta):
"""
def __init__(self) -> None:
- self._and_cache: weakref.WeakValueDictionary[
- Filter, _AndList
- ] = weakref.WeakValueDictionary()
- self._or_cache: weakref.WeakValueDictionary[
- Filter, _OrList
- ] = weakref.WeakValueDictionary()
+ self._and_cache: dict[Filter, Filter] = {}
+ self._or_cache: dict[Filter, Filter] = {}
self._invert_result: Filter | None = None
@abstractmethod
@@ -45,7 +40,7 @@ class Filter(metaclass=ABCMeta):
if other in self._and_cache:
return self._and_cache[other]
- result = _AndList([self, other])
+ result = _AndList.create([self, other])
self._and_cache[other] = result
return result
@@ -63,7 +58,7 @@ class Filter(metaclass=ABCMeta):
if other in self._or_cache:
return self._or_cache[other]
- result = _OrList([self, other])
+ result = _OrList.create([self, other])
self._or_cache[other] = result
return result
@@ -91,20 +86,49 @@ class Filter(metaclass=ABCMeta):
)
+def _remove_duplicates(filters: list[Filter]) -> list[Filter]:
+ result = []
+ for f in filters:
+ if f not in result:
+ result.append(f)
+ return result
+
+
class _AndList(Filter):
"""
Result of &-operation between several filters.
"""
- def __init__(self, filters: Iterable[Filter]) -> None:
+ def __init__(self, filters: list[Filter]) -> None:
super().__init__()
- self.filters: list[Filter] = []
+ self.filters = filters
+
+ @classmethod
+ def create(cls, filters: Iterable[Filter]) -> Filter:
+ """
+ Create a new filter by applying an `&` operator between them.
+
+ If there's only one unique filter in the given iterable, it will return
+ that one filter instead of an `_AndList`.
+ """
+ filters_2: list[Filter] = []
for f in filters:
if isinstance(f, _AndList): # Turn nested _AndLists into one.
- self.filters.extend(f.filters)
+ filters_2.extend(f.filters)
else:
- self.filters.append(f)
+ filters_2.append(f)
+
+ # Remove duplicates. This could speed up execution, and doesn't make a
+ # difference for the evaluation.
+ filters = _remove_duplicates(filters_2)
+
+ # If only one filter is left, return that without wrapping into an
+ # `_AndList`.
+ if len(filters) == 1:
+ return filters[0]
+
+ return cls(filters)
def __call__(self) -> bool:
return all(f() for f in self.filters)
@@ -118,15 +142,36 @@ class _OrList(Filter):
Result of |-operation between several filters.
"""
- def __init__(self, filters: Iterable[Filter]) -> None:
+ def __init__(self, filters: list[Filter]) -> None:
super().__init__()
- self.filters: list[Filter] = []
+ self.filters = filters
+
+ @classmethod
+ def create(cls, filters: Iterable[Filter]) -> Filter:
+ """
+ Create a new filter by applying an `|` operator between them.
+
+ If there's only one unique filter in the given iterable, it will return
+ that one filter instead of an `_OrList`.
+ """
+ filters_2: list[Filter] = []
for f in filters:
- if isinstance(f, _OrList): # Turn nested _OrLists into one.
- self.filters.extend(f.filters)
+ if isinstance(f, _OrList): # Turn nested _AndLists into one.
+ filters_2.extend(f.filters)
else:
- self.filters.append(f)
+ filters_2.append(f)
+
+ # Remove duplicates. This could speed up execution, and doesn't make a
+ # difference for the evaluation.
+ filters = _remove_duplicates(filters_2)
+
+ # If only one filter is left, return that without wrapping into an
+ # `_AndList`.
+ if len(filters) == 1:
+ return filters[0]
+
+ return cls(filters)
def __call__(self) -> bool:
return any(f() for f in self.filters)
diff --git a/src/prompt_toolkit/output/win32.py b/src/prompt_toolkit/output/win32.py
index 191a05d..297df6e 100644
--- a/src/prompt_toolkit/output/win32.py
+++ b/src/prompt_toolkit/output/win32.py
@@ -525,11 +525,6 @@ class Win32Output(Output):
if self.default_color_depth is not None:
return self.default_color_depth
- # For now, by default, always use 4 bit color on Windows 10 by default,
- # even when vt100 escape sequences with
- # ENABLE_VIRTUAL_TERMINAL_PROCESSING are supported. We don't have a
- # reliable way yet to know whether our console supports true color or
- # only 4-bit.
return ColorDepth.DEPTH_4_BIT
diff --git a/src/prompt_toolkit/output/windows10.py b/src/prompt_toolkit/output/windows10.py
index 5546ea0..6da2dc5 100644
--- a/src/prompt_toolkit/output/windows10.py
+++ b/src/prompt_toolkit/output/windows10.py
@@ -33,6 +33,7 @@ class Windows10_Output:
def __init__(
self, stdout: TextIO, default_color_depth: ColorDepth | None = None
) -> None:
+ self.default_color_depth = default_color_depth
self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth)
self.vt100_output = Vt100_Output(
stdout, lambda: Size(0, 0), default_color_depth=default_color_depth
@@ -74,12 +75,30 @@ class Windows10_Output:
"get_win32_screen_buffer_info",
"enable_bracketed_paste",
"disable_bracketed_paste",
- "get_default_color_depth",
):
return getattr(self.win32_output, name)
else:
return getattr(self.vt100_output, name)
+ def get_default_color_depth(self) -> ColorDepth:
+ """
+ Return the default color depth for a windows terminal.
+
+ Contrary to the Vt100 implementation, this doesn't depend on a $TERM
+ variable.
+ """
+ if self.default_color_depth is not None:
+ return self.default_color_depth
+
+ # Previously, we used `DEPTH_4_BIT`, even on Windows 10. This was
+ # because true color support was added after "Console Virtual Terminal
+ # Sequences" support was added, and there was no good way to detect
+ # what support was given.
+ # 24bit color support was added in 2016, so let's assume it's safe to
+ # take that as a default:
+ # https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/
+ return ColorDepth.TRUE_COLOR
+
Output.register(Windows10_Output)
diff --git a/tests/test_filter.py b/tests/test_filter.py
index 0d4ad13..9f13396 100644
--- a/tests/test_filter.py
+++ b/tests/test_filter.py
@@ -1,8 +1,11 @@
from __future__ import annotations
+import gc
+
import pytest
from prompt_toolkit.filters import Always, Condition, Filter, Never, to_filter
+from prompt_toolkit.filters.base import _AndList, _OrList
def test_never():
@@ -43,6 +46,32 @@ def test_and():
assert c3() == (a and b)
+def test_nested_and():
+ for a in (True, False):
+ for b in (True, False):
+ for c in (True, False):
+ c1 = Condition(lambda: a)
+ c2 = Condition(lambda: b)
+ c3 = Condition(lambda: c)
+ c4 = (c1 & c2) & c3
+
+ assert isinstance(c4, Filter)
+ assert c4() == (a and b and c)
+
+
+def test_nested_or():
+ for a in (True, False):
+ for b in (True, False):
+ for c in (True, False):
+ c1 = Condition(lambda: a)
+ c2 = Condition(lambda: b)
+ c3 = Condition(lambda: c)
+ c4 = (c1 | c2) | c3
+
+ assert isinstance(c4, Filter)
+ assert c4() == (a or b or c)
+
+
def test_to_filter():
f1 = to_filter(True)
f2 = to_filter(False)
@@ -60,3 +89,45 @@ def test_to_filter():
with pytest.raises(TypeError):
to_filter(4)
+
+
+def test_filter_cache_regression_1():
+ # See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1729
+
+ cond = Condition(lambda: True)
+
+ # The use of a `WeakValueDictionary` caused this following expression to
+ # fail. The problem is that the nested `(a & a)` expression gets garbage
+ # collected between the two statements and is removed from our cache.
+ x = (cond & cond) & cond
+ y = (cond & cond) & cond
+ assert x == y
+
+
+def test_filter_cache_regression_2():
+ cond1 = Condition(lambda: True)
+ cond2 = Condition(lambda: True)
+ cond3 = Condition(lambda: True)
+
+ x = (cond1 & cond2) & cond3
+ y = (cond1 & cond2) & cond3
+ assert x == y
+
+
+def test_filter_remove_duplicates():
+ cond1 = Condition(lambda: True)
+ cond2 = Condition(lambda: True)
+
+ # When a condition is appended to itself using an `&` or `|` operator, it
+ # should not be present twice. Having it twice in the `_AndList` or
+ # `_OrList` will make them more expensive to evaluate.
+
+ assert isinstance(cond1 & cond1, Condition)
+ assert isinstance(cond1 & cond1 & cond1, Condition)
+ assert isinstance(cond1 & cond1 & cond2, _AndList)
+ assert len((cond1 & cond1 & cond2).filters) == 2
+
+ assert isinstance(cond1 | cond1, Condition)
+ assert isinstance(cond1 | cond1 | cond1, Condition)
+ assert isinstance(cond1 | cond1 | cond2, _OrList)
+ assert len((cond1 | cond1 | cond2).filters) == 2