omlish 0.0.0.dev370__py3-none-any.whl → 0.0.0.dev371__py3-none-any.whl

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.
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev370'
2
- __revision__ = '4f5d5deabcef292ee9186fec6530569e82d0c387'
1
+ __version__ = '0.0.0.dev371'
2
+ __revision__ = 'c37d96ddcd83d914205dd76b0f859b9ca93b0397'
3
3
 
4
4
 
5
5
  #
omlish/reflect/types.py CHANGED
@@ -1,9 +1,17 @@
1
1
  """
2
+ This is all gross, but this business always tends to be, and it at least centralizes and abstracts away all the
3
+ grossness and hides it behind a small and stable interface. It supports exactly as much as is needed by the codebase at
4
+ any given moment. It is 90% of why the codebase is strictly 3.13+ - I've tried to maintain backwards compat with this
5
+ kind of thing many times before and it's not worth my time, especially with lite code as an alternative.
6
+
7
+ I'm exploring extracting and distilling down mypy's type system to replace all of this, both to add some formalism /
8
+ give it some guiding North Star to make all of its decisions for it, and to add some long sought capabilities (infer,
9
+ meet, join, solve, ...), but it's quite a bit of work and not a priority at the moment.
10
+
2
11
  TODO:
3
12
  - visitor / transformer
4
13
  - uniform collection isinstance - items() for mappings, iter() for other
5
14
  - also check instance type in isinstance not just items lol
6
- TODO:
7
15
  - ta.Generic in mro causing trouble - omit? no longer 1:1
8
16
  - cache this shit, esp generic_mro shit
9
17
  - cache __hash__ in Generic/Union
omlish/testing/testing.py CHANGED
@@ -1,3 +1,5 @@
1
+ # ruff: noqa: UP006 UP007 UP045
2
+ # @omlish-lite
1
3
  import functools
2
4
  import os
3
5
  import sys
@@ -27,16 +29,16 @@ DEFAULT_TIMEOUT_S = 30
27
29
 
28
30
  def call_many_with_timeout(
29
31
  fns: ta.Iterable[ta.Callable[[], T]],
30
- timeout_s: float | None = None,
32
+ timeout_s: ta.Optional[float] = None,
31
33
  timeout_exception: Exception = TimeoutError('Thread timeout'),
32
- ) -> list[T]:
34
+ ) -> ta.List[T]:
33
35
  if timeout_s is None:
34
36
  timeout_s = DEFAULT_TIMEOUT_S
35
37
 
36
38
  fns = list(fns)
37
39
  missing = object()
38
- rets: list[ta.Any] = [missing] * len(fns)
39
- thread_exception: Exception | None = None
40
+ rets: ta.List[ta.Any] = [missing] * len(fns)
41
+ thread_exception: ta.Optional[Exception] = None
40
42
 
41
43
  def inner(fn, idx):
42
44
  try:
@@ -61,12 +63,12 @@ def call_many_with_timeout(
61
63
  if ret is missing:
62
64
  raise ValueError
63
65
 
64
- return ta.cast(list[T], rets)
66
+ return ta.cast('ta.List[T]', rets)
65
67
 
66
68
 
67
69
  def run_with_timeout(
68
70
  *fns: ta.Callable[[], None],
69
- timeout_s: float | None = None,
71
+ timeout_s: ta.Optional[float] = None,
70
72
  timeout_exception: Exception = TimeoutError('Thread timeout'),
71
73
  ) -> None:
72
74
  call_many_with_timeout(fns, timeout_s, timeout_exception)
@@ -74,7 +76,7 @@ def run_with_timeout(
74
76
 
75
77
  def waitpid_with_timeout(
76
78
  pid: int,
77
- timeout_s: float | None = None,
79
+ timeout_s: ta.Optional[float] = None,
78
80
  timeout_exception: Exception = TimeoutError('waitpid timeout'),
79
81
  ) -> int:
80
82
  if timeout_s is None:
@@ -108,7 +110,7 @@ def xfail(fn):
108
110
  return inner
109
111
 
110
112
 
111
- def raise_in_thread(thr: threading.Thread, exc: BaseException | type[BaseException]) -> None:
113
+ def raise_in_thread(thr: threading.Thread, exc: ta.Union[BaseException, ta.Type[BaseException]]) -> None:
112
114
  if sys.implementation.name != 'cpython':
113
115
  raise RuntimeError(sys.implementation.name)
114
116
 
@@ -0,0 +1 @@
1
+ # @omlish-lite
@@ -0,0 +1,4 @@
1
+ if __name__ == '__main__':
2
+ from .main import _main
3
+
4
+ _main()
@@ -0,0 +1,115 @@
1
+ # ruff: noqa: UP006 UP007 UP045
2
+ """
3
+ https://docs.python.org/3/library/unittest.html#command-line-interface
4
+ ~ https://github.com/python/cpython/tree/f66c75f11d3aeeb614600251fd5d3fe1a34b5ff1/Lib/unittest
5
+ """
6
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
7
+ # --------------------------------------------
8
+ #
9
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
10
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
11
+ # documentation.
12
+ #
13
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
14
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
15
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
16
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights
17
+ # Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
18
+ #
19
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
20
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
21
+ # any such work a brief summary of the changes made to Python.
22
+ #
23
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
24
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
25
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
26
+ # RIGHTS.
27
+ #
28
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
29
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
30
+ # ADVISED OF THE POSSIBILITY THEREOF.
31
+ #
32
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
33
+ #
34
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
35
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
36
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
37
+ #
38
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
39
+ # License Agreement.
40
+ import abc
41
+ import dataclasses as dc
42
+ import types
43
+ import typing as ta
44
+ import unittest
45
+
46
+ from .types import Test
47
+
48
+
49
+ ##
50
+
51
+
52
+ class TestTargetLoader:
53
+ def __init__(
54
+ self,
55
+ *,
56
+ test_name_patterns: ta.Optional[ta.Sequence[str]] = None,
57
+ module: ta.Union[str, types.ModuleType, None] = None,
58
+ loader: ta.Optional[unittest.loader.TestLoader] = None,
59
+ ) -> None:
60
+ super().__init__()
61
+
62
+ self._test_name_patterns = test_name_patterns
63
+ self._module = module
64
+ self._loader = loader
65
+
66
+ #
67
+
68
+ class Target(abc.ABC): # noqa
69
+ pass
70
+
71
+ class ModuleTarget(Target):
72
+ pass
73
+
74
+ @dc.dataclass(frozen=True)
75
+ class NamesTarget(Target):
76
+ test_names: ta.Optional[ta.Sequence[str]] = None
77
+
78
+ @dc.dataclass(frozen=True)
79
+ class DiscoveryTarget(Target):
80
+ start: ta.Optional[str] = None
81
+ pattern: ta.Optional[str] = None
82
+ top: ta.Optional[str] = None
83
+
84
+ def load(self, target: Target) -> Test:
85
+ loader = self._loader
86
+ if loader is None:
87
+ loader = unittest.loader.TestLoader()
88
+
89
+ if self._test_name_patterns:
90
+ loader.testNamePatterns = self._test_name_patterns # type: ignore[assignment]
91
+
92
+ if isinstance(target, TestTargetLoader.DiscoveryTarget):
93
+ return ta.cast(Test, loader.discover(
94
+ target.start, # type: ignore[arg-type]
95
+ target.pattern, # type: ignore[arg-type]
96
+ target.top,
97
+ ))
98
+
99
+ module: ta.Any = self._module
100
+ if isinstance(module, str):
101
+ module = __import__(module)
102
+ for part in module.split('.')[1:]:
103
+ module = getattr(module, part)
104
+
105
+ if isinstance(target, TestTargetLoader.ModuleTarget):
106
+ return ta.cast(Test, loader.loadTestsFromModule(module))
107
+
108
+ elif isinstance(target, TestTargetLoader.NamesTarget):
109
+ return ta.cast(Test, loader.loadTestsFromNames(
110
+ target.test_names, # type: ignore[arg-type]
111
+ module,
112
+ ))
113
+
114
+ else:
115
+ raise TypeError(target)
@@ -0,0 +1,272 @@
1
+ # ruff: noqa: UP006 UP007 UP045
2
+ """
3
+ https://docs.python.org/3/library/unittest.html#command-line-interface
4
+ ~ https://github.com/python/cpython/tree/f66c75f11d3aeeb614600251fd5d3fe1a34b5ff1/Lib/unittest
5
+ """
6
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
7
+ # --------------------------------------------
8
+ #
9
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
10
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
11
+ # documentation.
12
+ #
13
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
14
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
15
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
16
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights
17
+ # Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
18
+ #
19
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
20
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
21
+ # any such work a brief summary of the changes made to Python.
22
+ #
23
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
24
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
25
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
26
+ # RIGHTS.
27
+ #
28
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
29
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
30
+ # ADVISED OF THE POSSIBILITY THEREOF.
31
+ #
32
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
33
+ #
34
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
35
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
36
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
37
+ #
38
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
39
+ # License Agreement.
40
+ import argparse
41
+ import dataclasses as dc
42
+ import importlib.util
43
+ import os
44
+ import re
45
+ import sys
46
+ import types
47
+ import typing as ta
48
+
49
+ from .loading import TestTargetLoader
50
+ from .running import TestRunner
51
+
52
+
53
+ ##
54
+
55
+
56
+ def _get_attr_dict(obj: ta.Optional[ta.Any], *attrs: str) -> ta.Dict[str, ta.Any]:
57
+ if obj is None:
58
+ return {}
59
+
60
+ return {
61
+ a: v
62
+ for a in attrs
63
+ if (v := getattr(obj, a, None)) is not None
64
+ }
65
+
66
+
67
+ class TestRunCli:
68
+ def __init__(self) -> None:
69
+ super().__init__()
70
+
71
+ self._parser = self._get_arg_parser()
72
+
73
+ #
74
+
75
+ DEFAULT_DISCOVERY_START = '.'
76
+ DEFAULT_DISCOVERY_PATTERN = 'test*.py'
77
+
78
+ def _get_arg_parser(self) -> argparse.ArgumentParser:
79
+ parser = argparse.ArgumentParser(add_help=False)
80
+
81
+ parser.add_argument(
82
+ '-v',
83
+ '--verbose',
84
+ dest='verbosity',
85
+ action='store_const',
86
+ const=2,
87
+ help='Verbose output',
88
+ )
89
+
90
+ parser.add_argument(
91
+ '-q',
92
+ '--quiet',
93
+ dest='verbosity',
94
+ action='store_const',
95
+ const=0,
96
+ help='Quiet output',
97
+ )
98
+
99
+ parser.add_argument(
100
+ '--locals',
101
+ dest='tb_locals',
102
+ action='store_true',
103
+ help='Show local variables in tracebacks',
104
+ )
105
+
106
+ parser.add_argument(
107
+ '-f',
108
+ '--failfast',
109
+ dest='failfast',
110
+ action='store_true',
111
+ help='Stop on first fail or error',
112
+ )
113
+
114
+ parser.add_argument(
115
+ '-c',
116
+ '--catch',
117
+ dest='catchbreak',
118
+ action='store_true',
119
+ help='Catch Ctrl-C and display results so far',
120
+ )
121
+
122
+ parser.add_argument(
123
+ '-b',
124
+ '--buffer',
125
+ dest='buffer',
126
+ action='store_true',
127
+ help='Buffer stdout and stderr during tests',
128
+ )
129
+
130
+ def _convert_select_pattern(pattern: str) -> str:
131
+ if '*' not in pattern:
132
+ pattern = f'*{pattern}*'
133
+ return pattern
134
+
135
+ parser.add_argument(
136
+ '-k',
137
+ dest='test_name_patterns',
138
+ action='append',
139
+ type=_convert_select_pattern,
140
+ help='Only run tests which match the given substring',
141
+ )
142
+
143
+ #
144
+
145
+ parser.epilog = (
146
+ 'For test discovery all test modules must be importable from the top level directory of the project.'
147
+ )
148
+
149
+ parser.add_argument(
150
+ '-s',
151
+ '--start-directory',
152
+ default=self.DEFAULT_DISCOVERY_START,
153
+ dest='start',
154
+ help="Directory to start discovery ('.' default)",
155
+ )
156
+
157
+ parser.add_argument(
158
+ '-p',
159
+ '--pattern',
160
+ default=self.DEFAULT_DISCOVERY_PATTERN,
161
+ dest='pattern',
162
+ help="Pattern to match tests ('test*.py' default)",
163
+ )
164
+
165
+ parser.add_argument(
166
+ '-t',
167
+ '--top-level-directory',
168
+ dest='top',
169
+ help='Top level directory of project (defaults to start directory)',
170
+ )
171
+
172
+ parser.add_argument(
173
+ 'target',
174
+ nargs='*',
175
+ default=argparse.SUPPRESS,
176
+ help=argparse.SUPPRESS,
177
+ )
178
+
179
+ return parser
180
+
181
+ #
182
+
183
+ @dc.dataclass(frozen=True)
184
+ class ParsedArgs:
185
+ args: ta.Optional[argparse.Namespace]
186
+ test_names: ta.Optional[ta.Sequence[str]] = None
187
+ module: ta.Union[str, types.ModuleType, None] = None
188
+
189
+ def parse_args(self, argv: ta.Sequence[str]) -> ParsedArgs:
190
+ args = self._parser.parse_args(argv)
191
+ return self.ParsedArgs(args)
192
+
193
+ #
194
+
195
+ IMPORT_PATH_PAT = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*')
196
+
197
+ def _build_target(self, name: str, args: ParsedArgs) -> TestTargetLoader.Target:
198
+ is_discovery = False
199
+ if os.path.isdir(name):
200
+ is_discovery = True
201
+ elif self.IMPORT_PATH_PAT.fullmatch(name):
202
+ spec = importlib.util.find_spec(name)
203
+ if spec is not None and spec.submodule_search_locations is not None: # is a package, not a module
204
+ is_discovery = True
205
+
206
+ if not is_discovery:
207
+ return TestTargetLoader.NamesTarget([name])
208
+
209
+ else:
210
+ return TestTargetLoader.DiscoveryTarget(
211
+ start=name,
212
+ **_get_attr_dict(
213
+ args.args,
214
+ 'pattern',
215
+ 'top',
216
+ ),
217
+ )
218
+
219
+ #
220
+
221
+ NO_TESTS_EXITCODE = 5
222
+
223
+ def run(
224
+ self,
225
+ args: ParsedArgs,
226
+ *,
227
+ exit: bool = False, # noqa
228
+ ) -> None:
229
+ loader = TestTargetLoader(**_get_attr_dict(
230
+ args.args,
231
+ 'test_name_patterns',
232
+ ))
233
+
234
+ tests = [
235
+ loader.load(self._build_target(target_arg, args))
236
+ for target_arg in (args.args.target if args.args is not None else None) or [] # noqa
237
+ ]
238
+
239
+ runner = TestRunner(TestRunner.Args(**_get_attr_dict(
240
+ args.args,
241
+ 'verbosity',
242
+ 'failfast',
243
+ 'catchbreak',
244
+ 'buffer',
245
+ 'warnings',
246
+ 'tb_locals',
247
+ )))
248
+
249
+ result = runner.run_many(tests)
250
+
251
+ runner.print(result)
252
+
253
+ if exit:
254
+ if not result.num_tests_run and not result.skipped:
255
+ sys.exit(self.NO_TESTS_EXITCODE)
256
+ elif result.was_successful:
257
+ sys.exit(0)
258
+ else:
259
+ sys.exit(1)
260
+
261
+
262
+ ##
263
+
264
+
265
+ def _main() -> None:
266
+ cli = TestRunCli()
267
+ args = cli.parse_args(sys.argv[1:])
268
+ cli.run(args, exit=True)
269
+
270
+
271
+ if __name__ == '__main__':
272
+ _main()
@@ -0,0 +1,330 @@
1
+ # ruff: noqa: UP006 UP007 UP045
2
+ """
3
+ https://docs.python.org/3/library/unittest.html#command-line-interface
4
+ ~ https://github.com/python/cpython/tree/f66c75f11d3aeeb614600251fd5d3fe1a34b5ff1/Lib/unittest
5
+ """
6
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
7
+ # --------------------------------------------
8
+ #
9
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
10
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
11
+ # documentation.
12
+ #
13
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
14
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
15
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
16
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights
17
+ # Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
18
+ #
19
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
20
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
21
+ # any such work a brief summary of the changes made to Python.
22
+ #
23
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
24
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
25
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
26
+ # RIGHTS.
27
+ #
28
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
29
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
30
+ # ADVISED OF THE POSSIBILITY THEREOF.
31
+ #
32
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
33
+ #
34
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
35
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
36
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
37
+ #
38
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
39
+ # License Agreement.
40
+ import contextlib
41
+ import dataclasses as dc
42
+ import sys
43
+ import time
44
+ import typing as ta
45
+ import unittest
46
+ import warnings
47
+
48
+ from .types import Test
49
+
50
+
51
+ ##
52
+
53
+
54
+ class _WritelnDecorator:
55
+ def __init__(self, stream):
56
+ super().__init__()
57
+
58
+ self.stream = stream
59
+
60
+ def __getattr__(self, attr):
61
+ if attr in ('stream', '__getstate__'):
62
+ raise AttributeError(attr)
63
+ return getattr(self.stream, attr)
64
+
65
+ def writeln(self, arg=None):
66
+ if arg:
67
+ self.write(arg)
68
+ self.write('\n') # text-mode streams translate to \r\n if needed
69
+
70
+
71
+ class TestRunner:
72
+ """
73
+ A test runner class that displays results in textual form.
74
+
75
+ It prints out the names of tests as they are run, errors as they occur, and a summary of the results at the end of
76
+ the test run.
77
+ """
78
+
79
+ @dc.dataclass(frozen=True)
80
+ class Args:
81
+ descriptions: bool = True
82
+ verbosity: int = 1
83
+ failfast: bool = False
84
+ buffer: bool = False
85
+ warnings: ta.Optional[str] = None
86
+ tb_locals: bool = False
87
+ catchbreak: bool = False
88
+
89
+ def __init__(
90
+ self,
91
+ args: Args = Args(),
92
+ *,
93
+ stream: ta.Optional[ta.Any] = None,
94
+ ):
95
+ super().__init__()
96
+
97
+ self._args = args
98
+
99
+ if stream is None:
100
+ stream = sys.stderr
101
+ self._stream = _WritelnDecorator(stream)
102
+
103
+ #
104
+
105
+ @contextlib.contextmanager
106
+ def _warnings_context(self) -> ta.Iterator[None]:
107
+ with warnings.catch_warnings():
108
+ w = self._args.warnings
109
+
110
+ if w is None and not sys.warnoptions:
111
+ # Even if DeprecationWarnings are ignored by default print them anyway unless other warnings settings
112
+ # are specified by the warnings arg or the -W python flag.
113
+ w = 'default' # noqa
114
+ else:
115
+ # Here self.warnings is set either to the value passed to the warnings args or to None. If the user
116
+ # didn't pass a value self.warnings will be None. This means that the behavior is unchanged and depends
117
+ # on the values passed to -W.
118
+ w = self._args.warnings # noqa
119
+
120
+ if w:
121
+ # If self.warnings is set, use it to filter all the warnings.
122
+ warnings.simplefilter(w) # noqa
123
+
124
+ # If the filter is 'default' or 'always', special-case the warnings from the deprecated unittest methods
125
+ # to show them no more than once per module, because they can be fairly noisy. The -Wd and -Wa flags
126
+ # can be used to bypass this only when self.warnings is None.
127
+ if w in ['default', 'always']:
128
+ warnings.filterwarnings(
129
+ 'module',
130
+ category=DeprecationWarning,
131
+ message=r'Please use assert\w+ instead.',
132
+ )
133
+
134
+ yield
135
+
136
+ #
137
+
138
+ def _make_result(self) -> unittest.TextTestResult:
139
+ return unittest.TextTestResult(
140
+ self._stream, # type: ignore[arg-type]
141
+ self._args.descriptions,
142
+ self._args.verbosity,
143
+ )
144
+
145
+ #
146
+
147
+ class _InternalRunTestResult(ta.NamedTuple):
148
+ result: unittest.TextTestResult
149
+ time_taken: float
150
+
151
+ def _internal_run_test(self, test: ta.Callable[[unittest.TestResult], None]) -> _InternalRunTestResult:
152
+ result = self._make_result()
153
+ unittest.registerResult(result)
154
+ result.failfast = self._args.failfast
155
+ result.buffer = self._args.buffer
156
+ result.tb_locals = self._args.tb_locals
157
+
158
+ #
159
+
160
+ if self._args.catchbreak:
161
+ unittest.signals.installHandler()
162
+
163
+ with self._warnings_context():
164
+ start_time = time.perf_counter()
165
+
166
+ start_test_run = getattr(result, 'startTestRun', None)
167
+ if start_test_run is not None:
168
+ start_test_run()
169
+
170
+ try:
171
+ test(result)
172
+
173
+ finally:
174
+ stop_test_run = getattr(result, 'stopTestRun', None)
175
+ if stop_test_run is not None:
176
+ stop_test_run()
177
+
178
+ stop_time = time.perf_counter()
179
+
180
+ time_taken = stop_time - start_time
181
+
182
+ return TestRunner._InternalRunTestResult(
183
+ result,
184
+ time_taken,
185
+ )
186
+
187
+ #
188
+
189
+ @dc.dataclass(frozen=True)
190
+ class RunResult:
191
+ raw_results: ta.Sequence[unittest.TextTestResult]
192
+ time_taken: float
193
+
194
+ num_tests_run: int
195
+ was_successful: bool
196
+
197
+ class TestAndReason(ta.NamedTuple):
198
+ test: str
199
+ reason: str
200
+
201
+ skipped: ta.Sequence[TestAndReason]
202
+ errors: ta.Sequence[TestAndReason]
203
+ failures: ta.Sequence[TestAndReason]
204
+
205
+ expected_failures: ta.Sequence[TestAndReason]
206
+ unexpected_successes: ta.Sequence[str]
207
+
208
+ @classmethod
209
+ def merge(cls, results: ta.Iterable['TestRunner.RunResult']) -> 'TestRunner.RunResult':
210
+ def reduce_attr(fn, a):
211
+ return fn(getattr(r, a) for r in results)
212
+
213
+ def merge_list_attr(a):
214
+ return [rr for r in results for rr in getattr(r, a)]
215
+
216
+ return cls(
217
+ raw_results=merge_list_attr('raw_results'),
218
+ time_taken=reduce_attr(sum, 'time_taken'),
219
+
220
+ num_tests_run=reduce_attr(sum, 'num_tests_run'),
221
+ was_successful=reduce_attr(all, 'was_successful'),
222
+
223
+ skipped=merge_list_attr('skipped'),
224
+ errors=merge_list_attr('errors'),
225
+ failures=merge_list_attr('failures'),
226
+
227
+ expected_failures=merge_list_attr('expected_failures'),
228
+ unexpected_successes=merge_list_attr('unexpected_successes'),
229
+ )
230
+
231
+ def _build_run_result(self, internal_result: _InternalRunTestResult) -> RunResult:
232
+ result = internal_result.result
233
+
234
+ def as_test_and_reasons(l):
235
+ return [
236
+ TestRunner.RunResult.TestAndReason(result.getDescription(t), r)
237
+ for t, r in l
238
+ ]
239
+
240
+ return TestRunner.RunResult(
241
+ raw_results=[result],
242
+ time_taken=internal_result.time_taken,
243
+
244
+ num_tests_run=result.testsRun,
245
+ was_successful=result.wasSuccessful(),
246
+
247
+ skipped=as_test_and_reasons(result.skipped),
248
+ errors=as_test_and_reasons(result.errors),
249
+ failures=as_test_and_reasons(result.failures),
250
+
251
+ expected_failures=as_test_and_reasons(result.expectedFailures),
252
+ unexpected_successes=[result.getDescription(t) for t in result.unexpectedSuccesses],
253
+ )
254
+
255
+ #
256
+
257
+ def run(self, test: Test) -> RunResult:
258
+ return self._build_run_result(self._internal_run_test(test))
259
+
260
+ def run_many(self, tests: ta.Iterable[Test]) -> RunResult:
261
+ return TestRunner.RunResult.merge([self.run(t) for t in tests])
262
+
263
+ #
264
+
265
+ separator1 = unittest.TextTestResult.separator1
266
+ separator2 = unittest.TextTestResult.separator2
267
+
268
+ def print(self, result: RunResult) -> None:
269
+ if self._args.verbosity > 0:
270
+ self._stream.writeln()
271
+ self._stream.flush()
272
+
273
+ for t, r in result.errors:
274
+ self._stream.writeln(self.separator1)
275
+ self._stream.writeln(f'ERROR: {t}')
276
+ self._stream.writeln(self.separator2)
277
+ self._stream.writeln(f'{r}')
278
+ self._stream.flush()
279
+
280
+ for t, r in result.failures:
281
+ self._stream.writeln(self.separator1)
282
+ self._stream.writeln(f'FAIL: {t}')
283
+ self._stream.writeln(self.separator2)
284
+ self._stream.writeln(f'{r}')
285
+ self._stream.flush()
286
+
287
+ if result.unexpected_successes:
288
+ self._stream.writeln(self.separator1)
289
+ for t in result.unexpected_successes:
290
+ self._stream.writeln(f'UNEXPECTED SUCCESS: {t}')
291
+ self._stream.flush()
292
+
293
+ self._stream.writeln(self.separator2)
294
+
295
+ self._stream.writeln(
296
+ f'Ran {result.num_tests_run:d} '
297
+ f'test{"s" if result.num_tests_run != 1 else ""} '
298
+ f'in {result.time_taken:.3f}s',
299
+ )
300
+ self._stream.writeln()
301
+
302
+ expected_fails = len(result.expected_failures)
303
+ unexpected_successes = len(result.unexpected_successes)
304
+ skipped = len(result.skipped)
305
+
306
+ infos: ta.List[str] = []
307
+
308
+ if not result.was_successful:
309
+ self._stream.write('FAILED')
310
+ failed, errored = len(result.failures), len(result.errors)
311
+ if failed:
312
+ infos.append(f'failures={failed:d}')
313
+ if errored:
314
+ infos.append(f'errors={errored:d}')
315
+ else:
316
+ self._stream.write('OK')
317
+
318
+ if skipped:
319
+ infos.append(f'skipped={skipped:d}')
320
+
321
+ if expected_fails:
322
+ infos.append(f'expected failures={expected_fails:d}')
323
+
324
+ if unexpected_successes:
325
+ infos.append(f'unexpected successes={unexpected_successes:d}')
326
+
327
+ if infos:
328
+ self._stream.writeln(f' ({", ".join(infos)})')
329
+ else:
330
+ self._stream.write('\n')
@@ -0,0 +1,5 @@
1
+ import typing as ta
2
+ import unittest
3
+
4
+
5
+ Test = ta.Callable[[unittest.TestResult], None] # ta.TypeAlias
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omlish
3
- Version: 0.0.0.dev370
3
+ Version: 0.0.0.dev371
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=aT8yZ-Zh-9wfHl5Ym5ouiWC1i0cy7Q7RlhzavB6VLPI,8587
2
- omlish/__about__.py,sha256=ppW6ebyCL8_ZT4j16N88-VXSlgE7mfjsGTbuhlNYrDw,3478
2
+ omlish/__about__.py,sha256=IuPF6dQwqBF37vj4nM3a2rfw3IW2B0qdP0SrL4-B5BI,3478
3
3
  omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
4
4
  omlish/c3.py,sha256=rer-TPOFDU6fYq_AWio_AmA-ckZ8JDY5shIzQ_yXfzA,8414
5
5
  omlish/cached.py,sha256=MLap_p0rdGoDIMVhXVHm1tsbcWobJF0OanoodV03Ju8,542
@@ -586,7 +586,7 @@ omlish/reflect/__init__.py,sha256=9pzXLXXNMHkLhhI79iUr-o0SMOtR6HMUmAEUplZkIdE,85
586
586
  omlish/reflect/inspect.py,sha256=WCo2YpBYauKw6k758FLlZ_H4Q05rgVPs96fEv9w6zHQ,1538
587
587
  omlish/reflect/ops.py,sha256=F77OTaw0Uw020cJCWX_Q4kL3wvxlJ8jV8wz7BctGL_k,2619
588
588
  omlish/reflect/subst.py,sha256=0bnbWPbSbabu47BVnZtCQAZFim4CNC6azEDCmJES32w,3696
589
- omlish/reflect/types.py,sha256=2LJjaa2SuyhmeMh4Vmrgq-pdqQ-diKGAJJv9ln-wKrg,11244
589
+ omlish/reflect/types.py,sha256=uMcxUHVJWYYcmF9ODeJ9WfcQ0DejsqsEVIX5K0HjJSM,12018
590
590
  omlish/secrets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
591
591
  omlish/secrets/all.py,sha256=gv_d9SfyMxso30HsrSz9XmIrSZOdl3rLA5MSH0ZXfgM,486
592
592
  omlish/secrets/crypto.py,sha256=9D21lnvPhStwu8arD4ssT0ih0bDG-nlqIRdVgYL40xA,3708
@@ -759,7 +759,7 @@ omlish/term/vt100/c.py,sha256=93HARU6Dd1rVF-n8cAyXqkEqleYLcofuLgSUNd6-GbU,3537
759
759
  omlish/term/vt100/states.py,sha256=OxPUxfFTcfz56MhtDgIigEApChOtN6XO1g6R2H08mu4,8303
760
760
  omlish/term/vt100/terminal.py,sha256=KUlg331ele7P6SHsBKdbpdQFDKsxSply1Ds27NkppTs,9359
761
761
  omlish/testing/__init__.py,sha256=M_BQrcCHkoL-ZvE-UpQ8XxXNYRRawhjUz4rCJnAqM2A,152
762
- omlish/testing/testing.py,sha256=zmBHw5gw1ZUUcDYC0uonSThjhRn0HNuorjpo0jLvju8,2885
762
+ omlish/testing/testing.py,sha256=XOZTPjL8pggx2v5TUHUDjSgaU6lMnuPJ58hWoFQyzPY,2979
763
763
  omlish/testing/pytest/__init__.py,sha256=i4ti6Q2rVYJ-XBk9UYDfUUagCrEDTC5jOeSykBjYYZQ,234
764
764
  omlish/testing/pytest/helpers.py,sha256=HxiKvpJQ4b6WCiQXOqQTqKbmr7CMAgCF6hViT6pfIuw,202
765
765
  omlish/testing/pytest/marks.py,sha256=g0RgdvFAd3xog3kKYyb9QtiUVPrEG3JX_abhDNrHJy0,285
@@ -789,6 +789,12 @@ omlish/testing/pytest/plugins/asyncs/backends/trio_asyncio.py,sha256=LCB8Yts5mLY
789
789
  omlish/testing/pytest/plugins/switches/__init__.py,sha256=KTdm9xe8AYQvNT-IKSGr9O8q_hgRGEqhK3zcto0EbWk,43
790
790
  omlish/testing/pytest/plugins/switches/plugin.py,sha256=RBxjefl9RDJuSmT_W0lTSd9DlAT-nkyy_U2fBYzdWNs,5835
791
791
  omlish/testing/pytest/plugins/switches/switches.py,sha256=lj8S9RMwUAW7a93ZqqTjoD4dRVkeGts2sl8Cn-H17hc,1890
792
+ omlish/testing/unittest/__init__.py,sha256=Y3l4WY4JRi2uLG6kgbGp93fuGfkxkKwZDvhsa0Rwgtk,15
793
+ omlish/testing/unittest/__main__.py,sha256=d23loR_cKfTYZwYiqpt_CmKI7dd5WcYFgIYzqMep75E,68
794
+ omlish/testing/unittest/loading.py,sha256=DT6vIZwXBkbEXocaxhrbWj8SqsoP7pyxD5K3bZlAE34,4764
795
+ omlish/testing/unittest/main.py,sha256=OrebAhWqlfNOx5LF-ZIu2KvclneJgBAi13LkvDiZ_yw,8408
796
+ omlish/testing/unittest/running.py,sha256=sifL1gjhn1VR965T9fdZy8NbJpVtpEQety8ngh5Fw7A,11821
797
+ omlish/testing/unittest/types.py,sha256=RvQwU9l2amH2mKv3W3QyPBFg0eXkQ8wWnIJeRXeDdi0,102
792
798
  omlish/text/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
793
799
  omlish/text/asdl.py,sha256=AS3irh-sag5pqyH3beJif78PjCbOaFso1NeKq-HXuTs,16867
794
800
  omlish/text/decoding.py,sha256=sQWGckWzRslRHYKpj1SBeoo6AVqXm5HFlWFRARN1QpM,1286
@@ -882,9 +888,9 @@ omlish/typedvalues/marshal.py,sha256=AtBz7Jq-BfW8vwM7HSxSpR85JAXmxK2T0xDblmm1HI0
882
888
  omlish/typedvalues/of_.py,sha256=UXkxSj504WI2UrFlqdZJbu2hyDwBhL7XVrc2qdR02GQ,1309
883
889
  omlish/typedvalues/reflect.py,sha256=PAvKW6T4cW7u--iX80w3HWwZUS3SmIZ2_lQjT65uAyk,1026
884
890
  omlish/typedvalues/values.py,sha256=ym46I-q2QJ_6l4UlERqv3yj87R-kp8nCKMRph0xQ3UA,1307
885
- omlish-0.0.0.dev370.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
886
- omlish-0.0.0.dev370.dist-info/METADATA,sha256=WQMJ4AZLxaxumd_FFL5iHiJ-yJ_PafoYdFF3vSKd15Y,4416
887
- omlish-0.0.0.dev370.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
888
- omlish-0.0.0.dev370.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
889
- omlish-0.0.0.dev370.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
890
- omlish-0.0.0.dev370.dist-info/RECORD,,
891
+ omlish-0.0.0.dev371.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
892
+ omlish-0.0.0.dev371.dist-info/METADATA,sha256=oPFYGURgWGkPAhtJkRCmiXVNzNFuNn8sXTBZAHZh1TI,4416
893
+ omlish-0.0.0.dev371.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
894
+ omlish-0.0.0.dev371.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
895
+ omlish-0.0.0.dev371.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
896
+ omlish-0.0.0.dev371.dist-info/RECORD,,