kisstdlib 0.0.4__tar.gz → 0.0.5__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 (35) hide show
  1. kisstdlib-0.0.5/LICENSE.txt +19 -0
  2. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/PKG-INFO +7 -1
  3. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/__init__.py +2 -2
  4. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/argparse/__init__.py +4 -2
  5. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/argparse/better.py +74 -52
  6. kisstdlib-0.0.5/kisstdlib/failure.py +60 -0
  7. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/io/__init__.py +2 -2
  8. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/io/base.py +63 -26
  9. kisstdlib-0.0.4/kisstdlib/io/encoder/base.py → kisstdlib-0.0.5/kisstdlib/io/encoder.py +21 -15
  10. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/io/stdio.py +4 -2
  11. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/io/wrapper.py +42 -33
  12. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/logging.py +11 -8
  13. kisstdlib-0.0.5/kisstdlib/os.py +528 -0
  14. kisstdlib-0.0.5/kisstdlib/parsing.py +202 -0
  15. {kisstdlib-0.0.4/kisstdlib/io/encoder → kisstdlib-0.0.5/kisstdlib}/pyrepr.py +39 -29
  16. kisstdlib-0.0.5/kisstdlib/sorted.py +215 -0
  17. kisstdlib-0.0.5/kisstdlib/tariter.py +286 -0
  18. kisstdlib-0.0.5/kisstdlib/time.py +414 -0
  19. kisstdlib-0.0.4/kisstdlib/exceptions.py → kisstdlib-0.0.5/kisstdlib/util.py +27 -14
  20. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib.egg-info/PKG-INFO +7 -1
  21. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib.egg-info/SOURCES.txt +13 -5
  22. kisstdlib-0.0.5/kisstdlib.egg-info/entry_points.txt +2 -0
  23. kisstdlib-0.0.5/kisstdlib.egg-info/requires.txt +1 -0
  24. kisstdlib-0.0.5/kisstdlib.egg-info/top_level.txt +2 -0
  25. kisstdlib-0.0.5/kisstdlib_bin/describe_dir.py +157 -0
  26. kisstdlib-0.0.5/pyproject.toml +105 -0
  27. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/setup.py +2 -0
  28. kisstdlib-0.0.4/kisstdlib/io/encoder/__init__.py +0 -24
  29. kisstdlib-0.0.4/kisstdlib/path.py +0 -163
  30. kisstdlib-0.0.4/kisstdlib.egg-info/top_level.txt +0 -1
  31. kisstdlib-0.0.4/pyproject.toml +0 -26
  32. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/README.md +0 -0
  33. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib/py.typed +0 -0
  34. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/kisstdlib.egg-info/dependency_links.txt +0 -0
  35. {kisstdlib-0.0.4 → kisstdlib-0.0.5}/setup.cfg +0 -0
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018-2025 Jan Malakhovski and `kisstdlib` contributors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -1,15 +1,21 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kisstdlib
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Keep It Simple STandarD LIBraries set for Python
5
5
  Author-email: Jan Malakhovski <oxij@oxij.org>
6
+ Project-URL: Homepage, https://oxij.org/software/kisstdlib/
7
+ Project-URL: GitHub, https://github.com/oxij/kisstdlib
8
+ Project-URL: Support Development, https://oxij.org/#support
6
9
  Keywords: sys,stdlib
7
10
  Classifier: Development Status :: 4 - Beta
8
11
  Classifier: Programming Language :: Python :: 3
9
12
  Classifier: License :: OSI Approved :: MIT License
10
13
  Classifier: License :: OSI Approved :: Python Software Foundation License
14
+ Classifier: License :: OSI Approved :: BSD License
11
15
  Requires-Python: >=3.10
12
16
  Description-Content-Type: text/markdown
17
+ License-File: LICENSE.txt
18
+ Requires-Dist: sortedcontainers==2.4.*
13
19
 
14
20
  # What?
15
21
 
@@ -1,6 +1,6 @@
1
- # Copyright (c) 2018-2023 Jan Malakhovski <oxij@oxij.org>
1
+ # Copyright (c) 2018-2025 Jan Malakhovski <oxij@oxij.org>
2
2
  #
