PyPtt 2.2.6__tar.gz → 2.2.7__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 (101) hide show
  1. {pyptt-2.2.6 → pyptt-2.2.7}/PKG-INFO +1 -1
  2. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/__init__.py +1 -1
  3. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_uao/__init__.py +19 -7
  4. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/connect_core.py +13 -9
  5. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/screens.py +9 -0
  6. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt.egg-info/PKG-INFO +1 -1
  7. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt.egg-info/SOURCES.txt +1 -0
  8. pyptt-2.2.7/tests/test_uao_codec.py +93 -0
  9. {pyptt-2.2.6 → pyptt-2.2.7}/LICENSE +0 -0
  10. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/PTT.py +0 -0
  11. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_bucket.py +0 -0
  12. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_call_status.py +0 -0
  13. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_change_pw.py +0 -0
  14. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_comment.py +0 -0
  15. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_del_post.py +0 -0
  16. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_board_info.py +0 -0
  17. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_board_list.py +0 -0
  18. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_bottom_post_list.py +0 -0
  19. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_favourite_board.py +0 -0
  20. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_newest_index.py +0 -0
  21. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_post.py +0 -0
  22. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_post_index.py +0 -0
  23. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_post_list.py +0 -0
  24. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_time.py +0 -0
  25. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_user.py +0 -0
  26. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_get_waterball.py +0 -0
  27. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_give_money.py +0 -0
  28. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_has_new_mail.py +0 -0
  29. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_loginout.py +0 -0
  30. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_lottery.py +0 -0
  31. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_mail.py +0 -0
  32. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_mark_post.py +0 -0
  33. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_post.py +0 -0
  34. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_reply_post.py +0 -0
  35. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_search_user.py +0 -0
  36. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_set_board_title.py +0 -0
  37. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_set_signature_file.py +0 -0
  38. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_api_util.py +0 -0
  39. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_uao/LICENSE +0 -0
  40. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_uao/b2u.py +0 -0
  41. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/_uao/u2b.py +0 -0
  42. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/api_server.py +0 -0
  43. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/check_value.py +0 -0
  44. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/command.py +0 -0
  45. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/config.py +0 -0
  46. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/data_type.py +0 -0
  47. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/exceptions.py +0 -0
  48. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/i18n.py +0 -0
  49. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/lang_en_US.py +0 -0
  50. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/lang_zh_TW.py +0 -0
  51. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/lib_util.py +0 -0
  52. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/log.py +0 -0
  53. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/service.py +0 -0
  54. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt/ssl_config.py +0 -0
  55. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt.egg-info/dependency_links.txt +0 -0
  56. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt.egg-info/requires.txt +0 -0
  57. {pyptt-2.2.6 → pyptt-2.2.7}/PyPtt.egg-info/top_level.txt +0 -0
  58. {pyptt-2.2.6 → pyptt-2.2.7}/README.md +0 -0
  59. {pyptt-2.2.6 → pyptt-2.2.7}/pyproject.toml +0 -0
  60. {pyptt-2.2.6 → pyptt-2.2.7}/setup.cfg +0 -0
  61. {pyptt-2.2.6 → pyptt-2.2.7}/setup.py +0 -0
  62. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_bucket.py +0 -0
  63. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_change_pw.py +0 -0
  64. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_comment.py +0 -0
  65. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_del_post.py +0 -0
  66. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_del_post_unit.py +0 -0
  67. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_exceptions.py +0 -0
  68. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_fast_post.py +0 -0
  69. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_board_info.py +0 -0
  70. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_board_list.py +0 -0
  71. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_bottom_post_list.py +0 -0
  72. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_content_formula.py +0 -0
  73. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_favourite_boards.py +0 -0
  74. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_mail.py +0 -0
  75. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_newest_index.py +0 -0
  76. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_post.py +0 -0
  77. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_post_list.py +0 -0
  78. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_post_list_parser.py +0 -0
  79. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_post_parser.py +0 -0
  80. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_time.py +0 -0
  81. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_user.py +0 -0
  82. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_get_waterball.py +0 -0
  83. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_give_money.py +0 -0
  84. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_i18n.py +0 -0
  85. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_incremental_parity.py +0 -0
  86. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_init.py +0 -0
  87. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_logger.py +0 -0
  88. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_login_logout.py +0 -0
  89. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_lottery.py +0 -0
  90. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_mail_send_and_del.py +0 -0
  91. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_mark_post.py +0 -0
  92. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_parse_query_post.py +0 -0
  93. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_post.py +0 -0
  94. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_post_anonymous.py +0 -0
  95. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_pyte_parity.py +0 -0
  96. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_reply_post.py +0 -0
  97. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_search_user.py +0 -0
  98. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_service.py +0 -0
  99. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_set_board_title.py +0 -0
  100. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_set_signature_file.py +0 -0
  101. {pyptt-2.2.6 → pyptt-2.2.7}/tests/test_vt100_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.2.6
