whichowns 0.0.1__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.
- whichowns-0.0.1/LICENSE +21 -0
- whichowns-0.0.1/PKG-INFO +23 -0
- whichowns-0.0.1/README.md +9 -0
- whichowns-0.0.1/pyproject.toml +72 -0
- whichowns-0.0.1/pyproject.toml.orig +78 -0
- whichowns-0.0.1/src/whichowns/__init__.py +1 -0
- whichowns-0.0.1/src/whichowns/__main__.py +8 -0
- whichowns-0.0.1/src/whichowns/cli.py +43 -0
- whichowns-0.0.1/src/whichowns/locate.py +29 -0
- whichowns-0.0.1/src/whichowns/managers/__init__.py +27 -0
- whichowns-0.0.1/src/whichowns/models.py +35 -0
whichowns-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kyle O'Malley
|
|
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.
|
whichowns-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whichowns
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Find which package manager owns a command.
|
|
5
|
+
Author: Kyle O'Malley
|
|
6
|
+
Author-email: Kyle O'Malley <j.kyle.omalley@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Project-URL: Homepage, https://github.com/jkomalley/whichowns
|
|
11
|
+
Project-URL: Repository, https://github.com/jkomalley/whichowns
|
|
12
|
+
Project-URL: Issues, https://github.com/jkomalley/whichowns/issues
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# whichowns
|
|
16
|
+
|
|
17
|
+
Find which package manager owns a command.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
uv tool install whichowns
|
|
23
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "whichowns"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Find which package manager owns a command."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[[project.authors]]
|
|
12
|
+
name = "Kyle O'Malley"
|
|
13
|
+
email = "j.kyle.omalley@gmail.com"
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/jkomalley/whichowns"
|
|
17
|
+
Repository = "https://github.com/jkomalley/whichowns"
|
|
18
|
+
Issues = "https://github.com/jkomalley/whichowns/issues"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
whichowns = "whichowns.cli:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.13.6,<0.14.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"pre-commit>=4.7.0",
|
|
30
|
+
"pytest>=9.2.0",
|
|
31
|
+
"pytest-cov>=7.1.0",
|
|
32
|
+
"ruff>=0.17.3",
|
|
33
|
+
"ty>=0.0.96",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["ALL"]
|
|
38
|
+
ignore = [
|
|
39
|
+
"D203",
|
|
40
|
+
"D213",
|
|
41
|
+
"COM812",
|
|
42
|
+
"ISC001",
|
|
43
|
+
"FIX002",
|
|
44
|
+
"TD002",
|
|
45
|
+
"TD003",
|
|
46
|
+
"TRY003",
|
|
47
|
+
"CPY001",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint.per-file-ignores]
|
|
51
|
+
"tests/**" = ["S101"]
|
|
52
|
+
|
|
53
|
+
[tool.ty.environment]
|
|
54
|
+
python-platform = "all"
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
strict = true
|
|
59
|
+
markers = ["integration: end-to-end tests with package managers; deselected by default"]
|
|
60
|
+
addopts = [
|
|
61
|
+
"-ra",
|
|
62
|
+
"-m",
|
|
63
|
+
"not integration",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.coverage.run]
|
|
67
|
+
source = ["whichowns"]
|
|
68
|
+
branch = true
|
|
69
|
+
|
|
70
|
+
[tool.coverage.report]
|
|
71
|
+
show_missing = true
|
|
72
|
+
exclude_also = ["if __name__ == .__main__."]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "whichowns"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Find which package manager owns a command."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Kyle O'Malley", email = "j.kyle.omalley@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/jkomalley/whichowns"
|
|
16
|
+
Repository = "https://github.com/jkomalley/whichowns"
|
|
17
|
+
Issues = "https://github.com/jkomalley/whichowns/issues"
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
whichowns = "whichowns.cli:main"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.13.6,<0.14.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pre-commit>=4.7.0",
|
|
29
|
+
"pytest>=9.2.0",
|
|
30
|
+
"pytest-cov>=7.1.0",
|
|
31
|
+
"ruff>=0.17.3",
|
|
32
|
+
"ty>=0.0.96",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["ALL"]
|
|
37
|
+
ignore = [
|
|
38
|
+
"D203", # one-blank-line-before-class (conflicts with D211)
|
|
39
|
+
"D213", # multi-line-summary-second-line (conflicts with D212)
|
|
40
|
+
"COM812", # missing trailing comma (conflicts with formatter)
|
|
41
|
+
"ISC001", # single-line implicit string concatenation (conflicts with formatter)
|
|
42
|
+
"FIX002", # line contains TODO
|
|
43
|
+
"TD002", # missing author in TODO
|
|
44
|
+
"TD003", # missing issue link in TODO
|
|
45
|
+
"TRY003", # avoid specifying long messages outside the exception class
|
|
46
|
+
"CPY001", # missing copyright notice
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint.per-file-ignores]
|
|
50
|
+
"tests/**" = [
|
|
51
|
+
"S101", # use of assert (pytest relies on it)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# ty skips code it considers unreachable on the host platform, so checking on
|
|
55
|
+
# Linux alone would silently skip macOS-only branches.
|
|
56
|
+
[tool.ty.environment]
|
|
57
|
+
python-platform = "all"
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
strict = true
|
|
62
|
+
markers = [
|
|
63
|
+
"integration: end-to-end tests with package managers; deselected by default",
|
|
64
|
+
]
|
|
65
|
+
addopts = [
|
|
66
|
+
"-ra",
|
|
67
|
+
"-m", "not integration",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.coverage.run]
|
|
71
|
+
source = ["whichowns"]
|
|
72
|
+
branch = true
|
|
73
|
+
|
|
74
|
+
[tool.coverage.report]
|
|
75
|
+
show_missing = true
|
|
76
|
+
exclude_also = [
|
|
77
|
+
"if __name__ == .__main__.",
|
|
78
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Find which package manager owns a command."""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Command-line interface: arguments, output and exit codes."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from whichowns.locate import locate
|
|
7
|
+
from whichowns.managers import find_owner
|
|
8
|
+
|
|
9
|
+
PROG = "whichowns"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
"""Report which package manager owns a command.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
argv: Arguments without the program name, or None to use sys.argv.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
0 if the command was found, 1 if not. Usage errors and --help exit
|
|
20
|
+
directly through argparse.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
# Without an explicit prog, `python -m whichowns` shows "__main__.py".
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog=PROG, description="Find which package manager owns a command."
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument("command", help="command name, or path to an executable")
|
|
28
|
+
parser.add_argument("-v", action="store_true", help="also show the resolved path")
|
|
29
|
+
args = parser.parse_args(argv)
|
|
30
|
+
|
|
31
|
+
location = locate(args.command)
|
|
32
|
+
if location is None:
|
|
33
|
+
sys.stderr.write(f"{PROG}: {args.command}: command not found\n")
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
owner = find_owner(location)
|
|
37
|
+
if owner is None:
|
|
38
|
+
sys.stdout.write(f"{args.command}: unmanaged\n")
|
|
39
|
+
else:
|
|
40
|
+
sys.stdout.write(f"{args.command}: {owner.package} ({owner.manager})\n")
|
|
41
|
+
if args.v:
|
|
42
|
+
sys.stdout.write(f"{location.resolved}\n")
|
|
43
|
+
return 0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Find a command on PATH and resolve its symlinks."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from whichowns.models import Location
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def locate(command: str) -> Location | None:
|
|
10
|
+
"""Find a command by name on PATH, or check a path directly.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
command: A command name, or a path to an executable (anything containing "/").
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
The command's location, or None if it isn't found or isn't executable.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
match = shutil.which(command)
|
|
20
|
+
if match is None:
|
|
21
|
+
return None
|
|
22
|
+
# which() can return relative paths, "..", and symlinked directories, but
|
|
23
|
+
# detectors compare against canonical paths, such as those in dpkg's file
|
|
24
|
+
# lists. Only the directory is resolved: the command itself may be a symlink
|
|
25
|
+
# that a package owns. Removing ".." as text would be wrong after a
|
|
26
|
+
# symlinked directory.
|
|
27
|
+
found = Path(match)
|
|
28
|
+
path = found.parent.resolve() / found.name
|
|
29
|
+
return Location(command=command, path=path, resolved=path.resolve())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Detector order and owner lookup."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from whichowns.models import Detector, Location, Owner
|
|
6
|
+
|
|
7
|
+
DETECTORS: tuple[Detector, ...] = ()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def find_owner(
|
|
11
|
+
location: Location, detectors: Iterable[Detector] = DETECTORS
|
|
12
|
+
) -> Owner | None:
|
|
13
|
+
"""Ask each detector in turn which package manager owns a command.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
location: Where the command was found.
|
|
17
|
+
detectors: Detectors to try, in order.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
The first owner a detector reports, or None if none match.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
for detect in detectors:
|
|
24
|
+
owner = detect(location)
|
|
25
|
+
if owner is not None:
|
|
26
|
+
return owner
|
|
27
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Data types shared by command lookup and the package manager detectors."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Location:
|
|
11
|
+
"""Where a command was found.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
command: The argument exactly as given.
|
|
15
|
+
path: The PATH match, or the given path, in its real directory.
|
|
16
|
+
Not followed if it's a symlink itself.
|
|
17
|
+
resolved: The final target after following every symlink.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
command: str
|
|
22
|
+
path: Path
|
|
23
|
+
resolved: Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Owner:
|
|
28
|
+
"""The package manager and package that own a command."""
|
|
29
|
+
|
|
30
|
+
manager: str
|
|
31
|
+
package: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# The `type` statement needs Python 3.12, and whichowns supports 3.11.
|
|
35
|
+
Detector: TypeAlias = Callable[[Location], Owner | None]
|