3
- # This file is a part of kisstdlib project.
3
+ # This file is a part of `kisstdlib` project.
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
- # Copyright (c) 2023 Jan Malakhovski <oxij@oxij.org>
1
+ # Copyright (c) 2023-2025 Jan Malakhovski <oxij@oxij.org>
2
2
  #
3
- # This file is a part of kisstdlib project.
3
+ # This file is a part of `kisstdlib` project.
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -20,4 +20,6 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
22
 
23
+ """Default re-exports."""
24
+
23
25
  from .better import *
@@ -1,6 +1,6 @@
1
- # Copyright (c) 2022-2023 Jan Malakhovski <oxij@oxij.org>
1
+ # Copyright (c) 2022-2025 Jan Malakhovski <oxij@oxij.org>
2
2
  #
3
- # This file is a part of kisstdlib project.
3
+ # This file is a part of `kisstdlib` project.
4
4
  #
5
5
  # This file can be distributed under the terms of the MIT-style license given
6
6
  # below or Python Software Foundation License version 2 (PSF-2.0) as published
@@ -24,17 +24,21 @@
24
24
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
25
  # SOFTWARE.
26
26
 
27
+ """Extensions for the standard `argparse` module."""
28
+
27
29
  import typing as _t
28
30
  from argparse import *
29
31
  from gettext import gettext as _
30
32
 
33
+
31
34
  class BetterHelpFormatter(HelpFormatter):
32
35
  """Like argparse.HelpFormatter, but with better formatting.
33
- Also, it adds `add_code` function.
36
+ Also, it adds `add_code` function.
34
37
  """
35
38
 
36
- def _fill_text(self, text : str, width : int, indent : str) -> str:
39
+ def _fill_text(self, text: str, width: int, indent: str) -> str:
37
40
  import textwrap
41
+
38
42
  res = []
39
43
  for line in text.splitlines():
40
44
  if line == "":
@@ -46,31 +50,34 @@ class BetterHelpFormatter(HelpFormatter):
46
50
  res.append(sub)
47
51
  return "\n".join(res)
48
52
 
49
- def _split_lines(self, text : str, width : int) -> list[str]:
53
+ def _split_lines(self, text: str, width: int) -> list[str]:
50
54
  import textwrap
55
+
51
56
  res = []
52
57
  for line in text.splitlines():
53
58
  res += textwrap.wrap(line, width)
54
59
  return res
55
60
 
56
- def add_code(self, text : str) -> None:
61
+ def add_code(self, text: str) -> None:
57
62
  self.add_text(text.strip())
58
63
 
64
+
59
65
  class MarkdownBetterHelpFormatter(BetterHelpFormatter):
60
66
  """BetterHelpFormatter that outputs stuff formatted in Markdown"""
61
67
 
62
- def add_code(self, text : str) -> None:
68
+ def add_code(self, text: str) -> None:
63
69
  self.add_text("```\n" + text.strip() + "\n```")
64
70
 
65
- def _format_usage(self, usage : str | None, actions : _t.Any, groups : _t.Any, prefix : str | None) -> str:
71
+ def _format_usage(
72
+ self, usage: str | None, actions: _t.Any, groups: _t.Any, prefix: str | None
73
+ ) -> str:
66
74
  return super()._format_usage(usage, actions, groups, "")
67
75
 
68
- def _format_action(self, action : _t.Any) -> str:
76
+ def _format_action(self, action: _t.Any) -> str:
69
77
  # determine the required width and the entry label
70
78
  action_header = self._format_action_invocation(action)
71
79
 
72
- tup = self._current_indent, '', "`" + action_header + "`"
73
- action_header = '%*s- %s\n' % tup
80
+ action_header = f"{' ' * self._current_indent}- `{action_header}`\n"
74
81
 
75
82
  # collect the pieces of the action help
76
83
  parts = [action_header]
@@ -80,14 +87,14 @@ class MarkdownBetterHelpFormatter(BetterHelpFormatter):
80
87
  first = True
81
88
  for line in self._expand_help(action).splitlines():
82
89
  if first:
83
- parts.append('%*s: %s\n' % (self._current_indent, '', line))
90
+ parts.append(f"{' ' * self._current_indent}: {line}\n")
84
91
  else:
85
- parts.append('%*s %s\n' % (self._current_indent, '', line))
92
+ parts.append(f"{' ' * self._current_indent} {line}\n")
86
93
  first = False
87
94
 
88
95
  # or add a newline if the description doesn't end with one
89
- elif not action_header.endswith('\n'):
90
- parts.append('\n')
96
+ elif not action_header.endswith("\n"):
97
+ parts.append("\n")
91
98
 
92
99
  # if there are any sub-actions, add their help as well
93
100
  for subaction in self._iter_indented_subactions(action):
@@ -96,52 +103,57 @@ class MarkdownBetterHelpFormatter(BetterHelpFormatter):
96
103
  # return a single string
97
104
  return self._join_parts(parts)
98
105
 
99
- class _Section(HelpFormatter._Section):
106
+ class _Section(HelpFormatter._Section): # pylint: disable=protected-access
100
107
  def format_help(self) -> str:
101
108
  if self.parent is not None:
102
- self.formatter._indent()
103
- join = self.formatter._join_parts
109
+ self.formatter._indent() # pylint: disable=protected-access
110
+ join = self.formatter._join_parts # pylint: disable=protected-access
104
111
  item_help = join([func(*args) for func, args in self.items])
105
112
  if self.parent is not None:
106
- self.formatter._dedent()
113
+ self.formatter._dedent() # pylint: disable=protected-access
107
114
 
108
115
  # return nothing if the section was empty
109
116
  if not item_help:
110
- return ''
117
+ return ""
111
118
 
112
119
  # add the heading if the section was non-empty
113
120
  if self.heading is not SUPPRESS and self.heading is not None:
114
- heading = '%*s- %s:\n' % (self.formatter._current_indent, '', self.heading)
121
+ heading = f"{' ' * self.formatter._current_indent}- {self.heading}:\n" # pylint: disable=protected-access
115
122
  else:
116
- heading = ''
123
+ heading = ""
117
124
 
118
125
  # join the section-initial newline, the heading and the help
119
- return join(['\n', heading, item_help, '\n'])
126
+ return join(["\n", heading, item_help, "\n"])
127
+
120
128
 
121
129
  class BetterArgumentParser(ArgumentParser):
122
130
  """Like argparse.ArgumentParser but uses BetterHelpFormatter by default,
123
- adds `--help` only to the root node, and
124
- that `--help` prints the help for all the subcommands at once.
125
- Also, provides `add_version` option.
131
+ adds `--help` only to the root node, and
132
+ that `--help` prints the help for all the subcommands at once.
133
+ Also, provides `add_version` option.
126
134
  """
127
135
 
128
- formatter_class : type[BetterHelpFormatter]
129
-
130
- def __init__(self,
131
- prog : str | None = None,
132
- version : str | None = None,
133
- add_version : bool = False, # we set these two to False by default
134
- add_help : bool = False, # so that subparsers don't get them enabled by default
135
- additional_sections : list[_t.Callable[[BetterHelpFormatter], None]] = [],
136
- formatter_class : type[BetterHelpFormatter] = BetterHelpFormatter,
137
- *args : _t.Any, **kwargs : _t.Any) -> None:
138
- super().__init__(prog, *args, formatter_class = formatter_class, add_help = False, **kwargs) # type: ignore
136
+ formatter_class: type[BetterHelpFormatter]
137
+
138
+ def __init__( # pylint: disable=dangerous-default-value
139
+ self,
140
+ *args: _t.Any,
141
+ prog: str | None = None,
142
+ version: str | None = None,
143
+ add_version: bool = False, # we set these two to False by default
144
+ add_help: bool = False, # so that subparsers don't get them enabled by default
145
+ additional_sections: list[_t.Callable[[BetterHelpFormatter], None]] = [],
146
+ formatter_class: type[BetterHelpFormatter] = BetterHelpFormatter,
147
+ **kwargs: _t.Any,
148
+ ) -> None:
149
+ super().__init__(prog, *args, formatter_class=formatter_class, add_help=False, **kwargs) # type: ignore
139
150
 