3
+ Version: 2.2.7
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -1,4 +1,4 @@
1
- __version__ = '2.2.6'
1
+ __version__ = '2.2.7'
2
2
  __author__ = 'CodingMan'
3
3
  __email__ = 'pttcodingman@gmail.com'
4
4
 
@@ -1,10 +1,16 @@
1
1
  """Vendored Big5-UAO codec.
2
2
 
3
- Vendored (unmodified) from PttCodingMan/pyUAO, a fork of eight04/pyUAO,
4
- to drop the external `uao` PyPI dependency (unmaintained upstream) and
5
- pick up this fork's correctness fixes and incremental/stream codec
6
- support. Vendoring is for dependency control, not performance: PyPtt is
7
- I/O bound, so codec speed is not the point here.
3
+ Vendored from PttCodingMan/pyUAO, a fork of eight04/pyUAO, to drop the
4
+ external `uao` PyPI dependency (unmaintained upstream) and pick up this
5
+ fork's correctness fixes and incremental/stream codec support.
6
+
7
+ Locally modified: `_decode` batches ASCII runs with a single C-level decode
8
+ instead of one chr()/append per byte. PTT screens are ~70% ASCII bytes even
9
+ in Big5, so this speeds up the receive path measurably while producing
10
+ byte-identical output (the byte→unicode table is unchanged). Python's stdlib
11
+ `big5` codec is *not* a usable fast path — it disagrees with the UAO table on
12
+ ~260 sequences (e.g. 0xa1 0x45 → '•' vs '‧') and can't decode UAO's
13
+ extensions at all.
8
14
 
9
15
  Source: https://github.com/PttCodingMan/pyUAO
10
16
  Commit: 9d9a8bf59dbe60800d214b66ce972bc2ddf8da89 (v0.3.0)
@@ -89,8 +95,14 @@ def _decode(input, errors, final):
89
95
  while i < n:
90
96
  c = input[i]
91
97
  if c < 0x80:
92
- append(chr(c))
93
- i += 1
98
+ # At a character boundary every byte < 0x80 is a standalone ASCII
99
+ # char, so decode the whole run at once. latin-1 maps 0x00-0x7f
100
+ # identically to ASCII and never raises.
101
+ j = i + 1
102
+ while j < n and input[j] < 0x80:
103
+ j += 1
104
+ append(input[i:j].decode('latin-1'))
105
+ i = j
94
106
  continue
95
107
  if i + 1 < n:
96
108
  u = _b2u_get(c * 0x100 + input[i + 1])
@@ -350,11 +350,11 @@ class API(object):
350
350
  return -1
351
351
 
352
352
  msg = ''
353
- receive_data_buffer = bytes()
354
353
  # Fresh incremental parsers for this screen sequence. Each arriving
355
354
  # chunk is fed once (per encoding) instead of re-parsing the whole
356
355
  # accumulated buffer every time — turning the receive loop from
357
- # O(N²) into O(N).
356
+ # O(N²) into O(N). The parsers also hold the rendered screen, so no
357
+ # separate raw byte buffer is accumulated.
358
358
  self._stream_parsers = {}
359
359
  start_time = time.time()
360
360
  find_target = False
@@ -372,7 +372,6 @@ class API(object):
372
372
 
373
373
  if isinstance(data_chunk, str):
374
374
  data_chunk = data_chunk.encode('utf-8')
375
- receive_data_buffer += data_chunk
376
375
 
377
376
  screen = self._stream_screen(self.current_encoding, data_chunk)
378
377
  screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
@@ -396,10 +395,17 @@ class API(object):
396
395
  if find_target:
397
396
  break
398
397
  except TimeoutError:
399
- if len(receive_data_buffer) > 0:
400
- vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding, self.config.screen_height)
401
- screens.show(self.config, vt100_p.screen)
402
- self._RDQ.add(vt100_p.screen)
398
+ # The incremental parser for the current encoding already holds
399
+ # the fully-rendered screen for every byte received this round
400
+ # (byte-identical to a fresh VT100Parser over the whole buffer —
401
+ # see tests/test_incremental_parity.py), so reuse it instead of
402
+ # re-decoding and re-parsing the entire buffer from scratch.
403
+ parser = self._stream_parsers.get(self.current_encoding)
404
+ if parser is not None:
405
+ screen = parser.screen
406
+ if len(screen) > 0:
407
+ screens.show(self.config, screen)
408
+ self._RDQ.add(screen)
403
409
  if use_too_many_res:
404
410
  raise exceptions.UseTooManyResources()
405
411
  return -1
@@ -450,7 +456,6 @@ class API(object):
450
456
  if break_detect_after_send:
451
457
  return -1
452
458
  msg = ''
453
- receive_data_buffer = bytes()
454
459
  self._stream_parsers = {}
455
460
  start_time = time.time()
456
461
  mid_time = time.time()
@@ -459,7 +464,6 @@ class API(object):
459
464
  data = self._core.read_very_eager()
460
465
  except EOFError:
461
466
  return -1
462
- receive_data_buffer += data
463
467
  screen = self._stream_screen(self.current_encoding, data)
464
468
  screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
465
469
  self._decode_screen(screen, start_time, target_list, is_secret, refresh, msg)
@@ -26,6 +26,8 @@ def _cell_width(ch: str) -> int:
26
26
  Ambiguous includes glyphs like ※, ←, →, ◎ that PTT's protocol treats
27
27
  as full-width when positioning the cursor.
28
28
  """
29
+ if ch.isascii(): # every ASCII char is a single narrow cell
30
+ return 1
29
31
  w = _cell_width_cache.get(ch)
30
32
  if w is None:
31
33
  w = 2 if unicodedata.east_asian_width(ch) in ('W', 'F', 'A') else 1
@@ -34,6 +36,11 @@ def _cell_width(ch: str) -> int:
34
36
 
35
37
 
36
38
  def _cell_len(text: str) -> int:
39
+ # PTT screens are mostly ASCII, and an all-ASCII string is exactly one cell
40
+ # per character — str.isascii() (a fast C scan) lets whole segments skip the
41
+ # per-character loop.
42
+ if text.isascii():
43
+ return len(text)
37
44
  return sum(_cell_width(c) for c in text)
38
45
 
39
46
 
@@ -45,6 +52,8 @@ def _str_pos_at_cells(line: str, cells: int) -> int:
45
52
  """
46
53
  if cells <= 0:
47
54
  return 0
55
+ if line.isascii(): # 1 cell per char → cell offset == string index
56
+ return min(cells, len(line))
48
57
  consumed = 0
49
58
  for i, ch in enumerate(line):
50
59
  if consumed >= cells:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.2.6
3
+ Version: 2.2.7
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -95,4 +95,5 @@ tests/test_search_user.py
95
95
  tests/test_service.py
96
96
  tests/test_set_board_title.py
97
97
  tests/test_set_signature_file.py
98
+ tests/test_uao_codec.py
98
99
  tests/test_vt100_parser.py
@@ -0,0 +1,93 @@
1
+ """
2
+ Regression tests for the vendored Big5-UAO codec (PyPtt/_uao).
3
+
4
+ `_decode` batches ASCII runs (one C-level decode instead of a chr()/append per
5
+ byte). These tests pin the invariant that the batched path produces exactly the
6
+ same output as a naive byte-by-byte decode, across ASCII runs, character
7
+ boundaries, incremental feeding and UAO-specific characters that stdlib `big5`
8
+ gets wrong or cannot represent.
9
+ """
10
+
11
+ import codecs
12
+
13
+ import pytest
14
+
15
+ import PyPtt.screens # noqa: F401 — importing registers the big5uao codec
16
+
17
+
18
+ def naive_decode(data: bytes, errors: str = 'replace') -> str:
19
+ """Reference: decode one unit at a time, no ASCII batching."""
20
+ from PyPtt._uao.b2u import b2u_table
21
+ get = b2u_table.get
22
+ out = []
23
+ i, n = 0, len(data)
24
+ while i < n:
25
+ c = data[i]
26
+ if c < 0x80:
27
+ out.append(chr(c))
28
+ i += 1
29
+ continue
30
+ pair = get(c * 0x100 + data[i + 1]) if i + 1 < n else None
31
+ if pair is not None:
32
+ out.append(pair)
33
+ i += 2
34
+ continue
35
+ single = get(c)
36
+ if single is not None:
37
+ out.append(single)
38
+ i += 1
39
+ continue
40
+ out.append('�') # errors='replace'
41
+ i += 1
42
+ return ''.join(out)
43
+
44
+
45
+ ASCII_CASES = [
46
+ b'',
47
+ b'hello world 12345',
48
+ b' leading and trailing spaces ',
49
+ b'[Ctrl-P] [d]delete [z]digest (100%)',
50
+ ]
51
+
52
+
53
+ @pytest.mark.parametrize('data', ASCII_CASES)
54
+ def test_ascii_runs_decode_verbatim(data):
55
+ assert data.decode('big5uao') == data.decode('ascii')
56
+
57
+
58
+ def test_mixed_ascii_and_cjk():
59
+ s = ' 9439 3 1/10 littrabble ◎ [問題] 測試中文標題 discussion'
60
+ assert s.encode('big5uao').decode('big5uao') == s
61
+
62
+
63
+ def test_uao_specific_characters_round_trip():
64
+ # Characters where stdlib big5 disagrees with UAO or can't encode them.
65
+ for s in ['‧', '~', '¥', '¢', '﹨', '⊕', '◎', '※', '←', '→', '─']:
66
+ assert s.encode('big5uao').decode('big5uao') == s
67
+
68
+
69
+ @pytest.mark.parametrize('errors', ['replace', 'ignore', 'strict'])
70
+ def test_batched_matches_naive_on_valid_content(errors):
71
+ s = '看板《Python》 test 混合 content 123 ‧~ ◎[軟工] tail'
72
+ data = s.encode('big5uao')
73
+ assert data.decode('big5uao', errors) == naive_decode(data)
74
+
75
+
76
+ def test_incremental_byte_by_byte_matches_batch():
77
+ s = '批踢踢 Python 測試 ‧~¥ ◎[軟工] 12345'
78
+ data = s.encode('big5uao')
79
+ dec = codecs.getincrementaldecoder('big5uao')('replace')
80
+ out = ''.join(dec.decode(bytes([b])) for b in data) + dec.decode(b'', final=True)
81
+ assert out == data.decode('big5uao') == s
82
+
83
+
84
+ def test_incremental_splits_multibyte_across_chunks():
85
+ data = '中文'.encode('big5uao')
86
+ dec = codecs.getincrementaldecoder('big5uao')('replace')
87
+ # Split inside the first character's byte pair.
88
+ out = dec.decode(data[:1]) + dec.decode(data[1:], final=True)
89
+ assert out == '中文'
90
+
91
+
92
+ def test_lone_0xff_maps_to_uao_private_use():
93
+ assert b'\xff'.decode('big5uao') == ''
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
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
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
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
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