Skip to content

Commit f4fde6c

Browse files
authored
Add paths parameter to the API (#4)
1 parent b64e2bc commit f4fde6c

File tree

3 files changed

+66
-16
lines changed

3 files changed

+66
-16
lines changed

docs/changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
## Unreleased
1010

11+
***Added:***
12+
13+
- Add `paths` parameter to the API
14+
1115
## 0.1.0 - 2024-09-22
1216

1317
This is the initial public release.

src/find_exe/_core.py

+44-16
Original file line numberDiff line numberDiff line change
@@ -12,59 +12,87 @@
1212
from collections.abc import Callable
1313

1414

15-
def with_prefix(prefix: str, *, mode: int = os.F_OK | os.X_OK, path: str | None = None) -> list[str]:
15+
def with_prefix(
16+
prefix: str,
17+
*,
18+
paths: list[str] | None = None,
19+
path: str | None = None,
20+
mode: int = os.F_OK | os.X_OK,
21+
) -> list[str]:
1622
"""
1723
Parameters:
1824
prefix: The prefix used for searching.
25+
paths: The list of paths to check. If `None`, the mutually exclusive `path` parameter is used.
26+
path: The PATH to check, with each path separated by [`os.pathsep`][]. If `None`, the PATH environment
27+
variable is used. Mutually exclusive with `paths`.
1928
mode: The file mode used for checking access.
20-
path: The PATH to check. If `None`, the PATH environment variable is used.
2129
2230
Returns:
23-
A list of absolute paths to executables that match the given prefix.
31+
A list of absolute paths to executables that start with the given prefix.
2432
"""
25-
return with_condition(lambda entry: entry.name.startswith(prefix), mode=mode, path=path)
33+
return with_condition(lambda entry: entry.name.startswith(prefix), paths=paths, path=path, mode=mode)
2634

2735

2836
def with_pattern(
29-
pattern: str | re.Pattern[str], *, mode: int = os.F_OK | os.X_OK, path: str | None = None
37+
pattern: str | re.Pattern[str],
38+
*,
39+
paths: list[str] | None = None,
40+
path: str | None = None,
41+
mode: int = os.F_OK | os.X_OK,
3042
) -> list[str]:
3143
"""
3244
Parameters:
3345
pattern: The pattern used for searching.
46+
paths: The list of paths to check. If `None`, the mutually exclusive `path` parameter is used.
47+
path: The PATH to check, with each path separated by [`os.pathsep`][]. If `None`, the PATH environment
48+
variable is used. Mutually exclusive with `paths`.
3449
mode: The file mode used for checking access.
35-
path: The PATH to check. If `None`, the PATH environment variable is used.
3650
3751
Returns:
3852
A list of absolute paths to executables that match the given pattern.
3953
"""
40-
return with_condition(lambda entry: re.search(pattern, entry.name) is not None, mode=mode, path=path)
54+
return with_condition(lambda entry: re.search(pattern, entry.name) is not None, paths=paths, path=path, mode=mode)
4155

4256

4357
def with_condition(
44-
condition: Callable[[os.DirEntry], bool], *, mode: int = os.F_OK | os.X_OK, path: str | None = None
58+
condition: Callable[[os.DirEntry], bool],
59+
*,
60+
paths: list[str] | None = None,
61+
path: str | None = None,
62+
mode: int = os.F_OK | os.X_OK,
4563
) -> list[str]:
4664
"""
4765
Parameters:
4866
condition: The condition used for searching.
67+
paths: The list of paths to check. If `None`, the mutually exclusive `path` parameter is used.
68+
path: The PATH to check, with each path separated by [`os.pathsep`][]. If `None`, the PATH environment
69+
variable is used. Mutually exclusive with `paths`.
4970
mode: The file mode used for checking access.
50-
path: The PATH to check. If `None`, the PATH environment variable is used.
5171
5272
Returns:
53-
A list of absolute paths to executables that match the given pattern.
73+
A list of absolute paths to executables that satisfy the given condition.
5474
"""
55-
if path is None:
75+
76+
search_paths: list[str] = []
77+
if paths is not None:
78+
if path is not None:
79+
message = 'the `paths` and `path` parameters are mutually exclusive'
80+
raise ValueError(message)
81+
82+
search_paths[:] = paths
83+
elif path is not None:
84+
search_paths[:] = path.split(os.pathsep)
85+
else:
5686
path = os.environ.get('PATH', None)
5787
if path is None:
5888
path = path_fallback()
5989

60-
executables: list[str] = []
61-
if not path:
62-
return executables
90+
search_paths[:] = path.split(os.pathsep)
6391

64-
search_paths = path.split(os.pathsep)
92+
executables: list[str] = []
6593
seen = set()
6694
for search_path in search_paths:
67-
if not os.path.isdir(search_path):
95+
if not (search_path and os.path.isdir(search_path)):
6896
continue
6997

7098
norm_path = os.path.normcase(search_path)

tests/test_condition.py

+18
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,21 @@ def test_duplicate_path(monkeypatch) -> None:
4444
exe_name = os.path.basename(sys.executable)
4545
monkeypatch.setenv('PATH', f'{exe_dir}{os.pathsep}{exe_dir}')
4646
assert find_exe.with_condition(lambda entry: entry.name == exe_name) == [sys.executable]
47+
48+
49+
class TestExplicitSearchPaths:
50+
def test_mutual_exclusion(self) -> None:
51+
with pytest.raises(ValueError, match='the `paths` and `path` parameters are mutually exclusive'):
52+
find_exe.with_condition(bool, paths=[], path='')
53+
54+
def test_list(self) -> None:
55+
exe_dir = os.path.dirname(sys.executable)
56+
exe_name = os.path.basename(sys.executable)
57+
paths = [exe_dir, exe_dir]
58+
assert find_exe.with_condition(lambda entry: entry.name == exe_name, paths=paths) == [sys.executable]
59+
60+
def test_string(self) -> None:
61+
exe_dir = os.path.dirname(sys.executable)
62+
exe_name = os.path.basename(sys.executable)
63+
path = f'{exe_dir}{os.pathsep}{exe_dir}'
64+
assert find_exe.with_condition(lambda entry: entry.name == exe_name, path=path) == [sys.executable]

0 commit comments

Comments
 (0)