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 +2 -2
- omlish/reflect/types.py +9 -1
- omlish/testing/testing.py +10 -8
- omlish/testing/unittest/__init__.py +1 -0
- omlish/testing/unittest/__main__.py +4 -0
- omlish/testing/unittest/loading.py +115 -0
- omlish/testing/unittest/main.py +272 -0
- omlish/testing/unittest/running.py +330 -0
- omlish/testing/unittest/types.py +5 -0
- {omlish-0.0.0.dev370.dist-info → omlish-0.0.0.dev371.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev370.dist-info → omlish-0.0.0.dev371.dist-info}/RECORD +15 -9
- {omlish-0.0.0.dev370.dist-info → omlish-0.0.0.dev371.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev370.dist-info → omlish-0.0.0.dev371.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev370.dist-info → omlish-0.0.0.dev371.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev370.dist-info → omlish-0.0.0.dev371.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
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
|
32
|
+
timeout_s: ta.Optional[float] = None,
|
31
33
|
timeout_exception: Exception = TimeoutError('Thread timeout'),
|
32
|
-
) ->
|
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:
|
39
|
-
thread_exception: Exception
|
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(
|
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
|
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
|
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
|
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,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')
|
@@ -1,5 +1,5 @@
|
|
1
1
|
omlish/.manifests.json,sha256=aT8yZ-Zh-9wfHl5Ym5ouiWC1i0cy7Q7RlhzavB6VLPI,8587
|
2
|
-
omlish/__about__.py,sha256=
|
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=
|
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=
|
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.
|
886
|
-
omlish-0.0.0.
|
887
|
-
omlish-0.0.0.
|
888
|
-
omlish-0.0.0.
|
889
|
-
omlish-0.0.0.
|
890
|
-
omlish-0.0.0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|