140
151
  if version is None:
141
152
  version = "dev"
142
153
  if prog is not None:
143
154
  try:
144
155
  import importlib.metadata as meta
156
+
145
157
  try:
146
158
  version = meta.version(prog)
147
159
  except meta.PackageNotFoundError:
@@ -154,26 +166,33 @@ class BetterArgumentParser(ArgumentParser):
154
166
  self.add_help = add_help
155
167
  self.additional_sections = additional_sections
156
168
 
157
- default_prefix = '-' if '-' in self.prefix_chars else self.prefix_chars[0]
169
+ default_prefix = "-" if "-" in self.prefix_chars else self.prefix_chars[0]
158
170
  if self.add_version:
159
- self.add_argument(default_prefix*2 + "version", action="version", version="%(prog)s " + version)
171
+ self.add_argument(
172
+ default_prefix * 2 + "version", action="version", version="%(prog)s " + version
173
+ )
160
174
 
161
175
  if self.add_help:
162
176
  self.add_argument(
163
- default_prefix + "h", default_prefix*2 + "help", action='help',
164
- default=SUPPRESS, help=_('show this help message and exit'))
165
-
166
- def set_formatter_class(self, formatter_class : type[BetterHelpFormatter]) -> None:
177
+ default_prefix + "h",
178
+ default_prefix * 2 + "help",
179
+ action="help",
180
+ default=SUPPRESS,
181
+ help=_("show this help message and exit"),
182
+ )
183
+
184
+ def set_formatter_class(self, formatter_class: type[BetterHelpFormatter]) -> None:
167
185
  self.formatter_class = formatter_class
168
186
  if hasattr(self._subparsers, "_group_actions"):
169
- for grp in self._subparsers._group_actions: # type: ignore
170
- for choice, e in grp.choices.items(): # type: ignore
187
+ for grp in self._subparsers._group_actions: # type: ignore # pylint: disable=protected-access
188
+ for _choice, e in grp.choices.items(): # type: ignore
171
189
  if e.formatter_class != formatter_class:
172
190
  e.formatter_class = formatter_class
173
191
 
174
- def format_help(self, width : int | None = None) -> str:
192
+ def format_help(self, width: int | None = None) -> str:
175
193
  if width is None:
176
194
  import shutil
195
+
177
196
  width = shutil.get_terminal_size().columns - 2
178
197
  formatter = self.formatter_class(prog=self.prog, width=width)
179
198
 
@@ -181,19 +200,22 @@ class BetterArgumentParser(ArgumentParser):
181
200
  formatter.add_text(self.description)
182
201
 
183
202
  if hasattr(self, "_action_groups"):
184
- for action_group in self._action_groups:
203
+ for action_group in self._action_groups: # pylint: disable=protected-access
185
204
  formatter.start_section(action_group.title)
186
205
  formatter.add_text(action_group.description)
187
- formatter.add_arguments(action_group._group_actions)
206
+ formatter.add_arguments(
207
+ action_group._group_actions # pylint: disable=protected-access
208
+ )
188
209
  formatter.end_section()
189
210
 
190
- res : str = "# " + formatter.format_help()
211
+ res: str = "# " + formatter.format_help()
191
212
 
192
213
  if hasattr(self._subparsers, "_group_actions"):
193
214
  seen = set()
194
- for grp in self._subparsers._group_actions: # type: ignore
195
- for choice, e in grp.choices.items(): # type: ignore
196
- if e in seen: continue
215
+ for grp in self._subparsers._group_actions: # type: ignore # pylint: disable=protected-access
216
+ for _choice, e in grp.choices.items(): # type: ignore
217
+ if e in seen:
218
+ continue
197
219
  seen.add(e)
198
220
  e.formatter_class = self.formatter_class
199
221
  res += "\n#" + e.format_help(width=width)
