multiCMD 1.52__tar.gz → 1.53__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {multicmd-1.52 → multicmd-1.53}/PKG-INFO +1 -1
  2. {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/PKG-INFO +1 -1
  3. {multicmd-1.52 → multicmd-1.53}/multiCMD.py +50 -38
  4. {multicmd-1.52 → multicmd-1.53}/tests/test_helpers.py +19 -0
  5. {multicmd-1.52 → multicmd-1.53}/README.md +0 -0
  6. {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/SOURCES.txt +0 -0
  7. {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/dependency_links.txt +0 -0
  8. {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/entry_points.txt +0 -0
  9. {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/top_level.txt +0 -0
  10. {multicmd-1.52 → multicmd-1.53}/setup.cfg +0 -0
  11. {multicmd-1.52 → multicmd-1.53}/setup.py +0 -0
  12. {multicmd-1.52 → multicmd-1.53}/tests/test_api.py +0 -0
  13. {multicmd-1.52 → multicmd-1.53}/tests/test_async_executor.py +0 -0
  14. {multicmd-1.52 → multicmd-1.53}/tests/test_cli.py +0 -0
  15. {multicmd-1.52 → multicmd-1.53}/tests/test_cli_input.py +0 -0
  16. {multicmd-1.52 → multicmd-1.53}/tests/test_cli_summary.py +0 -0
  17. {multicmd-1.52 → multicmd-1.53}/tests/test_display.py +0 -0
  18. {multicmd-1.52 → multicmd-1.53}/tests/test_expand_ranges.py +0 -0
  19. {multicmd-1.52 → multicmd-1.53}/tests/test_parsing.py +0 -0
  20. {multicmd-1.52 → multicmd-1.53}/tests/test_ping.py +0 -0
  21. {multicmd-1.52 → multicmd-1.53}/tests/test_pool.py +0 -0
  22. {multicmd-1.52 → multicmd-1.53}/tests/test_process_groups.py +0 -0
  23. {multicmd-1.52 → multicmd-1.53}/tests/test_pty.py +0 -0
  24. {multicmd-1.52 → multicmd-1.53}/tests/test_reader.py +0 -0
  25. {multicmd-1.52 → multicmd-1.53}/tests/test_regressions.py +0 -0
  26. {multicmd-1.52 → multicmd-1.53}/tests/test_run_command.py +0 -0
  27. {multicmd-1.52 → multicmd-1.53}/tests/test_run_commands.py +0 -0
  28. {multicmd-1.52 → multicmd-1.53}/tests/test_sudo.py +0 -0
  29. {multicmd-1.52 → multicmd-1.53}/tests/test_supervision.py +0 -0
  30. {multicmd-1.52 → multicmd-1.53}/tests/test_task.py +0 -0
  31. {multicmd-1.52 → multicmd-1.53}/tests/test_windows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiCMD
3
- Version: 1.52
3
+ Version: 1.53
4
4
  Summary: Run commands simultaneously
5
5
  Home-page: https://github.com/yufei-pan/multiCMD
6
6
  Author: Yufei Pan
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiCMD
3
- Version: 1.52
3
+ Version: 1.53
4
4
  Summary: Run commands simultaneously
5
5
  Home-page: https://github.com/yufei-pan/multiCMD
6
6
  Author: Yufei Pan
@@ -25,9 +25,9 @@ import time
25
25
  from pprint import pformat
26
26
 
27
27
  #%% global vars
28
- version = '1.52'
28
+ version = '1.53'
29
29
  __version__ = version
30
- COMMIT_DATE = '2026-10-10'
30
+ COMMIT_DATE = '2026-10-11'
31
31
  # fire-and-forget threads (run_commands with wait_for_return=False) that join_threads() waits on
32
32
  __running_threads = set()
33
33
  __running_threads_lock = threading.Lock()
@@ -1954,7 +1954,8 @@ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selectio
1954
1954
 
1955
1955
  def pretty_format_table(data, delimiter=None, header=None, full=False, remove_empty_columns=False):
1956
1956
  import re
1957
- version = 1.20
1957
+ import unicodedata
1958
+ version = 1.21
1958
1959
  _ = version
1959
1960
  if not data:
1960
1961
  return ""
@@ -1984,19 +1985,44 @@ def pretty_format_table(data, delimiter=None, header=None, full=False, remove_em
1984
1985
  header = data[0]
1985
1986
  data = data[1:]
1986
1987
  num_cols = len(header)
1988
+ # Widths count terminal columns: an East Asian wide character takes two,
1989
+ # a combining mark and an ANSI color code none.
1990
+ ansi = re.compile(r"(\x1b\[[0-?]*[ -/]*[@-~])")
1991
+ def char_width(ch):
1992
+ if ch < "\u0300": # ASCII and Latin-1
1993
+ return 1
1994
+ if unicodedata.combining(ch) or unicodedata.category(ch) in ("Mn", "Me", "Cf") or "\u1160" <= ch <= "\u11ff":
1995
+ return 0
1996
+ return 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
1987
1997
  def visible_len(s):
1988
- return len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", s))
1989
- # Ensure all rows have the same number of columns
1998
+ if "\x1b" in s:
1999
+ s = ansi.sub("", s)
2000
+ if not s or max(s) < "\u0300":
2001
+ return len(s)
2002
+ return sum(char_width(ch) for ch in s)
2003
+ def pad(s, width):
2004
+ return s + " " * (width - visible_len(s))
2005
+ def cut(s, width):
2006
+ # The characters that fit in ``width`` columns, and every color code.
2007
+ used, out = 0, []
2008
+ for i, piece in enumerate(ansi.split(s)):
2009
+ if i % 2:
2010
+ out.append(piece)
2011
+ continue
2012
+ for ch in piece:
2013
+ w = char_width(ch)
2014
+ if used + w > width:
2015
+ width = -1 # full: no further characters
2016
+ break
2017
+ out.append(ch)
2018
+ used += w
2019
+ return "".join(out)
1990
2020
  col_widths = [0] * num_cols
1991
- data_invisible_length = []
1992
2021
  for row in data:
1993
- dil = []
1994
- for item, col_width, cwi in zip(row, col_widths, range(num_cols)):
2022
+ for cwi, item in enumerate(row[:num_cols]):
1995
2023
  vl = visible_len(item)
1996
- if vl > col_width:
2024
+ if vl > col_widths[cwi]:
1997
2025
  col_widths[cwi] = vl
1998
- dil.append(len(item) - vl)
1999
- data_invisible_length.append(dil)
2000
2026
  cols_to_remove = []
2001
2027
  if remove_empty_columns and not full:
2002
2028
  cols_to_remove = [i for i, col_width in enumerate(col_widths) if col_width == 0]
@@ -2005,24 +2031,17 @@ def pretty_format_table(data, delimiter=None, header=None, full=False, remove_em
2005
2031
  num_cols = len(header)
2006
2032
  header_widths = [visible_len(h) for h in header]
2007
2033
  col_widths = [max(w, hw) for w, hw in zip(col_widths, header_widths)]
2008
- header = [item.ljust(col_width + len(item) - visible_len(item)) for item, col_width in zip(header, col_widths)]
2034
+ header = [pad(item, col_width) for item, col_width in zip(header, col_widths)]
2009
2035
  normalized_padded_data = []
2010
- new_data_invisible_length = []
2011
- for row, invisible_lengths in zip(data, data_invisible_length):
2036
+ for row in data:
2012
2037
  if cols_to_remove:
2013
2038
  row = [item for i, item in enumerate(row) if i not in cols_to_remove]
2014
- invisible_lengths = [il for i, il in enumerate(invisible_lengths) if i not in cols_to_remove]
2015
2039
  if not any(row):
2016
2040
  row = ['-' * col_width for col_width in col_widths]
2017
- elif len(row) < num_cols:
2018
- row = [row[i].ljust(col_widths[i] + invisible_lengths[i]) if i < len(row) else "".ljust(col_widths[i]) for i in range(num_cols)]
2019
- elif len(row) >= num_cols:
2020
- #row = row[:num_cols]
2021
- row = [item.ljust(col_width + invisible_length) for item, col_width, invisible_length in zip(row, col_widths, invisible_lengths)]
2041
+ else:
2042
+ row = [pad(row[i] if i < len(row) else "", col_widths[i]) for i in range(num_cols)]
2022
2043
  normalized_padded_data.append(row)
2023
- new_data_invisible_length.append(invisible_lengths)
2024
2044
  data = normalized_padded_data
2025
- data_invisible_length = new_data_invisible_length
2026
2045
  column_separator = " | "
2027
2046
  horizontal_separator = "-+-"
2028
2047
  terminal_width = get_terminal_size()[0]
@@ -2053,24 +2072,17 @@ def pretty_format_table(data, delimiter=None, header=None, full=False, remove_em
2053
2072
  reduce_by = min(diff, total_overflow_width)
2054
2073
  col_widths[i] -= reduce_by
2055
2074
  total_overflow_width -= reduce_by
2056
- def truncate_to_width(string, width,invisible_length):
2057
- s_len = len(string) - invisible_length
2058
- if s_len <= width:
2075
+ def truncate_to_width(string, width):
2076
+ if visible_len(string) <= width:
2059
2077
  return string
2060
- rstripedSize = len(string.rstrip()) - invisible_length
2061
- if rstripedSize <= width:
2062
- return string[:width + invisible_length]
2078
+ stripped = string.rstrip()
2079
+ if visible_len(stripped) <= width:
2080
+ return pad(stripped, width)
2063
2081
  if width < 2:
2064
- if width < 1:
2065
- return ""
2066
- else:
2067
- return '.'
2068
- return string[: width + invisible_length - 2] + ".."
2069
- data = [
2070
- [truncate_to_width(item, col_width, invisible_length) for item, col_width, invisible_length in zip(row, col_widths, dil)]
2071
- for row, dil in zip(data, data_invisible_length)
2072
- ]
2073
- header = [item[:col_width] for item, col_width in zip(header, col_widths)]
2082
+ return "."[:width]
2083
+ return pad(cut(string, width - 2) + "..", width)
2084
+ data = [[truncate_to_width(item, col_width) for item, col_width in zip(row, col_widths)] for row in data]
2085
+ header = [truncate_to_width(item, col_width) for item, col_width in zip(header, col_widths)]
2074
2086
  return render(header, data, col_widths, column_separator, horizontal_separator)
2075
2087
 
2076
2088
  def parseTable(data,sort=False,min_space=2):
@@ -2,6 +2,7 @@ import builtins
2
2
  import select
3
3
  import sys
4
4
 
5
+ import multiCMD
5
6
  from multiCMD import (
6
7
  _genrate_progress_bar,
7
8
  format_bytes,
@@ -22,6 +23,24 @@ def test_pretty_format_table_basic_and_remove_empty():
22
23
  assert out == "h1\n--\na \nb \n"
23
24
 
24
25
 
26
+ def test_pretty_format_table_pads_wide_characters_by_display_width():
27
+ # East Asian wide characters take two terminal columns, a combining mark none.
28
+ out = pretty_format_table([["name", "v"], ["中文", "1"], ["abcd", "2"], ["e\u0301", "3"]], full=True)
29
+ assert out == "name | v\n-----+--\n中文 | 1\nabcd | 2\ne\u0301 | 3\n"
30
+
31
+
32
+ def test_pretty_format_table_truncates_wide_characters_to_the_terminal(monkeypatch):
33
+ monkeypatch.setattr(multiCMD, "get_terminal_size", lambda: (12, 24))
34
+ out = pretty_format_table([["k", "v"], ["a", "中文中文中文"]])
35
+ assert out == "k|v \n-+----------\na|中文中文..\n"
36
+
37
+
38
+ def test_pretty_format_table_narrows_a_wide_header_by_display_width(monkeypatch):
39
+ monkeypatch.setattr(multiCMD, "get_terminal_size", lambda: (10, 24))
40
+ out = pretty_format_table([["名前", "v"], ["aaaaaaaaaa", "1"]])
41
+ assert out == "名前 |v\n--------+-\naaaaaa..|1\n"
42
+
43
+
25
44
  def test_parse_table():
26
45
  text = "col1 col2\nfoo bar\n"
27
46
  table = parseTable(text)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes