orionis 0.443.0__py3-none-any.whl → 0.445.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.
- orionis/console/args/argument.py +343 -0
- orionis/console/args/enums/actions.py +43 -0
- orionis/console/commands/version.py +19 -4
- orionis/foundation/application.py +0 -2
- orionis/foundation/providers/testing_provider.py +39 -2
- orionis/metadata/framework.py +1 -1
- orionis/test/core/unit_test.py +134 -5
- orionis/test/kernel.py +30 -83
- orionis/test/validators/workers.py +2 -2
- {orionis-0.443.0.dist-info → orionis-0.445.0.dist-info}/METADATA +1 -1
- {orionis-0.443.0.dist-info → orionis-0.445.0.dist-info}/RECORD +19 -27
- tests/example/test_example.py +163 -173
- orionis/foundation/providers/path_resolver_provider.py +0 -43
- orionis/services/paths/contracts/__init__.py +0 -0
- orionis/services/paths/contracts/resolver.py +0 -51
- orionis/services/paths/exceptions/__init__.py +0 -7
- orionis/services/paths/exceptions/exception.py +0 -19
- orionis/services/paths/exceptions/file.py +0 -19
- orionis/services/paths/resolver.py +0 -95
- orionis/support/facades/path_resolver.py +0 -15
- tests/services/path/__init__.py +0 -0
- tests/services/path/test_services_resolver.py +0 -112
- /orionis/console/{arguments → args}/__init__.py +0 -0
- /orionis/{services/paths → console/args/enums}/__init__.py +0 -0
- /orionis/console/{arguments → args}/parser.py +0 -0
- {orionis-0.443.0.dist-info → orionis-0.445.0.dist-info}/WHEEL +0 -0
- {orionis-0.443.0.dist-info → orionis-0.445.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.443.0.dist-info → orionis-0.445.0.dist-info}/top_level.txt +0 -0
- {orionis-0.443.0.dist-info → orionis-0.445.0.dist-info}/zip-safe +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, Optional, List, Type, Union, Dict
|
|
4
|
+
from orionis.console.args.enums.actions import ArgumentAction
|
|
5
|
+
from orionis.console.exceptions.cli_orionis_value_error import CLIOrionisValueError
|
|
6
|
+
|
|
7
|
+
@dataclass(kw_only=True, frozen=True, slots=True)
|
|
8
|
+
class CLIArgument:
|
|
9
|
+
"""
|
|
10
|
+
Represents a command-line argument for argparse.
|
|
11
|
+
|
|
12
|
+
This class encapsulates all the properties and validation logic needed to create
|
|
13
|
+
a command-line argument that can be added to an argparse ArgumentParser. It provides
|
|
14
|
+
automatic validation, type checking, and smart defaults for common argument patterns.
|
|
15
|
+
|
|
16
|
+
Attributes
|
|
17
|
+
----------
|
|
18
|
+
flags : List[str]
|
|
19
|
+
List of flags for the argument (e.g., ['--export', '-e']). Must contain at least one flag.
|
|
20
|
+
type : Type
|
|
21
|
+
Data type of the argument. Can be any Python type or custom type.
|
|
22
|
+
help : str
|
|
23
|
+
Description of the argument. If not provided, will be auto-generated from the primary flag.
|
|
24
|
+
default : Any, optional
|
|
25
|
+
Default value for the argument.
|
|
26
|
+
choices : List[Any], optional
|
|
27
|
+
List of valid values for the argument. All choices must match the specified type.
|
|
28
|
+
required : bool, default False
|
|
29
|
+
Whether the argument is required. Only applies to optional arguments.
|
|
30
|
+
metavar : str, optional
|
|
31
|
+
Metavar for displaying in help messages. Auto-generated from primary flag if not provided.
|
|
32
|
+
dest : str, optional
|
|
33
|
+
Destination name for the argument in the namespace. Auto-generated from primary flag if not provided.
|
|
34
|
+
action : Union[str, ArgumentAction], default ArgumentAction.STORE
|
|
35
|
+
Action to perform with the argument when it's encountered.
|
|
36
|
+
nargs : Union[int, str], optional
|
|
37
|
+
Number of arguments expected (e.g., 1, 2, '+', '*').
|
|
38
|
+
const : Any, optional
|
|
39
|
+
Constant value for store_const or append_const actions.
|
|
40
|
+
|
|
41
|
+
Raises
|
|
42
|
+
------
|
|
43
|
+
CLIOrionisValueError
|
|
44
|
+
If any validation fails during initialization.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Required fields
|
|
48
|
+
flags: List[str] = None
|
|
49
|
+
type: Type = None
|
|
50
|
+
help: str = None
|
|
51
|
+
|
|
52
|
+
default: Any = field(
|
|
53
|
+
default_factory = None,
|
|
54
|
+
metadata = {
|
|
55
|
+
"description": "Default value for the argument.",
|
|
56
|
+
"default": None
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
choices: Optional[List[Any]] = field(
|
|
61
|
+
default_factory = None,
|
|
62
|
+
metadata = {
|
|
63
|
+
"description": "List of valid choices for the argument.",
|
|
64
|
+
"default": None
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
required: bool = field(
|
|
69
|
+
default_factory = False,
|
|
70
|
+
metadata = {
|
|
71
|
+
"description": "Indicates if the argument is required.",
|
|
72
|
+
"default": False
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
metavar: Optional[str] = field(
|
|
77
|
+
default_factory = None,
|
|
78
|
+
metadata = {
|
|
79
|
+
"description": "Metavar for displaying in help messages.",
|
|
80
|
+
"default": None
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
dest: Optional[str] = field(
|
|
85
|
+
default_factory = None,
|
|
86
|
+
metadata = {
|
|
87
|
+
"description": "Destination name for the argument in the namespace.",
|
|
88
|
+
"default": None
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
action: Union[str, ArgumentAction] = field(
|
|
93
|
+
default_factory = ArgumentAction.STORE,
|
|
94
|
+
metadata = {
|
|
95
|
+
"description": "Action to perform with the argument.",
|
|
96
|
+
"default": ArgumentAction.STORE.value
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
nargs: Optional[Union[int, str]] = field(
|
|
101
|
+
default_factory = None,
|
|
102
|
+
metadata = {
|
|
103
|
+
"description": "Number of arguments expected (e.g., 1, 2, '+', '*').",
|
|
104
|
+
"default": None
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const: Any = field(
|
|
109
|
+
default_factory = None,
|
|
110
|
+
metadata = {
|
|
111
|
+
"description": "Constant value for store_const or append_const actions.",
|
|
112
|
+
"default": None
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def __post_init__(self):
|
|
117
|
+
"""
|
|
118
|
+
Validate and normalize all argument attributes after initialization.
|
|
119
|
+
|
|
120
|
+
This method performs comprehensive validation of all argument attributes
|
|
121
|
+
and applies smart defaults where appropriate. It ensures the argument
|
|
122
|
+
configuration is valid for use with argparse.
|
|
123
|
+
|
|
124
|
+
Raises
|
|
125
|
+
------
|
|
126
|
+
CLIOrionisValueError
|
|
127
|
+
If any validation fails or invalid values are provided.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
# Validate flags - must be provided and non-empty
|
|
131
|
+
if not self.flags:
|
|
132
|
+
raise CLIOrionisValueError(
|
|
133
|
+
"Flags list cannot be empty. Please provide at least one flag (e.g., ['--export', '-e'])"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Convert single string flag to list for consistency
|
|
137
|
+
if isinstance(self.flags, str):
|
|
138
|
+
object.__setattr__(self, 'flags', [self.flags])
|
|
139
|
+
|
|
140
|
+
# Ensure flags is a list
|
|
141
|
+
if not isinstance(self.flags, list):
|
|
142
|
+
raise CLIOrionisValueError("Flags must be a string or a list of strings")
|
|
143
|
+
|
|
144
|
+
# Validate each flag format and ensure they're strings
|
|
145
|
+
for flag in self.flags:
|
|
146
|
+
if not isinstance(flag, str):
|
|
147
|
+
raise CLIOrionisValueError("All flags must be strings")
|
|
148
|
+
|
|
149
|
+
# Check for duplicate flags
|
|
150
|
+
if len(set(self.flags)) != len(self.flags):
|
|
151
|
+
raise CLIOrionisValueError("Duplicate flags are not allowed in the flags list")
|
|
152
|
+
|
|
153
|
+
# Determine primary flag (longest one, or first if only one)
|
|
154
|
+
primary_flag = max(self.flags, key=len) if len(self.flags) > 1 else self.flags[0]
|
|
155
|
+
|
|
156
|
+
# Validate type is actually a type
|
|
157
|
+
if not isinstance(self.type, type):
|
|
158
|
+
raise CLIOrionisValueError("Type must be a valid Python type or custom type class")
|
|
159
|
+
|
|
160
|
+
# Auto-generate help if not provided
|
|
161
|
+
if self.help is None:
|
|
162
|
+
object.__setattr__(self, 'help', f"Argument for {primary_flag}")
|
|
163
|
+
|
|
164
|
+
# Ensure help is a string
|
|
165
|
+
if not isinstance(self.help, str):
|
|
166
|
+
raise CLIOrionisValueError("Help text must be a string")
|
|
167
|
+
|
|
168
|
+
# Validate choices if provided
|
|
169
|
+
if self.choices is not None:
|
|
170
|
+
# Ensure choices is a list
|
|
171
|
+
if not isinstance(self.choices, list):
|
|
172
|
+
raise CLIOrionisValueError("Choices must be provided as a list")
|
|
173
|
+
|
|
174
|
+
# Ensure all choices match the specified type
|
|
175
|
+
if self.type and not all(isinstance(choice, self.type) for choice in self.choices):
|
|
176
|
+
raise CLIOrionisValueError(
|
|
177
|
+
f"All choices must be of type {self.type.__name__}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Validate required is boolean
|
|
181
|
+
if not isinstance(self.required, bool):
|
|
182
|
+
raise CLIOrionisValueError("Required field must be a boolean value (True or False)")
|
|
183
|
+
|
|
184
|
+
# Auto-generate metavar if not provided
|
|
185
|
+
if self.metavar is None:
|
|
186
|
+
metavar = primary_flag.lstrip('-').upper().replace('-', '_')
|
|
187
|
+
object.__setattr__(self, 'metavar', metavar)
|
|
188
|
+
|
|
189
|
+
# Ensure metavar is a string
|
|
190
|
+
if not isinstance(self.metavar, str):
|
|
191
|
+
raise CLIOrionisValueError("Metavar must be a string")
|
|
192
|
+
|
|
193
|
+
# Auto-generate dest if not provided
|
|
194
|
+
if self.dest is None:
|
|
195
|
+
dest = primary_flag.lstrip('-').replace('-', '_')
|
|
196
|
+
object.__setattr__(self, 'dest', dest)
|
|
197
|
+
|
|
198
|
+
# Ensure dest is a string
|
|
199
|
+
if not isinstance(self.dest, str):
|
|
200
|
+
raise CLIOrionisValueError("Destination (dest) must be a string")
|
|
201
|
+
|
|
202
|
+
# Ensure dest is a valid Python identifier
|
|
203
|
+
if not self.dest.isidentifier():
|
|
204
|
+
raise CLIOrionisValueError(f"Destination '{self.dest}' is not a valid Python identifier")
|
|
205
|
+
|
|
206
|
+
# Normalize action value
|
|
207
|
+
if self.action is None:
|
|
208
|
+
object.__setattr__(self, 'action', ArgumentAction.STORE.value)
|
|
209
|
+
elif isinstance(self.action, str):
|
|
210
|
+
try:
|
|
211
|
+
action_enum = ArgumentAction(self.action)
|
|
212
|
+
object.__setattr__(self, 'action', action_enum.value)
|
|
213
|
+
except ValueError:
|
|
214
|
+
raise CLIOrionisValueError(f"Invalid action '{self.action}'. Please use a valid ArgumentAction value")
|
|
215
|
+
elif isinstance(self.action, ArgumentAction):
|
|
216
|
+
object.__setattr__(self, 'action', self.action.value)
|
|
217
|
+
else:
|
|
218
|
+
raise CLIOrionisValueError("Action must be a string or an ArgumentAction enum value")
|
|
219
|
+
|
|
220
|
+
# Special handling for boolean types
|
|
221
|
+
if self.type is bool:
|
|
222
|
+
|
|
223
|
+
# Auto-configure action based on default value
|
|
224
|
+
action = ArgumentAction.STORE_TRUE.value if not self.default else ArgumentAction.STORE_FALSE.value
|
|
225
|
+
object.__setattr__(self, 'action', action)
|
|
226
|
+
|
|
227
|
+
# argparse ignores type with store_true/false actions
|
|
228
|
+
object.__setattr__(self, 'type', None)
|
|
229
|
+
|
|
230
|
+
# Special handling for list types
|
|
231
|
+
if self.type is list and self.nargs is None:
|
|
232
|
+
|
|
233
|
+
# Auto-configure for accepting multiple values
|
|
234
|
+
object.__setattr__(self, 'nargs', '+')
|
|
235
|
+
object.__setattr__(self, 'type', str)
|
|
236
|
+
|
|
237
|
+
def addToParser(self, parser: argparse.ArgumentParser) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Add this argument to an argparse ArgumentParser instance.
|
|
240
|
+
|
|
241
|
+
This method integrates the CLIArgument configuration with an argparse
|
|
242
|
+
ArgumentParser by building the appropriate keyword arguments and adding
|
|
243
|
+
the argument with all its flags and options. The method handles all
|
|
244
|
+
necessary conversions and validations to ensure compatibility with
|
|
245
|
+
argparse's expected format.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
parser : argparse.ArgumentParser
|
|
250
|
+
The ArgumentParser instance to which this argument will be added.
|
|
251
|
+
The parser must be a valid argparse.ArgumentParser object.
|
|
252
|
+
|
|
253
|
+
Returns
|
|
254
|
+
-------
|
|
255
|
+
None
|
|
256
|
+
This method does not return any value. It modifies the provided
|
|
257
|
+
parser by adding the argument configuration to it.
|
|
258
|
+
|
|
259
|
+
Raises
|
|
260
|
+
------
|
|
261
|
+
CLIOrionisValueError
|
|
262
|
+
If there's an error adding the argument to the parser, such as
|
|
263
|
+
conflicting argument names, invalid configurations, or argparse
|
|
264
|
+
internal errors during argument registration.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
# Build the keyword arguments dictionary for argparse compatibility
|
|
268
|
+
# This filters out None values and handles special argument types
|
|
269
|
+
kwargs = self._buildParserKwargs()
|
|
270
|
+
|
|
271
|
+
# Attempt to add the argument to the parser with all flags and options
|
|
272
|
+
try:
|
|
273
|
+
# Use unpacking to pass all flags as positional arguments
|
|
274
|
+
# and all configuration options as keyword arguments
|
|
275
|
+
parser.add_argument(*self.flags, **kwargs)
|
|
276
|
+
|
|
277
|
+
# Catch any exception that occurs during argument addition
|
|
278
|
+
# and wrap it in our custom exception for consistent error handling
|
|
279
|
+
except Exception as e:
|
|
280
|
+
raise CLIOrionisValueError(f"Error adding argument {self.flags}: {e}")
|
|
281
|
+
|
|
282
|
+
def _buildParserKwargs(self) -> Dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
Build the keyword arguments dictionary for argparse compatibility.
|
|
285
|
+
|
|
286
|
+
This private method constructs a dictionary of keyword arguments that will be
|
|
287
|
+
passed to argparse's add_argument method. It handles the conversion from
|
|
288
|
+
CLIArgument attributes to argparse-compatible parameters, filtering out None
|
|
289
|
+
values and applying special handling for different argument types (optional
|
|
290
|
+
vs positional arguments).
|
|
291
|
+
|
|
292
|
+
The method ensures that the resulting kwargs dictionary contains only valid
|
|
293
|
+
argparse parameters with appropriate values, preventing errors during argument
|
|
294
|
+
registration with the ArgumentParser.
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
Dict[str, Any]
|
|
299
|
+
A dictionary containing keyword arguments ready to be unpacked and passed
|
|
300
|
+
to argparse.ArgumentParser.add_argument(). The dictionary includes only
|
|
301
|
+
non-None values and excludes parameters that are invalid for the specific
|
|
302
|
+
argument type (e.g., 'required' parameter for positional arguments).
|
|
303
|
+
|
|
304
|
+
Notes
|
|
305
|
+
-----
|
|
306
|
+
This method distinguishes between optional arguments (those starting with '-')
|
|
307
|
+
and positional arguments, applying different validation rules for each type.
|
|
308
|
+
Positional arguments cannot use the 'required' parameter, so it's automatically
|
|
309
|
+
removed from the kwargs if present.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
# Determine argument type by checking if any flag starts with a dash
|
|
313
|
+
# Optional arguments have flags like '--export' or '-e'
|
|
314
|
+
# Positional arguments have flags without dashes like 'filename'
|
|
315
|
+
is_optional = any(flag.startswith('-') for flag in self.flags)
|
|
316
|
+
is_positional = not is_optional
|
|
317
|
+
|
|
318
|
+
# Build the base kwargs dictionary with all possible argparse parameters
|
|
319
|
+
# Each key corresponds to a parameter accepted by argparse.add_argument()
|
|
320
|
+
kwargs = {
|
|
321
|
+
"help": self.help, # Help text displayed in usage messages
|
|
322
|
+
"default": self.default, # Default value when argument not provided
|
|
323
|
+
"required": self.required and is_optional, # Whether argument is mandatory
|
|
324
|
+
"metavar": self.metavar, # Name displayed in help messages
|
|
325
|
+
"dest": self.dest, # Attribute name in the parsed namespace
|
|
326
|
+
"choices": self.choices, # List of valid values for the argument
|
|
327
|
+
"action": self.action, # Action to take when argument is encountered
|
|
328
|
+
"nargs": self.nargs, # Number of command-line arguments expected
|
|
329
|
+
"type": self.type, # Type to convert the argument to
|
|
330
|
+
"const": self.const # Constant value for certain actions
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
# Filter out None values to prevent passing invalid parameters to argparse
|
|
334
|
+
# argparse will raise errors if None values are explicitly passed for certain parameters
|
|
335
|
+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
336
|
+
|
|
337
|
+
# Remove 'required' parameter for positional arguments since it's not supported
|
|
338
|
+
# Positional arguments are inherently required by argparse's design
|
|
339
|
+
if is_positional and 'required' in kwargs:
|
|
340
|
+
del kwargs['required']
|
|
341
|
+
|
|
342
|
+
# Return the cleaned and validated kwargs dictionary
|
|
343
|
+
return kwargs
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class ArgumentAction(Enum):
|
|
4
|
+
"""
|
|
5
|
+
Enumeration for valid argparse action types.
|
|
6
|
+
|
|
7
|
+
This enum provides a comprehensive list of all standard action types
|
|
8
|
+
that can be used with Python's argparse module when defining command
|
|
9
|
+
line arguments. Each enum member corresponds to a specific behavior
|
|
10
|
+
for how argument values should be processed and stored.
|
|
11
|
+
|
|
12
|
+
Returns
|
|
13
|
+
-------
|
|
14
|
+
str
|
|
15
|
+
The string value representing the argparse action type.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Store the argument value directly
|
|
19
|
+
STORE = "store"
|
|
20
|
+
|
|
21
|
+
# Store a constant value when the argument is specified
|
|
22
|
+
STORE_CONST = "store_const"
|
|
23
|
+
|
|
24
|
+
# Store True when the argument is specified
|
|
25
|
+
STORE_TRUE = "store_true"
|
|
26
|
+
|
|
27
|
+
# Store False when the argument is specified
|
|
28
|
+
STORE_FALSE = "store_false"
|
|
29
|
+
|
|
30
|
+
# Append each argument value to a list
|
|
31
|
+
APPEND = "append"
|
|
32
|
+
|
|
33
|
+
# Append a constant value to a list when the argument is specified
|
|
34
|
+
APPEND_CONST = "append_const"
|
|
35
|
+
|
|
36
|
+
# Count the number of times the argument is specified
|
|
37
|
+
COUNT = "count"
|
|
38
|
+
|
|
39
|
+
# Display help message and exit
|
|
40
|
+
HELP = "help"
|
|
41
|
+
|
|
42
|
+
# Display version information and exit
|
|
43
|
+
VERSION = "version"
|
|
@@ -15,17 +15,32 @@ class VersionCommand(BaseCommand):
|
|
|
15
15
|
|
|
16
16
|
def handle(self) -> None:
|
|
17
17
|
"""
|
|
18
|
-
|
|
18
|
+
Executes the version command to display the current Orionis framework version.
|
|
19
19
|
|
|
20
|
-
This method retrieves
|
|
20
|
+
This method retrieves the version number from the framework metadata and prints it
|
|
21
|
+
in a formatted, bold, and successful style to the console. If an unexpected error occurs
|
|
22
|
+
during execution, it raises a CLIOrionisRuntimeError with the original exception message.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
None
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
None
|
|
31
|
+
This method does not return any value. It outputs the version information to the console.
|
|
21
32
|
|
|
22
33
|
Raises
|
|
23
34
|
------
|
|
24
|
-
|
|
25
|
-
If an unexpected error occurs during execution, a
|
|
35
|
+
CLIOrionisRuntimeError
|
|
36
|
+
If an unexpected error occurs during execution, a CLIOrionisRuntimeError is raised
|
|
26
37
|
with the original exception message.
|
|
27
38
|
"""
|
|
39
|
+
|
|
40
|
+
# Print the Orionis framework version in a bold, success style
|
|
28
41
|
try:
|
|
29
42
|
self.textSuccessBold(f"Orionis Framework v{VERSION}")
|
|
43
|
+
|
|
44
|
+
# Raise a custom runtime error if any exception occurs
|
|
30
45
|
except Exception as e:
|
|
31
46
|
raise CLIOrionisRuntimeError(f"An unexpected error occurred: {e}") from e
|
|
@@ -174,7 +174,6 @@ class Application(Container, IApplication):
|
|
|
174
174
|
# Import core framework providers
|
|
175
175
|
from orionis.foundation.providers.console_provider import ConsoleProvider
|
|
176
176
|
from orionis.foundation.providers.dumper_provider import DumperProvider
|
|
177
|
-
from orionis.foundation.providers.path_resolver_provider import PathResolverProvider
|
|
178
177
|
from orionis.foundation.providers.progress_bar_provider import ProgressBarProvider
|
|
179
178
|
from orionis.foundation.providers.workers_provider import WorkersProvider
|
|
180
179
|
from orionis.foundation.providers.testing_provider import TestingProvider
|
|
@@ -183,7 +182,6 @@ class Application(Container, IApplication):
|
|
|
183
182
|
core_providers = [
|
|
184
183
|
ConsoleProvider,
|
|
185
184
|
DumperProvider,
|
|
186
|
-
PathResolverProvider,
|
|
187
185
|
ProgressBarProvider,
|
|
188
186
|
WorkersProvider,
|
|
189
187
|
LoggerProvider,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from orionis.container.providers.service_provider import ServiceProvider
|
|
2
2
|
from orionis.test.contracts.unit_test import IUnitTest
|
|
3
3
|
from orionis.test.core.unit_test import UnitTest
|
|
4
|
+
from orionis.foundation.config.testing.entities.testing import Testing
|
|
5
|
+
import os
|
|
4
6
|
|
|
5
7
|
class TestingProvider(ServiceProvider):
|
|
6
8
|
"""
|
|
@@ -28,7 +30,39 @@ class TestingProvider(ServiceProvider):
|
|
|
28
30
|
None
|
|
29
31
|
"""
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
# Create a Testing configuration instance from the application config
|
|
34
|
+
config = Testing(**self.app.config('testing'))
|
|
35
|
+
|
|
36
|
+
# Create a UnitTest instance
|
|
37
|
+
unit_test = UnitTest(
|
|
38
|
+
app=self.app,
|
|
39
|
+
storage=self.app.path('storage_testing')
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Configure the UnitTest instance with settings from the Testing configuration
|
|
43
|
+
unit_test.configure(
|
|
44
|
+
verbosity=config.verbosity,
|
|
45
|
+
execution_mode=config.execution_mode,
|
|
46
|
+
max_workers=config.max_workers,
|
|
47
|
+
fail_fast=config.fail_fast,
|
|
48
|
+
print_result=config.print_result,
|
|
49
|
+
throw_exception=config.throw_exception,
|
|
50
|
+
persistent=config.persistent,
|
|
51
|
+
persistent_driver=config.persistent_driver,
|
|
52
|
+
web_report=config.web_report
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Discover tests based on the configuration
|
|
56
|
+
unit_test.discoverTests(
|
|
57
|
+
base_path=config.base_path,
|
|
58
|
+
folder_path=config.folder_path,
|
|
59
|
+
pattern=config.pattern,
|
|
60
|
+
test_name_pattern=config.test_name_pattern,
|
|
61
|
+
tags=config.tags
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Register the UnitTest instance in the application container
|
|
65
|
+
self.app.instance(IUnitTest, unit_test, alias="core.orionis.testing")
|
|
32
66
|
|
|
33
67
|
def boot(self) -> None:
|
|
34
68
|
"""
|
|
@@ -42,4 +76,7 @@ class TestingProvider(ServiceProvider):
|
|
|
42
76
|
None
|
|
43
77
|
"""
|
|
44
78
|
|
|
45
|
-
|
|
79
|
+
# Ensure directory for testing storage exists
|
|
80
|
+
storage_path = self.app.path('storage_testing')
|
|
81
|
+
if not os.path.exists(storage_path):
|
|
82
|
+
os.makedirs(storage_path, exist_ok=True)
|
orionis/metadata/framework.py
CHANGED
orionis/test/core/unit_test.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import json
|
|
3
|
+
from os import walk
|
|
3
4
|
import re
|
|
4
5
|
import time
|
|
5
6
|
import traceback
|
|
@@ -9,6 +10,7 @@ from contextlib import redirect_stdout, redirect_stderr
|
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any, Dict, List, Optional, Tuple
|
|
13
|
+
from orionis.app import Orionis
|
|
12
14
|
from orionis.container.resolver.resolver import Resolver
|
|
13
15
|
from orionis.foundation.config.testing.enums.drivers import PersistentDrivers
|
|
14
16
|
from orionis.foundation.config.testing.enums.mode import ExecutionMode
|
|
@@ -102,7 +104,9 @@ class UnitTest(IUnitTest):
|
|
|
102
104
|
"""
|
|
103
105
|
|
|
104
106
|
def __init__(
|
|
105
|
-
self
|
|
107
|
+
self,
|
|
108
|
+
app: Optional[IApplication] = None,
|
|
109
|
+
storage: Optional[str] = None
|
|
106
110
|
) -> None:
|
|
107
111
|
"""
|
|
108
112
|
Initialize a UnitTest instance with default configuration and internal state.
|
|
@@ -113,11 +117,11 @@ class UnitTest(IUnitTest):
|
|
|
113
117
|
-------
|
|
114
118
|
None
|
|
115
119
|
"""
|
|
116
|
-
# Application instance for dependency injection
|
|
117
|
-
self.__app: Optional[IApplication] =
|
|
120
|
+
# Application instance for dependency injection
|
|
121
|
+
self.__app: Optional[IApplication] = app or Orionis()
|
|
118
122
|
|
|
119
|
-
# Storage path for test results
|
|
120
|
-
self.__storage: Optional[str] =
|
|
123
|
+
# Storage path for test results
|
|
124
|
+
self.__storage: Optional[str] = storage or self.__app.path('storage_testing')
|
|
121
125
|
|
|
122
126
|
# Configuration values (set via configure)
|
|
123
127
|
self.__verbosity: Optional[int] = None
|
|
@@ -220,6 +224,100 @@ class UnitTest(IUnitTest):
|
|
|
220
224
|
# Return the instance to allow method chaining
|
|
221
225
|
return self
|
|
222
226
|
|
|
227
|
+
def discoverTests(
|
|
228
|
+
self,
|
|
229
|
+
base_path: str | Path,
|
|
230
|
+
folder_path: str | List[str],
|
|
231
|
+
pattern: str,
|
|
232
|
+
test_name_pattern: Optional[str] = None,
|
|
233
|
+
tags: Optional[List[str]] = None
|
|
234
|
+
) -> 'UnitTest':
|
|
235
|
+
"""
|
|
236
|
+
Discover test cases from specified folders using flexible path discovery.
|
|
237
|
+
|
|
238
|
+
This method provides a convenient way to discover and load test cases from multiple folders
|
|
239
|
+
based on various path specifications. It supports wildcard discovery, single folder loading,
|
|
240
|
+
and multiple folder loading. The method automatically resolves paths relative to the base
|
|
241
|
+
directory and discovers all folders containing files matching the specified pattern.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
base_path : str or Path
|
|
246
|
+
Base directory path for resolving relative folder paths. This serves as the root
|
|
247
|
+
directory from which all folder searches are conducted.
|
|
248
|
+
folder_path : str or list of str
|
|
249
|
+
Specification of folders to search for test cases. Can be:
|
|
250
|
+
- '*' : Discover all folders containing matching files within base_path
|
|
251
|
+
- str : Single folder path relative to base_path
|
|
252
|
+
- list of str : Multiple folder paths relative to base_path
|
|
253
|
+
pattern : str
|
|
254
|
+
File name pattern to match test files, supporting wildcards (* and ?).
|
|
255
|
+
Examples: 'test_*.py', '*_test.py', 'test*.py'
|
|
256
|
+
test_name_pattern : str, optional
|
|
257
|
+
Regular expression pattern to filter test method names. Only tests whose
|
|
258
|
+
names match this pattern will be included. Default is None (no filtering).
|
|
259
|
+
tags : list of str, optional
|
|
260
|
+
List of tags to filter tests. Only tests decorated with matching tags
|
|
261
|
+
will be included. Default is None (no tag filtering).
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
UnitTest
|
|
266
|
+
The current UnitTest instance with discovered tests added to the suite,
|
|
267
|
+
enabling method chaining.
|
|
268
|
+
|
|
269
|
+
Notes
|
|
270
|
+
-----
|
|
271
|
+
- All paths are resolved as absolute paths relative to the base_path
|
|
272
|
+
- When folder_path is '*', the method searches recursively through all subdirectories
|
|
273
|
+
- The method uses the existing discoverTestsInFolder method for actual test discovery
|
|
274
|
+
- Duplicate folders are automatically eliminated using a set data structure
|
|
275
|
+
- The method does not validate the existence of specified folders; validation
|
|
276
|
+
occurs during the actual test discovery process
|
|
277
|
+
"""
|
|
278
|
+
# Resolve the base path as an absolute path from the current working directory
|
|
279
|
+
base_path = (Path.cwd() / base_path).resolve()
|
|
280
|
+
|
|
281
|
+
# Use a set to store discovered folders and automatically eliminate duplicates
|
|
282
|
+
discovered_folders = set()
|
|
283
|
+
|
|
284
|
+
# Handle wildcard discovery: search all folders containing matching files
|
|
285
|
+
if folder_path == '*':
|
|
286
|
+
|
|
287
|
+
# Search recursively through the entire base path for folders with matching files
|
|
288
|
+
discovered_folders.update(self.__listMatchingFolders(base_path, base_path, pattern))
|
|
289
|
+
|
|
290
|
+
# Handle multiple folder paths: process each folder in the provided list
|
|
291
|
+
elif isinstance(folder_path, list):
|
|
292
|
+
for custom in folder_path:
|
|
293
|
+
# Resolve each custom folder path relative to the base path
|
|
294
|
+
custom_path = (base_path / custom).resolve()
|
|
295
|
+
# Add all matching folders found within this custom path
|
|
296
|
+
discovered_folders.update(self.__listMatchingFolders(base_path, custom_path, pattern))
|
|
297
|
+
|
|
298
|
+
# Handle single folder path: process the single specified folder
|
|
299
|
+
else:
|
|
300
|
+
|
|
301
|
+
# Resolve the single folder path relative to the base path
|
|
302
|
+
custom_path = (base_path / folder_path).resolve()
|
|
303
|
+
# Add all matching folders found within this single path
|
|
304
|
+
discovered_folders.update(self.__listMatchingFolders(base_path, custom_path, pattern))
|
|
305
|
+
|
|
306
|
+
# Iterate through all discovered folders and perform test discovery
|
|
307
|
+
for folder in discovered_folders:
|
|
308
|
+
|
|
309
|
+
# Use the existing discoverTestsInFolder method to actually discover and load tests
|
|
310
|
+
self.discoverTestsInFolder(
|
|
311
|
+
base_path=base_path,
|
|
312
|
+
folder_path=folder,
|
|
313
|
+
pattern=pattern,
|
|
314
|
+
test_name_pattern=test_name_pattern or None,
|
|
315
|
+
tags=tags or None
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Return the current instance to enable method chaining
|
|
319
|
+
return self
|
|
320
|
+
|
|
223
321
|
def discoverTestsInFolder(
|
|
224
322
|
self,
|
|
225
323
|
*,
|
|
@@ -1342,6 +1440,37 @@ class UnitTest(IUnitTest):
|
|
|
1342
1440
|
# Return the suite containing only the filtered tests
|
|
1343
1441
|
return filtered_suite
|
|
1344
1442
|
|
|
1443
|
+
def __listMatchingFolders(
|
|
1444
|
+
self,
|
|
1445
|
+
base_path: Path,
|
|
1446
|
+
custom_path: Path,
|
|
1447
|
+
pattern: str
|
|
1448
|
+
) -> List[str]:
|
|
1449
|
+
"""
|
|
1450
|
+
List folders within a given path containing files matching a pattern.
|
|
1451
|
+
|
|
1452
|
+
Parameters
|
|
1453
|
+
----------
|
|
1454
|
+
base_path : Path
|
|
1455
|
+
The base directory path for calculating relative paths.
|
|
1456
|
+
custom_path : Path
|
|
1457
|
+
The directory path to search for matching files.
|
|
1458
|
+
pattern : str
|
|
1459
|
+
The filename pattern to match, supporting '*' and '?' wildcards.
|
|
1460
|
+
|
|
1461
|
+
Returns
|
|
1462
|
+
-------
|
|
1463
|
+
List[str]
|
|
1464
|
+
List of relative folder paths containing files matching the pattern.
|
|
1465
|
+
"""
|
|
1466
|
+
regex = re.compile('^' + pattern.replace('*', '.*').replace('?', '.') + '$')
|
|
1467
|
+
matched_folders = set()
|
|
1468
|
+
for root, _, files in walk(str(custom_path)):
|
|
1469
|
+
if any(regex.fullmatch(file) for file in files):
|
|
1470
|
+
rel_path = Path(root).relative_to(base_path).as_posix()
|
|
1471
|
+
matched_folders.add(rel_path)
|
|
1472
|
+
return list(matched_folders)
|
|
1473
|
+
|
|
1345
1474
|
def getTestNames(
|
|
1346
1475
|
self
|
|
1347
1476
|
) -> List[str]:
|