punit 0.0.27__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.
punit/TestResult.py ADDED
@@ -0,0 +1,180 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import sys
5
+ from typing import TextIO
6
+
7
+ class TextIOCapture:
8
+
9
+ __quiet:bool
10
+ output:str = None
11
+ target:TextIO
12
+
13
+ def __init__(self, target:TextIO, quiet:bool = False) -> None:
14
+ self.__quiet = quiet
15
+ self.output = None
16
+ self.target = target
17
+
18
+ def write(self, text:str):
19
+ if self.output is None:
20
+ self.output = text
21
+ else:
22
+ self.output += text
23
+ if not self.__quiet:
24
+ self.target.write(text)
25
+
26
+ class TestResult:
27
+
28
+ __className:str
29
+ __exception:Exception
30
+ __fileName:str
31
+ __hostName:str
32
+ __isSuccess:bool
33
+ __moduleName:str
34
+ __packageName:str
35
+ __properties:dict[str, str]
36
+ __startTime:float
37
+ __stderrCapture:TextIOCapture
38
+ __stdoutCapture:TextIOCapture
39
+ __stopTime:float
40
+ __testName:str
41
+
42
+ def __init__(self):
43
+ self.__className = None
44
+ self.__exception = None
45
+ self.__fileName = None
46
+ self.__hostName = None
47
+ self.__isSuccess = None
48
+ self.__moduleName = None
49
+ self.__packageName = None
50
+ self.__properties = {}
51
+ self.__startTime = None
52
+ self.__stderrCapture = None
53
+ self.__stdoutCapture = None
54
+ self.__stopTime = None
55
+ self.__testName = None
56
+
57
+ @property
58
+ def className(self) -> str:
59
+ return self.__className
60
+
61
+ @className.setter
62
+ def className(self, value:str) -> None:
63
+ self.__className = value
64
+
65
+ @property
66
+ def exception(self) -> Exception:
67
+ return self.__exception
68
+
69
+ @exception.setter
70
+ def exception(self, value:Exception) -> None:
71
+ self.__exception = value
72
+
73
+ @property
74
+ def fileName(self) -> str:
75
+ return self.__fileName
76
+
77
+ @fileName.setter
78
+ def fileName(self, value:str) -> None:
79
+ self.__fileName = value
80
+
81
+ @property
82
+ def hostName(self) -> str:
83
+ return self.__hostName
84
+
85
+ @hostName.setter
86
+ def hostName(self, value:str) -> None:
87
+ self.__hostName = value
88
+
89
+ @property
90
+ def isSuccess(self) -> bool:
91
+ return self.__isSuccess
92
+
93
+ @isSuccess.setter
94
+ def isSuccess(self, value:bool) -> None:
95
+ self.__isSuccess = value
96
+
97
+ @property
98
+ def moduleName(self) -> str:
99
+ return self.__moduleName
100
+
101
+ @moduleName.setter
102
+ def moduleName(self, value:str) -> None:
103
+ self.__moduleName = value
104
+
105
+ @property
106
+ def packageName(self) -> str:
107
+ return self.__packageName
108
+
109
+ @packageName.setter
110
+ def packageName(self, value:str) -> None:
111
+ self.__packageName = value
112
+
113
+ @property
114
+ def properties(self) -> dict[str, str]:
115
+ return self.__properties
116
+
117
+ @properties.setter
118
+ def properties(self, value:dict[str, str]) -> None:
119
+ self.__properties = value
120
+
121
+ @property
122
+ def startTime(self) -> float:
123
+ return self.__startTime
124
+
125
+ @startTime.setter
126
+ def startTime(self, value:float) -> None:
127
+ self.__startTime = value
128
+
129
+ @property
130
+ def stderr(self) -> str:
131
+ return None if self.__stderrCapture is None else self.__stderrCapture.output
132
+
133
+ @property
134
+ def stdout(self) -> str:
135
+ return None if self.__stdoutCapture is None else self.__stdoutCapture.output
136
+
137
+ @property
138
+ def stopTime(self) -> float:
139
+ return self.__stopTime
140
+
141
+ @stopTime.setter
142
+ def stopTime(self, value:float) -> None:
143
+ self.__stopTime = value
144
+
145
+ @property
146
+ def testName(self) -> str:
147
+ return self.__testName
148
+
149
+ @testName.setter
150
+ def testName(self, value:str) -> None:
151
+ self.__testName = value
152
+
153
+ @property
154
+ def took(self) -> float:
155
+ return self.__stopTime - self.__startTime
156
+
157
+ @property
158
+ def tookPretty(self) -> str:
159
+ took = self.took
160
+ if took >= 1:
161
+ return f'{took:.1f}'.rstrip('0').rstrip('.') + 's'
162
+ elif took >= 0.001:
163
+ return f'{(took*1000):.0f}ms'
164
+ elif took >= 0.000001:
165
+ return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
166
+ elif took >= 0.000000001:
167
+ return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'μs'
168
+ else:
169
+ return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
170
+
171
+
172
+ def captureOutput(self, quiet:bool = False) -> tuple[TextIO, TextIO]:
173
+ self.__stdoutCapture = TextIOCapture(sys.stdout, quiet)
174
+ self.__stderrCapture = TextIOCapture(sys.stderr, quiet)
175
+ sys.stdout = self.__stdoutCapture
176
+ sys.stderr = self.__stderrCapture
177
+
178
+ def releaseOutput(self) -> None:
179
+ sys.stdout = self.__stdoutCapture.target
180
+ sys.stderr = self.__stderrCapture.target
punit/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ __version__ = '0.0.27'
5
+
6
+ from .assertions import *
7
+ from .facts import *
8
+ from .theories import *
punit/__main__.py ADDED
@@ -0,0 +1,55 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import asyncio
5
+ from .cli import *
6
+ from .discovery import *
7
+ from .reports import HtmlReportGenerator, JUnitReportGenerator
8
+ from .runner import *
9
+
10
+ async def async_main():
11
+ ts = time.time()
12
+ cli = CommandLineInterface.parse()
13
+ if cli.help:
14
+ cli.printHelp()
15
+ elif cli.verbose and not cli.quiet:
16
+ cli.printSummary()
17
+ elif not cli.quiet:
18
+ cli.printVersion()
19
+ os.chdir(cli.workdir)
20
+ testModuleDiscovery = TestModuleDiscovery(
21
+ os.path.join(cli.workdir, cli.testPackageName),
22
+ cli.includePatterns,
23
+ cli.excludePatterns,
24
+ cli)
25
+ testModuleDiscovery.discover()
26
+ testRunner = TestRunner(cli.testPackageName, testModuleDiscovery.filenames, cli)
27
+ results = await testRunner.run()
28
+ totalTime = time.time() - ts
29
+ failureCount = 0
30
+ for result in results:
31
+ if not result.isSuccess:
32
+ failureCount += 1
33
+ if not cli.quiet:
34
+ print(f'Total: {len(results)}, Failures: {failureCount}, Took: {totalTime:.3f}s')
35
+ if cli.reportFormat is not None:
36
+ report:str = None
37
+ match cli.reportFormat:
38
+ case 'html':
39
+ report = HtmlReportGenerator().generate(results)
40
+ case 'junit':
41
+ report = JUnitReportGenerator().generate(results)
42
+ if cli.outputFilename is None:
43
+ print(report)
44
+ else:
45
+ with open(cli.outputFilename, 'wt') as file:
46
+ file.write(report)
47
+ print(f'\n("{cli.reportFormat}" report written to: {cli.outputFilename})')
48
+ if failureCount > 0:
49
+ exit(119)
50
+
51
+ def main():
52
+ asyncio.run(async_main())
53
+
54
+ if (__name__ == '__main__'):
55
+ main()
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ __all__ = [
5
+ 'collections',
6
+ 'exceptions'
7
+ ]
@@ -0,0 +1,18 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from typing import Any, Sequence
5
+
6
+ def areSame(a:Sequence[Any], b:Sequence[Any]) -> bool:
7
+ if a is b:
8
+ return True
9
+ elif a is None and b is not None:
10
+ return False
11
+ elif a is not None and b is None:
12
+ return False
13
+ elif len(a) != len(b):
14
+ return False
15
+ for i in range(len(a)):
16
+ if not a[i] == b[i]:
17
+ return False
18
+ return True
@@ -0,0 +1,33 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from typing import Callable, get_args
5
+
6
+ def isNoneOrWhiteSpace(input:str):
7
+ return input is None or len(input.lstrip()) == 0
8
+
9
+ def isNoneOrEmpty(input:str):
10
+ return input is None or len(input) == 0
11
+
12
+ class raises[TError:Exception]:
13
+
14
+ __action:Callable
15
+ __exact:bool
16
+ __expect:TError
17
+
18
+ def __init__(self, action:Callable, *, exact:bool = False, expect:TError = None) -> None:
19
+ self.__action = action
20
+ self.__exact = exact
21
+ self.__expect = expect
22
+
23
+ def __bool__(self) -> bool:
24
+ try:
25
+ self.__action()
26
+ except BaseException as ex:
27
+ expected = self.__expect
28
+ if expected is None:
29
+ expected = get_args(self.__orig_class__)[0]
30
+ if self.__exact:
31
+ return type(ex) is expected
32
+ else:
33
+ return issubclass(type(ex), expected)
punit/cli.py ADDED
@@ -0,0 +1,261 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import os
5
+ import sys
6
+ import typing
7
+ from . import __version__
8
+
9
+ CommandLineInterface = typing.ForwardRef('CommandLineInterface')
10
+
11
+ class CommandLineInterface:
12
+
13
+ __excludePatterns:list[str]
14
+ __failfast:bool
15
+ __filterPattern:str
16
+ __help:bool
17
+ __includePatterns:list[str]
18
+ __no_default_patterns:bool
19
+ __outputFilename:str
20
+ __quiet:bool
21
+ __reportFormat:str
22
+ __testPackageName:str
23
+ __verbose:bool
24
+ __workdir:str
25
+
26
+ def __init__(self):
27
+ self.__aliases = {}
28
+ self.__excludePatterns = []
29
+ self.__failfast = False
30
+ self.__filterPattern = '*'
31
+ self.__help = False
32
+ self.__includePatterns = []
33
+ self.__no_default_patterns = False
34
+ self.__outputFilename = None
35
+ self.__quiet = False
36
+ self.__reportFormat = None
37
+ self.__testPackageName = 'tests'
38
+ self.__workdir = os.path.curdir
39
+ self.__verbose = False
40
+
41
+ def __parse(self, argv:list[str]) -> CommandLineInterface:
42
+ aliasName:str = None
43
+ extractFilterPattern:bool = False
44
+ extractExcludePattern:bool = False
45
+ extractIncludePattern:bool = False
46
+ extractAliasName:bool = False
47
+ extractAliasPath:bool = False
48
+ extractReportFormat:bool = False
49
+ extractOutputFilename:bool = False
50
+ for arg in argv:
51
+ if extractFilterPattern:
52
+ self.__filterPattern = arg
53
+ extractFilterPattern = False
54
+ continue
55
+ if extractIncludePattern:
56
+ self.__includePatterns.append(arg)
57
+ extractIncludePattern = False
58
+ continue
59
+ elif extractExcludePattern:
60
+ self.__excludePatterns.append(arg)
61
+ extractExcludePattern = False
62
+ continue
63
+ elif extractAliasName:
64
+ aliasName = arg
65
+ extractAliasName = False
66
+ extractAliasPath = True
67
+ continue
68
+ elif extractAliasPath:
69
+ extractAliasPath = False
70
+ self.__aliases[aliasName] = arg
71
+ continue
72
+ elif self.__workdir is None:
73
+ self.__workdir = arg
74
+ continue
75
+ elif self.__testPackageName is None:
76
+ self.__testPackageName = arg
77
+ continue
78
+ elif extractReportFormat:
79
+ extractReportFormat = False
80
+ self.__reportFormat = arg.lower()
81
+ match self.__reportFormat:
82
+ case 'html' | 'junit':
83
+ pass
84
+ case _:
85
+ print(f'Unsupported value "{arg}" for --report argument, aborting.')
86
+ print(f'(valid values are "json" and "junit")')
87
+ exit(4)
88
+ continue
89
+ elif extractOutputFilename:
90
+ extractOutputFilename = False
91
+ self.__outputFilename = arg
92
+ continue
93
+ match arg:
94
+ case '-h' | '--help':
95
+ self.__help = True
96
+ case '-a':
97
+ extractAliasName = True
98
+ case '-t' | '--filter':
99
+ extractFilterPattern = True
100
+ case '-e' | '--exclude':
101
+ extractExcludePattern = True
102
+ case '-f' | '--failfast':
103
+ self.__failfast = True
104
+ case '-p' | '--test-package':
105
+ self.__testPackageName = None
106
+ case '-i' | '--include':
107
+ extractIncludePattern = True
108
+ case '-q' | '--quiet':
109
+ self.__quiet = True
110
+ case '-w' | '--working-directory':
111
+ self.__workdir = None
112
+ case '-v' | '--verbose':
113
+ self.__verbose = True
114
+ case '-n' | '--no-default-patterns':
115
+ self.__no_default_patterns = True
116
+ case '-r' | '--report':
117
+ extractReportFormat = True
118
+ case '-o' | '--output':
119
+ extractOutputFilename = True
120
+ case _:
121
+ continue
122
+ return self
123
+
124
+ @property
125
+ def aliases(self) -> bool:
126
+ return self.__aliases
127
+
128
+ @property
129
+ def failfast(self) -> bool:
130
+ return self.__failfast
131
+
132
+ @property
133
+ def filterPattern(self) -> str:
134
+ return self.__filterPattern
135
+
136
+ @property
137
+ def excludePatterns(self) -> list[str]:
138
+ return self.__excludePatterns
139
+
140
+ @property
141
+ def help(self) -> bool:
142
+ return self.__help
143
+
144
+ @property
145
+ def includePatterns(self) -> list[str]:
146
+ return self.__includePatterns
147
+
148
+ @property
149
+ def outputFilename(self) -> str|None:
150
+ return self.__outputFilename
151
+
152
+ @property
153
+ def quiet(self) -> bool:
154
+ return self.__quiet or (self.__reportFormat is not None and self.__outputFilename is None)
155
+
156
+ @property
157
+ def reportFormat(self) -> str|None:
158
+ return self.__reportFormat
159
+
160
+ @property
161
+ def testPackageName(self) -> str:
162
+ return self.__testPackageName
163
+
164
+ @property
165
+ def verbose(self) -> bool:
166
+ return self.__verbose and self.__reportFormat is None
167
+
168
+ @property
169
+ def workdir(self) -> str:
170
+ return self.__workdir
171
+
172
+ def printHelp(self) -> None:
173
+ self.printVersion()
174
+ print(
175
+ """
176
+ Usage: python3 -m punit [-h|--help]
177
+ [-q|--quiet] [-v|--verbose]
178
+ [-f|--failfast]
179
+ [-p|--test-package NAME]
180
+ [-i|--include PATTERN]
181
+ [-e|--exclude PATTERN]
182
+ [-t|--filter PATTERN]
183
+ [-w|--workdir DIRECTORY]
184
+ [-n|--no-default-patterns]
185
+ [-r|--report {junit|json}]
186
+ [-o|--output FILENAME]
187
+
188
+ Options:
189
+ -h, --help Show this help text and exit
190
+ -q, --quiet Quiet output
191
+ -v, --verbose Verbose output
192
+ -f, --failfast Stop on first failure or error
193
+ -p, --test-package NAME
194
+ Use NAME as the test package, all tests should
195
+ be locatable as modules in the named package.
196
+ Default: 'tests'
197
+ -i, --include PATTERN
198
+ Include any tests matching PATTERN
199
+ Default: '*.py'
200
+ -e, --exclude PATTERN
201
+ Exclude any tests matching PATTERN, overriding --include
202
+ Default: '__*__' (dunder files), '/.*/' (dot-directories)
203
+ -t, --filter
204
+ Only execute tests matching PATTERN
205
+ Default: '*'
206
+ -w, --working-directory DIRECTORY
207
+ Working directory (defaults to start directory)
208
+ -n, --no-default-patterns
209
+ Do not apply any default include/exclude patterns.
210
+ -r, --report {html|junit}
211
+ Generate a report to stdout using either an "html"
212
+ or "junit" format. When generating a report to stdout
213
+ all other output is suppressed, unless `--output`
214
+ is also specified.
215
+ -o, --output FILENAME
216
+ If `--report` is used, instead of writing to stdout
217
+ write to FILENAME. In this case `--report` does not
218
+ suppress any program output.
219
+ """
220
+ )
221
+ exit(0)
222
+
223
+ def printSummary(self) -> None:
224
+ self.printVersion()
225
+ print(f'Working Directory:\n\t{self.__workdir}')
226
+ print(f'Fail Fast: \n\t{"Yes" if self.__failfast else "No"}')
227
+ if len(self.__includePatterns) > 0:
228
+ print('Include Patterns:')
229
+ for pattern in self.__includePatterns:
230
+ print(f'\t{pattern}')
231
+ if len(self.__excludePatterns) > 0:
232
+ print('Exclude Patterns:')
233
+ for pattern in self.__excludePatterns:
234
+ print(f'\t{pattern}')
235
+ print('Filter Pattern:')
236
+ print(f'\t{self.__filterPattern}')
237
+ def printVersion(self) -> None:
238
+ print(f'pUnit {__version__}')
239
+
240
+ def validate(self) -> None:
241
+ if self.__workdir is None or len(self.__workdir.lstrip()) == 0 or self.__workdir.startswith('-'):
242
+ print(f'Invalid working directory specified: {self.__workdir}')
243
+ exit(1)
244
+ elif not os.path.isdir(self.__workdir):
245
+ print(f'Working directory does not exist: {self.__workdir}')
246
+ exit(2)
247
+ self.__workdir = os.path.abspath(self.__workdir)
248
+ if not self.__no_default_patterns:
249
+ # if no other patterns specified, default to including all files found in the directory matching `testPackageName`
250
+ if len(self.__includePatterns) == 0:
251
+ self.__includePatterns.append(f'/{self.testPackageName}/*.py')
252
+ # always exclude dot-folders (.git, .venv, etc)
253
+ self.__excludePatterns.append('/.*')
254
+ # always exclude dunder files
255
+ self.__excludePatterns.append('/__*__')
256
+
257
+ @staticmethod
258
+ def parse(argv:list[str] = sys.argv) -> CommandLineInterface:
259
+ result = CommandLineInterface().__parse(argv)
260
+ result.validate()
261
+ return result
@@ -0,0 +1,104 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import os
5
+ import re
6
+
7
+ from ..facts import FactManager
8
+ from ..theories import TheoryManager
9
+ from ..cli import CommandLineInterface
10
+
11
+ class TestModuleDiscovery:
12
+
13
+ __cli:CommandLineInterface
14
+ __excludePatterns:list[re.Pattern]
15
+ __filenames:list[str]
16
+ __filterPattern:re.Pattern
17
+ __includePatterns:list[re.Pattern]
18
+ __workdir:str
19
+
20
+ def __init__(self, workdir:str, includePatterns:list[str], excludePatterns:list[str], cli:CommandLineInterface):
21
+ self.__cli = cli
22
+ self.__excludePatterns = []
23
+ if excludePatterns is not None:
24
+ for pattern in excludePatterns:
25
+ self.__excludePatterns.append(
26
+ re.compile(
27
+ self.__convertPatternToRegex(pattern),
28
+ re.IGNORECASE))
29
+ self.__filenames = []
30
+ self.__filterPattern = re.compile(self.__convertPatternToRegex(cli.filterPattern))
31
+ self.__includePatterns = []
32
+ if includePatterns is not None:
33
+ for pattern in includePatterns:
34
+ self.__includePatterns.append(
35
+ re.compile(
36
+ self.__convertPatternToRegex(pattern),
37
+ re.IGNORECASE))
38
+ self.__workdir = workdir
39
+
40
+ def __convertPatternToRegex(self, pattern:str):
41
+ result = re.escape(pattern)\
42
+ .replace('\\\\', '/')\
43
+ .replace('\\*', r'.+')\
44
+ .replace('?', '.')
45
+ return result
46
+
47
+ def __testAnyInclude(self, input:str) -> bool:
48
+ for pat in self.__includePatterns:
49
+ if len(pat.findall(input)) > 0:
50
+ return True
51
+ return False
52
+
53
+ def __testAnyExclude(self, input:str) -> bool:
54
+ for pat in self.__excludePatterns:
55
+ if len(pat.findall(input)) > 0:
56
+ return True
57
+ return False
58
+
59
+ def __walkDirectory(self, path:str) -> list[str]:
60
+ filenames = []
61
+ if os.path.isdir(path):
62
+ for dname, dlist, flist in os.walk(path, topdown=True):
63
+ if self.__cli.verbose:
64
+ print(f'.. discovery for: {dname}')
65
+ dname2 = dname.replace('\\', '/')
66
+ if self.__testAnyExclude(dname2):
67
+ # directory is excluded, prune all children
68
+ if self.__cli.verbose:
69
+ print(f'Excluded: {dname}')
70
+ while len(dlist) > 0:
71
+ del dlist[0]
72
+ while len(flist) > 0:
73
+ del flist[0]
74
+ # determine if any individual subdirs should be pruned
75
+ dlnames = dlist.copy()
76
+ for dlname in dlnames:
77
+ dlname2 = dlname.replace('\\', '/')
78
+ if self.__testAnyExclude(dlname2):
79
+ if self.__cli.verbose:
80
+ print(f'Excluded: {dlname}')
81
+ dlist.remove(dlname)
82
+ # determine if any individual files should be pruned
83
+ for fname in flist:
84
+ fname2 = os.path.join(dname, fname).replace('\\', '/')
85
+ if self.__testAnyInclude(fname2) and not self.__testAnyExclude(fname2):
86
+ if fname.endswith('.py'):
87
+ if self.__cli.verbose:
88
+ print(f'Included: {fname}')
89
+ filenames.append(fname2)
90
+ return filenames
91
+
92
+ @property
93
+ def filenames(self) -> list[str]:
94
+ return self.__filenames
95
+
96
+ def discover(self) -> list[str]:
97
+ FactManager.instance().filterPattern = self.__filterPattern
98
+ TheoryManager.instance().filterPattern = self.__filterPattern
99
+ if self.__cli.verbose:
100
+ print(f'.. starting test discovery')
101
+ self.__filenames = self.__walkDirectory(self.__workdir)
102
+ if self.__cli.verbose:
103
+ print('.. finished test discovery.')
104
+ return self.__filenames
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ __all__ = [
5
+ 'TestModuleDiscovery'
6
+ ]
7
+
8
+ from .TestModuleDiscovery import *