vcti-error 1.0.4__tar.gz
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.
- vcti_error-1.0.4/LICENSE +8 -0
- vcti_error-1.0.4/PKG-INFO +171 -0
- vcti_error-1.0.4/README.md +156 -0
- vcti_error-1.0.4/pyproject.toml +35 -0
- vcti_error-1.0.4/setup.cfg +4 -0
- vcti_error-1.0.4/src/vcti/error/__init__.py +24 -0
- vcti_error-1.0.4/src/vcti/error/custom_exceptions.py +13 -0
- vcti_error-1.0.4/src/vcti/error/exception_mappings.py +41 -0
- vcti_error-1.0.4/src/vcti/error/exception_type.py +35 -0
- vcti_error-1.0.4/src/vcti/error/py.typed +0 -0
- vcti_error-1.0.4/src/vcti/error/system_error.py +24 -0
- vcti_error-1.0.4/src/vcti/error/utils.py +41 -0
- vcti_error-1.0.4/src/vcti_error.egg-info/PKG-INFO +171 -0
- vcti_error-1.0.4/src/vcti_error.egg-info/SOURCES.txt +17 -0
- vcti_error-1.0.4/src/vcti_error.egg-info/dependency_links.txt +1 -0
- vcti_error-1.0.4/src/vcti_error.egg-info/requires.txt +7 -0
- vcti_error-1.0.4/src/vcti_error.egg-info/top_level.txt +1 -0
- vcti_error-1.0.4/src/vcti_error.egg-info/zip-safe +1 -0
- vcti_error-1.0.4/tests/test_exceptions.py +160 -0
vcti_error-1.0.4/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
|
|
2
|
+
All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
This software is proprietary and confidential. Unauthorized copying,
|
|
5
|
+
distribution, or use of this software, via any medium, is strictly
|
|
6
|
+
prohibited. Access is granted only to authorized VCollab developers
|
|
7
|
+
and individuals explicitly authorized by Visual Collaboration
|
|
8
|
+
Technologies Inc.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-error
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Structured exception handling and error classification for Python
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
Requires-Python: <3.15,>=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Provides-Extra: test
|
|
10
|
+
Requires-Dist: pytest; extra == "test"
|
|
11
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
12
|
+
Provides-Extra: lint
|
|
13
|
+
Requires-Dist: ruff; extra == "lint"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# Error Framework
|
|
17
|
+
|
|
18
|
+
## Purpose
|
|
19
|
+
|
|
20
|
+
VCollab Python scripts are typically invoked by partner programs or
|
|
21
|
+
external applications. When a script finishes, the invoking application
|
|
22
|
+
needs to know what happened — did it succeed, fail due to a missing
|
|
23
|
+
file, a license issue, or something unexpected?
|
|
24
|
+
|
|
25
|
+
In standard subprocess semantics, the **exit code is the only reliable
|
|
26
|
+
contract** between caller and callee. The `vcti-error` package
|
|
27
|
+
formalizes this contract by ensuring that every known exception type
|
|
28
|
+
maps to a stable, unique integer exit code — consistent across all
|
|
29
|
+
VCollab applications.
|
|
30
|
+
|
|
31
|
+
This package has **zero external dependencies**.
|
|
32
|
+
|
|
33
|
+
### When to use this package
|
|
34
|
+
|
|
35
|
+
Use `vcti-error` when your Python script is invoked as a **subprocess** by
|
|
36
|
+
a partner application (e.g., VCollab Presenter, an external scheduler, or
|
|
37
|
+
a CI pipeline) and the caller interprets the process exit code to decide
|
|
38
|
+
what happened. If your code runs inside a larger Python process where
|
|
39
|
+
exceptions propagate normally, you likely don't need this package.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Latest main branch
|
|
47
|
+
pip install git+https://github.com/vcollab/vcti-python-error.git
|
|
48
|
+
|
|
49
|
+
# Specific version
|
|
50
|
+
pip install git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### From a GitHub Release
|
|
54
|
+
|
|
55
|
+
Download the wheel from the
|
|
56
|
+
[Releases](https://github.com/vcollab/vcti-python-error/releases)
|
|
57
|
+
page and install directly:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install vcti_error-1.0.4-py3-none-any.whl
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### In `requirements.txt`
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### In `pyproject.toml` dependencies
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
dependencies = [
|
|
73
|
+
"vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4",
|
|
74
|
+
]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
### The main() pattern
|
|
82
|
+
|
|
83
|
+
All CLI entry points follow this structure:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import logging
|
|
87
|
+
import traceback
|
|
88
|
+
from vcti.error import error_code
|
|
89
|
+
|
|
90
|
+
logger = logging.getLogger(__name__)
|
|
91
|
+
|
|
92
|
+
def main() -> int:
|
|
93
|
+
try:
|
|
94
|
+
cli_app()
|
|
95
|
+
return 0
|
|
96
|
+
except Exception as error:
|
|
97
|
+
logger.error("Error: %s", error)
|
|
98
|
+
traceback.print_exc()
|
|
99
|
+
return error_code(type(error))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `0` — success
|
|
103
|
+
- `!= 0` — failure (category encoded in exit code)
|
|
104
|
+
- `error_code(type(error))` — resolves exception class to exit code
|
|
105
|
+
- Unmapped exceptions fall back to `UNSPECIFIED` (exit code 1)
|
|
106
|
+
|
|
107
|
+
### Look up exit codes
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from vcti.error import error_code
|
|
111
|
+
|
|
112
|
+
error_code(FileNotFoundError) # 2
|
|
113
|
+
error_code(KeyError) # 4
|
|
114
|
+
error_code(RuntimeError) # 1 (UNSPECIFIED fallback)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Resolve exception class by name
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from vcti.error import exception_class
|
|
121
|
+
|
|
122
|
+
exception_class("file_not_found") # FileNotFoundError
|
|
123
|
+
exception_class("variable_undefined") # VariableUndefined
|
|
124
|
+
exception_class("unknown_name") # Exception (fallback)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Create system errors with errno
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from vcti.error import system_error
|
|
131
|
+
|
|
132
|
+
err = system_error(FileNotFoundError, "path/to/file")
|
|
133
|
+
# FileNotFoundError(errno.ENOENT, "No such file or directory", "path/to/file")
|
|
134
|
+
raise err
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Registered Exception Types
|
|
140
|
+
|
|
141
|
+
| Exception type | Exit code | Python class | Description |
|
|
142
|
+
|----------------|-----------|-------------|-------------|
|
|
143
|
+
| `UNSPECIFIED` | 1 | `Exception` | Fallback for unmapped exceptions |
|
|
144
|
+
| `FILE_NOT_FOUND` | 2 | `FileNotFoundError` | Expected file not found |
|
|
145
|
+
| `LICENSE_ERROR` | 3 | `LicenseError` | License validation failed |
|
|
146
|
+
| `KEY_ERROR` | 4 | `KeyError` | Missing key or identifier |
|
|
147
|
+
| `VARIABLE_UNDEFINED` | 5 | `VariableUndefined` | Undefined workflow variable |
|
|
148
|
+
|
|
149
|
+
**Stability guarantee:** Exit code values are part of the external
|
|
150
|
+
contract with partner applications. Once assigned, a value must never
|
|
151
|
+
be changed or reused.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Public API
|
|
156
|
+
|
|
157
|
+
| Function | Purpose |
|
|
158
|
+
|----------|---------|
|
|
159
|
+
| `error_code(exception_class)` | Returns exit code for an exception class |
|
|
160
|
+
| `exception_class(name)` | Resolves exception type name to Python class (case-insensitive) |
|
|
161
|
+
| `system_error(exception_class, *args)` | Creates exception with appropriate errno |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Documentation
|
|
166
|
+
|
|
167
|
+
- [Changelog](CHANGELOG.md) — Release history and versioning policy
|
|
168
|
+
- [Design](docs/design.md) — Architecture decisions and mapping pipeline
|
|
169
|
+
- [Source Guide](docs/source-guide.md) — File descriptions and execution flow traces
|
|
170
|
+
- [Extension Guide](docs/extending.md) — How to add new exception types
|
|
171
|
+
- [API Reference](docs/api.md) — Autodoc for all modules
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Error Framework
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
VCollab Python scripts are typically invoked by partner programs or
|
|
6
|
+
external applications. When a script finishes, the invoking application
|
|
7
|
+
needs to know what happened — did it succeed, fail due to a missing
|
|
8
|
+
file, a license issue, or something unexpected?
|
|
9
|
+
|
|
10
|
+
In standard subprocess semantics, the **exit code is the only reliable
|
|
11
|
+
contract** between caller and callee. The `vcti-error` package
|
|
12
|
+
formalizes this contract by ensuring that every known exception type
|
|
13
|
+
maps to a stable, unique integer exit code — consistent across all
|
|
14
|
+
VCollab applications.
|
|
15
|
+
|
|
16
|
+
This package has **zero external dependencies**.
|
|
17
|
+
|
|
18
|
+
### When to use this package
|
|
19
|
+
|
|
20
|
+
Use `vcti-error` when your Python script is invoked as a **subprocess** by
|
|
21
|
+
a partner application (e.g., VCollab Presenter, an external scheduler, or
|
|
22
|
+
a CI pipeline) and the caller interprets the process exit code to decide
|
|
23
|
+
what happened. If your code runs inside a larger Python process where
|
|
24
|
+
exceptions propagate normally, you likely don't need this package.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Latest main branch
|
|
32
|
+
pip install git+https://github.com/vcollab/vcti-python-error.git
|
|
33
|
+
|
|
34
|
+
# Specific version
|
|
35
|
+
pip install git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### From a GitHub Release
|
|
39
|
+
|
|
40
|
+
Download the wheel from the
|
|
41
|
+
[Releases](https://github.com/vcollab/vcti-python-error/releases)
|
|
42
|
+
page and install directly:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install vcti_error-1.0.4-py3-none-any.whl
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### In `requirements.txt`
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### In `pyproject.toml` dependencies
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
dependencies = [
|
|
58
|
+
"vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4",
|
|
59
|
+
]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
### The main() pattern
|
|
67
|
+
|
|
68
|
+
All CLI entry points follow this structure:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import logging
|
|
72
|
+
import traceback
|
|
73
|
+
from vcti.error import error_code
|
|
74
|
+
|
|
75
|
+
logger = logging.getLogger(__name__)
|
|
76
|
+
|
|
77
|
+
def main() -> int:
|
|
78
|
+
try:
|
|
79
|
+
cli_app()
|
|
80
|
+
return 0
|
|
81
|
+
except Exception as error:
|
|
82
|
+
logger.error("Error: %s", error)
|
|
83
|
+
traceback.print_exc()
|
|
84
|
+
return error_code(type(error))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `0` — success
|
|
88
|
+
- `!= 0` — failure (category encoded in exit code)
|
|
89
|
+
- `error_code(type(error))` — resolves exception class to exit code
|
|
90
|
+
- Unmapped exceptions fall back to `UNSPECIFIED` (exit code 1)
|
|
91
|
+
|
|
92
|
+
### Look up exit codes
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from vcti.error import error_code
|
|
96
|
+
|
|
97
|
+
error_code(FileNotFoundError) # 2
|
|
98
|
+
error_code(KeyError) # 4
|
|
99
|
+
error_code(RuntimeError) # 1 (UNSPECIFIED fallback)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Resolve exception class by name
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from vcti.error import exception_class
|
|
106
|
+
|
|
107
|
+
exception_class("file_not_found") # FileNotFoundError
|
|
108
|
+
exception_class("variable_undefined") # VariableUndefined
|
|
109
|
+
exception_class("unknown_name") # Exception (fallback)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Create system errors with errno
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from vcti.error import system_error
|
|
116
|
+
|
|
117
|
+
err = system_error(FileNotFoundError, "path/to/file")
|
|
118
|
+
# FileNotFoundError(errno.ENOENT, "No such file or directory", "path/to/file")
|
|
119
|
+
raise err
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Registered Exception Types
|
|
125
|
+
|
|
126
|
+
| Exception type | Exit code | Python class | Description |
|
|
127
|
+
|----------------|-----------|-------------|-------------|
|
|
128
|
+
| `UNSPECIFIED` | 1 | `Exception` | Fallback for unmapped exceptions |
|
|
129
|
+
| `FILE_NOT_FOUND` | 2 | `FileNotFoundError` | Expected file not found |
|
|
130
|
+
| `LICENSE_ERROR` | 3 | `LicenseError` | License validation failed |
|
|
131
|
+
| `KEY_ERROR` | 4 | `KeyError` | Missing key or identifier |
|
|
132
|
+
| `VARIABLE_UNDEFINED` | 5 | `VariableUndefined` | Undefined workflow variable |
|
|
133
|
+
|
|
134
|
+
**Stability guarantee:** Exit code values are part of the external
|
|
135
|
+
contract with partner applications. Once assigned, a value must never
|
|
136
|
+
be changed or reused.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Public API
|
|
141
|
+
|
|
142
|
+
| Function | Purpose |
|
|
143
|
+
|----------|---------|
|
|
144
|
+
| `error_code(exception_class)` | Returns exit code for an exception class |
|
|
145
|
+
| `exception_class(name)` | Resolves exception type name to Python class (case-insensitive) |
|
|
146
|
+
| `system_error(exception_class, *args)` | Creates exception with appropriate errno |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Documentation
|
|
151
|
+
|
|
152
|
+
- [Changelog](CHANGELOG.md) — Release history and versioning policy
|
|
153
|
+
- [Design](docs/design.md) — Architecture decisions and mapping pipeline
|
|
154
|
+
- [Source Guide](docs/source-guide.md) — File descriptions and execution flow traces
|
|
155
|
+
- [Extension Guide](docs/extending.md) — How to add new exception types
|
|
156
|
+
- [API Reference](docs/api.md) — Autodoc for all modules
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vcti-error"
|
|
7
|
+
version = "1.0.4"
|
|
8
|
+
description = "Structured exception handling and error classification for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Visual Collaboration Technologies Inc."}
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.12,<3.15"
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
test = ["pytest", "pytest-cov"]
|
|
18
|
+
lint = ["ruff"]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
22
|
+
include = ["vcti.error", "vcti.error.*"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
zip-safe = true
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
addopts = "--cov=vcti.error --cov-report=term-missing --cov-fail-under=95"
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
target-version = "py312"
|
|
32
|
+
line-length = 99
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
select = ["E", "F", "W", "I", "UP"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Error handling framework for custom and system exceptions.
|
|
4
|
+
|
|
5
|
+
This package provides a framework for defining custom exceptions, mapping them to error codes,
|
|
6
|
+
and handling system errors with errno integration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
|
|
11
|
+
__version__ = version("vcti-error")
|
|
12
|
+
|
|
13
|
+
from .custom_exceptions import *
|
|
14
|
+
from .system_error import system_error
|
|
15
|
+
from .utils import *
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"error_code",
|
|
20
|
+
"exception_class",
|
|
21
|
+
"system_error",
|
|
22
|
+
"LicenseError",
|
|
23
|
+
"VariableUndefined",
|
|
24
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Custom exception classes for the VCTI error framework."""
|
|
4
|
+
|
|
5
|
+
__all__ = ["LicenseError", "VariableUndefined"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LicenseError(Exception):
|
|
9
|
+
"""Raised when license validation fails."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VariableUndefined(Exception):
|
|
13
|
+
"""Raised when an undefined workflow variable is referenced."""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Defines mappings between exception types, classes, and system errors."""
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
|
|
8
|
+
from .custom_exceptions import LicenseError, VariableUndefined
|
|
9
|
+
from .exception_type import ExceptionType
|
|
10
|
+
|
|
11
|
+
# The single authoritative mapping: enum → Python class.
|
|
12
|
+
# All other mappings are derived from this.
|
|
13
|
+
EXCEPTION_TYPE_TO_CLASS: MappingProxyType = MappingProxyType(
|
|
14
|
+
{
|
|
15
|
+
ExceptionType.UNSPECIFIED: Exception,
|
|
16
|
+
ExceptionType.FILE_NOT_FOUND: FileNotFoundError,
|
|
17
|
+
ExceptionType.LICENSE_ERROR: LicenseError,
|
|
18
|
+
ExceptionType.KEY_ERROR: KeyError,
|
|
19
|
+
ExceptionType.VARIABLE_UNDEFINED: VariableUndefined,
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Map lowercase exception names to their corresponding exception types
|
|
24
|
+
EXCEPTION_NAME_TO_TYPE: MappingProxyType = MappingProxyType(
|
|
25
|
+
{et.name.lower(): et for et in EXCEPTION_TYPE_TO_CLASS}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Map exception classes to their corresponding error codes
|
|
29
|
+
EXCEPTION_CLASS_TO_ERROR_CODE: MappingProxyType = MappingProxyType(
|
|
30
|
+
{cls: et.value for et, cls in EXCEPTION_TYPE_TO_CLASS.items()}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Map system exceptions to their corresponding errno values
|
|
34
|
+
EXCEPTION_ERRNO_MAPPING: MappingProxyType = MappingProxyType(
|
|
35
|
+
{
|
|
36
|
+
FileNotFoundError: errno.ENOENT,
|
|
37
|
+
FileExistsError: errno.EEXIST,
|
|
38
|
+
IsADirectoryError: errno.EISDIR,
|
|
39
|
+
NotADirectoryError: errno.ENOTDIR,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Defines standard exception types using an enumeration.
|
|
4
|
+
|
|
5
|
+
WARNING: These integer values are part of the external contract with partner
|
|
6
|
+
applications. They are used as process exit codes and must remain stable
|
|
7
|
+
across releases. Never reuse or reassign an existing value. When adding a
|
|
8
|
+
new entry, assign the next available integer. When removing an entry, leave
|
|
9
|
+
a gap — do not renumber remaining entries.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from enum import IntEnum
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExceptionType(IntEnum):
|
|
16
|
+
"""
|
|
17
|
+
Enumeration of exception types.
|
|
18
|
+
|
|
19
|
+
Each value is a stable exit code communicated to partner applications.
|
|
20
|
+
Do not change existing values — they are part of the external contract.
|
|
21
|
+
To add a new entry, assign the next available integer after the last one.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
UNSPECIFIED: Represents an unspecified error.
|
|
25
|
+
FILE_NOT_FOUND: File not found error.
|
|
26
|
+
LICENSE_ERROR: License-related error.
|
|
27
|
+
KEY_ERROR: Key-related error.
|
|
28
|
+
VARIABLE_UNDEFINED: Undefined variable error.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
UNSPECIFIED = 1
|
|
32
|
+
FILE_NOT_FOUND = 2
|
|
33
|
+
LICENSE_ERROR = 3
|
|
34
|
+
KEY_ERROR = 4
|
|
35
|
+
VARIABLE_UNDEFINED = 5
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Provides an interface for handling system errors with errno integration."""
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from .exception_mappings import EXCEPTION_ERRNO_MAPPING
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def system_error(exception_class: type[Exception], *args, **kwargs) -> Exception:
|
|
12
|
+
"""
|
|
13
|
+
Creates an instance of the specified exception class with an appropriate errno value.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
exception_class (type): The exception class to instantiate.
|
|
17
|
+
*args: Additional positional arguments.
|
|
18
|
+
**kwargs: Additional keyword arguments.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Exception: An instance of the specified exception class.
|
|
22
|
+
"""
|
|
23
|
+
errno_value = EXCEPTION_ERRNO_MAPPING.get(exception_class, errno.EINVAL)
|
|
24
|
+
return exception_class(errno_value, os.strerror(errno_value), *args, **kwargs)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Utility functions for retrieving exception details."""
|
|
4
|
+
|
|
5
|
+
__all__ = ["error_code", "exception_class"]
|
|
6
|
+
|
|
7
|
+
from .exception_mappings import (
|
|
8
|
+
EXCEPTION_CLASS_TO_ERROR_CODE,
|
|
9
|
+
EXCEPTION_NAME_TO_TYPE,
|
|
10
|
+
EXCEPTION_TYPE_TO_CLASS,
|
|
11
|
+
)
|
|
12
|
+
from .exception_type import ExceptionType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def exception_class(exception_type_name: str) -> type:
|
|
16
|
+
"""
|
|
17
|
+
Retrieves the exception class corresponding to a given exception type name.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
exception_type_name (str): Name of the exception type (case-insensitive).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
type: Corresponding exception class, or `Exception` if not found.
|
|
24
|
+
"""
|
|
25
|
+
exception_type = EXCEPTION_NAME_TO_TYPE.get(exception_type_name.lower())
|
|
26
|
+
if exception_type is None:
|
|
27
|
+
return Exception
|
|
28
|
+
return EXCEPTION_TYPE_TO_CLASS.get(exception_type, Exception)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def error_code(exception_cls: type) -> int:
|
|
32
|
+
"""
|
|
33
|
+
Retrieves the error code corresponding to a given exception class.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
exception_cls (type): The exception class.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
int: Corresponding error code, or `ExceptionType.UNSPECIFIED.value` if not found.
|
|
40
|
+
"""
|
|
41
|
+
return EXCEPTION_CLASS_TO_ERROR_CODE.get(exception_cls, ExceptionType.UNSPECIFIED.value)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-error
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Structured exception handling and error classification for Python
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
Requires-Python: <3.15,>=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Provides-Extra: test
|
|
10
|
+
Requires-Dist: pytest; extra == "test"
|
|
11
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
12
|
+
Provides-Extra: lint
|
|
13
|
+
Requires-Dist: ruff; extra == "lint"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# Error Framework
|
|
17
|
+
|
|
18
|
+
## Purpose
|
|
19
|
+
|
|
20
|
+
VCollab Python scripts are typically invoked by partner programs or
|
|
21
|
+
external applications. When a script finishes, the invoking application
|
|
22
|
+
needs to know what happened — did it succeed, fail due to a missing
|
|
23
|
+
file, a license issue, or something unexpected?
|
|
24
|
+
|
|
25
|
+
In standard subprocess semantics, the **exit code is the only reliable
|
|
26
|
+
contract** between caller and callee. The `vcti-error` package
|
|
27
|
+
formalizes this contract by ensuring that every known exception type
|
|
28
|
+
maps to a stable, unique integer exit code — consistent across all
|
|
29
|
+
VCollab applications.
|
|
30
|
+
|
|
31
|
+
This package has **zero external dependencies**.
|
|
32
|
+
|
|
33
|
+
### When to use this package
|
|
34
|
+
|
|
35
|
+
Use `vcti-error` when your Python script is invoked as a **subprocess** by
|
|
36
|
+
a partner application (e.g., VCollab Presenter, an external scheduler, or
|
|
37
|
+
a CI pipeline) and the caller interprets the process exit code to decide
|
|
38
|
+
what happened. If your code runs inside a larger Python process where
|
|
39
|
+
exceptions propagate normally, you likely don't need this package.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Latest main branch
|
|
47
|
+
pip install git+https://github.com/vcollab/vcti-python-error.git
|
|
48
|
+
|
|
49
|
+
# Specific version
|
|
50
|
+
pip install git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### From a GitHub Release
|
|
54
|
+
|
|
55
|
+
Download the wheel from the
|
|
56
|
+
[Releases](https://github.com/vcollab/vcti-python-error/releases)
|
|
57
|
+
page and install directly:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install vcti_error-1.0.4-py3-none-any.whl
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### In `requirements.txt`
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### In `pyproject.toml` dependencies
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
dependencies = [
|
|
73
|
+
"vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4",
|
|
74
|
+
]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
### The main() pattern
|
|
82
|
+
|
|
83
|
+
All CLI entry points follow this structure:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import logging
|
|
87
|
+
import traceback
|
|
88
|
+
from vcti.error import error_code
|
|
89
|
+
|
|
90
|
+
logger = logging.getLogger(__name__)
|
|
91
|
+
|
|
92
|
+
def main() -> int:
|
|
93
|
+
try:
|
|
94
|
+
cli_app()
|
|
95
|
+
return 0
|
|
96
|
+
except Exception as error:
|
|
97
|
+
logger.error("Error: %s", error)
|
|
98
|
+
traceback.print_exc()
|
|
99
|
+
return error_code(type(error))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `0` — success
|
|
103
|
+
- `!= 0` — failure (category encoded in exit code)
|
|
104
|
+
- `error_code(type(error))` — resolves exception class to exit code
|
|
105
|
+
- Unmapped exceptions fall back to `UNSPECIFIED` (exit code 1)
|
|
106
|
+
|
|
107
|
+
### Look up exit codes
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from vcti.error import error_code
|
|
111
|
+
|
|
112
|
+
error_code(FileNotFoundError) # 2
|
|
113
|
+
error_code(KeyError) # 4
|
|
114
|
+
error_code(RuntimeError) # 1 (UNSPECIFIED fallback)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Resolve exception class by name
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from vcti.error import exception_class
|
|
121
|
+
|
|
122
|
+
exception_class("file_not_found") # FileNotFoundError
|
|
123
|
+
exception_class("variable_undefined") # VariableUndefined
|
|
124
|
+
exception_class("unknown_name") # Exception (fallback)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Create system errors with errno
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from vcti.error import system_error
|
|
131
|
+
|
|
132
|
+
err = system_error(FileNotFoundError, "path/to/file")
|
|
133
|
+
# FileNotFoundError(errno.ENOENT, "No such file or directory", "path/to/file")
|
|
134
|
+
raise err
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Registered Exception Types
|
|
140
|
+
|
|
141
|
+
| Exception type | Exit code | Python class | Description |
|
|
142
|
+
|----------------|-----------|-------------|-------------|
|
|
143
|
+
| `UNSPECIFIED` | 1 | `Exception` | Fallback for unmapped exceptions |
|
|
144
|
+
| `FILE_NOT_FOUND` | 2 | `FileNotFoundError` | Expected file not found |
|
|
145
|
+
| `LICENSE_ERROR` | 3 | `LicenseError` | License validation failed |
|
|
146
|
+
| `KEY_ERROR` | 4 | `KeyError` | Missing key or identifier |
|
|
147
|
+
| `VARIABLE_UNDEFINED` | 5 | `VariableUndefined` | Undefined workflow variable |
|
|
148
|
+
|
|
149
|
+
**Stability guarantee:** Exit code values are part of the external
|
|
150
|
+
contract with partner applications. Once assigned, a value must never
|
|
151
|
+
be changed or reused.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Public API
|
|
156
|
+
|
|
157
|
+
| Function | Purpose |
|
|
158
|
+
|----------|---------|
|
|
159
|
+
| `error_code(exception_class)` | Returns exit code for an exception class |
|
|
160
|
+
| `exception_class(name)` | Resolves exception type name to Python class (case-insensitive) |
|
|
161
|
+
| `system_error(exception_class, *args)` | Creates exception with appropriate errno |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Documentation
|
|
166
|
+
|
|
167
|
+
- [Changelog](CHANGELOG.md) — Release history and versioning policy
|
|
168
|
+
- [Design](docs/design.md) — Architecture decisions and mapping pipeline
|
|
169
|
+
- [Source Guide](docs/source-guide.md) — File descriptions and execution flow traces
|
|
170
|
+
- [Extension Guide](docs/extending.md) — How to add new exception types
|
|
171
|
+
- [API Reference](docs/api.md) — Autodoc for all modules
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/vcti/error/__init__.py
|
|
5
|
+
src/vcti/error/custom_exceptions.py
|
|
6
|
+
src/vcti/error/exception_mappings.py
|
|
7
|
+
src/vcti/error/exception_type.py
|
|
8
|
+
src/vcti/error/py.typed
|
|
9
|
+
src/vcti/error/system_error.py
|
|
10
|
+
src/vcti/error/utils.py
|
|
11
|
+
src/vcti_error.egg-info/PKG-INFO
|
|
12
|
+
src/vcti_error.egg-info/SOURCES.txt
|
|
13
|
+
src/vcti_error.egg-info/dependency_links.txt
|
|
14
|
+
src/vcti_error.egg-info/requires.txt
|
|
15
|
+
src/vcti_error.egg-info/top_level.txt
|
|
16
|
+
src/vcti_error.egg-info/zip-safe
|
|
17
|
+
tests/test_exceptions.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vcti
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import errno
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import vcti.error
|
|
8
|
+
from vcti.error.exception_mappings import (
|
|
9
|
+
EXCEPTION_CLASS_TO_ERROR_CODE,
|
|
10
|
+
EXCEPTION_ERRNO_MAPPING,
|
|
11
|
+
EXCEPTION_NAME_TO_TYPE,
|
|
12
|
+
EXCEPTION_TYPE_TO_CLASS,
|
|
13
|
+
)
|
|
14
|
+
from vcti.error.exception_type import ExceptionType
|
|
15
|
+
from vcti.error.system_error import system_error
|
|
16
|
+
from vcti.error.utils import error_code, exception_class
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestPublicAPI:
|
|
20
|
+
"""Tests for package public API surface."""
|
|
21
|
+
|
|
22
|
+
def test_version_exists(self):
|
|
23
|
+
"""Ensure __version__ is defined."""
|
|
24
|
+
assert hasattr(vcti.error, "__version__")
|
|
25
|
+
|
|
26
|
+
def test_version_is_valid_semver(self):
|
|
27
|
+
"""Ensure __version__ matches a valid version pattern."""
|
|
28
|
+
assert re.match(r"^\d+\.\d+\.\d+", vcti.error.__version__)
|
|
29
|
+
|
|
30
|
+
def test_all_exports(self):
|
|
31
|
+
"""Ensure __all__ lists exactly the documented public API."""
|
|
32
|
+
expected = {
|
|
33
|
+
"__version__",
|
|
34
|
+
"error_code",
|
|
35
|
+
"exception_class",
|
|
36
|
+
"system_error",
|
|
37
|
+
"LicenseError",
|
|
38
|
+
"VariableUndefined",
|
|
39
|
+
}
|
|
40
|
+
assert set(vcti.error.__all__) == expected
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestMappingImmutability:
|
|
44
|
+
"""Tests that mapping dictionaries are read-only at runtime."""
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize(
|
|
47
|
+
"mapping",
|
|
48
|
+
[
|
|
49
|
+
EXCEPTION_TYPE_TO_CLASS,
|
|
50
|
+
EXCEPTION_NAME_TO_TYPE,
|
|
51
|
+
EXCEPTION_CLASS_TO_ERROR_CODE,
|
|
52
|
+
EXCEPTION_ERRNO_MAPPING,
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
def test_mapping_is_immutable(self, mapping):
|
|
56
|
+
"""Ensure mappings raise TypeError on mutation attempts."""
|
|
57
|
+
with pytest.raises(TypeError):
|
|
58
|
+
mapping["__test__"] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestCustomExceptions:
|
|
62
|
+
"""Tests for custom exception classes."""
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize("exception_cls", EXCEPTION_TYPE_TO_CLASS.values())
|
|
65
|
+
def test_exception_creation(self, exception_cls):
|
|
66
|
+
"""Ensure custom exceptions exist and inherit from `Exception`."""
|
|
67
|
+
assert issubclass(exception_cls, Exception)
|
|
68
|
+
assert isinstance(exception_cls(), exception_cls)
|
|
69
|
+
|
|
70
|
+
def test_custom_exceptions_importable_from_package(self):
|
|
71
|
+
"""Ensure custom exceptions are importable from the public package path."""
|
|
72
|
+
from vcti.error import LicenseError, VariableUndefined
|
|
73
|
+
|
|
74
|
+
assert issubclass(LicenseError, Exception)
|
|
75
|
+
assert issubclass(VariableUndefined, Exception)
|
|
76
|
+
|
|
77
|
+
def test_exception_with_custom_message(self):
|
|
78
|
+
"""Ensure custom exceptions accept custom messages."""
|
|
79
|
+
custom_message = "Custom error message"
|
|
80
|
+
exception_cls = EXCEPTION_TYPE_TO_CLASS[ExceptionType.LICENSE_ERROR]
|
|
81
|
+
ex = exception_cls(custom_message)
|
|
82
|
+
assert str(ex) == custom_message
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestErrorCodeMappings:
|
|
86
|
+
"""Tests for exception-to-error-code mappings."""
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("exception_cls,expected_code", EXCEPTION_CLASS_TO_ERROR_CODE.items())
|
|
89
|
+
def test_error_code_mapping(self, exception_cls, expected_code):
|
|
90
|
+
"""Ensure correct error codes are assigned to exception classes."""
|
|
91
|
+
assert error_code(exception_cls) == expected_code
|
|
92
|
+
|
|
93
|
+
@pytest.mark.parametrize(
|
|
94
|
+
"builtin_exception,expected_code",
|
|
95
|
+
[
|
|
96
|
+
(FileNotFoundError, ExceptionType.FILE_NOT_FOUND.value),
|
|
97
|
+
(KeyError, ExceptionType.KEY_ERROR.value),
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
def test_error_code_for_builtin_exceptions(self, builtin_exception, expected_code):
|
|
101
|
+
"""Ensure built-in exceptions map to correct predefined error codes."""
|
|
102
|
+
assert error_code(builtin_exception) == expected_code
|
|
103
|
+
|
|
104
|
+
def test_unmapped_exception_returns_unspecified(self):
|
|
105
|
+
"""Ensure unmapped exceptions fall back to UNSPECIFIED."""
|
|
106
|
+
assert error_code(RuntimeError) == ExceptionType.UNSPECIFIED.value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestExceptionRetrieval:
|
|
110
|
+
"""Tests for retrieving exceptions by name."""
|
|
111
|
+
|
|
112
|
+
@pytest.mark.parametrize(
|
|
113
|
+
"exception_name,expected_cls",
|
|
114
|
+
[
|
|
115
|
+
("license_error", EXCEPTION_TYPE_TO_CLASS[ExceptionType.LICENSE_ERROR]),
|
|
116
|
+
("variable_undefined", EXCEPTION_TYPE_TO_CLASS[ExceptionType.VARIABLE_UNDEFINED]),
|
|
117
|
+
("file_not_found", FileNotFoundError),
|
|
118
|
+
("key_error", KeyError),
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
def test_exception_class_retrieval(self, exception_name, expected_cls):
|
|
122
|
+
"""Ensure `exception_class()` correctly retrieves exceptions by name."""
|
|
123
|
+
assert exception_class(exception_name) == expected_cls
|
|
124
|
+
|
|
125
|
+
def test_exception_class_not_found(self):
|
|
126
|
+
"""Check that an invalid exception name returns the default Exception class."""
|
|
127
|
+
assert exception_class("non_existent_error") == Exception
|
|
128
|
+
|
|
129
|
+
def test_exception_class_case_insensitive(self):
|
|
130
|
+
"""Ensure exception name lookup is case-insensitive."""
|
|
131
|
+
assert exception_class("FILE_NOT_FOUND") == FileNotFoundError
|
|
132
|
+
assert exception_class("File_Not_Found") == FileNotFoundError
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestSystemErrors:
|
|
136
|
+
"""Tests for system error handling with errno integration."""
|
|
137
|
+
|
|
138
|
+
@pytest.mark.parametrize("exception_cls,expected_errno", EXCEPTION_ERRNO_MAPPING.items())
|
|
139
|
+
def test_system_error_creation(self, exception_cls, expected_errno):
|
|
140
|
+
"""Ensure `system_error()` correctly assigns errno values."""
|
|
141
|
+
ex = system_error(exception_cls)
|
|
142
|
+
assert ex.args[0] == expected_errno
|
|
143
|
+
assert ex.args[1] == os.strerror(expected_errno)
|
|
144
|
+
|
|
145
|
+
def test_system_error_with_additional_args(self):
|
|
146
|
+
"""Ensure additional arguments are passed through to the exception."""
|
|
147
|
+
ex = system_error(FileNotFoundError, "/some/path")
|
|
148
|
+
assert ex.errno == errno.ENOENT
|
|
149
|
+
assert ex.strerror == os.strerror(errno.ENOENT)
|
|
150
|
+
assert ex.filename == "/some/path"
|
|
151
|
+
|
|
152
|
+
def test_system_error_default(self):
|
|
153
|
+
"""Ensure unknown exceptions receive `EINVAL` (Invalid argument) as errno."""
|
|
154
|
+
|
|
155
|
+
class UnknownException(Exception):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
ex = system_error(UnknownException)
|
|
159
|
+
assert ex.args[0] == errno.EINVAL
|
|
160
|
+
assert ex.args[1] == os.strerror(errno.EINVAL)
|