Skip to content

Commit 43ae630

Browse files
authored
Merge pull request #133 from IntelPython/task/SAT-8496
task: CLI patch methods
2 parents beb2212 + 8128918 commit 43ae630

7 files changed

Lines changed: 557 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [dev] (MM/DD/YYYY)
88

99
### Added
10+
* Added CLI patch management for NumPy random with persistent install/status/uninstall and one-shot command patching via `python -m mkl_random --patch <command>` and `python -m mkl_random --with-numpy-patch <command>` [gh-133](https://github.com/IntelPython/mkl_random/pull/133)
1011

1112
### Changed
1213
* Removed `numpy-base` dependency and `USE_NUMPY_BASE` environment variable from conda recipe [gh-124](https://github.com/IntelPython/mkl_random/pull/124)

README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,45 @@ The list of supported by `mkl_random.RandomState` constructor `brng` keywords is
7070

7171
# Patching Mechanisms
7272

73-
`mkl_random` provides a convenient programmatic patch method to enable MKL-accelerated random operations in NumPy.
73+
`mkl_random` provides convenient patch methods to enable MKL-accelerated
74+
random operations in NumPy with or without modifying your code.
75+
76+
## CLI Quickstart
77+
78+
### Persistent patch (all Python sessions)
79+
80+
```bash
81+
# Install
82+
python -m mkl_random --patch install
83+
84+
# Status (exit code: 0 = installed, 1 = not installed)
85+
python -m mkl_random --patch status
86+
87+
# Remove
88+
python -m mkl_random --patch uninstall
89+
```
90+
91+
### Verify current random backend
92+
93+
```bash
94+
python -c "import numpy; print(f'numpy.random.normal.__module__: {numpy.random.normal.__module__}')"
95+
```
96+
97+
### One-shot patch (single command only)
98+
99+
```bash
100+
# Script
101+
python -m mkl_random --with-numpy-patch my_script.py
102+
103+
# Pytest
104+
python -m mkl_random --with-numpy-patch -m pytest tests/
105+
106+
# One-liner
107+
python -m mkl_random --with-numpy-patch -c "import numpy; print(f\"numpy.random.normal.__module__: {numpy.random.normal.__module__}\")"
108+
109+
# Non-Python command
110+
python -m mkl_random --with-numpy-patch -- <command> [args...]
111+
```
74112

75113
## Programmatic Quickstart
76114

mkl_random/__main__.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright (c) 2026, Intel Corporation
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are met:
5+
#
6+
# * Redistributions of source code must retain the above copyright notice,
7+
# this list of conditions and the following disclaimer.
8+
# * Redistributions in binary form must reproduce the above copyright
9+
# notice, this list of conditions and the following disclaimer in the
10+
# documentation and/or other materials provided with the distribution.
11+
# * Neither the name of Intel Corporation nor the names of its contributors
12+
# may be used to endorse or promote products derived from this software
13+
# without specific prior written permission.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
19+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
26+
"""Command-line interface for mkl_random."""
27+
28+
import argparse
29+
import sys
30+
31+
32+
def main():
33+
"""Entry point for the CLI."""
34+
parser = argparse.ArgumentParser(
35+
prog="python -m mkl_random",
36+
description="MKL-accelerated random generation for NumPy",
37+
)
38+
parser.add_argument(
39+
"-v", "--verbose", action="store_true", help="Enable verbose output"
40+
)
41+
parser.add_argument(
42+
"--patch",
43+
choices=["install", "uninstall", "status"],
44+
help="Manage persistent NumPy random patching",
45+
)
46+
parser.add_argument(
47+
"--with-numpy-patch",
48+
dest="with_numpy_patch",
49+
nargs=argparse.REMAINDER,
50+
help="Run command with temporary NumPy random patch",
51+
)
52+
53+
args = parser.parse_args()
54+
55+
if args.patch:
56+
from mkl_random.patch import (
57+
PatchOperationError,
58+
check_status,
59+
install_patch,
60+
uninstall_patch,
61+
)
62+
63+
try:
64+
if args.patch == "install":
65+
install_patch(verbose=args.verbose)
66+
elif args.patch == "uninstall":
67+
uninstall_patch(verbose=args.verbose)
68+
elif args.patch == "status":
69+
sys.exit(0 if check_status(verbose=args.verbose) else 1)
70+
except PatchOperationError as exc:
71+
print(exc, file=sys.stderr)
72+
sys.exit(1)
73+
74+
elif args.with_numpy_patch is not None:
75+
from mkl_random.with_patch import run_with_numpy_patch
76+
77+
run_with_numpy_patch(args.with_numpy_patch)
78+
79+
else:
80+
parser.print_help()
81+
sys.exit(1)
82+
83+
84+
if __name__ == "__main__":
85+
main()

mkl_random/_patch_startup.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) 2026, Intel Corporation
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are met:
5+
#
6+
# * Redistributions of source code must retain the above copyright notice,
7+
# this list of conditions and the following disclaimer.
8+
# * Redistributions in binary form must reproduce the above copyright
9+
# notice, this list of conditions and the following disclaimer in the
10+
# documentation and/or other materials provided with the distribution.
11+
# * Neither the name of Intel Corporation nor the names of its contributors
12+
# may be used to endorse or promote products derived from this software
13+
# without specific prior written permission.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
19+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
26+
"""Helper module for .pth-based persistent patching with error handling."""
27+
28+
try:
29+
import mkl_random
30+
31+
mkl_random.patch_numpy_random()
32+
except Exception:
33+
pass

mkl_random/patch.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Copyright (c) 2026, Intel Corporation
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are met:
5+
#
6+
# * Redistributions of source code must retain the above copyright notice,
7+
# this list of conditions and the following disclaimer.
8+
# * Redistributions in binary form must reproduce the above copyright
9+
# notice, this list of conditions and the following disclaimer in the
10+
# documentation and/or other materials provided with the distribution.
11+
# * Neither the name of Intel Corporation nor the names of its contributors
12+
# may be used to endorse or promote products derived from this software
13+
# without specific prior written permission.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
19+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24+
25+
"""Persistent patch management for NumPy random submodule."""
26+
27+
import site
28+
import sys
29+
import warnings
30+
from pathlib import Path
31+
32+
33+
class PatchOperationError(RuntimeError):
34+
"""Raised when a persistent patch operation cannot be completed."""
35+
36+
37+
def get_pth_path():
38+
"""Get the path to mkl_random_patch.pth in the appropriate site-packages."""
39+
site_packages = site.getsitepackages()
40+
if site_packages:
41+
target_site = site_packages[0]
42+
else:
43+
target_site = site.getusersitepackages()
44+
return Path(target_site) / "mkl_random_patch.pth"
45+
46+
47+
PTH_CONTENT = """import mkl_random._patch_startup"""
48+
49+
50+
def install_patch(verbose=False):
51+
"""Install persistent NumPy random patch using .pth file."""
52+
pth_path = get_pth_path()
53+
54+
if pth_path.exists():
55+
if verbose:
56+
warnings.warn(
57+
f"Persistent patch already installed at {pth_path}",
58+
UserWarning,
59+
stacklevel=2,
60+
)
61+
return
62+
63+
try:
64+
pth_path.parent.mkdir(parents=True, exist_ok=True)
65+
pth_path.write_text(PTH_CONTENT)
66+
if verbose:
67+
print(f"Persistent patch installed at {pth_path}")
68+
print()
69+
print(
70+
"NumPy random will now use MKL-accelerated implementations in"
71+
)
72+
print("all Python sessions. To disable, run:")
73+
print(" python -m mkl_random --patch uninstall")
74+
except OSError as e:
75+
raise PatchOperationError(
76+
f"Error installing patch at {pth_path}: {e}\n\n"
77+
"You may need to run with appropriate permissions or install to "
78+
"a user site-packages directory."
79+
) from e
80+
81+
82+
def uninstall_patch(verbose=False):
83+
"""Uninstall persistent NumPy random patch."""
84+
pth_path = get_pth_path()
85+
86+
if not pth_path.exists():
87+
if verbose:
88+
print("No persistent patch found.")
89+
return
90+
91+
try:
92+
pth_path.unlink()
93+
if verbose:
94+
print(f"Persistent patch removed from {pth_path}")
95+
print()
96+
print("NumPy random will now use the default implementations.")
97+
except OSError as e:
98+
raise PatchOperationError(
99+
f"Error removing patch at {pth_path}: {e}"
100+
) from e
101+
102+
103+
def check_status(verbose=False):
104+
"""Check if persistent patch is installed."""
105+
pth_path = get_pth_path()
106+
107+
if pth_path.exists():
108+
if verbose:
109+
print(f"Persistent patch is installed at {pth_path}")
110+
print()
111+
print(
112+
"NumPy random is configured to use MKL-accelerated "
113+
"implementations."
114+
)
115+
return True
116+
else:
117+
if verbose:
118+
print("No persistent patch installed")
119+
print()
120+
print("To enable MKL-accelerated NumPy random globally, run:")
121+
print(" python -m mkl_random --patch install")
122+
return False
123+
124+
125+
if __name__ == "__main__":
126+
if len(sys.argv) < 2:
127+
print("Usage: python -m mkl_random.patch <install|uninstall|status>")
128+
sys.exit(1)
129+
130+
command = sys.argv[1]
131+
try:
132+
if command == "install":
133+
install_patch(verbose=True)
134+
elif command == "uninstall":
135+
uninstall_patch(verbose=True)
136+
elif command == "status":
137+
sys.exit(0 if check_status(verbose=True) else 1)
138+
else:
139+
print(f"Unknown command: {command}")
140+
sys.exit(1)
141+
except PatchOperationError as exc:
142+
print(exc, file=sys.stderr)
143+
sys.exit(1)

0 commit comments

Comments
 (0)