@@ -0,0 +1,60 @@
1
+ # Copyright (c) 2023-2025 Jan Malakhovski <oxij@oxij.org>
2
+ #
3
+ # This file is a part of `kisstdlib` project.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """`Exceptions` with printable and i18n-able descriptions."""
24
+
25
+ import typing as _t
26
+
27
+
28
+ class CatastrophicFailure(Exception):
29
+ """An `Exception` with printable and i18n-able description."""
30
+
31
+ info: list[tuple[str, tuple[_t.Any, ...]]]
32
+
33
+ def __init__(self, what: _t.Any, *args: _t.Any) -> None:
34
+ super().__init__()
35
+ if isinstance(what, CatastrophicFailure):
36
+ self.info = what.info
37
+ else:
38
+ self.info = [(what, args)]
39
+
40
+ def get_message(self, gettext: _t.Callable[[str], str], separator: str = ": ") -> str:
41
+ res = []
42
+ for what, args in self.info:
43
+ try:
44
+ t = gettext(what) % args
45
+ except Exception:
46
+ t = f"{repr(what)} % {repr(args)}"
47
+ res.append(t)
48
+ res.reverse()
49
+ return separator.join(res)
50
+
51
+ def __str__(self) -> str:
52
+ return self.get_message(lambda x: x)
53
+
54
+ def elaborate(self, what: str, *args: _t.Any) -> _t.Any:
55
+ self.info.append((what, args))
56
+ return self
57
+
58
+
59
+ class Failure(CatastrophicFailure):
60
+ """A non-catastrophic `CatastrophicFailure`."""
@@ -1,6 +1,6 @@
1
- # Copyright (c) 2018-2023 Jan Malakhovski <oxij@oxij.org>
1
+ # Copyright (c) 2018-2025 Jan Malakhovski <oxij@oxij.org>
2
2
  #
3
- # This file is a part of kisstdlib project.
3
+ # This file is a part of `kisstdlib` project.
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
- # Copyright (c) 2018-2023 Jan Malakhovski <oxij@oxij.org>
1
+ # Copyright (c) 2018-2025 Jan Malakhovski <oxij@oxij.org>
2
2
  #
3
- # This file is a part of kisstdlib project.
3
+ # This file is a part of `kisstdlib` project.
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -20,24 +20,31 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
22
 
23
+ """Most basic types and definitions."""
24
+
23
25
  import abc as _abc
24
26
  import sys as _sys
25
27
  import typing as _t
26
28
 
27
- MEGABYTE = 1024 * 1024
28
-
29
- # anything that can be `write`n
29
+ # file descriptor number
30
+ FDNo = _t.NewType("FDNo", int)
31
+ # anything that can be `write`ed to
30
32
  ByteString = _t.Union[bytes, bytearray, memoryview]
31
33
 
32
- ShutdownState = int # _t.NewType("ShutdownState", int), ideally, but then ~ and | operations won't work
33
- SHUT_NONE = ShutdownState(0)
34
- SHUT_READ = ShutdownState(1)
34
+ # ideally, this should be `_t.NewType("ShutdownState", int)`, but then `~` and
35
+ # `|` operations wouldn't work
36
+ ShutdownState = int
37
+ SHUT_NONE = ShutdownState(0)
38
+ SHUT_READ = ShutdownState(1)
35
39
  SHUT_WRITE = ShutdownState(2)
36
- SHUT_BOTH = ShutdownState(3)
40
+ SHUT_BOTH = ShutdownState(3)
41
+
42
+ MEGABYTE = 1024 * 1024
43
+
37
44
 
38
45
  class MinimalIO(metaclass=_abc.ABCMeta):
39
46
  def __repr__(self) -> str:
40
- return "<%s %s>" % (self.__class__.__name__, hex(id(self)))
47
+ return f"<{self.__class__.__name__} {hex(id(self))}>"
41
48
 
42
49
  @_abc.abstractmethod
43
50
  def close(self) -> None:
@@ -49,7 +56,7 @@ class MinimalIO(metaclass=_abc.ABCMeta):
49
56
  raise NotImplementedError()
50
57
 
51
58
  @_abc.abstractmethod
52
- def shutdown(self, what : ShutdownState) -> None:
59
+ def shutdown(self, what: ShutdownState) -> None:
53
60
  raise NotImplementedError()
54
61
 
55
62
  @property
@@ -57,48 +64,78 @@ class MinimalIO(metaclass=_abc.ABCMeta):
57
64
  def shutdown_state(self) -> ShutdownState:
