summaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
authorYDBot <[email protected]>2026-07-09 05:45:41 +0000
committerYDBot <[email protected]>2026-07-09 05:45:41 +0000
commitd2951c21babd78dc764a82ca618eed0bc147f091 (patch)
tree0d00b4992bae4b4f8dc1cd6d33a51f61c0dc851b /contrib/python
parentc925881bc6882d7a243a5cd51d108cd47ad6245c (diff)
parent4e76e5312e7d44e930036f0e0184d0ee21067432 (diff)
Merge pull request #45978 from ydb-platform/merge-rightlib-260709-0106
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/cryptography/next/rust/.yandex_meta/build.ym75
-rw-r--r--contrib/python/cryptography/next/rust/msan.cpp23
-rw-r--r--contrib/python/cryptography/next/rust/patches/msan.cpp23
-rw-r--r--contrib/python/cryptography/next/rust/ya.make7
-rw-r--r--contrib/python/deepmerge/py3/.dist-info/METADATA23
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/_version.py30
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/extended_set.py19
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py11
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py1
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py20
-rw-r--r--contrib/python/deepmerge/py3/deepmerge/tests/test_full.py37
-rw-r--r--contrib/python/deepmerge/py3/ya.make2
-rw-r--r--contrib/python/hypothesis/py3/.dist-info/METADATA2
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/core.py14
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/version.py2
-rw-r--r--contrib/python/hypothesis/py3/ya.make2
-rw-r--r--contrib/python/prettytable/py3/.dist-info/METADATA123
-rw-r--r--contrib/python/prettytable/py3/README.md116
-rw-r--r--contrib/python/prettytable/py3/patches/01-fix-tests.patch6
-rw-r--r--contrib/python/prettytable/py3/prettytable/__init__.py7
-rw-r--r--contrib/python/prettytable/py3/prettytable/_version.py26
-rw-r--r--contrib/python/prettytable/py3/prettytable/colortable.py35
-rw-r--r--contrib/python/prettytable/py3/prettytable/prettytable.py792
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_align_center.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_align_left.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_align_right.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_ansi_colors.txt7
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_cjk.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_combining_chars.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_complex_emoji.txt7
-rw-r--r--contrib/python/prettytable/py3/tests/test_colortable.py4
-rw-r--r--contrib/python/prettytable/py3/tests/test_html.py138
-rw-r--r--contrib/python/prettytable/py3/tests/test_json.py10
-rw-r--r--contrib/python/prettytable/py3/tests/test_mediawiki.py10
-rw-r--r--contrib/python/prettytable/py3/tests/test_prettytable.py578
-rw-r--r--contrib/python/prettytable/py3/tests/test_rst.py97
-rw-r--r--contrib/python/prettytable/py3/tests/test_sorting.py59
-rw-r--r--contrib/python/prettytable/py3/tests/test_style.py223
-rw-r--r--contrib/python/prettytable/py3/ya.make2
-rw-r--r--contrib/python/scramp/.dist-info/METADATA11
-rw-r--r--contrib/python/scramp/README.md9
-rw-r--r--contrib/python/scramp/scramp/__init__.py2
-rw-r--r--contrib/python/scramp/scramp/core.py71
-rw-r--r--contrib/python/scramp/scramp/exceptions.py8
-rw-r--r--contrib/python/scramp/scramp/utils.py8
-rw-r--r--contrib/python/scramp/ya.make3
46 files changed, 1856 insertions, 817 deletions
diff --git a/contrib/python/cryptography/next/rust/.yandex_meta/build.ym b/contrib/python/cryptography/next/rust/.yandex_meta/build.ym
index 1a9f1137767..9500b46cce9 100644
--- a/contrib/python/cryptography/next/rust/.yandex_meta/build.ym
+++ b/contrib/python/cryptography/next/rust/.yandex_meta/build.ym
@@ -30,6 +30,10 @@ _openssl
{% block current_version %}41.0.6{% endblock %}
{% block current_url %}https://github.com/pyca/cryptography/archive/refs/tags/{{self.version().strip()}}.tar.gz{% endblock %}
+{% block msan_cc_extra_flags %}
+-I{{arcadia_root}} -I{{arcadia_root}}/contrib/libs/python/Include -I{{arcadia_root}}/contrib/libs/openssl/include
+{% endblock %}
+
{% block patch_source %}
{{super()}}
sed -e 's|cdylib|staticlib|' -i Cargo.toml
@@ -52,33 +56,6 @@ export RUSTFLAGS="${RUSTFLAGS:-} -Cembed-bitcode=no"
rm -rf docs vectors tests
cd src/rust
{{super()}}
-find ${BUILD} -name '*.a' -type f | while read l; do
- echo "_rust_eh_personality {{self.mod_name().strip()}}__rust_eh_personality" >> prog
- llvm-objcopy --redefine-syms=prog ${l}
- rm prog
-done
-
-{% for p in self.msan_platforms().strip().split('\n') %}
-export CC_{{p.replace('-', '_')}}=target-cc-msan
-{% endfor %}
-export RUSTFLAGS="${RUSTFLAGS:-} -Zsanitizer=memory -Zsanitizer-memory-track-origins=2 -Ctarget-feature=-crt-static -Cunsafe-allow-abi-mismatch=sanitizer -Ccodegen-units=1"
-
-{% for arch in self.msan_platforms().strip().split('\n') %}
-LD_PRELOAD=${BIN}/getrandom.so cargo {{self.cargo_flags().strip()}} build {{self.cargo_build_flags().strip()}} --release --target {{arch}} {{self.binary_type().strip()}} --target-dir ${BUILD}-msan \
- {% if self.cargo_target() %}
---package {{self.cargo_target().strip()}}
- {% endif %}
-{% endfor %}
-
-find ${BUILD}-msan -name '*.a' -type f | while read l; do
- llvm-nm --extern-only --defined-only -A ${l} | sed -e 's|.* ||' | grep -v 'PyInit_' | sort | while read s; do
- echo "${s} {{self.mod_name().strip()}}_${s}" >> prog
- done
- echo "_rust_eh_personality {{self.mod_name().strip()}}__rust_eh_personality" >> prog
- llvm-objcopy --redefine-syms=prog ${l}
- rm prog
-done
-
rm -r .git
# generate _openssl.c
@@ -90,50 +67,13 @@ OUT_DIR=${SRC}/autogen_cffi ${BIN}/python3 _cffi_src/build_openssl.py
{% block move_to_output %}
{{super()}}
-pushd ${BUILD}-msan
-{% if self.executable().strip() %}
-
-{% for arch in self.msan_platforms().strip().split('\n') %}
- mkdir -p ${OUTPUT}/target/{{arch}}-msan/release
- cp {{arch}}/release/{{self.executable().strip()}} ${OUTPUT}/target/{{arch}}-msan/release
-{% endfor %}
-
-{% else %}
-(find . -type f -name '*.a'; find . -type f -name '*.lib') | sed -e 's|./||' | grep -v '/deps/' | while read l; do
- d=$(dirname ${l})
- d_msan=$(echo "${d}" | sed 's|^\([^/]*\)|\1-msan|')
- o=${OUTPUT}/a/${d_msan}
- mkdir -p ${o}
- cp ${l} ${o}/
-done
-{% endif %}
-popd
-
cp ${SRC}/autogen_cffi/_openssl.c ${OUTPUT}/
-cp ${OUTPUT}/patches/msan.cpp ${OUTPUT}/
cat {{arcadia_root}}/contrib/libs/openssl/redef.h | awk '/#define/{print $2" "$3}' > ${OUTPUT}/openssl_redefines.txt
cat {{arcadia_root}}/contrib/libs/openssl/redef.h | awk '/#define/{print "_"$2" _"$3}' >> ${OUTPUT}/openssl_redefines.txt
find ${OUTPUT} -name '*.a' | xargs -L1 llvm-objcopy --redefine-syms=${OUTPUT}/openssl_redefines.txt
rm ${OUTPUT}/openssl_redefines.txt
-
-find ${OUTPUT} -name '*.a' -path "*msan*" | xargs -L1 llvm-objcopy --redefine-sym=__msan_set_alloca_origin_with_descr=__safe_msan_set_alloca_origin_with_descr
-find ${OUTPUT} -name '*.a' -path "*msan*" | xargs -L1 llvm-objcopy --redefine-sym=malloc=__safe_malloc
-{% endblock %}
-
-{% block prepare_yamake %}
-{{super()}}
-
-sed -i '/IF (FALSE)/r'<(cat <<EOF
-{% for p in self.msan_platforms().strip().split('\n') %}
-ELSEIF ({{p | ya_platform}} AND SANITIZER_TYPE == "memory")
- COPY_FILE(a/{{p}}-msan/release/libcryptography_rust.a libcryptography_rust.a)
- SRCS(msan.cpp)
- CFLAGS(-fsanitize-memory-track-origins=2)
-{% endfor %}
-EOF
-) ya.make
{% endblock %}
{% block ya_make %}
@@ -177,12 +117,7 @@ cat << EOF > ${BIN}/target-cc-aarch
exec ${BIN}/target-cc -nostdinc -target aarch64-unknown-linux-gnu -I{{arcadia_root}}/contrib/libs/musl/include -I{{arcadia_root}}/contrib/libs/musl/arch/aarch64 -I{{arcadia_root}}/contrib/libs/musl/arch/generic "\${@}"
EOF
-cat << EOF > ${BIN}/target-cc-msan
-#!/usr/bin/env sh
-exec ${CLANG} -DUSE_PYTHON3 -fsanitize=memory -I{{arcadia_root}} -I{{arcadia_root}}/contrib/libs/python/Include -I{{arcadia_root}}/contrib/libs/openssl/include -x c "\${@}"
-EOF
-
-chmod +x ${BIN}/musl-gcc ${BIN}/target-cc ${BIN}/target-cc-aarch ${BIN}/target-cc-msan
+chmod +x ${BIN}/musl-gcc ${BIN}/target-cc ${BIN}/target-cc-aarch
# macOS
diff --git a/contrib/python/cryptography/next/rust/msan.cpp b/contrib/python/cryptography/next/rust/msan.cpp
deleted file mode 100644
index c653958f4b0..00000000000
--- a/contrib/python/cryptography/next/rust/msan.cpp
+++ /dev/null
@@ -1,23 +0,0 @@
-#include <stdint.h>
-#include <cstddef>
-
-typedef size_t uptr;
-typedef uint32_t u32;
-
-// defined in clang{XX}-rt/msan
-extern "C" void __msan_set_alloca_origin_with_descr(void *a, uptr size, u32 *id_ptr, char *descr);
-extern "C" void __msan_unpoison(void *a, uptr size);
-
-
-extern "C" void __safe_msan_set_alloca_origin_with_descr(void *a, uptr size, u32 *id_ptr, char *descr) {
- __msan_set_alloca_origin_with_descr(a, size, id_ptr, descr);
- __msan_unpoison(a, size);
-}
-
-
-extern "C" void* malloc(size_t size);
-extern "C" void *__safe_malloc(size_t size) {
- void* ret = malloc(size);
- __msan_unpoison(ret, size);
- return ret;
-}
diff --git a/contrib/python/cryptography/next/rust/patches/msan.cpp b/contrib/python/cryptography/next/rust/patches/msan.cpp
deleted file mode 100644
index c653958f4b0..00000000000
--- a/contrib/python/cryptography/next/rust/patches/msan.cpp
+++ /dev/null
@@ -1,23 +0,0 @@
-#include <stdint.h>
-#include <cstddef>
-
-typedef size_t uptr;
-typedef uint32_t u32;
-
-// defined in clang{XX}-rt/msan
-extern "C" void __msan_set_alloca_origin_with_descr(void *a, uptr size, u32 *id_ptr, char *descr);
-extern "C" void __msan_unpoison(void *a, uptr size);
-
-
-extern "C" void __safe_msan_set_alloca_origin_with_descr(void *a, uptr size, u32 *id_ptr, char *descr) {
- __msan_set_alloca_origin_with_descr(a, size, id_ptr, descr);
- __msan_unpoison(a, size);
-}
-
-
-extern "C" void* malloc(size_t size);
-extern "C" void *__safe_malloc(size_t size) {
- void* ret = malloc(size);
- __msan_unpoison(ret, size);
- return ret;
-}
diff --git a/contrib/python/cryptography/next/rust/ya.make b/contrib/python/cryptography/next/rust/ya.make
index 607916aaab1..4f3d4b559a8 100644
--- a/contrib/python/cryptography/next/rust/ya.make
+++ b/contrib/python/cryptography/next/rust/ya.make
@@ -16,11 +16,8 @@ ORIGINAL_SOURCE(https://github.com/pyca/cryptography/archive/refs/tags/41.0.6.ta
IF (FALSE)
ELSEIF (OS_LINUX AND ARCH_X86_64 AND NOT MUSL AND SANITIZER_TYPE == "memory")
COPY_FILE(a/x86_64-unknown-linux-gnu-msan/release/libcryptography_rust.a libcryptography_rust.a)
- SRCS(
- msan.cpp
- )
- CFLAGS(
- -fsanitize-memory-track-origins=2
+ PEERDIR(
+ contrib/restricted/rust_msan
)
ELSEIF (OS_LINUX AND ARCH_X86_64 AND NOT MUSL)
COPY_FILE(a/x86_64-unknown-linux-gnu/release/libcryptography_rust.a libcryptography_rust.a)
diff --git a/contrib/python/deepmerge/py3/.dist-info/METADATA b/contrib/python/deepmerge/py3/.dist-info/METADATA
index b5b331cb98a..627fadb4bb5 100644
--- a/contrib/python/deepmerge/py3/.dist-info/METADATA
+++ b/contrib/python/deepmerge/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: deepmerge
-Version: 2.0
+Version: 2.1.0
Summary: A toolset for deeply merging Python dictionaries.
Author-email: Yusuke Tsutsumi <[email protected]>
License: MIT Licence
@@ -19,15 +19,18 @@ Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-File: LICENSE
-Requires-Dist: typing-extensions ; python_version <= "3.9"
+Requires-Dist: typing_extensions; python_version <= "3.9"
Provides-Extra: dev
-Requires-Dist: validate-pyproject[all] ; extra == 'dev'
-Requires-Dist: pyupgrade ; extra == 'dev'
-Requires-Dist: black ; extra == 'dev'
-Requires-Dist: mypy ; extra == 'dev'
-Requires-Dist: pytest ; extra == 'dev'
-Requires-Dist: build ; extra == 'dev'
-Requires-Dist: twine ; extra == 'dev'
+Requires-Dist: validate-pyproject[all]; extra == "dev"
+Requires-Dist: pyupgrade; extra == "dev"
+Requires-Dist: black; extra == "dev"
+Requires-Dist: mypy; extra == "dev"
+Requires-Dist: pytest; extra == "dev"
+Requires-Dist: build; extra == "dev"
+Requires-Dist: twine; extra == "dev"
+Requires-Dist: sphinx; extra == "dev"
+Requires-Dist: sphinx-rtd-theme; extra == "dev"
+Dynamic: license-file
=========
deepmerge
diff --git a/contrib/python/deepmerge/py3/deepmerge/_version.py b/contrib/python/deepmerge/py3/deepmerge/_version.py
index 39e8a810ad2..514df6bc7bf 100644
--- a/contrib/python/deepmerge/py3/deepmerge/_version.py
+++ b/contrib/python/deepmerge/py3/deepmerge/_version.py
@@ -1,16 +1,24 @@
-# file generated by setuptools_scm
+# file generated by vcs-versioning
# don't change, don't track in version control
-TYPE_CHECKING = False
-if TYPE_CHECKING:
- from typing import Tuple, Union
- VERSION_TUPLE = Tuple[Union[int, str], ...]
-else:
- VERSION_TUPLE = object
+from __future__ import annotations
+
+__all__ = [
+ "__version__",
+ "__version_tuple__",
+ "version",
+ "version_tuple",
+ "__commit_id__",
+ "commit_id",
+]
version: str
__version__: str
-__version_tuple__: VERSION_TUPLE
-version_tuple: VERSION_TUPLE
+__version_tuple__: tuple[int | str, ...]
+version_tuple: tuple[int | str, ...]
+commit_id: str | None
+__commit_id__: str | None
+
+__version__ = version = '2.1.0'
+__version_tuple__ = version_tuple = (2, 1, 0)
-__version__ = version = '2.0'
-__version_tuple__ = version_tuple = (2, 0)
+__commit_id__ = commit_id = None
diff --git a/contrib/python/deepmerge/py3/deepmerge/extended_set.py b/contrib/python/deepmerge/py3/deepmerge/extended_set.py
index 63b6e823738..a692c019c36 100644
--- a/contrib/python/deepmerge/py3/deepmerge/extended_set.py
+++ b/contrib/python/deepmerge/py3/deepmerge/extended_set.py
@@ -1,4 +1,4 @@
-from typing import Sequence, Any
+from typing import Sequence, Any, Dict, List
class ExtendedSet(set):
@@ -13,10 +13,18 @@ class ExtendedSet(set):
"""
def __init__(self, elements: Sequence) -> None:
- self._values_by_hash = {self._hash_element(e): e for e in elements}
+ # Elements are grouped into buckets keyed by their (possibly lossy)
+ # hash. Membership and insertion compare a candidate against the
+ # members of the matching bucket using equality, so two distinct
+ # elements that merely collide on the hash are not treated as equal.
+ self._buckets: Dict[int, List[Any]] = {}
+ for element in elements:
+ self._insert(element)
def _insert(self, element: Any) -> None:
- self._values_by_hash[self._hash_element(element)] = element
+ bucket = self._buckets.setdefault(self._hash_element(element), [])
+ if not any(element == existing for existing in bucket):
+ bucket.append(element)
return
def _hash_element(self, element: Any) -> int:
@@ -29,4 +37,7 @@ class ExtendedSet(set):
return hash(str(element))
def __contains__(self, obj: Any) -> bool:
- return self._hash_element(obj) in self._values_by_hash
+ bucket = self._buckets.get(self._hash_element(obj))
+ if bucket is None:
+ return False
+ return any(obj == existing for existing in bucket)
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py b/contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py
index d62ea7ce11b..7dfb2e050a5 100644
--- a/contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/__init__.py
@@ -4,3 +4,14 @@ from .fallback import FallbackStrategies
from .list import ListStrategies
from .set import SetStrategies
from .type_conflict import TypeConflictStrategies
+
+__all__ = [
+ "StrategyList",
+ "StrategyCallable",
+ "StrategyListInitable",
+ "DictStrategies",
+ "FallbackStrategies",
+ "ListStrategies",
+ "SetStrategies",
+ "TypeConflictStrategies",
+]
diff --git a/contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py b/contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py
index 7cfbaaadfc1..109b21c951b 100644
--- a/contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py
+++ b/contrib/python/deepmerge/py3/deepmerge/strategy/fallback.py
@@ -5,7 +5,6 @@ from typing import Any, TypeVar
import deepmerge.merger
from .core import StrategyList
-
T = TypeVar("T")
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py
index 7211757c363..db26d584885 100644
--- a/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/strategy/test_list.py
@@ -43,3 +43,23 @@ def test_strategy_append_similar_dict(custom_merger):
result = custom_merger.merge(base, nxt)
assert result == [{"bar": "bob", "foo": "baz"}, {"x": "y"}]
+
+
+def test_strategy_append_unique_keeps_hash_colliding_dicts(custom_merger):
+ """append_unique must keep distinct dicts even when their lossy
+ "key:value" hash collides (e.g. ``{"a": "1", "b": "2"}`` and
+ ``{"a": "1,b:2"}`` both hash to ``"a:1,b:2"``), and must not treat an
+ ``int`` value as equal to its ``str`` form.
+ """
+ assert custom_merger.merge([{"a": "1", "b": "2"}], [{"a": "1,b:2"}]) == [
+ {"a": "1", "b": "2"},
+ {"a": "1,b:2"},
+ ]
+ assert custom_merger.merge([{"k": 1}], [{"k": "1"}]) == [{"k": 1}, {"k": "1"}]
+
+
+def test_strategy_append_unique_hashable_hash_collision(custom_merger):
+ """A hashable element must not be considered present just because a
+ different element shares its hash (e.g. ``hash(-1) == hash(-2)``).
+ """
+ assert custom_merger.merge([-1], [-2]) == [-1, -2]
diff --git a/contrib/python/deepmerge/py3/deepmerge/tests/test_full.py b/contrib/python/deepmerge/py3/deepmerge/tests/test_full.py
index bc6bdee8a30..c9246ffbb7e 100644
--- a/contrib/python/deepmerge/py3/deepmerge/tests/test_full.py
+++ b/contrib/python/deepmerge/py3/deepmerge/tests/test_full.py
@@ -58,3 +58,40 @@ def test_subtypes():
result = always_merger.merge(base, next)
assert dict(result) == {"foo": "value", "bar": "value2", "baz": ["a", "b"]}
+
+
+ "initializer,expectation",
+ [
+ (None, {"foo": "value", "bar": "value3", "baz": ["a", "b", "c"]}),
+ ({}, {"foo": "value", "bar": "value3", "baz": ["a", "b", "c"]}),
+ (
+ {"init": "value"},
+ {"foo": "value", "bar": "value3", "baz": ["a", "b", "c"], "init": "value"},
+ ),
+ ],
+ ids=["no initilaizer", "empty initializer", "set initializer"],
+)
+def test_reduce(initializer, expectation):
+ # Given
+ from functools import reduce
+
+ base = {"foo": "value", "baz": ["a"]}
+ next = {"bar": "value2", "baz": ["b"]}
+ more = {"bar": "value3", "baz": ["c"]}
+
+ list_to_merge = [base, next, more]
+
+ # When
+ if initializer is None:
+ result = reduce(always_merger.merge, list_to_merge)
+ else:
+ result = reduce(always_merger.merge, list_to_merge, initializer)
+
+ # Then
+ assert result == expectation
+
+ if initializer is None:
+ assert result is base
+ else:
+ assert result is not base
diff --git a/contrib/python/deepmerge/py3/ya.make b/contrib/python/deepmerge/py3/ya.make
index 1cb42fc56ae..367b4da494b 100644
--- a/contrib/python/deepmerge/py3/ya.make
+++ b/contrib/python/deepmerge/py3/ya.make
@@ -4,7 +4,7 @@ PY3_LIBRARY()
SUBSCRIBER(g:python-contrib)
-VERSION(2.0)
+VERSION(2.1.0)
LICENSE(MIT)
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA
index 087eb11eaaa..2efad59a484 100644
--- a/contrib/python/hypothesis/py3/.dist-info/METADATA
+++ b/contrib/python/hypothesis/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: hypothesis
-Version: 6.155.2
+Version: 6.155.3
Summary: The property-based testing library for Python
Author-email: "David R. MacIver and Zac Hatfield-Dodds" <[email protected]>
License-Expression: MPL-2.0
diff --git a/contrib/python/hypothesis/py3/hypothesis/core.py b/contrib/python/hypothesis/py3/hypothesis/core.py
index 44418c255ed..5c8b279464b 100644
--- a/contrib/python/hypothesis/py3/hypothesis/core.py
+++ b/contrib/python/hypothesis/py3/hypothesis/core.py
@@ -1205,6 +1205,7 @@ class StateForActualGivenExecution:
``StopTest`` must be a fatal error, and should stop the entire engine.
"""
trace: Trace = frozenset()
+ backend_cannot_proceed = False
try:
with Tracer(should_trace=self._should_trace()) as tracer:
try:
@@ -1233,7 +1234,16 @@ class StateForActualGivenExecution:
# This was unexpected, meaning that the assume was flaky.
# Report it as such.
raise self._flaky_replay_to_failure(err, e) from None
- except (StopTest, BackendCannotProceed):
+ except BackendCannotProceed:
+ # The engine discards this iteration entirely (see engine.py,
+ # "we're pretending this never happened"), so we shouldn't emit a
+ # test_case observation for it either -- otherwise an alternative
+ # backend that aborts before running the test (e.g. crosshair when
+ # it has exhausted its paths) surfaces a spurious, draw-less
+ # "passed" observation with an empty representation.
+ backend_cannot_proceed = True
+ raise
+ except StopTest:
# The engine knows how to handle this control exception, so it's
# OK to re-raise it.
raise
@@ -1320,7 +1330,7 @@ class StateForActualGivenExecution:
except BackendCannotProceed:
data.events = {}
- if observability_enabled():
+ if observability_enabled() and not backend_cannot_proceed:
if runner := getattr(self, "_runner", None):
phase = runner._current_phase
else: # pragma: no cover # in case of messing with internals
diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py
index b71a3693b09..175cbc5c08a 100644
--- a/contrib/python/hypothesis/py3/hypothesis/version.py
+++ b/contrib/python/hypothesis/py3/hypothesis/version.py
@@ -8,5 +8,5 @@
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
-__version_info__ = (6, 155, 2)
+__version_info__ = (6, 155, 3)
__version__ = ".".join(map(str, __version_info__))
diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make
index 59ebb1c2ba5..8ab046f972a 100644
--- a/contrib/python/hypothesis/py3/ya.make
+++ b/contrib/python/hypothesis/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(6.155.2)
+VERSION(6.155.3)
LICENSE(MPL-2.0)
diff --git a/contrib/python/prettytable/py3/.dist-info/METADATA b/contrib/python/prettytable/py3/.dist-info/METADATA
index 6c57a35f2bc..85d2d452c4b 100644
--- a/contrib/python/prettytable/py3/.dist-info/METADATA
+++ b/contrib/python/prettytable/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: prettytable
-Version: 3.17.0
+Version: 3.18.0
Summary: A simple Python library for easily displaying tabular data in a visually appealing ASCII table format
Project-URL: Changelog, https://github.com/prettytable/prettytable/releases
Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-prettytable?utm_source=pypi-prettytable&utm_medium=pypi
@@ -18,16 +18,17 @@ Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
+Classifier: Programming Language :: Python :: 3.16
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Text Processing
Classifier: Typing :: Typed
Requires-Python: >=3.10
-Requires-Dist: wcwidth
+Requires-Dist: wcwidth>=0.3.5
Provides-Extra: tests
-Requires-Dist: pytest; extra == 'tests'
Requires-Dist: pytest-cov; extra == 'tests'
Requires-Dist: pytest-lazy-fixtures; extra == 'tests'
+Requires-Dist: pytest>=9; extra == 'tests'
Description-Content-Type: text/markdown
# PrettyTable
@@ -61,11 +62,11 @@ PrettyTable lets you print tables in an attractive ASCII form:
Install via pip:
- python -m pip install -U prettytable
+ python3 -m pip install -U prettytable
Install latest development version:
- python -m pip install -U git+https://github.com/prettytable/prettytable
+ python3 -m pip install -U git+https://github.com/prettytable/prettytable
Or from `requirements.txt`:
@@ -135,13 +136,10 @@ method, which takes two arguments - a string which is the name for the field the
you are adding corresponds to, and a list or tuple which contains the column data:
```python
-table.add_column("City name",
-["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"])
+table.add_column("City name", ["Adelaide", "Brisbane", "Darwin", "Hobart", "Sydney", "Melbourne", "Perth"])
table.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
-table.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092,
-1554769])
-table.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9,
-869.4])
+table.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 1554769])
+table.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 869.4])
```
#### Mixing and matching
@@ -255,6 +253,30 @@ def my_cli_function(table_format: str = 'text'):
print(table.get_formatted_string(table_format))
```
+#### Paginating your table
+
+If you have a large table and want to split it into multiple pages, you can use the
+`paginate` method. This method splits the table into pages of a specified length and
+separates them with a line break character (by default, a form feed character `\f`):
+
+```python
+paginated_string = table.paginate(page_length=10)
+print(paginated_string)
+```
+
+The `page_length` parameter controls how many rows appear on each page (default: 58).
+The `line_break` parameter specifies the string used to separate pages (default: `"\f"`
+form feed). You can also pass any keyword arguments that are accepted by `get_string()`
+to control the formatting of each page:
+
+```python
+paginated_string = table.paginate(page_length=20, line_break="\n\n---\n\n", border=True)
+print(paginated_string)
+```
+
+This is particularly useful when printing large tables to a terminal or when you want to
+format output for pagination in documents.
+
#### Controlling which data gets displayed
If you like, you can restrict the output of `print(table)` or `table.get_string` to only
@@ -497,6 +519,39 @@ to get a table like this:
Any added dividers will be removed if a table is sorted.
+### Adding a title to your table
+
+You can add a title to your table with the `title` attribute:
+
+```python
+table.title = "Australian Cities"
+```
+
+Titles can span multiple lines by including `\n` in the string:
+
+```python
+table.title = "Australian Cities\nPopulation Data"
+print(table)
+```
+
+This produces:
+
+```
++-------------------------------------------------+
+| Australian Cities |
+| Population Data |
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
++-----------+------+------------+-----------------+
+```
+
+Each line of the title is centered and bordered independently. Multiline titles also
+work with HTML output (using `<br>` in the `<caption>` tag) and Markdown output.
+
### Changing the appearance of your table - the easy way
By default, PrettyTable produces ASCII tables that look like the ones used in SQL
@@ -525,6 +580,8 @@ In addition to `MARKDOWN` you can use these in-built styles:
- `MSWORD_FRIENDLY` - A format which works nicely with Microsoft Word's "Convert to
table" feature
- `ORGMODE` - A table style that fits [Org mode](https://orgmode.org/) syntax
+- `RST` - A table style that produces reStructuredText
+ [grid tables](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#tables)
- `SINGLE_BORDER` and `DOUBLE_BORDER` - Styles that use continuous single/double border
lines with Box drawing characters for a fancier display on terminal
@@ -642,6 +699,36 @@ print(table.get_string(border=False))
print(table)
```
+#### Custom format example (with _colors_)
+
+The `custom_format` attribute allows you to define a function for custom cell value
+formatting. For example, the following code demonstrates how to apply a single function
+across all columns to display strings in `blue`, positive numbers in `green`, and
+negative numbers in `red`, all with a precision of two decimal places. Your custom
+function can also inspect the `field` parameter to apply field-specific formatting.
+
+```python
+from prettytable import PrettyTable
+from typing import Any
+from termcolor import colored
+
+def _colored(field: str, val: Any) -> str:
+ if isinstance(val, (int, float)):
+ if val >= 0:
+ return colored(f"{val:.2f}", "green")
+ return colored(f"{val:.2f}", "red")
+ elif isinstance(val, str):
+ return colored(val, "blue")
+ return f"{val}"
+
+table = PrettyTable(("Name", "Overtime"))
+table.custom_format = _colored
+
+for row in [["John Doe", 5.0], ["Jane Smith", -2.0]]:
+ table.add_row([row[0], row[1]])
+print(table)
+```
+
### Changing the appearance of your table - with _colors_!
PrettyTable has the functionality of printing your table with ANSI color codes. This
@@ -806,10 +893,18 @@ new_table = old_table[0:5]
## Contributing
-After editing files, use the [Black](https://github.com/psf/black) linter to auto-format
+After editing files, use at least [Black](https://github.com/psf/black) to auto-format
changed lines.
```sh
-python -m pip install black
-black prettytable*.py
+python3 -m pip install black
+black .
+```
+
+To run all pre-commit checks, linters, formatters (including Black), and tests use
+[tox](https://github.com/tox-dev/tox):
+
+```sh
+python3 -m pip install tox
+tox
```
diff --git a/contrib/python/prettytable/py3/README.md b/contrib/python/prettytable/py3/README.md
index a06e6c35cf6..52c3c694cba 100644
--- a/contrib/python/prettytable/py3/README.md
+++ b/contrib/python/prettytable/py3/README.md
@@ -29,11 +29,11 @@ PrettyTable lets you print tables in an attractive ASCII form:
Install via pip:
- python -m pip install -U prettytable
+ python3 -m pip install -U prettytable
Install latest development version:
- python -m pip install -U git+https://github.com/prettytable/prettytable
+ python3 -m pip install -U git+https://github.com/prettytable/prettytable
Or from `requirements.txt`:
@@ -103,13 +103,10 @@ method, which takes two arguments - a string which is the name for the field the
you are adding corresponds to, and a list or tuple which contains the column data:
```python
-table.add_column("City name",
-["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"])
+table.add_column("City name", ["Adelaide", "Brisbane", "Darwin", "Hobart", "Sydney", "Melbourne", "Perth"])
table.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
-table.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092,
-1554769])
-table.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9,
-869.4])
+table.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 1554769])
+table.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 869.4])
```
#### Mixing and matching
@@ -223,6 +220,30 @@ def my_cli_function(table_format: str = 'text'):
print(table.get_formatted_string(table_format))
```
+#### Paginating your table
+
+If you have a large table and want to split it into multiple pages, you can use the
+`paginate` method. This method splits the table into pages of a specified length and
+separates them with a line break character (by default, a form feed character `\f`):
+
+```python
+paginated_string = table.paginate(page_length=10)
+print(paginated_string)
+```
+
+The `page_length` parameter controls how many rows appear on each page (default: 58).
+The `line_break` parameter specifies the string used to separate pages (default: `"\f"`
+form feed). You can also pass any keyword arguments that are accepted by `get_string()`
+to control the formatting of each page:
+
+```python
+paginated_string = table.paginate(page_length=20, line_break="\n\n---\n\n", border=True)
+print(paginated_string)
+```
+
+This is particularly useful when printing large tables to a terminal or when you want to
+format output for pagination in documents.
+
#### Controlling which data gets displayed
If you like, you can restrict the output of `print(table)` or `table.get_string` to only
@@ -465,6 +486,39 @@ to get a table like this:
Any added dividers will be removed if a table is sorted.
+### Adding a title to your table
+
+You can add a title to your table with the `title` attribute:
+
+```python
+table.title = "Australian Cities"
+```
+
+Titles can span multiple lines by including `\n` in the string:
+
+```python
+table.title = "Australian Cities\nPopulation Data"
+print(table)
+```
+
+This produces:
+
+```
++-------------------------------------------------+
+| Australian Cities |
+| Population Data |
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
++-----------+------+------------+-----------------+
+```
+
+Each line of the title is centered and bordered independently. Multiline titles also
+work with HTML output (using `<br>` in the `<caption>` tag) and Markdown output.
+
### Changing the appearance of your table - the easy way
By default, PrettyTable produces ASCII tables that look like the ones used in SQL
@@ -493,6 +547,8 @@ In addition to `MARKDOWN` you can use these in-built styles:
- `MSWORD_FRIENDLY` - A format which works nicely with Microsoft Word's "Convert to
table" feature
- `ORGMODE` - A table style that fits [Org mode](https://orgmode.org/) syntax
+- `RST` - A table style that produces reStructuredText
+ [grid tables](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#tables)
- `SINGLE_BORDER` and `DOUBLE_BORDER` - Styles that use continuous single/double border
lines with Box drawing characters for a fancier display on terminal
@@ -610,6 +666,36 @@ print(table.get_string(border=False))
print(table)
```
+#### Custom format example (with _colors_)
+
+The `custom_format` attribute allows you to define a function for custom cell value
+formatting. For example, the following code demonstrates how to apply a single function
+across all columns to display strings in `blue`, positive numbers in `green`, and
+negative numbers in `red`, all with a precision of two decimal places. Your custom
+function can also inspect the `field` parameter to apply field-specific formatting.
+
+```python
+from prettytable import PrettyTable
+from typing import Any
+from termcolor import colored
+
+def _colored(field: str, val: Any) -> str:
+ if isinstance(val, (int, float)):
+ if val >= 0:
+ return colored(f"{val:.2f}", "green")
+ return colored(f"{val:.2f}", "red")
+ elif isinstance(val, str):
+ return colored(val, "blue")
+ return f"{val}"
+
+table = PrettyTable(("Name", "Overtime"))
+table.custom_format = _colored
+
+for row in [["John Doe", 5.0], ["Jane Smith", -2.0]]:
+ table.add_row([row[0], row[1]])
+print(table)
+```
+
### Changing the appearance of your table - with _colors_!
PrettyTable has the functionality of printing your table with ANSI color codes. This
@@ -774,10 +860,18 @@ new_table = old_table[0:5]
## Contributing
-After editing files, use the [Black](https://github.com/psf/black) linter to auto-format
+After editing files, use at least [Black](https://github.com/psf/black) to auto-format
changed lines.
```sh
-python -m pip install black
-black prettytable*.py
+python3 -m pip install black
+black .
+```
+
+To run all pre-commit checks, linters, formatters (including Black), and tests use
+[tox](https://github.com/tox-dev/tox):
+
+```sh
+python3 -m pip install tox
+tox
```
diff --git a/contrib/python/prettytable/py3/patches/01-fix-tests.patch b/contrib/python/prettytable/py3/patches/01-fix-tests.patch
new file mode 100644
index 00000000000..c439ba2902c
--- /dev/null
+++ b/contrib/python/prettytable/py3/patches/01-fix-tests.patch
@@ -0,0 +1,6 @@
+--- contrib/python/prettytable/py3/tests/test_style.py (index)
++++ contrib/python/prettytable/py3/tests/test_style.py (working tree)
+@@ -18 +18,2 @@ from prettytable.prettytable import _str_block_width
+-DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
++import yatest.common as yc
++DATA_DIR = os.path.join(os.path.dirname(yc.source_path(__file__)), "data")
diff --git a/contrib/python/prettytable/py3/prettytable/__init__.py b/contrib/python/prettytable/py3/prettytable/__init__.py
index f28c03658ab..5876dcfeac4 100644
--- a/contrib/python/prettytable/py3/prettytable/__init__.py
+++ b/contrib/python/prettytable/py3/prettytable/__init__.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Any
+__lazy_modules__ = {"prettytable._version", "prettytable.prettytable"}
from ._version import __version__
from .prettytable import ( # noqa: F401
@@ -31,6 +31,11 @@ from .prettytable import ( # noqa: F401
from_mediawiki,
)
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import Any
+
+
__all__ = [
"ALL",
"DEFAULT",
diff --git a/contrib/python/prettytable/py3/prettytable/_version.py b/contrib/python/prettytable/py3/prettytable/_version.py
index b6f2ce2e738..d38ec03f655 100644
--- a/contrib/python/prettytable/py3/prettytable/_version.py
+++ b/contrib/python/prettytable/py3/prettytable/_version.py
@@ -1,5 +1,6 @@
-# file generated by setuptools-scm
+# file generated by vcs-versioning
# don't change, don't track in version control
+from __future__ import annotations
__all__ = [
"__version__",
@@ -10,25 +11,14 @@ __all__ = [
"commit_id",
]
-TYPE_CHECKING = False
-if TYPE_CHECKING:
- from typing import Tuple
- from typing import Union
-
- VERSION_TUPLE = Tuple[Union[int, str], ...]
- COMMIT_ID = Union[str, None]
-else:
- VERSION_TUPLE = object
- COMMIT_ID = object
-
version: str
__version__: str
-__version_tuple__: VERSION_TUPLE
-version_tuple: VERSION_TUPLE
-commit_id: COMMIT_ID
-__commit_id__: COMMIT_ID
+__version_tuple__: tuple[int | str, ...]
+version_tuple: tuple[int | str, ...]
+commit_id: str | None
+__commit_id__: str | None
-__version__ = version = '3.17.0'
-__version_tuple__ = version_tuple = (3, 17, 0)
+__version__ = version = '3.18.0'
+__version_tuple__ = version_tuple = (3, 18, 0)
__commit_id__ = commit_id = None
diff --git a/contrib/python/prettytable/py3/prettytable/colortable.py b/contrib/python/prettytable/py3/prettytable/colortable.py
index 52e1fa20a4c..0f9d417a4e3 100644
--- a/contrib/python/prettytable/py3/prettytable/colortable.py
+++ b/contrib/python/prettytable/py3/prettytable/colortable.py
@@ -2,6 +2,11 @@ from __future__ import annotations
from .prettytable import PrettyTable
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Sequence
+ from typing import ClassVar, Final
+
try:
from colorama import init
@@ -10,10 +15,18 @@ except ImportError:
pass
-RESET_CODE = "\x1b[0m"
+RESET_CODE: Final = "\x1b[0m"
class Theme:
+ default_color: Final[str]
+ vertical_char: Final[str]
+ vertical_color: Final[str]
+ horizontal_char: Final[str]
+ horizontal_color: Final[str]
+ junction_char: Final[str]
+ junction_color: Final[str]
+
def __init__(
self,
default_color: str = "",
@@ -44,50 +57,50 @@ class Theme:
class Themes:
- DEFAULT = Theme()
- DYSLEXIA_FRIENDLY = Theme(
+ DEFAULT: ClassVar[Theme] = Theme()
+ DYSLEXIA_FRIENDLY: ClassVar[Theme] = Theme(
default_color="38;5;223",
vertical_color="38;5;22",
horizontal_color="38;5;22",
junction_color="38;5;58",
)
- EARTH = Theme(
+ EARTH: ClassVar[Theme] = Theme(
default_color="33",
vertical_color="38;5;94",
horizontal_color="38;5;22",
junction_color="38;5;130",
)
- GLARE_REDUCTION = Theme(
+ GLARE_REDUCTION: ClassVar[Theme] = Theme(
default_color="38;5;252",
vertical_color="38;5;240",
horizontal_color="38;5;240",
junction_color="38;5;246",
)
- HIGH_CONTRAST = Theme(
+ HIGH_CONTRAST: ClassVar[Theme] = Theme(
default_color="97",
vertical_color="91",
horizontal_color="94",
junction_color="93",
)
- LAVENDER = Theme(
+ LAVENDER: ClassVar[Theme] = Theme(
default_color="38;5;183",
vertical_color="35",
horizontal_color="38;5;147",
junction_color="38;5;219",
)
- OCEAN = Theme(
+ OCEAN: ClassVar[Theme] = Theme(
default_color="96",
vertical_color="34",
horizontal_color="34",
junction_color="36",
)
- OCEAN_DEEP = Theme(
+ OCEAN_DEEP: ClassVar[Theme] = Theme(
default_color="96",
vertical_color="34",
horizontal_color="36",
junction_color="94",
)
- PASTEL = Theme(
+ PASTEL: ClassVar[Theme] = Theme(
default_color="38;5;223",
vertical_color="38;5;152",
horizontal_color="38;5;187",
@@ -96,7 +109,7 @@ class Themes:
class ColorTable(PrettyTable):
- def __init__(self, field_names=None, **kwargs) -> None:
+ def __init__(self, field_names: Sequence[str] | None = None, **kwargs) -> None:
super().__init__(field_names=field_names, **kwargs)
# TODO: Validate option
diff --git a/contrib/python/prettytable/py3/prettytable/prettytable.py b/contrib/python/prettytable/py3/prettytable/prettytable.py
index 1b6c964652c..d3cdf990fb7 100644
--- a/contrib/python/prettytable/py3/prettytable/prettytable.py
+++ b/contrib/python/prettytable/py3/prettytable/prettytable.py
@@ -1,5 +1,3 @@
-#!/usr/bin/env python
-#
# Copyright (c) 2009-2014, Luke Maurits <[email protected]>
# All rights reserved.
# With contributions from:
@@ -33,22 +31,71 @@
from __future__ import annotations
-import io
-import re
from enum import IntEnum
from functools import lru_cache
-from html.parser import HTMLParser
-from typing import Any, Literal, TypedDict, cast
+from typing import Any, Literal, cast
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Mapping, Sequence
- from sqlite3 import Cursor
- from typing import Final, TypeAlias
+ from typing import Final, TypeAlias, TypedDict
from _typeshed import SupportsRichComparison
from typing_extensions import Self
+ class OptionsType(TypedDict):
+ title: str | None
+ start: int
+ end: int | None
+ fields: Sequence[str | None] | None
+ header: bool
+ use_header_width: bool
+ border: bool
+ preserve_internal_border: bool
+ sortby: str | None
+ reversesort: bool
+ sort_key: Callable[[RowType], SupportsRichComparison]
+ row_filter: Callable[[RowType], bool]
+ attributes: dict[str, str]
+ format: bool
+ hrules: HRuleStyle
+ vrules: VRuleStyle
+ int_format: str | dict[str, str] | None
+ float_format: str | dict[str, str] | None
+ custom_format: (
+ Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None
+ )
+ min_table_width: int | None
+ max_table_width: int | None
+ padding_width: int
+ left_padding_width: int | None
+ right_padding_width: int | None
+ vertical_char: str
+ horizontal_char: str
+ horizontal_align_char: str
+ header_horizontal_char: str | None
+ junction_char: str
+ header_style: HeaderStyleType
+ xhtml: bool
+ print_empty: bool
+ oldsortslice: bool
+ top_junction_char: str
+ bottom_junction_char: str
+ right_junction_char: str
+ left_junction_char: str
+ top_right_junction_char: str
+ top_left_junction_char: str
+ bottom_right_junction_char: str
+ bottom_left_junction_char: str
+ align: dict[str, AlignType]
+ valign: dict[str, VAlignType]
+ min_width: int | dict[str, int] | None
+ max_width: int | dict[str, int] | None
+ none_format: str | dict[str, str | None] | None
+ escape_header: bool
+ escape_data: bool
+ break_on_hyphens: bool
+
class HRuleStyle(IntEnum):
FRAME = 0
@@ -71,6 +118,7 @@ class TableStyle(IntEnum):
ORGMODE = 14
DOUBLE_BORDER = 15
SINGLE_BORDER = 16
+ RST = 17
RANDOM = 20
@@ -97,63 +145,39 @@ VAlignType: TypeAlias = Literal["t", "m", "b"]
HeaderStyleType: TypeAlias = Literal["cap", "title", "upper", "lower"] | None
-class OptionsType(TypedDict):
- title: str | None
- start: int
- end: int | None
- fields: Sequence[str | None] | None
- header: bool
- use_header_width: bool
- border: bool
- preserve_internal_border: bool
- sortby: str | None
- reversesort: bool
- sort_key: Callable[[RowType], SupportsRichComparison]
- row_filter: Callable[[RowType], bool]
- attributes: dict[str, str]
- format: bool
- hrules: HRuleStyle
- vrules: VRuleStyle
- int_format: str | dict[str, str] | None
- float_format: str | dict[str, str] | None
- custom_format: (
- Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None
- )
- min_table_width: int | None
- max_table_width: int | None
- padding_width: int
- left_padding_width: int | None
- right_padding_width: int | None
- vertical_char: str
- horizontal_char: str
- horizontal_align_char: str
- junction_char: str
- header_style: HeaderStyleType
- xhtml: bool
- print_empty: bool
- oldsortslice: bool
- top_junction_char: str
- bottom_junction_char: str
- right_junction_char: str
- left_junction_char: str
- top_right_junction_char: str
- top_left_junction_char: str
- bottom_right_junction_char: str
- bottom_left_junction_char: str
- align: dict[str, AlignType]
- valign: dict[str, VAlignType]
- min_width: int | dict[str, int] | None
- max_width: int | dict[str, int] | None
- none_format: str | dict[str, str | None] | None
- escape_header: bool
- escape_data: bool
- break_on_hyphens: bool
+class ObservableDict(dict[str, Any]):
+ """A dictionary that notifies a callback when items are set or changed.
+
+ This dictionary subclass allows setting a callback function that will be
+ invoked whenever an item is set or its value changes. This is used to
+ maintain consistency between related format dictionaries.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """Initialize the observable dictionary.
+
+ Arguments:
+ *args: Positional arguments passed to dict.__init__
+ **kwargs: Keyword arguments passed to dict.__init__
+ """
+ super().__init__(*args, **kwargs)
+ self.callback = None
+
+ def __setitem__(self, key: str, value: Any):
+ """Set an item and trigger callback if value changed.
+ Sets the item in the dictionary and calls the callback function
+ (if set) with the key, old value, and new value when the value
+ actually changes.
-# ANSI colour codes
-_re = re.compile(r"\033\[[0-9;]*m|\033\(B")
-# OSC 8 hyperlinks
-_osc8_re = re.compile(r"\033\]8;;.*?\033\\(.*?)\033\]8;;\033\\")
+ Arguments:
+ key: The dictionary key
+ value: The new value to set
+ """
+ old_value = self.get(key)
+ if self.callback is not None and old_value != value:
+ self.callback(key, old_value, value)
+ super().__setitem__(key, value)
@lru_cache
@@ -196,6 +220,7 @@ class PrettyTable:
_vertical_char: str
_horizontal_char: str
_horizontal_align_char: str | None
+ _header_horizontal_char: str | None
_junction_char: str
_top_junction_char: str | None
_bottom_junction_char: str | None
@@ -253,6 +278,8 @@ class PrettyTable:
vertical_char - single character string used to draw vertical lines
horizontal_char - single character string used to draw horizontal lines
horizontal_align_char - single character string used to indicate alignment
+ header_horizontal_char - single character string used to draw the header
+ separator, or None to use the same as horizontal_char
junction_char - single character string used to draw line junctions
top_junction_char - single character string used to draw top line junctions
bottom_junction_char -
@@ -274,7 +301,7 @@ class PrettyTable:
valign - default valign for each row (None, "t", "m" or "b")
reversesort - True or False to sort in descending or ascending order
oldsortslice - Slice rows before sorting in the "old style"
- break_on_hyphens - Whether long lines are broken on hypens or not, default: True
+ break_on_hyphens - Whether long lines are broken on hyphens or not, default: True
"""
self.encoding = kwargs.get("encoding", "UTF-8")
@@ -282,13 +309,6 @@ class PrettyTable:
self._field_names: list[str] = []
self._rows: list[RowType] = []
self._dividers: list[bool] = []
- self.align = {}
- self.valign = {}
- self.max_width = {}
- self.min_width = {}
- self.int_format = {}
- self.float_format = {}
- self.custom_format = {}
self._style = None
# Options
@@ -320,6 +340,7 @@ class PrettyTable:
"vertical_char",
"horizontal_char",
"horizontal_align_char",
+ "header_horizontal_char",
"junction_char",
"header_style",
"xhtml",
@@ -343,7 +364,31 @@ class PrettyTable:
"break_on_hyphens",
]
- self._none_format: dict[str, str | None] = {}
+ self._none_format: dict[str, str | None] = ObservableDict()
+ self._none_format.callback = self._remove_custom_format_callback
+
+ self._int_format: dict[str, str | None] = ObservableDict()
+ self._int_format.callback = self._remove_custom_format_callback
+
+ self._float_format: dict[str, str | None] = ObservableDict()
+ self._float_format.callback = self._remove_custom_format_callback
+
+ self._custom_format: dict[str, Callable[[str, Any], str]] = ObservableDict()
+ self._custom_format.callback = self._custom_format_callback
+
+ self._align: dict[str, str | None] = ObservableDict()
+ self._align[BASE_ALIGN_VALUE] = "c"
+ self._align.callback = self._align_callback
+
+ self._valign: dict[str, str | None] = ObservableDict()
+ self._valign.callback = self._valign_callback
+
+ self._max_width: dict[str, int | None] = ObservableDict()
+ self._max_width.callback = self._max_width_callback
+
+ self._min_width: dict[str, int | None] = ObservableDict()
+ self._min_width.callback = self._min_width_callback
+
self._kwargs = {}
if field_names:
self.field_names = field_names
@@ -414,6 +459,7 @@ class PrettyTable:
self._vertical_char = kwargs["vertical_char"] or "|"
self._horizontal_char = kwargs["horizontal_char"] or "-"
self._horizontal_align_char = kwargs["horizontal_align_char"]
+ self._header_horizontal_char = kwargs["header_horizontal_char"]
self._junction_char = kwargs["junction_char"] or "+"
self._top_junction_char = kwargs["top_junction_char"]
self._bottom_junction_char = kwargs["bottom_junction_char"]
@@ -457,36 +503,20 @@ class PrettyTable:
)
def _justify(self, text: str, width: int, align: AlignType) -> str:
- excess = width - _str_block_width(text)
+ import wcwidth
+
if align == "l":
- return text + excess * " "
+ return wcwidth.ljust(text, width)
elif align == "r":
- return excess * " " + text
+ return wcwidth.rjust(text, width)
else:
- if excess % 2:
- # Uneven padding
- # Put more space on right if text is of odd length...
- if _str_block_width(text) % 2:
- return (excess // 2) * " " + text + (excess // 2 + 1) * " "
- # and more space on left if text is of even length
- else:
- return (excess // 2 + 1) * " " + text + (excess // 2) * " "
- # Why distribute extra space this way? To match the behaviour of
- # the inbuilt str.center() method.
- else:
- # Equal padding on either side
- return (excess // 2) * " " + text + (excess // 2) * " "
+ return wcwidth.center(text, width)
def __getattr__(self, name):
if name == "rowcount":
return len(self._rows)
elif name == "colcount":
- if self._field_names:
- return len(self._field_names)
- elif self._rows:
- return len(self._rows[0])
- else:
- return 0
+ return len(self._field_names)
else:
raise AttributeError(name)
@@ -494,8 +524,7 @@ class PrettyTable:
new = PrettyTable()
new.field_names = self.field_names
for attr in self._options:
- setattr(new, "_" + attr, getattr(self, "_" + attr))
- setattr(new, "_align", getattr(self, "_align"))
+ setattr(new, f"_{attr}", getattr(self, f"_{attr}"))
if isinstance(index, slice):
for row in self._rows[index]:
new.add_row(row)
@@ -587,6 +616,7 @@ class PrettyTable:
"vertical_char",
"horizontal_char",
"horizontal_align_char",
+ "header_horizontal_char",
"junction_char",
"top_junction_char",
"bottom_junction_char",
@@ -701,7 +731,7 @@ class PrettyTable:
def _validate_function(self, name, val):
try:
- assert hasattr(val, "__call__")
+ assert callable(val)
except AssertionError:
msg = f"Invalid value for {name}. Must be a function."
raise ValueError(msg)
@@ -782,9 +812,7 @@ class PrettyTable:
val - The alternative representation to be used for None values
"""
- if not self._field_names:
- self._none_format = {}
- elif isinstance(val, str):
+ if isinstance(val, str):
for field in self._field_names:
self._none_format[field] = None
self._validate_none_format(val)
@@ -795,8 +823,7 @@ class PrettyTable:
self._validate_none_format(fval)
self._none_format[field] = fval
else:
- for field in self._field_names:
- self._none_format[field] = None
+ self._none_format.clear()
@property
def field_names(self) -> list[str]:
@@ -822,8 +849,8 @@ class PrettyTable:
for old_name, new_name in zip(old_names, val):
self._align[new_name] = self._align[old_name]
for old_name in old_names:
- if old_name not in self._align:
- self._align.pop(old_name)
+ if old_name not in val:
+ self._align.pop(old_name, None)
elif self._align:
for field_name in self._field_names:
self._align[field_name] = self._align[BASE_ALIGN_VALUE]
@@ -833,11 +860,25 @@ class PrettyTable:
for old_name, new_name in zip(old_names, val):
self._valign[new_name] = self._valign[old_name]
for old_name in old_names:
- if old_name not in self._valign:
- self._valign.pop(old_name)
+ if old_name not in val:
+ self._valign.pop(old_name, None)
else:
self.valign = "t"
+ def _align_callback(self, field_name, old_value, new_value):
+ """Callback to call validators if dict attrs are modified.
+
+ This callback is triggered when a field is modified from align dict and
+ calls the validator for the new value.
+
+ Arguments:
+ field_name: Name of the field being modified
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+
+ """
+ self._validate_align(new_value)
+
@property
def align(self) -> dict[str, AlignType]:
"""Controls alignment of fields
@@ -849,7 +890,6 @@ class PrettyTable:
@align.setter
def align(self, val: AlignType | dict[str, AlignType] | None) -> None:
if isinstance(val, str):
- self._validate_align(val)
if not self._field_names:
self._align = {BASE_ALIGN_VALUE: val}
else:
@@ -857,14 +897,26 @@ class PrettyTable:
self._align[field] = val
elif isinstance(val, dict) and val:
for field, fval in val.items():
- self._validate_align(fval)
self._align[field] = fval
+ elif self._field_names:
+ for field in self._field_names:
+ self._align[field] = "c"
else:
- if not self._field_names:
- self._align = {BASE_ALIGN_VALUE: "c"}
- else:
- for field in self._field_names:
- self._align[field] = "c"
+ self._align = {BASE_ALIGN_VALUE: "c"}
+
+ def _valign_callback(self, field_name, old_value, new_value):
+ """Callback to call validators if dict attrs are modified.
+
+ This callback is triggered when a field is modified from valign dict
+ and calls the validator for the new value.
+
+ Arguments:
+ field_name: Name of the field being modified
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+
+ """
+ self._validate_valign(new_value)
@property
def valign(self) -> dict[str, VAlignType]:
@@ -877,19 +929,31 @@ class PrettyTable:
@valign.setter
def valign(self, val: VAlignType | dict[str, VAlignType] | None) -> None:
if not self._field_names:
- self._valign = {}
+ self._valign.clear()
if isinstance(val, str):
- self._validate_valign(val)
for field in self._field_names:
self._valign[field] = val
elif isinstance(val, dict) and val:
for field, fval in val.items():
- self._validate_valign(fval)
self._valign[field] = fval
else:
for field in self._field_names:
self._valign[field] = "t"
+ def _max_width_callback(self, field_name, old_value, new_value):
+ """Callback to call validators if dict attrs are modified.
+
+ This callback is triggered when a field is modified from max_width dict
+ and calls the validator for the new value.
+
+ Arguments:
+ field_name: Name of the field being modified
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+
+ """
+ self._validate_option("max_width", new_value)
+
@property
def max_width(self) -> dict[str, int]:
"""Controls maximum width of fields
@@ -901,15 +965,27 @@ class PrettyTable:
@max_width.setter
def max_width(self, val: int | dict[str, int] | None) -> None:
if isinstance(val, int):
- self._validate_option("max_width", val)
for field in self._field_names:
self._max_width[field] = val
elif isinstance(val, dict) and val:
for field, fval in val.items():
- self._validate_option("max_width", fval)
self._max_width[field] = fval
else:
- self._max_width = {}
+ self._max_width.clear()
+
+ def _min_width_callback(self, field_name, old_value, new_value):
+ """Callback to call validators if dict attrs are modified.
+
+ This callback is triggered when a field is modified from min_width dict
+ and calls the validator for the new value.
+
+ Arguments:
+ field_name: Name of the field being modified
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+
+ """
+ self._validate_option("min_width", new_value)
@property
def min_width(self) -> dict[str, int]:
@@ -922,15 +998,13 @@ class PrettyTable:
@min_width.setter
def min_width(self, val: int | dict[str, int] | None) -> None:
if isinstance(val, int):
- self._validate_option("min_width", val)
for field in self._field_names:
self._min_width[field] = val
elif isinstance(val, dict) and val:
for field, fval in val.items():
- self._validate_option("min_width", fval)
self._min_width[field] = fval
else:
- self._min_width = {}
+ self._min_width.clear()
@property
def min_table_width(self) -> int | None:
@@ -1180,7 +1254,7 @@ class PrettyTable:
self._validate_option("int_format", fval)
self._int_format[field] = fval
else:
- self._int_format = {}
+ self._int_format.clear()
@property
def float_format(self) -> dict[str, str]:
@@ -1201,7 +1275,41 @@ class PrettyTable:
self._validate_option("float_format", fval)
self._float_format[field] = fval
else:
- self._float_format = {}
+ self._float_format.clear()
+
+ def _remove_custom_format_callback(self, field_name, old_value, new_value):
+ """Callback to remove custom format when a field is removed from format dicts.
+
+ This callback is triggered when a field is removed from _none_format,
+ _int_format, or _float_format dictionaries, and removes the corresponding
+ entry from _custom_format if it exists.
+
+ Arguments:
+ field_name: Name of the field being removed
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+ """
+ if field_name in self._custom_format:
+ del self._custom_format[field_name]
+
+ def _custom_format_callback(self, field_name, old_value, new_value):
+ """Callback to remove std formats when a field is removed from custom_format.
+
+ This callback is triggered when a field is removed from _custom_format
+ dictionary, and removes the corresponding entries from _float_format,
+ _int_format, and _none_format if they exist.
+
+ Arguments:
+ field_name: Name of the field being removed
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+ """
+ if field_name in self._float_format:
+ del self._float_format[field_name]
+ if field_name in self._int_format:
+ del self._int_format[field_name]
+ if field_name in self._none_format:
+ del self._none_format[field_name]
@property
def custom_format(self) -> dict[str, Callable[[str, Any], str]]:
@@ -1216,13 +1324,24 @@ class PrettyTable:
self,
val: Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None,
):
+ """Set custom format for columns using callable functions.
+
+ Arguments:
+ val: Can be:
+ - None: Clears all custom formats
+ - dict: Dictionary mapping field names to callable functions
+ - callable: A single callable function applied to all fields
+
+ Raises:
+ TypeError: If val is not None, a dict, or a callable
+ """
if val is None:
- self._custom_format = {}
+ self._custom_format.clear()
elif isinstance(val, dict):
- for k, v in val.items():
- self._validate_function(f"custom_value.{k}", v)
- self._custom_format = val
- elif hasattr(val, "__call__"):
+ for field, fval in val.items():
+ self._validate_function(f"custom_value.{field}", fval)
+ self._custom_format[field] = fval
+ elif callable(val):
self._validate_function("custom_value", val)
for field in self._field_names:
self._custom_format[field] = val
@@ -1309,7 +1428,7 @@ class PrettyTable:
Arguments:
horizontal_align_char - single character string used to indicate alignment"""
- return self._bottom_left_junction_char or self.junction_char
+ return self._horizontal_align_char or self.junction_char
@horizontal_align_char.setter
def horizontal_align_char(self, val: str) -> None:
@@ -1318,6 +1437,23 @@ class PrettyTable:
self._horizontal_align_char = val
@property
+ def header_horizontal_char(self) -> str | None:
+ """The character used when printing the header separator line
+
+ Arguments:
+
+ header_horizontal_char - single character string used to draw the header
+ separator, or None to use the same as horizontal_char"""
+ return self._header_horizontal_char
+
+ @header_horizontal_char.setter
+ def header_horizontal_char(self, val: str | None) -> None:
+ if val is not None:
+ val = str(val)
+ self._validate_option("header_horizontal_char", val)
+ self._header_horizontal_char = val
+
+ @property
def junction_char(self) -> str:
"""The character used when printing table borders to draw line junctions
@@ -1558,7 +1694,7 @@ class PrettyTable:
options[option] = kwargs[option]
else:
options[option] = getattr(self, option)
- return cast(OptionsType, options)
+ return cast("OptionsType", options)
##############################
# PRESET STYLE LOGIC #
@@ -1579,6 +1715,8 @@ class PrettyTable:
self._set_double_border_style()
elif style == TableStyle.SINGLE_BORDER:
self._set_single_border_style()
+ elif style == TableStyle.RST:
+ self._set_rst_style()
elif style == TableStyle.RANDOM:
self._set_random_style()
elif style != TableStyle.DEFAULT:
@@ -1599,6 +1737,19 @@ class PrettyTable:
self.junction_char = "|"
self._horizontal_align_char = ":"
+ def _set_rst_style(self) -> None:
+ self.header = True
+ self.border = True
+ self._hrules = HRuleStyle.ALL
+ self.padding_width = 1
+ self.left_padding_width = 1
+ self.right_padding_width = 1
+ self.vertical_char = "|"
+ self.junction_char = "+"
+ self.horizontal_char = "-"
+ self._horizontal_align_char = None
+ self.header_horizontal_char = "="
+
def _set_default_style(self) -> None:
self.header = True
self.border = True
@@ -1610,6 +1761,7 @@ class PrettyTable:
self.vertical_char = "|"
self.horizontal_char = "-"
self._horizontal_align_char = None
+ self.header_horizontal_char = None
self.junction_char = "+"
self._top_junction_char = None
self._bottom_junction_char = None
@@ -1866,12 +2018,24 @@ class PrettyTable:
return (f"%{self._float_format[field]}f") % value
formatter = self._custom_format.get(field, (lambda f, v: str(v)))
- return formatter(field, value)
+ # PrettyTable is unaware of a terminal's tabstops, and it does not know at
+ # what specific location of the screen it will be displayed, so it also cannot
+ # calculate tabstop positions or width: A '\t' character is variable-width,
+ # depending on the location of the screen it is displayed.
+ #
+ # Although wcwidth library functions like width() do measure tab control
+ # character as a width of 8, it would require PrettyTable to display the left
+ # margin of the table's contents to begin "at the tabstop" for the table
+ # contents and dividers to line up correctly.
+ #
+ # PrettyTable is "screen unaware", so it is best to alter tabstops to a fixed
+ # width, to allow same-width display anywhere on the screen.
+ return formatter(field, value).expandtabs()
def _compute_table_width(self, options) -> int:
if options["vrules"] == VRuleStyle.FRAME:
table_width = 2
- if options["vrules"] == VRuleStyle.ALL:
+ elif options["vrules"] == VRuleStyle.ALL:
table_width = 1
else:
table_width = 0
@@ -1932,7 +2096,10 @@ class PrettyTable:
# Are we under min_table_width or title width?
if self._min_table_width or options["title"]:
if options["title"]:
- title_width = _str_block_width(options["title"]) + per_col_padding
+ title_width = (
+ max(_str_block_width(line) for line in options["title"].split("\n"))
+ + per_col_padding
+ )
if options["vrules"] in (VRuleStyle.FRAME, VRuleStyle.ALL):
title_width += 2
else:
@@ -2058,7 +2225,8 @@ class PrettyTable:
vertical_char - single character string used to draw vertical lines
horizontal_char - single character string used to draw horizontal lines
horizontal_align_char - single character string used to indicate alignment
- junction_char - single character string used to draw line junctions
+ header_horizontal_char - single character string used to draw the header
+ separator, or None to use the same as horizontal_char
junction_char - single character string used to draw line junctions
top_junction_char - single character string used to draw top line junctions
bottom_junction_char -
@@ -2103,7 +2271,10 @@ class PrettyTable:
# Add title
title = options["title"] or self._title
if title:
- lines.append(self._stringify_title(title, options))
+ if self._style != TableStyle.MARKDOWN:
+ lines.append(self._stringify_title(title, options))
+ else:
+ lines.extend([f"**{line}**" for line in title.split("\n")] + [""])
# Add header or top of border
if options["header"]:
@@ -2158,12 +2329,12 @@ class PrettyTable:
return ""
lpad, rpad = self._get_padding_widths(options)
if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
- bits = [options[where + "left_junction_char"]] # type: ignore[literal-required]
+ bits = [options[f"{where}left_junction_char"]] # type: ignore[literal-required]
else:
bits = [options["horizontal_char"]]
# For tables with no data or fieldnames
if not self._field_names:
- bits.append(options[where + "right_junction_char"]) # type: ignore[literal-required]
+ bits.append(options[f"{where}right_junction_char"]) # type: ignore[literal-required]
return "".join(bits)
for field, width in zip(self._field_names, self._widths):
if options["fields"] and field not in options["fields"]:
@@ -2180,12 +2351,12 @@ class PrettyTable:
bits.append(line)
if options["vrules"] == VRuleStyle.ALL:
- bits.append(options[where + "junction_char"]) # type: ignore[literal-required]
+ bits.append(options[f"{where}junction_char"]) # type: ignore[literal-required]
else:
bits.append(options["horizontal_char"])
if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
bits.pop()
- bits.append(options[where + "right_junction_char"]) # type: ignore[literal-required]
+ bits.append(options[f"{where}right_junction_char"]) # type: ignore[literal-required]
if options["preserve_internal_border"] and not options["border"]:
bits = bits[1:-1]
@@ -2194,7 +2365,6 @@ class PrettyTable:
def _stringify_title(self, title: str, options: OptionsType) -> str:
lines: list[str] = []
- lpad, rpad = self._get_padding_widths(options)
if options["border"]:
if options["vrules"] == VRuleStyle.ALL:
options["vrules"] = VRuleStyle.FRAME
@@ -2202,21 +2372,19 @@ class PrettyTable:
options["vrules"] = VRuleStyle.ALL
elif options["vrules"] == VRuleStyle.FRAME:
lines.append(self._stringify_hrule(options, "top_"))
- bits: list[str] = []
endpoint = (
options["vertical_char"]
if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME)
and options["border"]
else " "
)
- bits.append(endpoint)
- title = " " * lpad + title + " " * rpad
lpad, rpad = self._get_padding_widths(options)
sum_widths = sum([n + lpad + rpad + 1 for n in self._widths])
-
- bits.append(self._justify(title, sum_widths - 1, "c"))
- bits.append(endpoint)
- lines.append("".join(bits))
+ for title_line in title.split("\n"):
+ padded = " " * lpad + title_line + " " * rpad
+ lines.append(
+ endpoint + self._justify(padded, sum_widths - 1, "c") + endpoint
+ )
return "\n".join(lines)
def _stringify_header(self, options: OptionsType) -> str:
@@ -2287,11 +2455,18 @@ class PrettyTable:
"hrules"
] != HRuleStyle.NONE:
bits.append("\n")
- bits.append(self._hrule)
+ if options["header_horizontal_char"]:
+ header_options = {
+ **options,
+ "horizontal_char": options["header_horizontal_char"],
+ }
+ bits.append(self._stringify_hrule(header_options)) # type: ignore[arg-type]
+ else:
+ bits.append(self._hrule)
return "".join(bits)
def _stringify_row(self, row: list[str], options: OptionsType, hrule: str) -> str:
- import textwrap
+ import wcwidth
for index, field, value, width in zip(
range(len(row)), self._field_names, row, self._widths
@@ -2306,19 +2481,17 @@ class PrettyTable:
):
line = none_val
if _str_block_width(line) > width:
- line = textwrap.fill(
- line, width, break_on_hyphens=options["break_on_hyphens"]
+ line = "\n".join(
+ wcwidth.wrap(
+ line, width, break_on_hyphens=options["break_on_hyphens"]
+ )
)
new_lines.append(line)
lines = new_lines
value = "\n".join(lines)
row[index] = value
- row_height = 0
- for c in row:
- h = _get_size(c)[1]
- if h > row_height:
- row_height = h
+ row_height = max(_get_size(c)[1] for c in row)
bits: list[list[str]] = []
lpad, rpad = self._get_padding_widths(options)
@@ -2382,6 +2555,18 @@ class PrettyTable:
return "\n".join(bits_str)
def paginate(self, page_length: int = 58, line_break: str = "\f", **kwargs) -> str:
+ """Return string representation of table split into pages.
+
+ Arguments:
+
+ page_length - number of rows per page (default: 58)
+ line_break - string used to separate pages (default: "\f" form feed)
+ **kwargs - additional keyword arguments passed to get_string() method,
+ such as title, fields, header, border, etc.
+
+ The table is split into pages of the specified length, with each page
+ separated by the line_break character. All formatting options available
+ in get_string() can be used via kwargs."""
pages: list[str] = []
kwargs["start"] = kwargs.get("start", 0)
true_end = kwargs.get("end", self.rowcount)
@@ -2406,6 +2591,7 @@ class PrettyTable:
delimiter as a csv.writer keyword argument.
"""
import csv
+ import io
options = self._get_options(kwargs)
csv_options = {
@@ -2422,7 +2608,7 @@ class PrettyTable:
else:
csv_writer.writerow(self._field_names)
- rows = self._get_rows(options)
+ rows = self._format_rows(self._get_rows(options))
if options["fields"]:
rows = [
[d for f, d in zip(self._field_names, row) if f in options["fields"]]
@@ -2465,17 +2651,12 @@ class PrettyTable:
objects.append(self.field_names)
rows = self._get_rows(options)
if options["fields"]:
- for row in rows:
- objects.append(
- {
- f: d
- for f, d in zip(self._field_names, row)
- if f in options["fields"]
- }
- )
+ objects.extend(
+ {f: d for f, d in zip(self._field_names, row) if f in options["fields"]}
+ for row in rows
+ )
else:
- for row in rows:
- objects.append(dict(zip(self._field_names, row)))
+ objects.extend(dict(zip(self._field_names, row)) for row in rows)
return json.dumps(objects, **json_options)
@@ -2518,7 +2699,7 @@ class PrettyTable:
styling options (True or False)
escape_data - escapes the text within a data field (True or False)
xhtml - print <br/> tags if True, <br> tags if False
- break_on_hyphens - Whether long lines are broken on hypens or not, default: True
+ break_on_hyphens - Whether long lines are broken on hyphens or not, default: True
"""
options = self._get_options(kwargs)
@@ -2549,7 +2730,8 @@ class PrettyTable:
# Title
title = options["title"] or self._title
if title:
- lines.append(f" <caption>{escape(title)}</caption>")
+ caption = escape(title).replace("\n", linebreak)
+ lines.append(f" <caption>{caption}</caption>")
# Headers
if options["header"]:
@@ -2635,7 +2817,8 @@ class PrettyTable:
# Title
title = options["title"] or self._title
if title:
- lines.append(f" <caption>{escape(title)}</caption>")
+ caption = escape(title).replace("\n", linebreak)
+ lines.append(f" <caption>{caption}</caption>")
# Headers
if options["header"]:
@@ -2661,15 +2844,14 @@ class PrettyTable:
lines.append(" <tbody>")
rows = self._get_rows(options)
formatted_rows = self._format_rows(rows)
- aligns: list[str] = []
- valigns: list[str] = []
- for field in self._field_names:
- aligns.append(
- {"l": "left", "r": "right", "c": "center"}[self._align[field]]
- )
- valigns.append(
- {"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]]
- )
+ aligns: list[str] = [
+ {"l": "left", "r": "right", "c": "center"}[self._align[field]]
+ for field in self._field_names
+ ]
+ valigns: list[str] = [
+ {"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]]
+ for field in self._field_names
+ ]
for row in formatted_rows:
lines.append(" <tr>")
for field, datum, align, valign in zip(
@@ -2734,7 +2916,6 @@ class PrettyTable:
def _get_simple_latex_string(self, options: OptionsType) -> str:
lines: list[str] = []
- wanted_fields = []
if options["fields"]:
wanted_fields = [
field for field in self._field_names if field in options["fields"]
@@ -2744,12 +2925,12 @@ class PrettyTable:
alignments = "".join([self._align[field] for field in wanted_fields])
- begin_cmd = f"\\begin{{tabular}}{{{alignments}}}"
+ begin_cmd = rf"\begin{{tabular}}{{{alignments}}}"
lines.append(begin_cmd)
# Headers
if options["header"]:
- lines.append(" & ".join(wanted_fields) + " \\\\")
+ lines.append(" & ".join(wanted_fields) + r" \\")
# Data
rows = self._get_rows(options)
@@ -2758,9 +2939,9 @@ class PrettyTable:
wanted_data = [
d for f, d in zip(self._field_names, row) if f in wanted_fields
]
- lines.append(" & ".join(wanted_data) + " \\\\")
+ lines.append(" & ".join(wanted_data) + r" \\")
- lines.append("\\end{tabular}")
+ lines.append(r"\end{tabular}")
return "\r\n".join(lines)
@@ -2786,40 +2967,39 @@ class PrettyTable:
VRuleStyle.ALL,
VRuleStyle.FRAME,
]:
- alignment_str = "|" + alignment_str + "|"
+ alignment_str = f"|{alignment_str}|"
- begin_cmd = f"\\begin{{tabular}}{{{alignment_str}}}"
+ begin_cmd = rf"\begin{{tabular}}{{{alignment_str}}}"
lines.append(begin_cmd)
if options["border"] and options["hrules"] in [
HRuleStyle.ALL,
HRuleStyle.FRAME,
]:
- lines.append("\\hline")
+ lines.append(r"\hline")
# Headers
if options["header"]:
- lines.append(" & ".join(wanted_fields) + " \\\\")
+ lines.append(" & ".join(wanted_fields) + r" \\")
if (options["border"] or options["preserve_internal_border"]) and options[
"hrules"
] in [HRuleStyle.ALL, HRuleStyle.HEADER]:
- lines.append("\\hline")
+ lines.append(r"\hline")
# Data
rows = self._get_rows(options)
formatted_rows = self._format_rows(rows)
- rows = self._get_rows(options)
for row in formatted_rows:
wanted_data = [
d for f, d in zip(self._field_names, row) if f in wanted_fields
]
- lines.append(" & ".join(wanted_data) + " \\\\")
+ lines.append(" & ".join(wanted_data) + r" \\")
if options["border"] and options["hrules"] == HRuleStyle.ALL:
- lines.append("\\hline")
+ lines.append(r"\hline")
if options["border"] and options["hrules"] == HRuleStyle.FRAME:
- lines.append("\\hline")
+ lines.append(r"\hline")
- lines.append("\\end{tabular}")
+ lines.append(r"\end{tabular}")
return "\r\n".join(lines)
@@ -2845,42 +3025,42 @@ class PrettyTable:
options = self._get_options(kwargs)
lines: list[str] = []
- if (
- options.get("attributes")
- and isinstance(options["attributes"], dict)
- and options["attributes"]
- ):
- attr_str = " ".join(f'{k}="{v}"' for k, v in options["attributes"].items())
- lines.append("{| " + attr_str)
+ attributes_option = options.get("attributes")
+ if attributes_option and isinstance(attributes_option, dict):
+ attr_str = " ".join(f'{k}="{v}"' for k, v in attributes_option.items())
+ lines.append(f"{{| {attr_str}")
else:
lines.append('{| class="wikitable"')
caption = options.get("title", self._title)
if caption:
- lines.append("|+ " + caption)
+ lines.append(f"|+ {caption}")
+ fields_option = options.get("fields")
if options.get("header"):
lines.append("|-")
- headers = []
- fields_option = options.get("fields")
- for field in self._field_names:
- if fields_option is not None and field not in fields_option:
- continue
- headers.append(field)
+ if fields_option is None:
+ headers = self._field_names
+ else:
+ headers = [
+ field for field in self._field_names if field in fields_option
+ ]
if headers:
header_line = " !! ".join(headers)
- lines.append("! " + header_line)
+ lines.append(f"! {header_line}")
rows = self._get_rows(options)
formatted_rows = self._format_rows(rows)
for row in formatted_rows:
lines.append("|-")
- cells = []
- fields_option = options.get("fields")
- for field, cell in zip(self._field_names, row):
- if fields_option is not None and field not in fields_option:
- continue
- cells.append(cell)
+ if fields_option is None:
+ cells = row
+ else:
+ cells = [
+ cell
+ for field, cell in zip(self._field_names, row)
+ if field in fields_option
+ ]
if cells:
lines.append("| " + " || ".join(cells))
@@ -2897,8 +3077,7 @@ class PrettyTable:
def _str_block_width(val: str) -> int:
import wcwidth
- val = _osc8_re.sub(r"\1", val)
- return wcwidth.wcswidth(_re.sub("", val))
+ return wcwidth.width(val)
##############################
@@ -2941,7 +3120,7 @@ def from_csv(fp, field_names: Sequence[str] | None = None, **kwargs) -> PrettyTa
return table
-def from_db_cursor(cursor: Cursor, **kwargs) -> PrettyTable | None:
+def from_db_cursor(cursor: Any, **kwargs) -> PrettyTable | None:
if cursor.description:
table = PrettyTable(**kwargs)
table.field_names = [col[0] for col in cursor.description]
@@ -2963,77 +3142,101 @@ def from_json(json_string: str | bytes, **kwargs) -> PrettyTable:
return table
-class TableHandler(HTMLParser):
- def __init__(self, **kwargs) -> None:
- HTMLParser.__init__(self)
- self.kwargs = kwargs
- self.tables: list[PrettyTable] = []
- self.last_row: list[str] = []
- self.rows: list[tuple[list[str], bool]] = []
- self.max_row_width = 0
- self.active: str | None = None
- self.last_content = ""
- self.is_last_row_header = False
- self.colspan = 0
+def _make_table_handler():
+ from html.parser import HTMLParser
- def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
- self.active = tag
- if tag == "th":
- self.is_last_row_header = True
- for key, value in attrs:
- if key == "colspan":
- self.colspan = int(value) # type: ignore[arg-type]
+ class _TableHandler(HTMLParser):
+ def __init__(self, **kwargs) -> None:
+ HTMLParser.__init__(self)
+ self.kwargs = kwargs
+ self.tables: list[PrettyTable] = []
+ self.last_row: list[str] = []
+ self.rows: list[tuple[list[str], bool]] = []
+ self.max_row_width = 0
+ self.active: str | None = None
+ self.last_content = ""
+ self.is_last_row_header = False
+ self.colspan = 0
- def handle_endtag(self, tag: str) -> None:
- if tag in ["th", "td"]:
- stripped_content = self.last_content.strip()
- self.last_row.append(stripped_content)
- if self.colspan:
- for _ in range(1, self.colspan):
- self.last_row.append("")
- self.colspan = 0
+ def handle_starttag(
+ self, tag: str, attrs: list[tuple[str, str | None]]
+ ) -> None:
+ self.active = tag
+ if tag == "th":
+ self.is_last_row_header = True
+ for key, value in attrs:
+ if key == "colspan":
+ self.colspan = int(value) # type: ignore[arg-type]
- if tag == "tr":
- self.rows.append((self.last_row, self.is_last_row_header))
- self.max_row_width = max(self.max_row_width, len(self.last_row))
- self.last_row = []
- self.is_last_row_header = False
- if tag == "table":
- table = self.generate_table(self.rows)
- self.tables.append(table)
- self.rows = []
- self.last_content = " "
- self.active = None
+ def handle_endtag(self, tag: str) -> None:
+ if tag in ["th", "td"]:
+ stripped_content = self.last_content.strip()
+ self.last_row.append(stripped_content)
+ if self.colspan:
+ for _ in range(1, self.colspan):
+ self.last_row.append("")
+ self.colspan = 0
- def handle_data(self, data: str) -> None:
- self.last_content += data
+ if tag == "tr":
+ self.rows.append((self.last_row, self.is_last_row_header))
+ self.max_row_width = max(self.max_row_width, len(self.last_row))
+ self.last_row = []
+ self.is_last_row_header = False
+ if tag == "table":
+ table = self.generate_table(self.rows)
+ self.tables.append(table)
+ self.rows = []
+ self.last_content = " "
+ self.active = None
- def generate_table(self, rows: list[tuple[list[str], bool]]) -> PrettyTable:
- """
- Generates from a list of rows a PrettyTable object.
- """
- table = PrettyTable(**self.kwargs)
- for row in self.rows:
- if len(row[0]) < self.max_row_width:
- appends = self.max_row_width - len(row[0])
- for i in range(1, appends):
- row[0].append("-")
+ def handle_data(self, data: str) -> None:
+ self.last_content += data
- if row[1]:
- self.make_fields_unique(row[0])
- table.field_names = row[0]
- else:
- table.add_row(row[0])
- return table
+ def generate_table(self, rows: list[tuple[list[str], bool]]) -> PrettyTable:
+ """
+ Generates from a list of rows a PrettyTable object.
+ """
+ table = PrettyTable(**self.kwargs)
+ for row in self.rows:
+ if len(row[0]) < self.max_row_width:
+ appends = self.max_row_width - len(row[0])
+ for i in range(1, appends):
+ row[0].append("-")
- def make_fields_unique(self, fields: list[str]) -> None:
- """
- iterates over the row and make each field unique
- """
- for i in range(len(fields)):
- for j in range(i + 1, len(fields)):
- if fields[i] == fields[j]:
- fields[j] += "'"
+ if row[1]:
+ self.make_fields_unique(row[0])
+ table.field_names = row[0]
+ else:
+ table.add_row(row[0])
+ return table
+
+ def make_fields_unique(self, fields: list[str]) -> None:
+ """
+ iterates over the row and make each field unique
+ """
+ for i in range(len(fields)):
+ for j in range(i + 1, len(fields)):
+ if fields[i] == fields[j]:
+ fields[j] += "'"
+
+ return _TableHandler
+
+
+class TableHandler:
+ """Deprecated: use from_html or from_html_one instead."""
+
+ def __init__(self, **kwargs) -> None:
+ import warnings
+
+ warnings.warn(
+ "TableHandler is deprecated and will be removed in a future release. "
+ "Use from_html() or from_html_one() instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ cls = _make_table_handler()
+ self.__class__ = cls
+ cls.__init__(self, **kwargs)
def from_html(html_code: str, **kwargs) -> list[PrettyTable]:
@@ -3041,8 +3244,7 @@ def from_html(html_code: str, **kwargs) -> list[PrettyTable]:
Generates a list of PrettyTables from a string of HTML code. Each <table> in
the HTML becomes one PrettyTable object.
"""
-
- parser = TableHandler(**kwargs)
+ parser = _make_table_handler()(**kwargs)
parser.feed(html_code)
return parser.tables
@@ -3089,11 +3291,11 @@ def from_mediawiki(wiki_text: str, **kwargs) -> PrettyTable:
if line.startswith("|+"):
continue
if line.startswith("!"):
- header = [cell.strip() for cell in re.split(r"\s*!!\s*", line[1:])]
+ header = [cell.strip() for cell in line[1:].split("!!")]
table.field_names = header
continue
if line.startswith("|"):
- row_data = [cell.strip() for cell in re.split(r"\s*\|\|\s*", line[1:])]
+ row_data = [cell.strip() for cell in line[1:].split("||")]
rows.append(row_data)
continue
@@ -3129,4 +3331,20 @@ def _warn_deprecation(name: str, module_globals: dict[str, Any]) -> Any:
def __getattr__(name: str) -> Any:
+ if name == "OptionsType":
+ import warnings
+ from typing import TypedDict
+
+ warnings.warn(
+ "OptionsType is deprecated and will be removed in a future release",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ class OptionsType(TypedDict):
+ pass
+
+ globals()[name] = OptionsType
+ return OptionsType
+
return _warn_deprecation(name, module_globals=globals())
diff --git a/contrib/python/prettytable/py3/tests/data/table_align_center.txt b/contrib/python/prettytable/py3/tests/data/table_align_center.txt
new file mode 100644
index 00000000000..c25becb19d7
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_align_center.txt
@@ -0,0 +1,6 @@
++------+
+| Name |
++------+
+| 👨‍👩‍👧 |
+| Hi |
++------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_align_left.txt b/contrib/python/prettytable/py3/tests/data/table_align_left.txt
new file mode 100644
index 00000000000..ad65ed94378
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_align_left.txt
@@ -0,0 +1,6 @@
++------+
+| Name |
++------+
+| 👨‍👩‍👧 |
+| Hi |
++------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_align_right.txt b/contrib/python/prettytable/py3/tests/data/table_align_right.txt
new file mode 100644
index 00000000000..3e1e73875d0
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_align_right.txt
@@ -0,0 +1,6 @@
++------+
+| Name |
++------+
+| 👨‍👩‍👧 |
+| Hi |
++------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_ansi_colors.txt b/contrib/python/prettytable/py3/tests/data/table_ansi_colors.txt
new file mode 100644
index 00000000000..4dc5d86f520
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_ansi_colors.txt
@@ -0,0 +1,7 @@
++--------+-------+
+| Status | Count |
++--------+-------+
+| OK | 10 |
+| Failed | 2 |
+| Normal | 5 |
++--------+-------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_cjk.txt b/contrib/python/prettytable/py3/tests/data/table_cjk.txt
new file mode 100644
index 00000000000..e928b529282
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_cjk.txt
@@ -0,0 +1,6 @@
++------+-------+
+| CJK | Width |
++------+-------+
+| 中文 | 4 |
+| Test | 4 |
++------+-------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_combining_chars.txt b/contrib/python/prettytable/py3/tests/data/table_combining_chars.txt
new file mode 100644
index 00000000000..c0da2c2b05f
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_combining_chars.txt
@@ -0,0 +1,6 @@
++------+------+
+| Word | Lang |
++------+------+
+| café | FR |
+| cafe | EN |
++------+------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_complex_emoji.txt b/contrib/python/prettytable/py3/tests/data/table_complex_emoji.txt
new file mode 100644
index 00000000000..2a39e8164f3
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_complex_emoji.txt
@@ -0,0 +1,7 @@
++-------+--------+
+| Emoji | Name |
++-------+--------+
+| 👨‍👩‍👧 | Family |
+| 🇺🇸 | USA |
+| Hi | Text |
++-------+--------+
diff --git a/contrib/python/prettytable/py3/tests/test_colortable.py b/contrib/python/prettytable/py3/tests/test_colortable.py
index 40a287169a0..5f25acf7c6b 100644
--- a/contrib/python/prettytable/py3/tests/test_colortable.py
+++ b/contrib/python/prettytable/py3/tests/test_colortable.py
@@ -157,7 +157,7 @@ class TestColorTableRendering:
table.header = with_header
table.add_row([1, 2, 3, 4, 5, 6])
- expected = header_str + "\n" + body_str + "\x1b[0m"
+ expected = f"{header_str}\n{body_str}\x1b[0m"
result = str(table)
assert expected == result
@@ -170,7 +170,7 @@ class TestColorTableRendering:
table.title = "Theme Test"
table.add_row([1, 2, 3, 4, 5, 6])
- for theme_name, theme in vars(Themes).items():
+ for theme in vars(Themes).values():
if isinstance(theme, Theme):
table.theme = theme
result = str(table)
diff --git a/contrib/python/prettytable/py3/tests/test_html.py b/contrib/python/prettytable/py3/tests/test_html.py
index 0c3ef03a21d..acfd31c2a00 100644
--- a/contrib/python/prettytable/py3/tests/test_html.py
+++ b/contrib/python/prettytable/py3/tests/test_html.py
@@ -26,9 +26,7 @@ class TestHtmlConstructor:
class TestHtmlOutput:
def test_html_output(self, helper_table: PrettyTable) -> None:
result = helper_table.get_html_string()
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -60,13 +58,10 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_formatted(self, helper_table: PrettyTable) -> None:
result = helper_table.get_html_string(format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -97,17 +92,14 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_with_title(self, helper_table: PrettyTable) -> None:
helper_table.title = "Title & Title"
result = helper_table.get_html_string(
attributes={"bgcolor": "red", "a<b": "1<2"}
)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table bgcolor="red" a&lt;b="1&lt;2">
<caption>Title &amp; Title</caption>
<thead>
@@ -140,16 +132,13 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_formatted_with_title(self, helper_table: PrettyTable) -> None:
helper_table.title = "Title & Title"
result = helper_table.get_html_string(
attributes={"bgcolor": "red", "a<b": "1<2"}, format=True
)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols" bgcolor="red" a&lt;b="1&lt;2">
<caption>Title &amp; Title</caption>
<thead>
@@ -181,8 +170,7 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_without_escaped_header(
self, empty_helper_table: PrettyTable
@@ -194,9 +182,7 @@ class TestHtmlOutput:
"<a href='#'>Field 3</a>",
]
result = empty_helper_table.get_html_string(escape_header=False)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -210,7 +196,6 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_without_escaped_data(
self, empty_helper_table: PrettyTable
@@ -224,9 +209,7 @@ class TestHtmlOutput:
]
)
result = empty_helper_table.get_html_string(escape_data=False)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -246,7 +229,6 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_with_escaped_header(
self, empty_helper_table: PrettyTable
@@ -258,9 +240,7 @@ class TestHtmlOutput:
"<a href='#'>Field 3</a>",
]
result = empty_helper_table.get_html_string(escape_header=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -274,7 +254,6 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_with_escaped_data(
self, empty_helper_table: PrettyTable
@@ -288,9 +267,7 @@ class TestHtmlOutput:
]
)
result = empty_helper_table.get_html_string(escape_data=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -309,8 +286,7 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_formatted_without_escaped_header(
self, empty_helper_table: PrettyTable
@@ -322,9 +298,7 @@ class TestHtmlOutput:
"<a href='#'>Field 3</a>",
]
result = empty_helper_table.get_html_string(escape_header=False, format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -337,8 +311,7 @@ class TestHtmlOutput:
<tbody>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_formatted_without_escaped_data(
self, empty_helper_table: PrettyTable
@@ -352,9 +325,7 @@ class TestHtmlOutput:
]
)
result = empty_helper_table.get_html_string(escape_data=False, format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -373,8 +344,7 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_formatted_with_escaped_header(
self, empty_helper_table: PrettyTable
@@ -386,9 +356,7 @@ class TestHtmlOutput:
"<a href='#'>Field 3</a>",
]
result = empty_helper_table.get_html_string(escape_header=True, format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -401,8 +369,7 @@ class TestHtmlOutput:
<tbody>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_formatted_with_escaped_data(
self, empty_helper_table: PrettyTable
@@ -416,9 +383,7 @@ class TestHtmlOutput:
]
)
result = empty_helper_table.get_html_string(escape_data=True, format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -437,8 +402,47 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
+
+ def test_escape_ctor(self) -> None:
+ table = PrettyTable(["<>", "&"], escape_data=False, escape_header=False)
+ table.add_row(["<>", "&"])
+ assert table.get_html_string().strip() == """
+<table>
+ <thead>
+ <tr>
+ <th><></th>
+ <th>&</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><></td>
+ <td>&</td>
+ </tr>
+ </tbody>
+</table>""".strip()
+
+ def test_escape_setter(self) -> None:
+ table = PrettyTable(["<>", "&"])
+ table.escape_data = False
+ table.escape_header = False
+ table.add_row(["<>", "&"])
+ assert table.get_html_string().strip() == """
+<table>
+ <thead>
+ <tr>
+ <th><></th>
+ <th>&</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><></td>
+ <td>&</td>
+ </tr>
+ </tbody>
+</table>""".strip()
def test_table_formatted_html_autoindex(self) -> None:
"""See also #199"""
@@ -450,9 +454,7 @@ class TestHtmlOutput:
table.format = True
table.add_autoindex("I")
- assert (
- table.get_html_string().strip()
- == """
+ assert table.get_html_string().strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -482,17 +484,14 @@ class TestHtmlOutput:
<td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 207</td>
</tr>
</tbody>
-</table>""".strip() # noqa: E501
- )
+</table>""".strip()
def test_internal_border_preserved_html(self, helper_table: PrettyTable) -> None:
helper_table.format = True
helper_table.border = False
helper_table.preserve_internal_border = True
- assert (
- helper_table.get_html_string().strip()
- == """
+ assert helper_table.get_html_string().strip() == """
<table rules="cols">
<thead>
<tr>
@@ -523,17 +522,14 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_break_line_html(self) -> None:
table = PrettyTable(["Field 1", "Field 2"])
table.add_row(["value 1", "value2\nsecond line"])
table.add_row(["value 3", "value4"])
result = table.get_html_string(hrules=HRuleStyle.ALL)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -553,16 +549,13 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_break_line_xhtml(self) -> None:
table = PrettyTable(["Field 1", "Field 2"])
table.add_row(["value 1", "value2\nsecond line"])
table.add_row(["value 3", "value4"])
result = table.get_html_string(hrules=HRuleStyle.ALL, xhtml=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -582,4 +575,3 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
diff --git a/contrib/python/prettytable/py3/tests/test_json.py b/contrib/python/prettytable/py3/tests/test_json.py
index 0e2b3659397..c04b72c2876 100644
--- a/contrib/python/prettytable/py3/tests/test_json.py
+++ b/contrib/python/prettytable/py3/tests/test_json.py
@@ -6,9 +6,7 @@ from prettytable import PrettyTable, from_json
class TestJSONOutput:
def test_json_output(self, helper_table: PrettyTable) -> None:
result = helper_table.get_json_string()
- assert (
- result.strip()
- == """
+ assert result.strip() == """
[
[
"",
@@ -35,12 +33,9 @@ class TestJSONOutput:
"Field 3": "value9"
}
]""".strip()
- )
options = {"fields": ["Field 1", "Field 3"]}
result = helper_table.get_json_string(**options)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
[
[
"Field 1",
@@ -59,7 +54,6 @@ class TestJSONOutput:
"Field 3": "value9"
}
]""".strip()
- )
def test_json_output_options(self, helper_table: PrettyTable) -> None:
result = helper_table.get_json_string(
diff --git a/contrib/python/prettytable/py3/tests/test_mediawiki.py b/contrib/python/prettytable/py3/tests/test_mediawiki.py
index 7684b617324..f472440a53a 100644
--- a/contrib/python/prettytable/py3/tests/test_mediawiki.py
+++ b/contrib/python/prettytable/py3/tests/test_mediawiki.py
@@ -7,9 +7,7 @@ from prettytable import PrettyTable, from_mediawiki
class TestMediaWikiOutput:
def test_mediawiki_output(self, helper_table: PrettyTable) -> None:
- assert (
- helper_table.get_mediawiki_string(header=True).strip()
- == """
+ assert helper_table.get_mediawiki_string(header=True).strip() == """
{| class="wikitable"
|-
! !! Field 1 !! Field 2 !! Field 3
@@ -21,12 +19,9 @@ class TestMediaWikiOutput:
| 7 || value 7 || value8 || value9
|}
""".strip()
- )
def test_mediawiki_output_without_header(self, helper_table: PrettyTable) -> None:
- assert (
- helper_table.get_mediawiki_string(header=False).strip()
- == """
+ assert helper_table.get_mediawiki_string(header=False).strip() == """
{| class="wikitable"
|-
| 1 || value 1 || value2 || value3
@@ -36,7 +31,6 @@ class TestMediaWikiOutput:
| 7 || value 7 || value8 || value9
|}
""".strip()
- )
def test_mediawiki_output_with_caption(self, helper_table: PrettyTable) -> None:
assert (
diff --git a/contrib/python/prettytable/py3/tests/test_prettytable.py b/contrib/python/prettytable/py3/tests/test_prettytable.py
index 7d85fad86f1..01bf4ac0cb5 100644
--- a/contrib/python/prettytable/py3/tests/test_prettytable.py
+++ b/contrib/python/prettytable/py3/tests/test_prettytable.py
@@ -52,107 +52,112 @@ class TestNoneOption:
def test_no_value_replace_none(self) -> None:
table = PrettyTable(["Field 1", "Field 2", "Field 3"])
table.add_row(["value 1", None, "value 2"])
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+---------+---------+
| Field 1 | Field 2 | Field 3 |
+---------+---------+---------+
| value 1 | None | value 2 |
+---------+---------+---------+
""".strip()
- )
def test_no_value_replace_none_with_default_field_names(self) -> None:
table = PrettyTable()
table.add_row(["value 1", "None", "value 2"])
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+---------+---------+
| Field 1 | Field 2 | Field 3 |
+---------+---------+---------+
| value 1 | None | value 2 |
+---------+---------+---------+
""".strip()
- )
def test_replace_none_all(self) -> None:
table = PrettyTable(
["Field 1", "Field 2", "Field 3", "Field 4"], none_format="N/A"
)
table.add_row(["value 1", None, "None", ""])
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+---------+---------+---------+
| Field 1 | Field 2 | Field 3 | Field 4 |
+---------+---------+---------+---------+
| value 1 | N/A | N/A | |
+---------+---------+---------+---------+
""".strip()
- )
def test_replace_none_by_col(self) -> None:
table = PrettyTable(["Field 1", "Field 2", "Field 3"])
table.none_format["Field 2"] = "N/A"
table.none_format["Field 3"] = ""
table.add_row(["value 1", None, None])
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+---------+---------+
| Field 1 | Field 2 | Field 3 |
+---------+---------+---------+
| value 1 | N/A | |
+---------+---------+---------+
""".strip()
- )
def test_replace_none_recompute_width(self) -> None:
table = PrettyTable()
table.add_row([None])
table.none_format = "0123456789"
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+------------+
| Field 1 |
+------------+
| 0123456789 |
+------------+
""".strip()
- )
def test_replace_none_maintain_width_on_recompute(self) -> None:
table = PrettyTable()
table.add_row(["Hello"])
table.none_format = "0123456789"
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+
| Field 1 |
+---------+
| Hello |
+---------+
""".strip()
- )
def test_replace_none_recompute_width_multi_column(self) -> None:
table = PrettyTable()
table.add_row(["Hello", None, "World"])
table.none_format = "0123456789"
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
++---------+------------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+------------+---------+
+| Hello | 0123456789 | World |
++---------+------------+---------+
+""".strip()
+
+ def test_replace_custom_format_with_none(self) -> None:
+ table = PrettyTable()
+ table.custom_format = {"Field 2": lambda f, v: f"'{v}'"}
+ table.add_row(["Hello", None, "World"])
+ table.none_format = "0123456789"
+ assert table.get_string().strip() == """
++---------+------------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+------------+---------+
+| Hello | 0123456789 | World |
++---------+------------+---------+
+""".strip()
+
+ def test_replace_custom_format_with_none_dict(self) -> None:
+ table = PrettyTable()
+ table.custom_format = {"Field 2": lambda f, v: f"'{v}'"}
+ table.add_row(["Hello", None, "World"])
+ table.none_format = {"Field 2": "0123456789"}
+ assert table.get_string().strip() == """
+---------+------------+---------+
| Field 1 | Field 2 | Field 3 |
+---------+------------+---------+
| Hello | 0123456789 | World |
+---------+------------+---------+
""".strip()
- )
class TestBuildEquivalence:
@@ -361,6 +366,50 @@ class TestAlignment:
header=True
) == aligned_after_table.get_mediawiki_string(header=True)
+ def test_aligned_one_column(self, city_data: PrettyTable) -> None:
+ city_data.align["Population"] = "r"
+ assert city_data.get_string() == """
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Sydney | 2058 | 4336374 | 1214.8 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
++-----------+------+------------+-----------------+""".strip()
+
+ def test_aligned_one_column_invalid(self, city_data: PrettyTable) -> None:
+ with pytest.raises(ValueError):
+ city_data.align["Population"] = "unexpected" # type: ignore[assignment]
+
+ def test_aligned_one_column_invalid_dict(self, city_data: PrettyTable) -> None:
+ with pytest.raises(ValueError):
+ city_data.align = {"Population": "unexpected"} # type: ignore[dict-item]
+
+ def test_rename_drops_stale_align_keys(self) -> None:
+ table = PrettyTable()
+ table.field_names = ["a", "b", "c"]
+ table.add_row([1, 2, 3])
+ table.align["a"] = "l"
+ table.field_names = ["x", "y", "z"]
+ assert set(table.field_names) == {"x", "y", "z"}
+ for old in ("a", "b", "c"):
+ assert old not in table._align
+ assert old not in table._valign
+ assert table.align["x"] == "l"
+
+ def test_rename_keeps_overlapping_align_keys(self) -> None:
+ table = PrettyTable()
+ table.field_names = ["a", "b", "c"]
+ table.add_row([1, 2, 3])
+ table.field_names = ["x", "b", "z"]
+ assert "b" in table._align
+ assert "a" not in table._align
+ assert "c" not in table._align
+
class TestOptionOverride:
"""Make sure all options are properly overwritten by get_string."""
@@ -415,11 +464,42 @@ class TestOptionAttribute:
assert city_data.get_string() == city_data[:].get_string()
def test_set_for_one_column(self, city_data: PrettyTable) -> None:
- city_data.align["Rainfall"] = "l"
- city_data.max_width["Name"] = 10
- city_data.int_format["Population"] = "4"
- city_data.float_format["Area"] = "2.2"
- assert city_data.get_string() == city_data[:].get_string()
+ city_data.min_width["City name"] = 15
+ city_data.int_format["Area"] = "6"
+ city_data.float_format["Annual Rainfall"] = "6.2"
+ city_data.int_format["Population"] = "10"
+ city_data.align["Annual Rainfall"] = "l"
+ assert city_data.get_string() == """
++-----------------+--------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------------+--------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.50 |
+| Brisbane | 5905 | 1857594 | 1146.40 |
+| Darwin | 112 | 120900 | 1714.70 |
+| Hobart | 1357 | 205556 | 619.50 |
+| Sydney | 2058 | 4336374 | 1214.80 |
+| Melbourne | 1566 | 3806092 | 646.90 |
+| Perth | 5386 | 1554769 | 869.40 |
++-----------------+--------+------------+-----------------+""".strip()
+
+ def test_set_for_one_column2(self, city_data: PrettyTable) -> None:
+ city_data.min_width = {"City name": 15}
+ city_data.int_format = {"Area": "6"}
+ city_data.float_format = {"Annual Rainfall": "6.2"}
+ city_data.int_format = {"Population": "10"}
+ city_data.align = {"Annual Rainfall": "l"}
+ assert city_data.get_string() == """
++-----------------+--------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------------+--------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.50 |
+| Brisbane | 5905 | 1857594 | 1146.40 |
+| Darwin | 112 | 120900 | 1714.70 |
+| Hobart | 1357 | 205556 | 619.50 |
+| Sydney | 2058 | 4336374 | 1214.80 |
+| Melbourne | 1566 | 3806092 | 646.90 |
+| Perth | 5386 | 1554769 | 869.40 |
++-----------------+--------+------------+-----------------+""".strip()
def test_preserve_internal_border(self) -> None:
table = PrettyTable(preserve_internal_border=True)
@@ -429,16 +509,13 @@ class TestOptionAttribute:
helper_table.border = False
helper_table.preserve_internal_border = True
- assert (
- helper_table.get_string().strip()
- == """
+ assert helper_table.get_string().strip() == """
| Field 1 | Field 2 | Field 3
---+---------+---------+---------
1 | value 1 | value2 | value3
4 | value 4 | value5 | value6
7 | value 7 | value8 | value9
""".strip() # noqa: W291
- )
@pytest.fixture(scope="module")
@@ -481,6 +558,15 @@ class TestBasic:
table.add_rows([])
assert str(city_data) == str(table)
+ def test_add_row_toolong(self) -> None:
+ table = PrettyTable(CITY_DATA_HEADER)
+ with pytest.raises(ValueError):
+ table.add_row(["Geelong", 1, 308915, 123.1, 1])
+
+ def test_add_column_invalid(self, city_data: PrettyTable) -> None:
+ with pytest.raises(ValueError):
+ city_data.add_column("City name", ["Geelong", 2, 3, 4, 5, 6, 7, 8, 9, 10])
+
def _test_no_blank_lines(self, table: PrettyTable) -> None:
string = table.get_string()
lines = string.split("\n")
@@ -515,6 +601,33 @@ class TestBasic:
city_data.title = "My table (75 characters wide) " + "=" * 45
self._test_all_length_equal(city_data)
+ def test_multiline_title(self, city_data: PrettyTable) -> None:
+ """A title with \\n should produce multiple bordered title lines."""
+ city_data.title = "Line 1\nLine 2"
+ assert city_data.get_string() == """
++-------------------------------------------------+
+| Line 1 |
+| Line 2 |
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Sydney | 2058 | 4336374 | 1214.8 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
++-----------+------+------------+-----------------+
+""".strip()
+
+ def test_multiline_title_html(self, city_data: PrettyTable) -> None:
+ """Multiline titles should use <br> in HTML caption."""
+ city_data.title = "Line 1\nLine 2"
+ html = city_data.get_html_string()
+ assert "<br>" in html
+ assert "<caption>" in html
+
def test_no_blank_lines_without_border(self, city_data: PrettyTable) -> None:
"""No table should ever have blank lines in it."""
city_data.border = False
@@ -577,12 +690,12 @@ class TestBasic:
def test_no_blank_lines_with_float_format(self, city_data: PrettyTable) -> None:
"""No table should ever have blank lines in it."""
- city_data.float_format = "6.2f"
+ city_data.float_format = "6.2"
self._test_no_blank_lines(city_data)
def test_all_lengths_equal_with_float_format(self, city_data: PrettyTable) -> None:
"""All lines in a table should be of the same length."""
- city_data.float_format = "6.2f"
+ city_data.float_format = "6.2"
self._test_all_length_equal(city_data)
def test_no_blank_lines_from_csv(self, city_data_from_csv: PrettyTable) -> None:
@@ -611,6 +724,10 @@ class TestBasic:
def test_colcount(self, city_data: PrettyTable) -> None:
assert city_data.colcount == 4
+ def test_colcount_empty(self) -> None:
+ table = PrettyTable()
+ assert table.colcount == 0
+
def test_getitem(self, city_data: PrettyTable) -> None:
assert (
city_data[1].get_string()
@@ -625,6 +742,83 @@ class TestBasic:
with pytest.raises(IndexError):
assert city_data[10]
+ def test_header_style_upper(self) -> None:
+ table = PrettyTable(header_style="upper")
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+---------+
+| FIELD 1 | FIELD 2 | FIELD 3 |
++---------+---------+---------+
+| 1 | 2 | 3 |
++---------+---------+---------+
+""".strip()
+
+ def test_header_style_upper_setter(self) -> None:
+ table = PrettyTable()
+ table.header_style = "upper"
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+---------+
+| FIELD 1 | FIELD 2 | FIELD 3 |
++---------+---------+---------+
+| 1 | 2 | 3 |
++---------+---------+---------+
+""".strip()
+
+ def test_header_style_cap(self) -> None:
+ table = PrettyTable(["cOl oNE", "col two", "COL THREE"], header_style="cap")
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+-----------+
+| Col one | Col two | Col three |
++---------+---------+-----------+
+| 1 | 2 | 3 |
++---------+---------+-----------+
+""".strip()
+
+ def test_header_style_title(self) -> None:
+ table = PrettyTable(["cOl oNE", "col two", "COL THREE"], header_style="title")
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+-----------+
+| Col One | Col Two | Col Three |
++---------+---------+-----------+
+| 1 | 2 | 3 |
++---------+---------+-----------+
+""".strip()
+
+ def test_header_style_lower(self) -> None:
+ table = PrettyTable(header_style="lower")
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+---------+
+| field 1 | field 2 | field 3 |
++---------+---------+---------+
+| 1 | 2 | 3 |
++---------+---------+---------+
+""".strip()
+
+ def test_horizontal_align_char_setter(self) -> None:
+ table = PrettyTable()
+ table.horizontal_align_char = "!"
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++ !-----! + !-----! + !-----! +
+| Field 1 | Field 2 | Field 3 |
++ !-----! + !-----! + !-----! +
+| 1 | 2 | 3 |
++ !-----! + !-----! + !-----! +
+""".strip()
+
+ def test_horizontal_align_char_getter(self) -> None:
+ table = PrettyTable()
+ table.horizontal_align_char = ":"
+ assert table.horizontal_align_char == ":"
+
+ def test_header_style_invalid(self) -> None:
+ with pytest.raises(ValueError):
+ PrettyTable(header_style="FooBar")
+
@pytest.mark.usefixtures("init_db")
def test_no_blank_lines_from_db(self, db_cursor: sqlite3.Cursor) -> None:
"""No table should ever have blank lines in it."""
@@ -641,10 +835,38 @@ class TestBasic:
assert table is not None
self._test_all_length_equal(table)
+ def test_border(self) -> None:
+ table = PrettyTable(border=False)
+ table.add_row([1, 2, "3"])
+ assert table.get_string().strip() == """
+Field 1 Field 2 Field 3
+ 1 2 3 """.strip() # noqa: W291
+
+ def test_reversesort(self) -> None:
+ table = PrettyTable(["A", "B", "C"], sortby="A", reversesort=True)
+ table.add_row([1, 2, "3"])
+ table.add_row([3, 4, "5"])
+ assert table.get_string().strip() == """
++---+---+---+
+| A | B | C |
++---+---+---+
+| 3 | 4 | 5 |
+| 1 | 2 | 3 |
++---+---+---+""".strip()
+
class TestEmptyTable:
"""Make sure the print_empty option works"""
+ def test_print_empty(self) -> None:
+ table = PrettyTable(print_empty=False)
+ assert table.get_string().strip() == ""
+
+ def test_print_empty_setter(self) -> None:
+ table = PrettyTable()
+ table.print_empty = False
+ assert table.get_string().strip() == ""
+
def test_print_empty_true(self, city_data: PrettyTable) -> None:
table = PrettyTable()
table.field_names = CITY_DATA_HEADER
@@ -742,12 +964,24 @@ def float_pt() -> PrettyTable:
class TestFloatFormat:
+ def test_empty(self, float_pt: PrettyTable) -> None:
+ float_pt.float_format = ""
+ assert float_pt.get_string() == """
++----------+----------+
+| Constant | Value |
++----------+----------+
+| Pi | 3.141593 |
+| e | 2.718282 |
+| sqrt(2) | 1.414214 |
++----------+----------+
+""".strip()
+
def test_no_decimals(self, float_pt: PrettyTable) -> None:
- float_pt.float_format = ".0f"
+ float_pt.float_format = ".0"
assert "." not in float_pt.get_string()
def test_round_to_5dp(self, float_pt: PrettyTable) -> None:
- float_pt.float_format = ".5f"
+ float_pt.float_format = ".5"
string = float_pt.get_string()
assert "3.14159" in string
assert "3.141592" not in string
@@ -758,7 +992,7 @@ class TestFloatFormat:
assert "1.414213" not in string
def test_pad_with_2zeroes(self, float_pt: PrettyTable) -> None:
- float_pt.float_format = "06.2f"
+ float_pt.float_format = "06.2"
string = float_pt.get_string()
assert "003.14" in string
assert "002.72" in string
@@ -768,9 +1002,7 @@ class TestFloatFormat:
class TestColumnFormattingfromDict:
def test_set_align_format(self, city_data: PrettyTable) -> None:
city_data.align = {"Annual Rainfall": "r"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -783,7 +1015,6 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.4 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_set_valign_format(self, city_data: PrettyTable) -> None:
table = PrettyTable(
@@ -802,9 +1033,7 @@ class TestColumnFormattingfromDict:
]
)
- assert (
- table.get_string()
- == """
+ assert table.get_string() == """
+---------+----------------------+---------+------------+---------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 | Field 5 | Field 6 |
+---------+----------------------+---------+------------+---------+------------+
@@ -818,7 +1047,6 @@ class TestColumnFormattingfromDict:
| | | | diam | | diam |
+---------+----------------------+---------+------------+---------+------------+
""".strip()
- )
def test_max_width(
self,
@@ -838,9 +1066,7 @@ class TestColumnFormattingfromDict:
]
)
- assert (
- table.get_string()
- == """
+ assert table.get_string() == """
+---------+----------------------+---------+------------+---------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 | Field 5 | Field 6 |
+---------+----------------------+---------+------------+---------+------------+
@@ -854,7 +1080,6 @@ class TestColumnFormattingfromDict:
| | | | diam | | diam |
+---------+----------------------+---------+------------+---------+------------+
""".strip()
- )
def test_min_width(self, city_data: PrettyTable) -> None:
city_data.min_width = {
@@ -863,9 +1088,7 @@ class TestColumnFormattingfromDict:
"Population": 20,
"Annual Rainfall": 20,
}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+----------------------+------------+----------------------+----------------------+
| City name | Area | Population | Annual Rainfall |
+----------------------+------------+----------------------+----------------------+
@@ -878,13 +1101,10 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.4 |
+----------------------+------------+----------------------+----------------------+
""".strip()
- )
def test_set_int_format(self, city_data: PrettyTable) -> None:
city_data.int_format = {"Population": "20"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+-----------+------+----------------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+----------------------+-----------------+
@@ -897,13 +1117,77 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.4 |
+-----------+------+----------------------+-----------------+
""".strip()
- )
+
+ def test_set_int_format_overwrite_dict(self, city_data: PrettyTable) -> None:
+ city_data.custom_format = {"Population": lambda f, v: f"'{v}'"}
+ city_data.int_format = {"Population": "20"}
+ assert city_data.get_string() == """
++-----------+------+----------------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+----------------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Sydney | 2058 | 4336374 | 1214.8 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
++-----------+------+----------------------+-----------------+
+""".strip()
+
+ def test_set_int_format_overwrite(self, city_data: PrettyTable) -> None:
+ city_data.custom_format = {"Population": lambda f, v: f"'{v}'"}
+ city_data.int_format = "20"
+ assert city_data.get_string() == """
++-----------+----------------------+----------------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+----------------------+----------------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Sydney | 2058 | 4336374 | 1214.8 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
++-----------+----------------------+----------------------+-----------------+""".strip()
def test_set_float_format(self, city_data: PrettyTable) -> None:
city_data.float_format = {"Annual Rainfall": "4.2"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.50 |
+| Brisbane | 5905 | 1857594 | 1146.40 |
+| Darwin | 112 | 120900 | 1714.70 |
+| Hobart | 1357 | 205556 | 619.50 |
+| Sydney | 2058 | 4336374 | 1214.80 |
+| Melbourne | 1566 | 3806092 | 646.90 |
+| Perth | 5386 | 1554769 | 869.40 |
++-----------+------+------------+-----------------+
+""".strip()
+
+ def test_set_float_format_overwrite_dict(self, city_data: PrettyTable) -> None:
+ city_data.custom_format = {"Annual Rainfall": lambda f, v: f"'{v}'"}
+ city_data.float_format = {"Annual Rainfall": "4.2"}
+ assert city_data.get_string() == """
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.50 |
+| Brisbane | 5905 | 1857594 | 1146.40 |
+| Darwin | 112 | 120900 | 1714.70 |
+| Hobart | 1357 | 205556 | 619.50 |
+| Sydney | 2058 | 4336374 | 1214.80 |
+| Melbourne | 1566 | 3806092 | 646.90 |
+| Perth | 5386 | 1554769 | 869.40 |
++-----------+------+------------+-----------------+
+""".strip()
+
+ def test_set_float_format_overwrite(self, city_data: PrettyTable) -> None:
+ city_data.custom_format = {"Annual Rainfall": lambda f, v: f"'{v}'"}
+ city_data.float_format = "4.2"
+ assert city_data.get_string() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -916,13 +1200,10 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.40 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_set_custom_format(self, city_data: PrettyTable) -> None:
city_data.custom_format = {"Annual Rainfall": lambda f, v: f"{v:.2f}"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -935,22 +1216,18 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.40 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_set_none_format(self, city_data: PrettyTable) -> None:
city_data.clear_rows()
city_data.add_row([None, None, None, None])
city_data.none_format = {"Annual Rainfall": "N/A"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
| None | None | None | N/A |
+-----------+------+------------+-----------------+
""".strip()
- )
class TestBreakLine:
@@ -1050,6 +1327,22 @@ class TestCsvOutput:
"value 7,value9\r\n"
)
+ def test_csv_formatting(self) -> None:
+ table = PrettyTable()
+ table.add_column("Name", ["Alice", "Bob", "Charlie"])
+ table.add_column("int_format", [7, 0, -42])
+ table.add_column("float_format", [3.14159, 10.0, -2.71828])
+ table.add_column("custom_format", [1234, 0, -5678])
+ table.int_format["int_format"] = "05"
+ table.float_format["float_format"] = ".1"
+ table.custom_format["custom_format"] = lambda f, v: f"{v:,}"
+ assert table.get_csv_string() == (
+ "Name,int_format,float_format,custom_format\r\n"
+ 'Alice,00007,3.1,"1,234"\r\n'
+ "Bob,00000,10.0,0\r\n"
+ 'Charlie,-0042,-2.7,"-5,678"\r\n'
+ )
+
def test_paginate(city_data: PrettyTable) -> None:
expected_page_1 = """
@@ -1168,9 +1461,7 @@ class TestCustomFormatter:
def test_use_custom_formatter_for_int(self, city_data: PrettyTable) -> None:
city_data.custom_format["Annual Rainfall"] = lambda n, v: f"{v:.2f}"
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -1183,7 +1474,6 @@ class TestCustomFormatter:
| Perth | 5386 | 1554769 | 869.40 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_custom_format_multi_type(self) -> None:
table = PrettyTable(["col_date", "col_str", "col_float", "col_int"])
@@ -1192,9 +1482,7 @@ class TestCustomFormatter:
table.custom_format["col_date"] = lambda f, v: v.strftime("%d %b %Y")
table.custom_format["col_float"] = lambda f, v: f"{v:.3f}"
table.custom_format["col_int"] = lambda f, v: f"{v:,}"
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+-------------+----------+-----------+------------+
| col_date | col_str | col_float | col_int |
+-------------+----------+-----------+------------+
@@ -1202,7 +1490,6 @@ class TestCustomFormatter:
| 01 Feb 2021 | February | 54321.123 | 87,654,321 |
+-------------+----------+-----------+------------+
""".strip()
- )
def test_custom_format_multi_type_using_on_function(self) -> None:
table = PrettyTable(["col_date", "col_str", "col_float", "col_int"])
@@ -1219,9 +1506,7 @@ class TestCustomFormatter:
return str(value)
table.custom_format = my_format
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+-------------+----------+-----------+------------+
| col_date | col_str | col_float | col_int |
+-------------+----------+-----------+------------+
@@ -1229,7 +1514,41 @@ class TestCustomFormatter:
| 01 Feb 2021 | February | 54321.123 | 87,654,321 |
+-------------+----------+-----------+------------+
""".strip()
- )
+
+ def test_overwrite_default_format(self, city_data: PrettyTable) -> None:
+ city_data.float_format = "6.2"
+ city_data.int_format = "10"
+ city_data.custom_format["Annual Rainfall"] = lambda f, v: f"+++{v:.4f}+++"
+ assert city_data.get_string() == """
++-----------+------------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | +++600.5000+++ |
+| Brisbane | 5905 | 1857594 | +++1146.4000+++ |
+| Darwin | 112 | 120900 | +++1714.7000+++ |
+| Hobart | 1357 | 205556 | +++619.5000+++ |
+| Sydney | 2058 | 4336374 | +++1214.8000+++ |
+| Melbourne | 1566 | 3806092 | +++646.9000+++ |
+| Perth | 5386 | 1554769 | +++869.4000+++ |
++-----------+------------+------------+-----------------+""".strip()
+
+ def test_overwrite_default_format_dict(self, city_data: PrettyTable) -> None:
+ city_data.none_format = "N/A"
+ city_data.float_format = "6.2"
+ city_data.int_format = "10"
+ city_data.custom_format = {"Annual Rainfall": lambda f, v: f"+++{v:.4f}+++"}
+ assert city_data.get_string() == """
++-----------+------------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | +++600.5000+++ |
+| Brisbane | 5905 | 1857594 | +++1146.4000+++ |
+| Darwin | 112 | 120900 | +++1714.7000+++ |
+| Hobart | 1357 | 205556 | +++619.5000+++ |
+| Sydney | 2058 | 4336374 | +++1214.8000+++ |
+| Melbourne | 1566 | 3806092 | +++646.9000+++ |
+| Perth | 5386 | 1554769 | +++869.4000+++ |
++-----------+------------+------------+-----------------+""".strip()
class TestRepr:
@@ -1289,9 +1608,7 @@ class TestWidth:
table.add_row([self.colored, self.colored])
table.add_row(["nothing", "neither"])
result = table.get_string()
- assert (
- result.strip()
- == f"""
+ assert result.strip() == f"""
+---------+---------+
| Field 1 | Field 2 |
+---------+---------+
@@ -1299,16 +1616,13 @@ class TestWidth:
| nothing | neither |
+---------+---------+
""".strip()
- )
def test_reset(self) -> None:
table = PrettyTable(["Field 1", "Field 2"])
table.add_row(["abc def\033(B", "\033[31mabc def\033[m"])
table.add_row(["nothing", "neither"])
result = table.get_string()
- assert (
- result.strip()
- == """
+ assert result.strip() == """
+---------+---------+
| Field 1 | Field 2 |
+---------+---------+
@@ -1316,7 +1630,15 @@ class TestWidth:
| nothing | neither |
+---------+---------+
""".strip()
- )
+
+ def test_tab_expansion_aligns_columns(self) -> None:
+ """Ensure expandtabs() is used in PrettyTable._format_value()."""
+ table = PrettyTable(["code", "note"])
+ table.add_row(["if x:\n\treturn 1", "tab-indented"])
+ result = table.get_string()
+ assert "\t" not in result
+ line_widths = {len(line) for line in result.splitlines()}
+ assert len(line_widths) == 1
@pytest.mark.parametrize(
"loops, fields, desired_width, border, internal_border",
@@ -1380,16 +1702,13 @@ class TestWidth:
table.add_row([0])
# FIXME: Table is wider than table.max_table_width
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+----+
| Fi |
+----+
| 0 |
+----+
""".strip()
- )
def test_max_table_width_wide(self) -> None:
table = PrettyTable()
@@ -1407,9 +1726,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---+---+---+---+---+------------------------------+
| F | F | F | F | F | Field 6 |
+---+---+---+---+---+------------------------------+
@@ -1420,7 +1737,6 @@ class TestWidth:
| | | | | | dolore magna aliquyam erat, |
| | | | | | sed diam voluptua |
+---+---+---+---+---+------------------------------+""".strip()
- )
def test_max_table_width_wide2(self) -> None:
table = PrettyTable()
@@ -1436,9 +1752,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---+-----------------+---+-----------------+---+-----------------+
| F | Field 2 | F | Field 4 | F | Field 6 |
+---+-----------------+---+-----------------+---+-----------------+
@@ -1448,7 +1762,6 @@ class TestWidth:
| e | sadipscing | u | sadipscing | o | sadipscing |
| m | elitr, sed diam | m | elitr, sed diam | r | elitr, sed diam |
+---+-----------------+---+-----------------+---+-----------------+""".strip()
- )
@pytest.mark.parametrize("set_width_parameter", [True, False])
def test_table_max_width_wo_header_width(self, set_width_parameter: bool) -> None:
@@ -1496,9 +1809,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+------------+---------+------------+---------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 | Field 5 | Field 6 |
+---------+------------+---------+------------+---------+------------+
@@ -1511,7 +1822,6 @@ class TestWidth:
| | elitr, sed | | elitr, sed | | elitr, sed |
| | diam | | diam | | diam |
+---------+------------+---------+------------+---------+------------+""".strip()
- )
def test_table_width_on_init_with_columns(self) -> None:
"""See also #272"""
@@ -1530,9 +1840,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+------------+---------+------------+---------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 | Field 5 | Field 6 |
+---------+------------+---------+------------+---------+------------+
@@ -1545,7 +1853,6 @@ class TestWidth:
| | elitr, sed | | elitr, sed | | elitr, sed |
| | diam | | diam | | diam |
+---------+------------+---------+------------+---------+------------+""".strip()
- )
def test_table_minwidth_on_init_with_columns(self) -> None:
table = PrettyTable(["Field 1", "Field 2"], min_width=20)
@@ -1562,7 +1869,7 @@ class TestWidth:
| Field 1 | Field 2 |
+----------------------+--------------------------------------------------------------------+
| Lorem | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam |
-+----------------------+--------------------------------------------------------------------+""" # noqa: E501
++----------------------+--------------------------------------------------------------------+"""
)
def test_table_min_max_width_on_init_with_columns(self) -> None:
@@ -1590,15 +1897,12 @@ class TestWidth:
table.field_names = ["Metric", "Initial sol.", "Best sol."]
table.add_rows([["foo", 1.0 / 3.0, 1.0 / 3.0]])
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+--------+--------------+------------+
| Metric | Initial sol. | Best sol. |
+--------+--------------+------------+
| foo | 0.33 | 0.33 |
+--------+--------------+------------+""".strip()
- )
def test_max_table_width_wide_vrules_frame(self) -> None:
table = PrettyTable()
@@ -1617,9 +1921,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+--------------------------------------------------+
| F F F F F Field 6 |
+--------------------------------------------------+
@@ -1630,7 +1932,6 @@ class TestWidth:
| dolore magna aliquyam erat, |
| sed diam voluptua |
+--------------------------------------------------+""".strip()
- )
def test_max_table_width_wide_vrules_none(self) -> None:
table = PrettyTable()
@@ -1649,9 +1950,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
----------------------------------------------------
F F F F F Field 6
----------------------------------------------------
@@ -1662,7 +1961,6 @@ class TestWidth:
dolore magna aliquyam erat,
sed diam voluptua
----------------------------------------------------""".strip() # noqa: W291
- )
class TestFields:
@@ -1673,8 +1971,7 @@ class TestFields:
)
for row in CITY_DATA:
table.add_row(row)
- assert (
- """+-----------+-----------------+
+ assert """+-----------+-----------------+
| City name | Annual Rainfall |
+-----------+-----------------+
| Adelaide | 600.5 |
@@ -1684,9 +1981,7 @@ class TestFields:
| Sydney | 1214.8 |
| Melbourne | 646.9 |
| Perth | 869.4 |
-+-----------+-----------------+"""
- == table.get_string().strip()
- )
++-----------+-----------------+""" == table.get_string().strip()
def test_fields(self) -> None:
table = PrettyTable()
@@ -1694,8 +1989,7 @@ class TestFields:
table.fields = ["City name", "Annual Rainfall"]
for row in CITY_DATA:
table.add_row(row)
- assert (
- """+-----------+-----------------+
+ assert """+-----------+-----------------+
| City name | Annual Rainfall |
+-----------+-----------------+
| Adelaide | 600.5 |
@@ -1705,9 +1999,7 @@ class TestFields:
| Sydney | 1214.8 |
| Melbourne | 646.9 |
| Perth | 869.4 |
-+-----------+-----------------+"""
- == table.get_string().strip()
- )
++-----------+-----------------+""" == table.get_string().strip()
class TestGeneralOutput:
@@ -1815,3 +2107,15 @@ class TestDeprecations:
) -> None:
with pytest.deprecated_call(match=f"the '{name}' constant is deprecated"):
exec(f"from {module_name} import {name}")
+
+ def test_options_type_deprecation(self) -> None:
+ with pytest.deprecated_call(match="OptionsType"):
+ from prettytable.prettytable import OptionsType
+
+ assert OptionsType is not None
+
+ def test_table_handler_deprecation(self) -> None:
+ with pytest.deprecated_call(match="TableHandler is deprecated"):
+ from prettytable import TableHandler
+
+ TableHandler()
diff --git a/contrib/python/prettytable/py3/tests/test_rst.py b/contrib/python/prettytable/py3/tests/test_rst.py
new file mode 100644
index 00000000000..bc4e4e8498c
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/test_rst.py
@@ -0,0 +1,97 @@
+from __future__ import annotations
+
+from prettytable import PrettyTable, TableStyle
+
+
+def test_rst_output_without_header(helper_table: PrettyTable) -> None:
+ helper_table.set_style(TableStyle.RST)
+ assert helper_table.get_string(header=False).strip() == """
++---+---------+--------+--------+
+| 1 | value 1 | value2 | value3 |
++---+---------+--------+--------+
+| 4 | value 4 | value5 | value6 |
++---+---------+--------+--------+
+| 7 | value 7 | value8 | value9 |
++---+---------+--------+--------+
+""".strip()
+
+
+def test_rst_output_with_fields(helper_table: PrettyTable) -> None:
+ helper_table.set_style(TableStyle.RST)
+ assert helper_table.get_string(fields=["Field 1", "Field 3"]).strip() == """
++---------+---------+
+| Field 1 | Field 3 |
++=========+=========+
+| value 1 | value3 |
++---------+---------+
+| value 4 | value6 |
++---------+---------+
+| value 7 | value9 |
++---------+---------+
+""".strip()
+
+
+def test_rst_header_uses_equals(helper_table: PrettyTable) -> None:
+ # Arrange
+ helper_table.set_style(TableStyle.RST)
+
+ # Act
+ result = helper_table.get_string()
+
+ # Assert
+ lines = result.splitlines()
+ # The header separator (line after header row) should use "="
+ header_sep = lines[2]
+ assert "=" in header_sep
+ assert header_sep.startswith("+")
+ assert header_sep.endswith("+")
+ # Data separators should use "-"
+ data_sep = lines[4]
+ assert "=" not in data_sep
+ assert "-" in data_sep
+
+
+def test_rst_style_does_not_leak(helper_table: PrettyTable) -> None:
+ # Arrange
+ original = helper_table.get_string()
+
+ # Act
+ helper_table.set_style(TableStyle.RST)
+ helper_table.set_style(TableStyle.DEFAULT)
+
+ # Assert
+ assert helper_table.get_string() == original
+
+
+def test_rst_output_with_multiline_title(helper_table: PrettyTable) -> None:
+ # Arrange
+ helper_table.set_style(TableStyle.RST)
+ # Act
+ helper_table.title = "Line 1\nLine 2"
+ # Assert
+ assert helper_table.get_string() == """
++---------------------------------+
+| Line 1 |
+| Line 2 |
++---+---------+---------+---------+
+| | Field 1 | Field 2 | Field 3 |
++===+=========+=========+=========+
+| 1 | value 1 | value2 | value3 |
++---+---------+---------+---------+
+| 4 | value 4 | value5 | value6 |
++---+---------+---------+---------+
+| 7 | value 7 | value8 | value9 |
++---+---------+---------+---------+
+""".strip()
+
+
+def test_markdown_to_rst_does_not_leak(helper_table: PrettyTable) -> None:
+ # Arrange
+ helper_table.set_style(TableStyle.MARKDOWN)
+ helper_table.set_style(TableStyle.RST)
+
+ # Act
+ result = helper_table.get_string()
+
+ # Assert: Markdown's ":" alignment char should not appear in RST output
+ assert ":" not in result
diff --git a/contrib/python/prettytable/py3/tests/test_sorting.py b/contrib/python/prettytable/py3/tests/test_sorting.py
index 1968c718f40..172e6347450 100644
--- a/contrib/python/prettytable/py3/tests/test_sorting.py
+++ b/contrib/python/prettytable/py3/tests/test_sorting.py
@@ -34,9 +34,7 @@ class TestSorting:
city_data.sortby = "City name"
city_data.sort_key = key
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -49,7 +47,6 @@ class TestSorting:
| Melbourne | 1566 | 3806092 | 646.9 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_sort_key_at_class_declaration(self) -> None:
# Test sorting by length of city name
@@ -65,8 +62,7 @@ class TestSorting:
assert table.sort_key == key
for row in CITY_DATA:
table.add_row(row)
- assert (
- """+-----------+------+------------+-----------------+
+ assert """+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
| Perth | 5386 | 1554769 | 869.4 |
@@ -76,11 +72,9 @@ class TestSorting:
| Adelaide | 1295 | 1158259 | 600.5 |
| Brisbane | 5905 | 1857594 | 1146.4 |
| Melbourne | 1566 | 3806092 | 646.9 |
-+-----------+------+------------+-----------------+"""
- == table.get_string().strip()
- )
++-----------+------+------------+-----------------+""" == table.get_string().strip()
- def test_sort_slice(self) -> None:
+ def test_sort_slice_get_string(self) -> None:
"""Make sure sorting and slicing interact in the expected way"""
table = PrettyTable(["Foo"])
for i in range(20, 0, -1):
@@ -92,6 +86,44 @@ class TestSorting:
assert "10" not in oldstyle
assert "20" in oldstyle
+ def test_sort_slice(self) -> None:
+ """Make sure sorting and slicing interact in the expected way"""
+ # Arrange
+ table_new = PrettyTable(["Foo"])
+ table_old = PrettyTable(["Foo"], oldsortslice=True)
+ for i in range(20, 0, -1):
+ table_new.add_row([i])
+ table_old.add_row([i])
+
+ # Act
+ new_style = table_new.get_string(sortby="Foo", end=10)
+ old_style = table_old.get_string(sortby="Foo", end=10)
+
+ # Assert
+ assert "10" in new_style
+ assert "20" not in new_style
+ assert "10" not in old_style
+ assert "20" in old_style
+
+ def test_sort_slice_setter(self) -> None:
+ # Arrange
+ table_new = PrettyTable(["Foo"])
+ table_old = PrettyTable(["Foo"])
+ table_old.oldsortslice = True
+ for i in range(20, 0, -1):
+ table_new.add_row([i])
+ table_old.add_row([i])
+
+ # Act
+ new_style = table_new.get_string(sortby="Foo", end=10)
+ old_style = table_old.get_string(sortby="Foo", end=10)
+
+ # Assert
+ assert "10" in new_style
+ assert "20" not in new_style
+ assert "10" not in old_style
+ assert "20" in old_style
+
def test_sortby_at_class_declaration(self) -> None:
"""
Fix #354 where initialization of a table with sortby fails
@@ -103,8 +135,7 @@ class TestSorting:
assert table.sortby == "Area"
for row in CITY_DATA:
table.add_row(row)
- assert (
- """+-----------+------+------------+-----------------+
+ assert """+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
| Darwin | 112 | 120900 | 1714.7 |
@@ -114,6 +145,4 @@ class TestSorting:
| Sydney | 2058 | 4336374 | 1214.8 |
| Perth | 5386 | 1554769 | 869.4 |
| Brisbane | 5905 | 1857594 | 1146.4 |
-+-----------+------+------------+-----------------+"""
- == table.get_string().strip()
- )
++-----------+------+------------+-----------------+""" == table.get_string().strip()
diff --git a/contrib/python/prettytable/py3/tests/test_style.py b/contrib/python/prettytable/py3/tests/test_style.py
index 2ae696e4bc3..1887fa0d40e 100644
--- a/contrib/python/prettytable/py3/tests/test_style.py
+++ b/contrib/python/prettytable/py3/tests/test_style.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+import os
import random
+from typing import Literal
import pytest
from pytest_lazy_fixtures import lf
@@ -8,6 +10,14 @@ from pytest_lazy_fixtures import lf
from prettytable import HRuleStyle, PrettyTable, TableStyle, VRuleStyle
from prettytable.prettytable import _str_block_width
+# these tables don't display well outside of raw dump to a terminal, so they are
+# moved to external files, where they may be very easy to visually align, by command:
+#
+# $ cat tests/data/*.txt
+#
+import yatest.common as yc
+DATA_DIR = os.path.join(os.path.dirname(yc.source_path(__file__)), "data")
+
class TestPositionalJunctions:
"""Verify different cases for positional-junction characters"""
@@ -15,9 +25,7 @@ class TestPositionalJunctions:
def test_default(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═══════════╦══════╦════════════╦═════════════════╗
║ City name ║ Area ║ Population ║ Annual Rainfall ║
╠═══════════╬══════╬════════════╬═════════════════╣
@@ -29,15 +37,12 @@ class TestPositionalJunctions:
║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
- )
def test_no_header(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.header = False
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═══════════╦══════╦═════════╦════════╗
║ Adelaide ║ 1295 ║ 1158259 ║ 600.5 ║
║ Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
@@ -47,15 +52,12 @@ class TestPositionalJunctions:
║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩═════════╩════════╝""".strip()
- )
def test_with_title(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.title = "Title"
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═════════════════════════════════════════════════╗
║ Title ║
╠═══════════╦══════╦════════════╦═════════════════╣
@@ -69,15 +71,12 @@ class TestPositionalJunctions:
║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
- )
def test_with_title_no_header(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.title = "Title"
city_data.header = False
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═════════════════════════════════════╗
║ Title ║
╠═══════════╦══════╦═════════╦════════╣
@@ -89,15 +88,12 @@ class TestPositionalJunctions:
║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩═════════╩════════╝""".strip()
- )
def test_hrule_all(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.title = "Title"
city_data.hrules = HRuleStyle.ALL
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═════════════════════════════════════════════════╗
║ Title ║
╠═══════════╦══════╦════════════╦═════════════════╣
@@ -117,7 +113,6 @@ class TestPositionalJunctions:
╠═══════════╬══════╬════════════╬═════════════════╣
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
- )
def test_vrules_none(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
@@ -141,9 +136,7 @@ class TestPositionalJunctions:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.vrules = VRuleStyle.FRAME
city_data.title = "Title"
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═════════════════════════════════════════════════╗
║ Title ║
╠═════════════════════════════════════════════════╣
@@ -157,7 +150,6 @@ class TestPositionalJunctions:
║ Melbourne 1566 3806092 646.9 ║
║ Perth 5386 1554769 869.4 ║
╚═════════════════════════════════════════════════╝""".strip()
- )
class TestStyle:
@@ -167,6 +159,8 @@ class TestStyle:
pytest.param(
TableStyle.DEFAULT,
"""
++---------------------------------+
+| Table Caption |
+---+---------+---------+---------+
| | Field 1 | Field 2 | Field 3 |
+---+---------+---------+---------+
@@ -180,17 +174,21 @@ class TestStyle:
pytest.param(
TableStyle.MARKDOWN, # TODO fix
"""
+**Table Caption**
+
| | Field 1 | Field 2 | Field 3 |
| :-: | :-----: | :-----: | :-----: |
| 1 | value 1 | value2 | value3 |
| 4 | value 4 | value5 | value6 |
| 7 | value 7 | value8 | value9 |
-""",
+""".strip(),
id="MARKDOWN",
),
pytest.param(
TableStyle.MSWORD_FRIENDLY,
"""
++---------------------------------+
+| Table Caption |
| | Field 1 | Field 2 | Field 3 |
| 1 | value 1 | value2 | value3 |
| 4 | value 4 | value5 | value6 |
@@ -201,6 +199,8 @@ class TestStyle:
pytest.param(
TableStyle.ORGMODE,
"""
+|---------------------------------|
+| Table Caption |
|---+---------+---------+---------|
| | Field 1 | Field 2 | Field 3 |
|---+---------+---------+---------|
@@ -214,6 +214,7 @@ class TestStyle:
pytest.param(
TableStyle.PLAIN_COLUMNS,
"""
+Table Caption
Field 1 Field 2 Field 3
1 value 1 value2 value3
4 value 4 value5 value6
@@ -225,6 +226,8 @@ class TestStyle:
TableStyle.RANDOM,
"""
'^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^'
+% Table Caption %
+'^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^'
% 1 value 1 value2 value3%
% 4 value 4 value5 value6%
% 7 value 7 value8 value9%
@@ -235,7 +238,9 @@ class TestStyle:
pytest.param(
TableStyle.DOUBLE_BORDER,
"""
-╔═══╦═════════╦═════════╦═════════╗
+╔═════════════════════════════════╗
+║ Table Caption ║
+╠═══╦═════════╦═════════╦═════════╣
║ ║ Field 1 ║ Field 2 ║ Field 3 ║
╠═══╬═════════╬═════════╬═════════╣
║ 1 ║ value 1 ║ value2 ║ value3 ║
@@ -247,7 +252,9 @@ class TestStyle:
pytest.param(
TableStyle.SINGLE_BORDER,
"""
-┌───┬─────────┬─────────┬─────────┐
+┌─────────────────────────────────┐
+│ Table Caption │
+├───┬─────────┬─────────┬─────────┤
│ │ Field 1 │ Field 2 │ Field 3 │
├───┼─────────┼─────────┼─────────┤
│ 1 │ value 1 │ value2 │ value3 │
@@ -256,12 +263,30 @@ class TestStyle:
└───┴─────────┴─────────┴─────────┘
""",
),
+ pytest.param(
+ TableStyle.RST,
+ """
++---------------------------------+
+| Table Caption |
++---+---------+---------+---------+
+| | Field 1 | Field 2 | Field 3 |
++===+=========+=========+=========+
+| 1 | value 1 | value2 | value3 |
++---+---------+---------+---------+
+| 4 | value 4 | value5 | value6 |
++---+---------+---------+---------+
+| 7 | value 7 | value8 | value9 |
++---+---------+---------+---------+
+""",
+ id="RST",
+ ),
],
)
def test_style(
self, helper_table: PrettyTable, style: TableStyle, expected: str
) -> None:
random.seed(1234)
+ helper_table.title = "Table Caption"
helper_table.set_style(style)
assert helper_table.get_string().strip() == expected.strip()
@@ -375,6 +400,22 @@ class TestStyle:
""",
id="SINGLE_BORDER",
),
+ pytest.param(
+ TableStyle.MARKDOWN,
+ TableStyle.RST,
+ """
++---+---------+---------+---------+
+| | Field 1 | Field 2 | Field 3 |
++===+=========+=========+=========+
+| 1 | value 1 | value2 | value3 |
++---+---------+---------+---------+
+| 4 | value 4 | value5 | value6 |
++---+---------+---------+---------+
+| 7 | value 7 | value8 | value9 |
++---+---------+---------+---------+
+""",
+ id="RST",
+ ),
],
)
def test_style_reset(
@@ -523,7 +564,7 @@ class TestMultiPattern:
| \x1b[38;5;228;5m ⚡\x1b[38;5;111;25mʻ ʻ\x1b[38;5;228;5m⚡\x1b[38;5;111;25mʻ ʻ \x1b[0m | \x1b[38;5;21;1m ‚ʻ\x1b[38;5;228;5m⚡\x1b[38;5;21;25mʻ‚\x1b[38;5;228;5m⚡\x1b[38;5;21;25m‚ʻ \x1b[0m |
| \x1b[38;5;111m ʻ ʻ ʻ ʻ \x1b[0m | \x1b[38;5;21;1m ‚ʻ‚ʻ\x1b[38;5;228;5m⚡\x1b[38;5;21;25mʻ‚ʻ \x1b[0m |
+-----------------+-----------------+
- """, # noqa: E501
+ """,
"Emoji table",
),
],
@@ -539,15 +580,12 @@ class TestMultiPattern:
def test_colored_table() -> None:
table = PrettyTable(field_names=["Namespace", "Count"])
table.title = "\x1b[34mHere be Table caption\x1b[39m"
- assert (
- table.get_string()
- == """+-----------------------+
+ assert table.get_string() == """+-----------------------+
| \x1b[34mHere be Table caption\x1b[39m |
+-------------+---------+
| Namespace | Count |
+-------------+---------+
+-------------+---------+"""
- )
def test_link_and_color() -> None:
@@ -560,9 +598,7 @@ def test_link_and_color() -> None:
text = "Click \x1b[34mhere\x1b[39m"
table.add_row([f"\033]8;;https://example.com\033\\{text}\033]8;;\033\\", "3"])
- assert (
- table.get_string()
- == """\
+ assert table.get_string() == """\
+------------+-------+
| Link | Count |
+------------+-------+
@@ -570,7 +606,6 @@ def test_link_and_color() -> None:
| No link | 2 |
| \033]8;;https://example.com\033\\Click \x1b[34mhere\x1b[39m\033]8;;\033\\ | 3 |
+------------+-------+"""
- )
@pytest.mark.parametrize(
@@ -585,7 +620,123 @@ def test_link_and_color() -> None:
("\033]8;;https://example.com\033\\\x1b[34mblue link\x1b[39m\033]8;;\033\\", 9),
# link inside colour
("\x1b[34m\033]8;;https://example.com\033\\blue link\033]8;;\033\\\x1b[39m", 9),
+ # 中文 CJK wide characters
+ ("\u4e2d\u6587", 4),
+ # café (combining acute accent)
+ ("cafe\u0301", 4),
+ # 👨‍👩‍👧 ZWJ family
+ ("\U0001f468\u200d\U0001f469\u200d\U0001f467", 2),
+ # ☺️ VS16 emoji
+ ("\u263a\ufe0f", 2),
+ # 🇺🇸 regional flag
+ ("\U0001f1fa\U0001f1f8", 2),
+ # control code (bell)
+ ("abc\x07def", 6),
],
)
def test__str_block_width(test_input: str, expected: int) -> None:
assert _str_block_width(test_input) == expected
+
+
+ ["fields", "rows", "expected_file"],
+ [
+ (
+ ["Emoji", "Name"],
+ [
+ ["\U0001f468\u200d\U0001f469\u200d\U0001f467", "Family"],
+ ["\U0001f1fa\U0001f1f8", "USA"],
+ ["Hi", "Text"],
+ ],
+ "table_complex_emoji.txt",
+ ),
+ (
+ ["Word", "Lang"],
+ [["cafe\u0301", "FR"], ["cafe", "EN"]],
+ "table_combining_chars.txt",
+ ),
+ (
+ ["CJK", "Width"],
+ [["\u4e2d\u6587", "4"], ["Test", "4"]],
+ "table_cjk.txt",
+ ),
+ (
+ ["Status", "Count"],
+ [
+ ["\x1b[32mOK\x1b[0m", "10"],
+ ["\x1b[31mFailed\x1b[0m", "2"],
+ ["Normal", "5"],
+ ],
+ "table_ansi_colors.txt",
+ ),
+ ],
+)
+def test_table_unicode_width(
+ fields: list[str],
+ rows: list[list[str]],
+ expected_file: str,
+) -> None:
+ table = PrettyTable(fields)
+ for row in rows:
+ table.add_row(row)
+ with open(os.path.join(DATA_DIR, expected_file), encoding="utf-8") as fin:
+ expected_from_file = fin.read()
+ assert table.get_string().rstrip() == expected_from_file.rstrip()
+
+
+ ["align", "expected_file"],
+ [
+ ("l", "table_align_left.txt"),
+ ("r", "table_align_right.txt"),
+ ("c", "table_align_center.txt"),
+ ],
+)
+def test_table_alignment_with_emoji(
+ align: Literal["l", "c", "r"], expected_file: str
+) -> None:
+ table = PrettyTable(["Name"])
+ table.align["Name"] = align
+ table.add_row(["\U0001f468\u200d\U0001f469\u200d\U0001f467"]) # 👨‍👩‍👧
+ table.add_row(["Hi"])
+ with open(os.path.join(DATA_DIR, expected_file), encoding="utf-8") as fin:
+ expected_from_file = fin.read()
+ assert table.get_string().rstrip() == expected_from_file.strip()
+
+
+def test_ansi_wrap_width():
+ """ANSI escape sequences should not count toward column width when wrapping."""
+ table = PrettyTable(["Key", "Value"])
+ table.header = False
+ table.max_width["Value"] = 35
+ table.add_row(
+ [
+ "NEW_ENVIRON",
+ "\x1b[38;5;208mOversharing: HOME, PWD, SHELL,"
+ " SSH_AUTH_SOCK, XDG_SESSION_PATH\x1b[0m",
+ ]
+ )
+ result = table.get_string()
+ for line in result.split("\n"):
+ if "|" not in line:
+ continue
+ parts = line.split("|")
+ value_cell = parts[2] if len(parts) > 2 else ""
+ assert "\x1b[38;5;208m" not in value_cell or "\x1b[0m" in value_cell
+
+
+def test_ansi_wrap_no_bleed():
+ """Wrapped ANSI-colored text must close SGR on every line."""
+ table = PrettyTable(["Key", "Value"])
+ table.header = False
+ table.max_width["Value"] = 30
+ table.add_row(
+ [
+ "TEST",
+ "\x1b[31mRed text that is long enough to wrap across lines\x1b[0m",
+ ]
+ )
+ result = table.get_string()
+ for line in result.split("\n"):
+ if "\x1b[31m" in line:
+ assert "\x1b[0m" in line
diff --git a/contrib/python/prettytable/py3/ya.make b/contrib/python/prettytable/py3/ya.make
index 487fd84e582..1e97cf3a7d0 100644
--- a/contrib/python/prettytable/py3/ya.make
+++ b/contrib/python/prettytable/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(3.17.0)
+VERSION(3.18.0)
LICENSE(BSD-3-Clause)
diff --git a/contrib/python/scramp/.dist-info/METADATA b/contrib/python/scramp/.dist-info/METADATA
index 0699f73b120..d3b2cda4342 100644
--- a/contrib/python/scramp/.dist-info/METADATA
+++ b/contrib/python/scramp/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: scramp
-Version: 1.4.8
+Version: 1.4.9
Summary: An implementation of the SCRAM protocol.
Project-URL: Homepage, https://codeberg.org/tlocke/scramp
Author: The Contributors
@@ -54,6 +54,7 @@ Scramp supports the following mechanisms:
* [OpenSSF Scorecard](#openssf-scorecard)
* [Doing A Release Of Scramp](#doing-a-release-of-scramp)
* [Release Notes](#release-notes)
+ * [Version 1.4.9, 2026-06-19](#version-149-2026-06-19)
* [Version 1.4.8, 2026-01-06](#version-148-2026-01-06)
* [Version 1.4.7, 2026-01-04](#version-147-2026-01-04)
* [Version 1.4.6, 2025-07-05](#version-146-2025-07-05)
@@ -383,6 +384,14 @@ Run `tox` to make sure all tests pass, then update the release notes, then do:
## Release Notes
+### Version 1.4.9, 2026-06-19
+
+- Fix typo in error message.
+* Strictly validate on base 64 decoding
+* Handle escaping in usernames according to the spec.
+* [SASLprep](https://www.rfc-editor.org/info/rfc8265/): Raise exception rather than using assert
+
+
### Version 1.4.8, 2026-01-06
- Use the [`secrets`](https://docs.python.org/3/library/secrets.html) module from
diff --git a/contrib/python/scramp/README.md b/contrib/python/scramp/README.md
index dc6707f325d..1ccdd745c56 100644
--- a/contrib/python/scramp/README.md
+++ b/contrib/python/scramp/README.md
@@ -33,6 +33,7 @@ Scramp supports the following mechanisms:
* [OpenSSF Scorecard](#openssf-scorecard)
* [Doing A Release Of Scramp](#doing-a-release-of-scramp)
* [Release Notes](#release-notes)
+ * [Version 1.4.9, 2026-06-19](#version-149-2026-06-19)
* [Version 1.4.8, 2026-01-06](#version-148-2026-01-06)
* [Version 1.4.7, 2026-01-04](#version-147-2026-01-04)
* [Version 1.4.6, 2025-07-05](#version-146-2025-07-05)
@@ -362,6 +363,14 @@ Run `tox` to make sure all tests pass, then update the release notes, then do:
## Release Notes
+### Version 1.4.9, 2026-06-19
+
+- Fix typo in error message.
+* Strictly validate on base 64 decoding
+* Handle escaping in usernames according to the spec.
+* [SASLprep](https://www.rfc-editor.org/info/rfc8265/): Raise exception rather than using assert
+
+
### Version 1.4.8, 2026-01-06
- Use the [`secrets`](https://docs.python.org/3/library/secrets.html) module from
diff --git a/contrib/python/scramp/scramp/__init__.py b/contrib/python/scramp/scramp/__init__.py
index 4d7a7083e5e..1391c72297b 100644
--- a/contrib/python/scramp/scramp/__init__.py
+++ b/contrib/python/scramp/scramp/__init__.py
@@ -2,10 +2,10 @@ from importlib.metadata import version
from scramp.core import (
ScramClient,
- ScramException,
ScramMechanism,
make_channel_binding,
)
+from scramp.exceptions import ScramException
__all__ = ["ScramClient", "ScramException", "ScramMechanism", "make_channel_binding"]
diff --git a/contrib/python/scramp/scramp/core.py b/contrib/python/scramp/scramp/core.py
index 0c645a8cba9..7bfd362914a 100644
--- a/contrib/python/scramp/scramp/core.py
+++ b/contrib/python/scramp/scramp/core.py
@@ -23,6 +23,7 @@ from stringprep import (
from asn1crypto.x509 import Certificate
+from scramp.exceptions import ScramException
from scramp.utils import b64dec, b64enc, h, hmac, uenc, xor
# https://tools.ietf.org/html/rfc5802
@@ -58,16 +59,6 @@ def _check_stage(Stages, current_stage, next_stage):
)
-class ScramException(Exception):
- def __init__(self, message, server_error=None):
- super().__init__(message)
- self.server_error = server_error
-
- def __str__(self):
- s_str = "" if self.server_error is None else f": {self.server_error}"
- return super().__str__() + s_str
-
-
MECHANISMS = (
"SCRAM-SHA-1",
"SCRAM-SHA-1-PLUS",
@@ -405,7 +396,11 @@ def _make_cbind_input(channel_binding, use_binding):
raise ScramException(f"The gs2_cbind_flag '{gs2_cbind_flag}' is not recognized")
-def _parse_message(msg, desc, *validations):
+def _print_set(s):
+ return "{" + ", ".join(sorted(set(s))) + "}"
+
+
+def _parse_message(msg, desc, *expected_attr_sets):
m = {}
for p in msg.split(","):
if len(p) < 2 or p[1] != "=":
@@ -414,33 +409,35 @@ def _parse_message(msg, desc, *validations):
f"each attribute must start with a letter followed by a '='",
SERVER_ERROR_OTHER_ERROR,
)
- m[p[0]] = p[2:]
-
- m = {e[0]: e[2:] for e in msg.split(",")}
-
- keystr = "".join(m.keys())
- for validation in validations:
- if keystr == validation:
- return m
+ k = p[0]
+ if k in m:
+ raise ScramException(
+ f"Duplicate attributes not allowed in message. The duplicated "
+ f"attribute is {k}. ",
+ SERVER_ERROR_OTHER_ERROR,
+ )
+ m[k] = p[2:]
- if len(validations) == 1:
- val_str = f"'{validations[0]}'"
- else:
- val_str = f"one of {validations}"
+ attr_set = m.keys()
+ if attr_set in expected_attr_sets:
+ return m
raise ScramException(
- f"Malformed {desc} message. Expected the attribute list to be {val_str} but "
- f"found '{keystr}'",
+ f"Malformed {desc} message. Expected the attribute set to be one of "
+ f"[{', '.join([_print_set(s) for s in expected_attr_sets])}] but found "
+ f"{_print_set(attr_set)}",
SERVER_ERROR_OTHER_ERROR,
)
def _get_client_first(username, c_nonce, channel_binding, use_binding):
try:
- u = saslprep(username)
+ prepped_u = saslprep(username)
except ScramException as e:
raise ScramException(e.args[0], SERVER_ERROR_INVALID_USERNAME_ENCODING)
+ u = prepped_u.replace("=", "=3D").replace(",", "=2C")
+
bare = ",".join((f"n={u}", f"r={c_nonce}"))
_, gs2_header = _make_gs2_header(channel_binding, use_binding)
return bare, gs2_header + bare
@@ -469,7 +466,7 @@ def _set_client_first(client_first, s_nonce, channel_binding, use_binding):
if gs2_char == "y":
if channel_binding is not None:
raise ScramException(
- "Recieved GS2 flag 'y' which indicates that the client doesn't think "
+ "Received GS2 flag 'y' which indicates that the client doesn't think "
"the server supports channel binding, but in fact it does",
SERVER_ERROR_SERVER_DOES_SUPPORT_CHANNEL_BINDING,
)
@@ -508,11 +505,11 @@ def _set_client_first(client_first, s_nonce, channel_binding, use_binding):
)
client_first_bare = client_first[second_comma + 1 :]
- msg = _parse_message(client_first_bare, "client first bare", "nr")
+ msg = _parse_message(client_first_bare, "client first bare", {"n", "r"})
c_nonce = msg["r"]
nonce = c_nonce + s_nonce
- user = msg["n"]
+ user = msg["n"].replace("=2C", ",").replace("=3D", "=")
return nonce, user, client_first_bare, upgrade_mechanism
@@ -522,7 +519,7 @@ def _get_server_first(nonce, salt, iterations):
def _set_server_first(server_first, c_nonce):
- msg = _parse_message(server_first, "server first", "rsi")
+ msg = _parse_message(server_first, "server first", {"r", "s", "i"})
if "e" in msg:
raise ScramException(f"The server returned the error: {msg['e']}")
@@ -567,7 +564,6 @@ def _get_client_final(
SERVER_ERROR_INVALID_ENCODING = "invalid-encoding"
SERVER_ERROR_EXTENSIONS_NOT_SUPPORTED = "extensions-not-supported"
SERVER_ERROR_INVALID_PROOF = "invalid-proof"
-SERVER_ERROR_INVALID_ENCODING = "invalid-encoding"
SERVER_ERROR_CHANNEL_BINDINGS_DONT_MATCH = "channel-bindings-dont-match"
SERVER_ERROR_SERVER_DOES_SUPPORT_CHANNEL_BINDING = "server-does-support-channel-binding"
SERVER_ERROR_SERVER_DOES_NOT_SUPPORT_CHANNEL_BINDING = (
@@ -592,7 +588,7 @@ def _set_client_final(
channel_binding,
use_binding,
):
- msg = _parse_message(client_final, "client final", "crp")
+ msg = _parse_message(client_final, "client final", {"c", "r", "p"})
chan_binding = msg["c"]
nonce = msg["r"]
@@ -623,7 +619,7 @@ def _get_server_final(server_signature, error):
def _set_server_final(message, server_signature):
- msg = _parse_message(message, "server final", "v", "e")
+ msg = _parse_message(message, "server final", {"v"}, {"e"})
if "e" in msg:
raise ScramException(f"The server returned the error: {msg['e']}")
@@ -668,12 +664,11 @@ def saslprep(source):
# check for prohibited output
# stringprep tables A.1, B.1, C.1.2, C.2 - C.9
for c in data:
- # check for chars mapping stage should have removed
- assert not in_table_b1(c), "failed to strip B.1 in mapping stage"
- assert not in_table_c12(c), "failed to replace C.1.2 in mapping stage"
-
- # check for forbidden chars
for f, msg in (
+ # check for chars mapping stage should have removed
+ (in_table_b1, "failed to strip B.1 in mapping stage"),
+ (in_table_c12, "failed to replace C.1.2 in mapping stage"),
+ # check for forbidden chars
(in_table_a1, "unassigned code points forbidden"),
(in_table_c21_c22, "control characters forbidden"),
(in_table_c3, "private use characters forbidden"),
diff --git a/contrib/python/scramp/scramp/exceptions.py b/contrib/python/scramp/scramp/exceptions.py
new file mode 100644
index 00000000000..a1e585769a4
--- /dev/null
+++ b/contrib/python/scramp/scramp/exceptions.py
@@ -0,0 +1,8 @@
+class ScramException(Exception):
+ def __init__(self, message, server_error=None):
+ super().__init__(message)
+ self.server_error = server_error
+
+ def __str__(self):
+ s_str = "" if self.server_error is None else f": {self.server_error}"
+ return super().__str__() + s_str
diff --git a/contrib/python/scramp/scramp/utils.py b/contrib/python/scramp/scramp/utils.py
index 7fcd365f6b3..da864d2fe2f 100644
--- a/contrib/python/scramp/scramp/utils.py
+++ b/contrib/python/scramp/scramp/utils.py
@@ -1,6 +1,9 @@
+import binascii
import hmac as hmaca
from base64 import b64decode, b64encode
+from scramp.exceptions import ScramException
+
def hmac(hf, key, msg):
return hmaca.new(key, msg=msg, digestmod=hf).digest()
@@ -19,7 +22,10 @@ def b64enc(binary):
def b64dec(string):
- return b64decode(string)
+ try:
+ return b64decode(string, validate=True)
+ except binascii.Error as e:
+ raise ScramException("Invalid base64 encoding.") from e
def uenc(string):
diff --git a/contrib/python/scramp/ya.make b/contrib/python/scramp/ya.make
index 74bbaca906b..9453f63f0af 100644
--- a/contrib/python/scramp/ya.make
+++ b/contrib/python/scramp/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(1.4.8)
+VERSION(1.4.9)
LICENSE(MIT-0)
@@ -16,6 +16,7 @@ PY_SRCS(
TOP_LEVEL
scramp/__init__.py
scramp/core.py
+ scramp/exceptions.py
scramp/utils.py
)