pyneedy 1.0.0__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.
- pyneedy-1.0.0/.gitignore +25 -0
- pyneedy-1.0.0/LICENSE +21 -0
- pyneedy-1.0.0/PKG-INFO +118 -0
- pyneedy-1.0.0/README.md +99 -0
- pyneedy-1.0.0/needy/__init__.py +2 -0
- pyneedy-1.0.0/needy/callsite.py +32 -0
- pyneedy-1.0.0/needy/factory.py +44 -0
- pyneedy-1.0.0/needy/modules.py +215 -0
- pyneedy-1.0.0/needy/py.typed +0 -0
- pyneedy-1.0.0/pyproject.toml +94 -0
pyneedy-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
.coverage
|
|
9
|
+
|
|
10
|
+
# Environment files
|
|
11
|
+
.env
|
|
12
|
+
.env.*
|
|
13
|
+
|
|
14
|
+
# Virtual environments, caches, and IDE settings
|
|
15
|
+
.venv
|
|
16
|
+
.vscode
|
|
17
|
+
.pytest_cache
|
|
18
|
+
.ruff_cache
|
|
19
|
+
|
|
20
|
+
# Documentation build
|
|
21
|
+
site/
|
|
22
|
+
|
|
23
|
+
# Agent Files
|
|
24
|
+
CLAUDE.md
|
|
25
|
+
.claude/
|
pyneedy-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zurab Mujirishvili
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyneedy-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyneedy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A minimal library for writing tests with optional dependencies.
|
|
5
|
+
Project-URL: Homepage, https://pytooling.gitlab.io/needy/
|
|
6
|
+
Project-URL: Repository, https://gitlab.com/pytooling/needy
|
|
7
|
+
Project-URL: Documentation, https://pytooling.gitlab.io/needy/
|
|
8
|
+
Project-URL: Bug Tracker, https://gitlab.com/pytooling/needy/-/issues
|
|
9
|
+
Project-URL: Changelog, https://gitlab.com/pytooling/needy/-/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Zurab Mujirishvili <zurab.mu@gmail.com>
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Needy
|
|
21
|
+
|
|
22
|
+
A minimal library for writing tests with optional dependencies.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Python 3.11+ is required.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install pyneedy
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
Define "probes" for the optional dependencies you want to use, e.g. JAX.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
class check:
|
|
38
|
+
@staticmethod
|
|
39
|
+
def jax() -> None:
|
|
40
|
+
# A probe just tries to import the optional dependency.
|
|
41
|
+
import jax as jax
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then wrap imports related to the optional dependency in a `needs.modules(...)` scope like this:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from needy import needs
|
|
48
|
+
|
|
49
|
+
from pytest import mark
|
|
50
|
+
|
|
51
|
+
with needs.modules(check.jax):
|
|
52
|
+
import jax.numpy as jnp
|
|
53
|
+
|
|
54
|
+
@mark.parametrize(
|
|
55
|
+
["array", "expected_sum"],
|
|
56
|
+
[
|
|
57
|
+
(
|
|
58
|
+
array := jnp.array([1.0, 2.0]),
|
|
59
|
+
expected_sum := 3.0
|
|
60
|
+
),
|
|
61
|
+
(
|
|
62
|
+
array := jnp.array([]),
|
|
63
|
+
expected_sum := 0.0
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
def test_that_a_sum_is_computed(array, expected_sum) -> None:
|
|
68
|
+
assert array.sum() == expected_sum
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If `jax` is installed, the imports inside the `with` block resolve normally. If it isn't, every missing import inside the block is replaced with a proxy, and only the first real use of the missing modules will raise an error.
|
|
72
|
+
|
|
73
|
+
See the [docs](https://pytooling.gitlab.io/needy/) for more.
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
### Prerequisites
|
|
78
|
+
|
|
79
|
+
- Python >= 3.11
|
|
80
|
+
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
|
81
|
+
- [just](https://github.com/casey/just) (optional, for convenience)
|
|
82
|
+
|
|
83
|
+
### Getting Started
|
|
84
|
+
|
|
85
|
+
1. Clone the repository:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://gitlab.com/pytooling/needy.git
|
|
89
|
+
cd needy
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
1. Create a virtual environment and install dependencies:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
uv sync
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
1. Install pre-commit hooks:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
uv run pre-commit install
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Running Checks
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
just check
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or manually:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv run ruff check --fix && uv run ruff format && uv run pyright && uv run pytest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
pyneedy-1.0.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Needy
|
|
2
|
+
|
|
3
|
+
A minimal library for writing tests with optional dependencies.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Python 3.11+ is required.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pyneedy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
Define "probes" for the optional dependencies you want to use, e.g. JAX.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
class check:
|
|
19
|
+
@staticmethod
|
|
20
|
+
def jax() -> None:
|
|
21
|
+
# A probe just tries to import the optional dependency.
|
|
22
|
+
import jax as jax
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then wrap imports related to the optional dependency in a `needs.modules(...)` scope like this:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from needy import needs
|
|
29
|
+
|
|
30
|
+
from pytest import mark
|
|
31
|
+
|
|
32
|
+
with needs.modules(check.jax):
|
|
33
|
+
import jax.numpy as jnp
|
|
34
|
+
|
|
35
|
+
@mark.parametrize(
|
|
36
|
+
["array", "expected_sum"],
|
|
37
|
+
[
|
|
38
|
+
(
|
|
39
|
+
array := jnp.array([1.0, 2.0]),
|
|
40
|
+
expected_sum := 3.0
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
array := jnp.array([]),
|
|
44
|
+
expected_sum := 0.0
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
def test_that_a_sum_is_computed(array, expected_sum) -> None:
|
|
49
|
+
assert array.sum() == expected_sum
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If `jax` is installed, the imports inside the `with` block resolve normally. If it isn't, every missing import inside the block is replaced with a proxy, and only the first real use of the missing modules will raise an error.
|
|
53
|
+
|
|
54
|
+
See the [docs](https://pytooling.gitlab.io/needy/) for more.
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
### Prerequisites
|
|
59
|
+
|
|
60
|
+
- Python >= 3.11
|
|
61
|
+
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
|
62
|
+
- [just](https://github.com/casey/just) (optional, for convenience)
|
|
63
|
+
|
|
64
|
+
### Getting Started
|
|
65
|
+
|
|
66
|
+
1. Clone the repository:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://gitlab.com/pytooling/needy.git
|
|
70
|
+
cd needy
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
1. Create a virtual environment and install dependencies:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv sync
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
1. Install pre-commit hooks:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv run pre-commit install
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Running Checks
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
just check
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or manually:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv run ruff check --fix && uv run ruff format && uv run pyright && uv run pytest
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from types import FrameType
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
PACKAGE: Final = __name__.split(".")[0]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _is_internal_frame(frame: FrameType) -> bool:
|
|
10
|
+
return (name := frame.f_globals.get("__name__", "")) == PACKAGE or name.startswith( # pyright: ignore[reportAny]
|
|
11
|
+
f"{PACKAGE}."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CallSite:
|
|
17
|
+
file: str
|
|
18
|
+
line: int
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def capture(cls) -> "CallSite":
|
|
22
|
+
try:
|
|
23
|
+
frame = sys._getframe(1) # pyright: ignore[reportPrivateUsage]
|
|
24
|
+
while frame is not None:
|
|
25
|
+
if not _is_internal_frame(frame):
|
|
26
|
+
return cls(frame.f_code.co_filename, frame.f_lineno)
|
|
27
|
+
|
|
28
|
+
frame = frame.f_back
|
|
29
|
+
except (AttributeError, ValueError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
return cls("<unknown>", 0)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from needy.modules import ModuleNeeds, ModuleProbe
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class needs:
|
|
5
|
+
"""Entry point for declaring optional module dependencies.
|
|
6
|
+
|
|
7
|
+
The class is a namespace, not something you instantiate. Use its
|
|
8
|
+
classmethods (currently just `modules`) to open a scope in which
|
|
9
|
+
missing optional dependencies are tolerated.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def modules(*probes: ModuleProbe) -> ModuleNeeds:
|
|
14
|
+
"""Open a scope where missing imports are replaced with proxies.
|
|
15
|
+
|
|
16
|
+
Each probe is called once on entry. If *any* probe raises
|
|
17
|
+
`ModuleNotFoundError`, the import machinery is patched for
|
|
18
|
+
the duration of the `with` block so that imports of missing
|
|
19
|
+
modules return a `Proxy` instead of
|
|
20
|
+
raising. The original import machinery is restored on exit.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
*probes: Zero or more callables that each attempt to import
|
|
24
|
+
the optional dependencies the block requires.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A context manager that installs and removes the import patch.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
def jax_probe() -> None:
|
|
32
|
+
import jax as jax
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
with needs.modules(jax_probe):
|
|
36
|
+
import jax.numpy as jnp
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def combined() -> None:
|
|
41
|
+
for probe in probes:
|
|
42
|
+
probe()
|
|
43
|
+
|
|
44
|
+
return ModuleNeeds.checking(combined)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Callable, NoReturn, Protocol, final, override
|
|
5
|
+
|
|
6
|
+
from needy.callsite import CallSite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UnknownModuleUsageError(ModuleNotFoundError):
|
|
10
|
+
"""Raised when a proxied (missing) module is used in a way that
|
|
11
|
+
cannot be deferred any further.
|
|
12
|
+
|
|
13
|
+
This is a subclass of `ModuleNotFoundError`, so existing
|
|
14
|
+
`except ModuleNotFoundError:` handlers will still catch it. The
|
|
15
|
+
extra attributes record where the failure originated so the
|
|
16
|
+
operation that triggered it can be traced back to the original
|
|
17
|
+
import.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
module_name: Dotted name of the proxy at the point of failure,
|
|
21
|
+
including any attribute accesses (e.g. `"jax.numpy.asarray"`).
|
|
22
|
+
operation: Name of the dunder method that forced materialization
|
|
23
|
+
(e.g. `"__bool__"`, `"__eq__"`).
|
|
24
|
+
source_file: File where the missing module was originally imported.
|
|
25
|
+
source_line: Line where the missing module was originally imported.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
module_name: str
|
|
29
|
+
operation: str
|
|
30
|
+
source_file: str
|
|
31
|
+
source_line: int
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
module_name: str,
|
|
37
|
+
operation: str,
|
|
38
|
+
source_file: str,
|
|
39
|
+
source_line: int,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.module_name = module_name
|
|
42
|
+
self.operation = operation
|
|
43
|
+
self.source_file = source_file
|
|
44
|
+
self.source_line = source_line
|
|
45
|
+
root = module_name.split(".")[0]
|
|
46
|
+
super().__init__(
|
|
47
|
+
f"Attempted to use {module_name!r} (operation: {operation}), "
|
|
48
|
+
+ f"but module {root!r} was not installed when imported at "
|
|
49
|
+
+ f"{source_file}:{source_line}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ModuleProbe(Protocol):
|
|
54
|
+
"""Callable that tries to import one or more optional dependencies.
|
|
55
|
+
|
|
56
|
+
A probe takes no arguments and returns nothing. It should attempt
|
|
57
|
+
the imports it represents; if any of them raise `ModuleNotFoundError`,
|
|
58
|
+
the surrounding `needs.modules(...)` scope activates its proxy
|
|
59
|
+
patch.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __call__(self) -> None:
|
|
63
|
+
"""Attempts to import a module."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@final
|
|
68
|
+
class Proxy:
|
|
69
|
+
__slots__: tuple[str, ...] = ("_name_", "_site_")
|
|
70
|
+
_name_: str
|
|
71
|
+
_site_: CallSite
|
|
72
|
+
|
|
73
|
+
def __init__(self, name: str, site: CallSite) -> None:
|
|
74
|
+
self._name_ = name
|
|
75
|
+
self._site_ = site
|
|
76
|
+
|
|
77
|
+
def __getattr__(self, attr: str) -> "Proxy":
|
|
78
|
+
return Proxy(f"{self._name_}.{attr}", self._site_)
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
return f"<Proxy: {self._name_}>"
|
|
83
|
+
|
|
84
|
+
def __bool__(self) -> bool:
|
|
85
|
+
self._fail_("__bool__")
|
|
86
|
+
|
|
87
|
+
def __len__(self) -> int:
|
|
88
|
+
self._fail_("__len__")
|
|
89
|
+
|
|
90
|
+
def __iter__(self) -> "Proxy":
|
|
91
|
+
self._fail_("__iter__")
|
|
92
|
+
|
|
93
|
+
def __next__(self) -> object:
|
|
94
|
+
self._fail_("__next__")
|
|
95
|
+
|
|
96
|
+
def __int__(self) -> int:
|
|
97
|
+
self._fail_("__int__")
|
|
98
|
+
|
|
99
|
+
def __float__(self) -> float:
|
|
100
|
+
self._fail_("__float__")
|
|
101
|
+
|
|
102
|
+
def __index__(self) -> int:
|
|
103
|
+
self._fail_("__index__")
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
def __eq__(self, other: object) -> bool:
|
|
107
|
+
self._fail_("__eq__")
|
|
108
|
+
|
|
109
|
+
@override
|
|
110
|
+
def __ne__(self, other: object) -> bool:
|
|
111
|
+
self._fail_("__ne__")
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
def __hash__(self) -> int:
|
|
115
|
+
self._fail_("__hash__")
|
|
116
|
+
|
|
117
|
+
def __lt__(self, other: object) -> bool:
|
|
118
|
+
self._fail_("__lt__")
|
|
119
|
+
|
|
120
|
+
def __le__(self, other: object) -> bool:
|
|
121
|
+
self._fail_("__le__")
|
|
122
|
+
|
|
123
|
+
def __gt__(self, other: object) -> bool:
|
|
124
|
+
self._fail_("__gt__")
|
|
125
|
+
|
|
126
|
+
def __ge__(self, other: object) -> bool:
|
|
127
|
+
self._fail_("__ge__")
|
|
128
|
+
|
|
129
|
+
def _chain_(self, *args: object, **kwargs: object) -> "Proxy": # pyright: ignore[reportUnusedParameter]
|
|
130
|
+
return Proxy(self._name_, self._site_)
|
|
131
|
+
|
|
132
|
+
__call__ = _chain_
|
|
133
|
+
__getitem__ = _chain_
|
|
134
|
+
|
|
135
|
+
__add__ = __radd__ = _chain_
|
|
136
|
+
__sub__ = __rsub__ = _chain_
|
|
137
|
+
__mul__ = __rmul__ = _chain_
|
|
138
|
+
__truediv__ = __rtruediv__ = _chain_
|
|
139
|
+
__floordiv__ = __rfloordiv__ = _chain_
|
|
140
|
+
__mod__ = __rmod__ = _chain_
|
|
141
|
+
__pow__ = __rpow__ = _chain_
|
|
142
|
+
__matmul__ = __rmatmul__ = _chain_
|
|
143
|
+
|
|
144
|
+
__and__ = __rand__ = _chain_
|
|
145
|
+
__or__ = __ror__ = _chain_
|
|
146
|
+
__xor__ = __rxor__ = _chain_
|
|
147
|
+
__lshift__ = __rlshift__ = _chain_
|
|
148
|
+
__rshift__ = __rrshift__ = _chain_
|
|
149
|
+
|
|
150
|
+
__neg__ = __pos__ = __invert__ = __abs__ = _chain_
|
|
151
|
+
|
|
152
|
+
def _fail_(self, operation: str) -> NoReturn:
|
|
153
|
+
raise UnknownModuleUsageError(
|
|
154
|
+
module_name=self._name_,
|
|
155
|
+
operation=operation,
|
|
156
|
+
source_file=self._site_.file,
|
|
157
|
+
source_line=self._site_.line,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class ModuleNeeds:
|
|
163
|
+
"""Context manager returned by `needs.modules(...)`.
|
|
164
|
+
|
|
165
|
+
On entry the probe is invoked. If it raises `ModuleNotFoundError`,
|
|
166
|
+
`builtins.__import__` is replaced with a patched version that
|
|
167
|
+
returns a `Proxy` for any module that fails to import. On exit the
|
|
168
|
+
original `__import__` is restored.
|
|
169
|
+
|
|
170
|
+
You normally do not construct this directly; use
|
|
171
|
+
`needs.modules(...)`.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
probe: ModuleProbe
|
|
175
|
+
original_import: Callable[..., object] | None
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def checking(probe: ModuleProbe) -> "ModuleNeeds":
|
|
179
|
+
"""Build a `ModuleNeeds` from a probe."""
|
|
180
|
+
return ModuleNeeds(probe, original_import=None)
|
|
181
|
+
|
|
182
|
+
def __enter__(self) -> None:
|
|
183
|
+
try:
|
|
184
|
+
self.probe()
|
|
185
|
+
except ModuleNotFoundError:
|
|
186
|
+
self.original_import = builtins.__import__
|
|
187
|
+
builtins.__import__ = self._patched_import
|
|
188
|
+
|
|
189
|
+
def _patched_import(
|
|
190
|
+
self,
|
|
191
|
+
name: str,
|
|
192
|
+
globals: dict[str, object] | None = None,
|
|
193
|
+
locals: dict[str, object] | None = None,
|
|
194
|
+
fromlist: tuple[str, ...] = (),
|
|
195
|
+
level: int = 0,
|
|
196
|
+
) -> object:
|
|
197
|
+
assert self.original_import is not None, (
|
|
198
|
+
"The patched import function should not be called, when a "
|
|
199
|
+
"reference to the original import function is not stored."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
return self.original_import(name, globals, locals, fromlist, level)
|
|
204
|
+
except ModuleNotFoundError:
|
|
205
|
+
return Proxy(name.split(".")[0], CallSite.capture())
|
|
206
|
+
|
|
207
|
+
def __exit__(
|
|
208
|
+
self,
|
|
209
|
+
exc_type: type[BaseException] | None,
|
|
210
|
+
exc_val: BaseException | None,
|
|
211
|
+
exc_tb: TracebackType | None,
|
|
212
|
+
) -> None:
|
|
213
|
+
if self.original_import is not None:
|
|
214
|
+
builtins.__import__ = self.original_import
|
|
215
|
+
self.original_import = None
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyneedy"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "A minimal library for writing tests with optional dependencies."
|
|
5
|
+
authors = [{ name = "Zurab Mujirishvili", email = "zurab.mu@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
license-files = ["LICEN[CS]E*"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Typing :: Typed",
|
|
15
|
+
]
|
|
16
|
+
dependencies = []
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://pytooling.gitlab.io/needy/"
|
|
20
|
+
Repository = "https://gitlab.com/pytooling/needy"
|
|
21
|
+
Documentation = "https://pytooling.gitlab.io/needy/"
|
|
22
|
+
"Bug Tracker" = "https://gitlab.com/pytooling/needy/-/issues"
|
|
23
|
+
Changelog = "https://gitlab.com/pytooling/needy/-/blob/main/CHANGELOG.md"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"pyright>=1.1.409",
|
|
28
|
+
"ruff>=0.15.4",
|
|
29
|
+
"pre-commit>=4.5.1",
|
|
30
|
+
]
|
|
31
|
+
test = [
|
|
32
|
+
"pytest>=9.0.3",
|
|
33
|
+
"pytest-cov>=7.1.0",
|
|
34
|
+
"pytest-subtests>=0.15.0",
|
|
35
|
+
"pytest-watcher>=0.6.3",
|
|
36
|
+
]
|
|
37
|
+
doc = [
|
|
38
|
+
"zensical>=0.0.41",
|
|
39
|
+
"mkdocstrings[python]>=1.0.4",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.uv]
|
|
43
|
+
default-groups = ["dev", "test", "doc"]
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
47
|
+
build-backend = "hatchling.build"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version]
|
|
50
|
+
source = "vcs"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.version.raw-options]
|
|
53
|
+
local_scheme = "no-local-version"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.sdist]
|
|
56
|
+
include = [
|
|
57
|
+
"needy/",
|
|
58
|
+
"LICENSE",
|
|
59
|
+
"README.md",
|
|
60
|
+
"pyproject.toml",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["needy"]
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
pythonpath = ["."]
|
|
69
|
+
|
|
70
|
+
[tool.pyright]
|
|
71
|
+
include = ["needy", "tests"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint]
|
|
74
|
+
extend-select = ["I", "RUF018", "D"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint.per-file-ignores]
|
|
77
|
+
"*" = [
|
|
78
|
+
"D10", "D205"
|
|
79
|
+
]
|
|
80
|
+
"tests/**" = [
|
|
81
|
+
"F841"
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint.isort]
|
|
85
|
+
combine-as-imports = true
|
|
86
|
+
known-first-party = ["needy"]
|
|
87
|
+
known-local-folder = ["tests"]
|
|
88
|
+
section-order = ["future", "standard-library", "first-party", "third-party", "local-folder"]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.pydocstyle]
|
|
91
|
+
convention = "google"
|
|
92
|
+
|
|
93
|
+
[tool.ruff.format]
|
|
94
|
+
docstring-code-format = true
|