58
65
  raise NotImplementedError()
59
66
 
67
+
68
+ IncompleteResultType = _t.TypeVar("IncompleteResultType")
69
+
70
+
71
+ class IncompleteResultError(Exception, _t.Generic[IncompleteResultType]):
72
+ """En `Exception` signifying an opeartion did not complete, but did
73
+ produce a partial result.
74
+ """
75
+
76
+ def __init__(self, data: IncompleteResultType) -> None:
77
+ super().__init__()
78
+ self.data = data
79
+
80
+
81
+ class IncompleteReadError(IncompleteResultError[bytes]):
82
+ """When a `read` operation was not complete. `.data` stores the chunk that was
83
+ read successfully.
84
+ """
85
+
86
+
87
+ class IncompleteWriteError(IncompleteResultError[bytes]):
88
+ """When a `write` operation was not complete. `.data` stores the leftover
89
+ unwritten chunk."""
90
+
91
+
60
92
  class MinimalIOReader(MinimalIO):
61
93
  @_abc.abstractmethod
62
- def read_some_bytes(self, size : int) -> bytes:
94
+ def read_some_bytes(self, size: int) -> bytes:
63
95
  raise NotImplementedError()
64
96
 
65
- def read_all_bytes(self, chunk_size : int = MEGABYTE) -> bytes:
66
- data : list[bytes] = []
97
+ def read_all_bytes(self, chunk_size: int = MEGABYTE) -> bytes:
98
+ data: list[bytes] = []
67
99
  while True:
68
100
  res = self.read_some_bytes(chunk_size)
69
101
  rlen = len(res)
70
- if rlen == 0: break
102
+ if rlen == 0:
103
+ break
71
104
  data.append(res)
72
105
  return b"".join(data)
73
106
 
74
- def read_exactly_bytes(self, size : int) -> bytes:
75
- data : list[bytes] = []
107
+ def read_bytes(self, size: int | None = None) -> bytes:
108
+ if size is None:
109
+ return self.read_all_bytes()
110
+ return self.read_some_bytes(size)
111
+
112
+ def read_exactly_bytes(self, size: int) -> bytes:
113
+ data: list[bytes] = []
76
114
  total = 0
77
115
  while total < size:
78
116
  res = self.read_some_bytes(size - total)
79
117
  rlen = len(res)
80
- if rlen == 0: break
118
+ if rlen == 0:
119
+ raise IncompleteReadError(b"".join(data))
81
120
  data.append(res)
82
121
  total += rlen
83
122
  return b"".join(data)
84
123
 
85
- def read_bytes(self, size : int | None = None) -> bytes:
86
- if size is None:
87
- return self.read_all_bytes()
88
- else:
89
- return self.read_exactly_bytes(size)
90
124
 
91
125
  class MinimalIOWriter(MinimalIO):
92
126
  @_abc.abstractmethod
93
- def write_some_bytes(self, data : ByteString) -> int:
127
+ def write_some_bytes(self, data: ByteString) -> int:
94
128
  raise NotImplementedError()
95
129
 
96
- def write_bytes(self, data : ByteString) -> None:
130
+ def write_bytes(self, data: ByteString) -> None:
97
131
  done = 0
98
132
  view = memoryview(data)
99
133
  datalen = len(data)
100
134
  while done < datalen:
101
- done += self.write_some_bytes(view[done:])
135
+ res = self.write_some_bytes(view[done:])
136
+ if res == 0:
137
+ raise IncompleteWriteError(data[done:])
138
+ done += res
102
139
 
103
140
  @_abc.abstractmethod
104
141
  def flush(self) -> None:
@@ -1,6 +1,6 @@
1
- # Copyright (c) 2023 Jan Malakhovski <oxij@oxij.org>
1
+ # Copyright (c) 2023-2025 Jan Malakhovski <oxij@oxij.org>
2
2
  #
3
- # This file is a part of kisstdlib project.
3
+ # This file is a part of `kisstdlib` project.
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -20,20 +20,25 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
22
 
23
+ """Most basic types and definitions."""
24
+
23
25
  import sys as _sys
24
26
  import typing as _t
25
27
 
