pUnit 1.2.0__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.
@@ -0,0 +1,93 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import traceback
5
+ from ..TestResult import TestResult
6
+
7
+ class HtmlReportGenerator:
8
+
9
+ def __init__(self):
10
+ pass
11
+
12
+ def generate(self, testResults:list[TestResult]) -> str:
13
+ failureCount = 0
14
+ totalCount = 0
15
+ for testResult in testResults:
16
+ totalCount += 1
17
+ if not testResult.isSuccess:
18
+ failureCount += 1
19
+ testResults = testResults.copy()
20
+ testResults.sort(key=lambda e : e.moduleName)
21
+ lines = []
22
+ lines.append('<html>')
23
+ lines.append('<head><title>Test Results</title></head>')
24
+ lines.append('<body>')
25
+ lines.append("""
26
+ <style>
27
+ body { font-family: sans-serif }
28
+ .testresults-summary { border-bottom:solid 3px steelblue; font-size: 2em; display: flex; flex-direction: row; justify-content: center }
29
+ .testresults-summary div { flex-grow: 1 }
30
+ .testresults-summary .pass { color: green; }
31
+ .testresults-summary .fail { color: red; }
32
+ .module-name { font-style: italic }
33
+ .testresult { background-color: #EEE; margin-bottom: 2em; padding: 1em }
34
+ .testresult-pass { border: solid 1px green }
35
+ .testresult-fail { border: solid 1px red }
36
+ .testresult .test-time { border: solid 1px black; padding: 0.3em }
37
+ .percent-pass { color: green }
38
+ .percent-fail { color: red }
39
+ .testresult-body { margin: 1em; border: solid 1px silver; background-color: #FFF }
40
+ .testresult-error { border-left: solid 3px red; font-family: monospace; padding: 0.8em }
41
+ .testresult-stdout { font-family: monospace; max-height: 10em; overflow: auto; padding: 0.8em }
42
+ .testresult-stderr { font-family: monospace; max-height: 10em; overflow: auto; padding: 0.8em }
43
+ .test-time-pass { background-color: #EFE }
44
+ .test-time-fail { background-color: #FEE }
45
+ </style>
46
+ """)
47
+
48
+ lines.append('<div class="testresults-summary">')
49
+ lines.append('<div>&nbsp;</div>')
50
+ lines.append(f'<div>Total Executed: <span class="pass">{totalCount-failureCount}</span></div>')
51
+ if failureCount > 0:
52
+ lines.append('<div>&nbsp;</div>')
53
+ lines.append(f'<div>Total Failed: <span class="fail">{failureCount}</span></div>')
54
+ lines.append('<div>&nbsp;</div>')
55
+ percentstyle = 'pass' if failureCount == 0 else 'fail'
56
+ lines.append(f'<div class="passfail-percent">Pass/Fail&nbsp;<span class="percent-{percentstyle}">{100-(((failureCount/totalCount) if totalCount > 0 else 1)*100):.1f}%</span></div>')
57
+ lines.append('<div>&nbsp;</div>')
58
+ lines.append('</div>')
59
+ currentModuleName = None
60
+ for testResult in testResults:
61
+ if currentModuleName != testResult.moduleName:
62
+ if currentModuleName is not None:
63
+ lines.append('</div>')
64
+ lines.append('<div class="testresults-module">')
65
+ lines.append(f'<h2 class="module-name">{testResult.packageName}/{testResult.moduleName}</h2>')
66
+ currentModuleName = testResult.moduleName
67
+ passfailstyle = '-pass' if testResult.isSuccess else '-fail'
68
+ passfailglyph = '🟩' if testResult.isSuccess else '🟥'
69
+ lines.append(f'<div class="testresult testresult{passfailstyle}">')
70
+ lines.append('<div class="testresult-heading">')
71
+ lines.append(f'<span class="glyph glyph{passfailstyle}">{passfailglyph}</span>')
72
+ lines.append(f'<span class="test-class">{"" if testResult.className is None else testResult.className}</span>')
73
+ lines.append(f'<span class="test-name">{testResult.testName}</span>')
74
+ lines.append(f'<span class="test-time test-time{passfailstyle}">{testResult.tookPretty}</span>')
75
+ lines.append('</div>')
76
+ if not testResult.isSuccess or testResult.stdout is not None or testResult.stderr is not None:
77
+ lines.append('<div class="testresult-body">')
78
+ if not testResult.isSuccess:
79
+ lines.append('<div class="testresult-error">')
80
+ if testResult.exception is not None:
81
+ if len(f'{testResult.exception}') > 0:
82
+ lines.append(f'Error:<br/>&nbsp;&nbsp;{testResult.exception}<br/>')
83
+ lines.append(f'Traceback:<br/>{"".join(traceback.format_tb(testResult.exception.__traceback__)).replace('\n', '<br/>').replace(' ', '&nbsp;')}')
84
+ lines.append('</pre></div>')
85
+ if testResult.stdout is not None:
86
+ lines.append(f'<div class="testresult-stdout"><pre>{testResult.stdout}</pre></div>')
87
+ if testResult.stderr is not None:
88
+ lines.append(f'<div class="testresult-stderr"><pre>{testResult.stderr}</pre></div>')
89
+ lines.append('</div>')
90
+ lines.append('</div>')
91
+ lines.append('</body>')
92
+ lines.append('</html>')
93
+ return '\n'.join(lines)
@@ -0,0 +1,196 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from typing import Optional
5
+ import datetime
6
+ import xml.etree.ElementTree as et
7
+ from xml.sax.saxutils import escape
8
+ from ..TestResult import TestResult
9
+
10
+
11
+ class JUnitError:
12
+ message:Optional[str] = None
13
+ type:Optional[str] = None
14
+
15
+
16
+ class JUnitTestCase:
17
+ # attrs
18
+ classname:Optional[str] = None
19
+ disabled:Optional[bool] = None
20
+ name:Optional[str] = None
21
+ time:Optional[float] = None
22
+ # eles
23
+ error:Optional[JUnitError] = None
24
+ failure:Optional[JUnitError] = None
25
+ stdout:Optional[str] = None
26
+ stderr:Optional[str] = None
27
+ skipped:Optional[bool] = None
28
+ def __init__(self, testResult:TestResult) -> None:
29
+ self.disabled = False
30
+ self.classname = testResult.moduleName if testResult.className is None else f'{testResult.moduleName}.{testResult.className}'
31
+ self.time = testResult.took
32
+ data = testResult.properties.get('data')
33
+ if data is not None and len(data) > 0:
34
+ data = f'({",".join([str(e) for e in data])})'
35
+ else:
36
+ data = ''
37
+ self.name = f'{testResult.testName}{data}'
38
+ if not testResult.isSuccess:
39
+ exc_type = type(testResult.exception)
40
+ error = JUnitError()
41
+ error.message = f'{testResult.exception}'
42
+ error.type = getattr(exc_type, '__name__') if not hasattr(exc_type, '__qualname__') else getattr(exc_type, '__qualname__')
43
+ if issubclass(exc_type, AssertionError):
44
+ self.failure = error
45
+ else:
46
+ self.error = error
47
+ self.stdout = None if testResult.stdout is None else escape(testResult.stdout)
48
+ self.stderr = None if testResult.stderr is None else escape(testResult.stderr)
49
+ self.skipped = False
50
+
51
+
52
+ class JUnitTestSuite:
53
+ # attrs
54
+ name:Optional[str] = None
55
+ hostname:Optional[str] = None
56
+ id:Optional[int] = None
57
+ name:Optional[str] = None
58
+ package:Optional[str] = None
59
+ timestamp:Optional[datetime.datetime] = None
60
+ # eles
61
+ testCases:Optional[list[JUnitTestCase]] = None
62
+
63
+ @property
64
+ def disabled(self) -> int:
65
+ result:int = 0
66
+ if self.testCases is not None:
67
+ for testCase in self.testCases:
68
+ if testCase.disabled == True:
69
+ result += 1
70
+ return result
71
+
72
+ @property
73
+ def errors(self) -> int:
74
+ result:int = 0
75
+ if self.testCases is not None:
76
+ for testCase in self.testCases:
77
+ if testCase.error is not None:
78
+ result += 1
79
+ return result
80
+
81
+ @property
82
+ def failures(self) -> int:
83
+ result:int = 0
84
+ if self.testCases is not None:
85
+ for testCase in self.testCases:
86
+ if testCase.failure is not None:
87
+ result += 1
88
+ return result
89
+
90
+ @property
91
+ def skipped(self) -> int:
92
+ result:int = 0
93
+ if self.testCases is not None:
94
+ for testCase in self.testCases:
95
+ if testCase.skipped:
96
+ result += 1
97
+ return result
98
+
99
+ @property
100
+ def tests(self) -> float:
101
+ return 0 if self.testCases is None else len(self.testCases)
102
+
103
+ @property
104
+ def time(self) -> float:
105
+ result:float = 0.0
106
+ if self.testCases is not None:
107
+ for testCase in self.testCases:
108
+ result += testCase.time if testCase.time is not None else 0
109
+ return result
110
+
111
+
112
+ class JUnitReportGenerator:
113
+
114
+ def __init__(self):
115
+ pass
116
+
117
+ def generate(self, testResults:list[TestResult]) -> str:
118
+ # transform to intermediary model
119
+ testSuites:dict[str, JUnitTestSuite] = {}
120
+ ts = 0
121
+ for testResult in testResults:
122
+ if testResult.stopTime is not None and ts < testResult.stopTime:
123
+ ts = testResult.stopTime
124
+ testSuite:JUnitTestSuite|None = testSuites.get(testResult.moduleName)
125
+ if testSuite is None:
126
+ testSuite = JUnitTestSuite()
127
+ testSuite.name = testResult.moduleName
128
+ testSuite.id = len(testSuites)
129
+ testSuite.hostname = testResult.hostName
130
+ testSuite.package = testResult.packageName
131
+ testSuite.testCases = []
132
+ testSuites[testResult.moduleName] = testSuite
133
+ if testSuite.testCases is None:
134
+ testSuite.testCases = []
135
+ testSuite.testCases.append(JUnitTestCase(testResult))
136
+ testSuite.timestamp = datetime.datetime.fromtimestamp(ts)
137
+ # materialize as xml
138
+ totalDisabledCount = 0
139
+ totalErrorCount = 0
140
+ totalFailureCount = 0
141
+ totalTestCount = 0
142
+ totalTime = 0
143
+ testSuitesEle:et.Element = et.Element('testsuites')
144
+ for testSuiteName in testSuites:
145
+ testSuite:JUnitTestSuite|None = testSuites[testSuiteName]
146
+ if testSuite is not None:
147
+ totalTime += testSuite.time
148
+ totalDisabledCount += testSuite.disabled
149
+ totalErrorCount += testSuite.errors
150
+ totalFailureCount += testSuite.failures
151
+ totalTestCount += testSuite.tests
152
+ testSuiteEle = et.SubElement(testSuitesEle, 'testsuite')
153
+ testSuiteEle.attrib['disabled'] = str(testSuite.disabled)
154
+ testSuiteEle.attrib['errors'] = str(testSuite.errors)
155
+ testSuiteEle.attrib['failures'] = str(testSuite.failures)
156
+ testSuiteEle.attrib['id'] = str(testSuite.id)
157
+ testSuiteEle.attrib['name'] = str(testSuite.name)
158
+ testSuiteEle.attrib['package'] = str(testSuite.package)
159
+ testSuiteEle.attrib['timestamp'] = str(testSuite.timestamp)
160
+ testSuiteEle.attrib['hostname'] = str(testSuite.hostname)
161
+ testSuiteEle.attrib['tests'] = str(testSuite.tests)
162
+ testSuiteEle.attrib['time'] = f'{testSuite.time:.6f}'.rstrip('0').rstrip('.')
163
+ if testSuite.testCases is not None:
164
+ for testCase in testSuite.testCases:
165
+ testCaseEle = et.SubElement(testSuiteEle, 'testcase')
166
+ testCaseEle.attrib['name'] = testCase.name if testCase.name is not None else 'UNKNOWN'
167
+ if testCase.classname is not None:
168
+ testCaseEle.attrib['classname'] = testCase.classname
169
+ testCaseEle.attrib['time'] = f'{testCase.time:.6f}'.rstrip('0').rstrip('.')
170
+ if testCase.skipped:
171
+ et.SubElement(testCaseEle, 'skipped')
172
+ if testCase.error is not None:
173
+ ele = et.SubElement(testCaseEle, 'error')
174
+ if testCase.error.type is not None:
175
+ ele.attrib['type'] = testCase.error.type
176
+ if testCase.error.message is not None:
177
+ ele.attrib['message'] = testCase.error.message
178
+ if testCase.failure is not None:
179
+ ele = et.SubElement(testCaseEle, 'failure')
180
+ if testCase.failure.type is not None:
181
+ ele.attrib['type'] = testCase.failure.type
182
+ if testCase.failure.message is not None:
183
+ ele.attrib['message'] = testCase.failure.message
184
+ if testCase.stdout is not None:
185
+ ele = et.SubElement(testCaseEle, 'system-out')
186
+ ele.text = testCase.stdout
187
+ if testCase.stderr is not None:
188
+ ele = et.SubElement(testCaseEle, 'system-err')
189
+ ele.text = testCase.stderr
190
+ testSuitesEle.attrib['disabled'] = str(totalDisabledCount)
191
+ testSuitesEle.attrib['errors'] = str(totalErrorCount)
192
+ testSuitesEle.attrib['failures'] = str(totalErrorCount)
193
+ testSuitesEle.attrib['tests'] = str(totalTestCount)
194
+ testSuitesEle.attrib['time'] = f'{totalTime:.6f}'.rstrip('0').rstrip('.')
195
+ et.indent(testSuitesEle, space=" ")
196
+ return f'<?xml version="1.0" encoding="UTF-8"?>\n{et.tostring(testSuitesEle).decode()}\n'
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from .HtmlReportGenerator import HtmlReportGenerator
5
+ from .JUnitReportGenerator import JUnitReportGenerator
punit/runner.py ADDED
@@ -0,0 +1,118 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import importlib
5
+ import os
6
+ import socket
7
+ import time
8
+ import traceback
9
+ from .cli import CommandLineInterface
10
+ from .facts.FactManager import FactManager
11
+ from .theories.TheoryManager import TheoryManager
12
+ from .TestResult import TestResult
13
+
14
+ class TestRunner:
15
+
16
+ __cli:CommandLineInterface
17
+ __filenames:list[str]
18
+ __testPackageName:str
19
+
20
+ def __init__(self, testPackageName:str, filenames:list[str], cli:CommandLineInterface):
21
+ self.__cli = cli
22
+ self.__filenames = filenames
23
+ self.__testPackageName = testPackageName
24
+
25
+ def printTestResult(self, testResult:TestResult):
26
+ if self.__cli.quiet:
27
+ return
28
+ glyph = '🟩' if testResult.isSuccess else '🟥'
29
+ data = testResult.properties.get('data')
30
+ if data is None:
31
+ data = ''
32
+ else:
33
+ try:
34
+ data = f'({",".join([str(e) for e in data])})'
35
+ except:
36
+ data = '(???)'
37
+ print(f'{glyph} {testResult.moduleName}/{"" if testResult.className is None or len(testResult.className) == 0 else f"{testResult.className}/"}{testResult.testName}{data} [{testResult.tookPretty}]')
38
+ if self.__cli.verbose and (not testResult.isSuccess) and testResult.exception is not None:
39
+ print(f'Test File:\n {testResult.fileName}\nError:\n {testResult.exception}\n Traceback:\n{"".join(traceback.format_tb(testResult.exception.__traceback__))}')
40
+
41
+ async def run(self) -> list[TestResult]:
42
+ results:list[TestResult] = []
43
+ # TODO: aliasing
44
+ hostName:str = socket.gethostname()
45
+ testPackagePath = os.path.join(os.path.abspath(os.curdir), self.__testPackageName).replace('\\', '/')
46
+ for filename in self.__filenames:
47
+ ts = time.time()
48
+ moduleImportName = filename.replace(testPackagePath, '').replace('/', '.').replace('.py', '')
49
+ moduleReportName = moduleImportName.lstrip('.')
50
+ try:
51
+ testModule = importlib.import_module(moduleImportName, self.__testPackageName)
52
+ # execute all facts
53
+ facts = FactManager.instance().get(testModule.__name__)
54
+ for fact in facts:
55
+ result:TestResult = TestResult()
56
+ result.hostName = hostName
57
+ result.packageName = self.__testPackageName
58
+ result.fileName = filename
59
+ result.moduleName = moduleReportName
60
+ result.startTime = time.time()
61
+ result.captureOutput(not self.__cli.verbose)
62
+ try:
63
+ await fact.execute(testModule)
64
+ result.isSuccess = True
65
+ except Exception as ex:
66
+ result.isSuccess = False
67
+ result.exception = ex
68
+ result.releaseOutput()
69
+ result.stopTime = time.time()
70
+ result.className = fact.className
71
+ result.testName = fact.testName
72
+ results.append(result)
73
+ self.printTestResult(result)
74
+ if self.__cli.failfast and not result.isSuccess:
75
+ return results
76
+ # execute all theories
77
+ theories = TheoryManager.instance().get(testModule.__name__)
78
+ for theory in theories:
79
+ for data in theory.datas:
80
+ result:TestResult = TestResult()
81
+ result.hostName = hostName
82
+ result.packageName = self.__testPackageName
83
+ result.properties['data'] = data
84
+ result.fileName = filename
85
+ result.moduleName = moduleReportName
86
+ result.startTime = time.time()
87
+ result.captureOutput(not self.__cli.verbose)
88
+ try:
89
+ await theory.execute(testModule, data)
90
+ result.isSuccess = True
91
+ except Exception as ex:
92
+ result.isSuccess = False
93
+ result.exception = ex
94
+ result.releaseOutput()
95
+ result.stopTime = time.time()
96
+ result.className = theory.className
97
+ result.testName = theory.testName
98
+ results.append(result)
99
+ self.printTestResult(result)
100
+ if self.__cli.failfast and not result.isSuccess:
101
+ return results
102
+ except Exception as ex:
103
+ # module-level failure, report test failure against the module
104
+ # this is a best-attempt to create output that report readers
105
+ # can consume to show there was a broad failure in a module.
106
+ result = TestResult()
107
+ result.className = '*'
108
+ result.moduleName = moduleReportName
109
+ result.testName = '*'
110
+ result.startTime = ts
111
+ result.stopTime = time.time()
112
+ result.isSuccess = False
113
+ result.exception = ex
114
+ results.append(result)
115
+ self.printTestResult(result)
116
+ if self.__cli.failfast:
117
+ return results
118
+ return results
@@ -0,0 +1,100 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+ ##
4
+
5
+ import inspect
6
+ from types import FunctionType, MethodType, ModuleType
7
+ from typing import Callable, Coroutine, Optional, cast
8
+
9
+ from ..traits.Trait import Trait
10
+
11
+
12
+ class Theory:
13
+
14
+ __className:Optional[str]
15
+ __datas:list[tuple]
16
+ __moduleName:str
17
+ __target:FunctionType|MethodType
18
+ __testName:Optional[str]
19
+ __traits:list[Trait]
20
+
21
+ def __init__(self, moduleName:str, target:FunctionType|MethodType|Callable, className:Optional[str] = None, testName:Optional[str] = None):
22
+ self.__className = className
23
+ self.__datas = []
24
+ self.__moduleName = moduleName
25
+ self.__target = target
26
+ self.__testName = testName
27
+ self.__traits = []
28
+
29
+ @property
30
+ def className(self) -> Optional[str]:
31
+ return self.__className
32
+
33
+ @property
34
+ def datas(self) -> list[tuple]:
35
+ return self.__datas
36
+
37
+ @property
38
+ def moduleName(self) -> str:
39
+ return self.__moduleName
40
+
41
+ @property
42
+ def target(self) -> FunctionType|MethodType:
43
+ return self.__target
44
+
45
+ @property
46
+ def testName(self) -> str:
47
+ return self.__testName if self.__testName is not None else self.__target.__qualname__.split('.')[-1]
48
+
49
+ @property
50
+ def traits(self) -> list[Trait]:
51
+ return self.__traits
52
+
53
+ @property
54
+ def filterName(self) -> str:
55
+ return f'{self.moduleName}/{"" if self.className is None or len(self.className) == 0 else f"{self.className}/"}{self.testName}'
56
+
57
+ async def execute(self, module:ModuleType, data:tuple) -> None:
58
+ coro:Coroutine|None = None
59
+ if hasattr(self.__target, '__qualname__') and self.__target.__qualname__.find('.') > -1:
60
+ qnparts = self.__target.__qualname__.split('.')
61
+ if isinstance(self.__target, staticmethod):
62
+ coro = self.__target(*data)
63
+ self.__className = '.'.join(qnparts[0:-1])
64
+ self.__testName = qnparts[-1]
65
+ else:
66
+ qntarget = module
67
+ self.__testName = qnparts[-1]
68
+ self.__className = '.'.join(qnparts[0:-1])
69
+ for qnpart in qnparts[0:-1]:
70
+ qntarget = getattr(qntarget, qnpart)
71
+ if isinstance(self.__target, classmethod):
72
+ args = (qntarget,) + data
73
+ coro = self.__target.__func__(*args)
74
+ else:
75
+ args = (cast(Callable,qntarget)(),) + data
76
+ coro = self.__target(*args)
77
+ else:
78
+ self.__className = None
79
+ self.__testName = self.__target.__name__
80
+ coro = self.__target(*data)
81
+ if inspect.iscoroutine(coro):
82
+ await coro
83
+
84
+
85
+ def theory(target:Callable) -> Callable:
86
+ from .TheoryManager import TheoryManager
87
+ if (not inspect.isfunction(target)) and (not isinstance(target, classmethod)) and (not isinstance(target, staticmethod)):
88
+ raise Exception('@theory can only be applied to functions and methods.')
89
+ theory:Theory = Theory(target.__module__, target)
90
+ TheoryManager.instance().put(theory)
91
+ return target
92
+
93
+
94
+ def inlinedata(*args) -> Callable:
95
+ def wrapper(target:Callable) -> Callable:
96
+ if args is not None and len(args) > 0:
97
+ from .TheoryManager import TheoryManager
98
+ TheoryManager.instance().withData(target, args)
99
+ return target
100
+ return wrapper
@@ -0,0 +1,110 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+ ##
4
+
5
+ import re
6
+ from typing import Callable, Optional
7
+
8
+ from ..traits.Trait import Trait
9
+ from .Theory import Theory
10
+
11
+
12
+ class TheoryManager:
13
+
14
+ __excludeTraits:list[Trait]
15
+ __filterPattern:Optional[re.Pattern]
16
+ __includeTraits:list[Trait]
17
+ __instance:'TheoryManager'|None = None
18
+ __modules:dict[str, list[Theory]]
19
+ __datas:dict[Callable, list[tuple]]
20
+ __traits:dict[Callable, list[Trait]]
21
+
22
+ def __init__(self):
23
+ if TheoryManager.__instance is not None:
24
+ raise Exception('Cannot create more than one instance of TheoryManager')
25
+ self.__filterPattern = None
26
+ self.__modules = {}
27
+ self.__datas = {}
28
+ self.__traits = {}
29
+
30
+ @staticmethod
31
+ def instance() -> TheoryManager:
32
+ if TheoryManager.__instance is None:
33
+ TheoryManager.__instance = TheoryManager()
34
+ return TheoryManager.__instance
35
+ @property
36
+ def excludeTraits(self) -> list[Trait]:
37
+ return [] if self.__excludeTraits is None else self.excludeTraits
38
+
39
+ @excludeTraits.setter
40
+ def excludeTraits(self, value:list[Trait]) -> None:
41
+ self.__excludeTraits = value
42
+
43
+ @property
44
+ def filterPattern(self) -> Optional[re.Pattern]:
45
+ return self.__filterPattern
46
+
47
+ @filterPattern.setter
48
+ def filterPattern(self, value:re.Pattern) -> None:
49
+ self.__filterPattern = value
50
+
51
+ @property
52
+ def includeTraits(self) -> list[Trait]:
53
+ return [] if self.__includeTraits is None else self.includeTraits
54
+
55
+ @includeTraits.setter
56
+ def includeTraits(self, value:list[Trait]) -> None:
57
+ self.__includeTraits = value
58
+
59
+ def __excludeByTraits(self, theory:Theory) -> bool:
60
+ if self.__excludeTraits is not None and len(self.__excludeTraits) > 0:
61
+ for trait in self.__excludeTraits:
62
+ for L_trait in theory.traits:
63
+ if trait.name == L_trait.name and (trait.value is None or (trait.value == L_trait.value)):
64
+ return True
65
+ if self.__includeTraits is not None and len(self.__includeTraits) > 0:
66
+ for trait in self.__includeTraits:
67
+ for L_trait in theory.traits:
68
+ if trait.name == L_trait.name and (trait.value is None or (trait.value == L_trait.value)):
69
+ return False
70
+ return True
71
+ return False
72
+
73
+ def get(self, moduleName:str) -> list[Theory]:
74
+ l = self.__modules.get(moduleName)
75
+ if l is None:
76
+ l = []
77
+ self.__modules[moduleName] = l
78
+ return l
79
+
80
+ def put(self, theory:Theory) -> None:
81
+ if self.__filterPattern is None or len(self.__filterPattern.findall(theory.filterName)) > 0:
82
+ l = self.get(theory.moduleName)
83
+ d = self.__datas.get(theory.target)
84
+ if d is not None:
85
+ d.reverse()
86
+ for data in d:
87
+ theory.datas.append(data)
88
+ t = self.__traits.get(theory.target)
89
+ if t is not None:
90
+ for trait in t:
91
+ theory.traits.append(trait)
92
+ if not self.__excludeByTraits(theory):
93
+ l.append(theory)
94
+
95
+ def withData(self, target:Callable, data:tuple) -> None:
96
+ # TODO: data acquisition should be deferred until put() since that is where filterPattern
97
+ # is applied, but for current implementation `@inlinedata()` is not affected. more advanced
98
+ # data decorators may benefit from deferral (for example, data coming from an API or DB.)
99
+ d = self.__datas.get(target)
100
+ if d is None:
101
+ d = []
102
+ self.__datas[target] = d
103
+ d.append(data)
104
+
105
+ def withTrait(self, target:Callable, trait:Trait) -> None:
106
+ t = self.__traits.get(target)
107
+ if t is None:
108
+ t = []
109
+ self.__traits[target] = t
110
+ t.append(trait)
@@ -0,0 +1,11 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from .Theory import Theory, theory, inlinedata
5
+ from .TheoryManager import TheoryManager
6
+
7
+
8
+ __all__ = [
9
+ 'Theory', 'theory', 'inlinedata',
10
+ 'TheoryManager'
11
+ ]
punit/traits/Trait.py ADDED
@@ -0,0 +1,34 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+ ##
4
+
5
+ from typing import Callable, Optional
6
+
7
+
8
+ class Trait:
9
+
10
+ __name:str
11
+ __value:str|None
12
+
13
+ def __init__(self, name:str, value:Optional[str] = None):
14
+ self.__name = name
15
+ self.__value = value
16
+
17
+ @property
18
+ def name(self) -> str:
19
+ return self.__name
20
+
21
+ @property
22
+ def value(self) -> str|None:
23
+ return self.__value
24
+
25
+
26
+ def trait(name:str, value:Optional[str]=None) -> Callable:
27
+ def wrapper(target:Callable) -> Callable:
28
+ from ..theories.TheoryManager import TheoryManager
29
+ from ..facts.FactManager import FactManager
30
+ trait = Trait(name, value)
31
+ TheoryManager.instance().withTrait(target, trait)
32
+ FactManager.instance().withTrait(target, trait)
33
+ return target
34
+ return wrapper
@@ -0,0 +1,9 @@
1
+ # SPDX-FileCopyrightText: © Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from .Trait import Trait, trait
5
+
6
+
7
+ __all__ = [
8
+ 'Trait', 'trait'
9
+ ]