26
- from ..wrapper import *
28
+ from .wrapper import *
29
+
27
30
 
28
31
  class TIOEncoder(TIOWrappedWriter):
29
- default : _t.Callable[[_t.Any, _t.Any], None]
32
+ default: _t.Callable[[_t.Any, _t.Any], None]
30
33
 
31
- def __init__(self,
32
- fobj : _t.Any,
33
- eol : bytes = b"\n",
34
- encoding : str = _sys.getdefaultencoding(),
35
- encoders : dict[type[_t.Any], _t.Callable[[_t.Any, _t.Any], None]] = {},
36
- default : _t.Callable[[_t.Any, _t.Any], None] | None = None) -> None:
34
+ def __init__( # pylint: disable=dangerous-default-value
35
+ self,
36
+ fobj: _t.Any,
37
+ eol: bytes = b"\n",
38
+ encoding: str = _sys.getdefaultencoding(),
39
+ encoders: dict[type[_t.Any], _t.Callable[[_t.Any, _t.Any], None]] = {},
40
+ default: _t.Callable[[_t.Any, _t.Any], None] | None = None,
41
+ ) -> None:
37
42
  super().__init__(fobj, eol, encoding)
38
43
  self.encoders = encoders.copy()
39
44
  if default is None:
@@ -41,16 +46,16 @@ class TIOEncoder(TIOWrappedWriter):
41
46
  else:
42
47
  self.default = default
43
48
 
44
- def not_implemented(self, obj : _t.Any) -> None:
49
+ def not_implemented(self, obj: _t.Any) -> None:
45
50
  raise NotImplementedError("don't know how to encode %s", type(obj))
46
51
 
47
- def _find_encoder(self, obj_type : _t.Any) -> _t.Any:
52
+ def _find_encoder(self, obj_type: _t.Any) -> _t.Any:
48
53
  for typ, enc in self.encoders.items():
49
54
  if issubclass(obj_type, typ):
50
55
  return enc
51
56
  return self.default
52
57
 
53
- def encode(self, obj : _t.Any) -> None:
58
+ def encode(self, obj: _t.Any) -> None:
54
59
  typ = type(obj)
55
60
  try:
56
61
  enc = self.encoders[typ]
@@ -59,7 +64,8 @@ class TIOEncoder(TIOWrappedWriter):
59
64
  self.encoders[typ] = enc
60
65
  enc(self, obj)
61
66
 
62
- tio_default_encoders : dict[type[_t.Any], _t.Callable[[TIOWrappedWriter, _t.Any], None]] = {
67
+
68
+ tio_default_encoders: dict[type[_t.Any], _t.Callable[[TIOWrappedWriter, _t.Any], None]] = {
63
69
  bool: TIOEncoder.write_strable,
64
70
  int: TIOEncoder.write_strable,
65
71
  float: TIOEncoder.write_strable,
@@ -67,7 +73,7 @@ tio_default_encoders : dict[type[_t.Any], _t.Callable[[TIOWrappedWriter, _t.Any]
67
73
  bytes: TIOEncoder.write_bytes,
68
74
  }
69
75
 
70
- tio_default_encoders_ln : dict[type[_t.Any], _t.Callable[[TIOWrappedWriter, _t.Any], None]] = {
76
+ tio_default_encoders_ln: dict[type[_t.Any], _t.Callable[[TIOWrappedWriter, _t.Any], None]] = {
71
77
  bool: TIOEncoder.write_strable_ln,
72
78
  int: TIOEncoder.write_strable_ln,
73
79
  float: TIOEncoder.write_strable_ln,
@@ -1,6 +1,6 @@
1
- # Copyright (c) 2018-2024 Jan Malakhovski <oxij@oxij.org>
1
+ # Copyright (c) 2018-2025 Jan Malakhovski <oxij@oxij.org>
2
2
  #
3
- # This file is a part of kisstdlib project.
3
+ # This file is a part of `kisstdlib` project.
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -20,6 +20,8 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
22
 
23
+ """`TIOWrapped*`s for `stdin`, `stdout`, and `stderr`."""
24
+
23
25
  import os as _os
24
26
  import sys as _sys
25
27