acbox 0.0.0b1__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.
acbox-0.0.0b1/PKG-INFO ADDED
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: acbox
3
+ Version: 0.0.0b1
4
+ Summary: Collection of small tools
5
+ Author-email: Antonio Cavallo <a.cavallo@cavallinux.eu>
6
+ License: MIT
7
+ Project-URL: Source, https://github.com/cav71/acbox
8
+ Project-URL: Issues, https://github.com/cav71/acbox
9
+ Project-URL: Documentation, https://github.com/cav71/acbox
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Provides-Extra: extra
20
+
21
+ ## ACBox my little toolbox
22
+
23
+
24
+ ### Development
25
+
26
+ ```
27
+ python3.13 -m venv .venv
28
+ source .venv/bin/activate
29
+ ```
30
+
31
+ ```
32
+ python -m pip install --upgrade pip
33
+ python -m pip install --group dev
34
+ pre-commit install
35
+ ```
36
+
37
+ Ready.
@@ -0,0 +1,17 @@
1
+ ## ACBox my little toolbox
2
+
3
+
4
+ ### Development
5
+
6
+ ```
7
+ python3.13 -m venv .venv
8
+ source .venv/bin/activate
9
+ ```
10
+
11
+ ```
12
+ python -m pip install --upgrade pip
13
+ python -m pip install --group dev
14
+ pre-commit install
15
+ ```
16
+
17
+ Ready.
@@ -0,0 +1,107 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "acbox"
7
+ version = "0.0.0b1"
8
+ description = "Collection of small tools"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+
12
+ requires-python = ">= 3.9"
13
+
14
+ authors = [
15
+ { name = "Antonio Cavallo", email = "a.cavallo@cavallinux.eu" },
16
+ ]
17
+
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Programming Language :: Python",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ ]
27
+
28
+ dependencies = []
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "build",
33
+ "lxml",
34
+ "mypy",
35
+ "pre-commit",
36
+ "pytest",
37
+ "pytest-asyncio",
38
+ "pytest-cov",
39
+ "pytest-html",
40
+ "ruff",
41
+ "types-python-dateutil",
42
+ ]
43
+
44
+
45
+ [project.optional-dependencies]
46
+ extra = []
47
+
48
+ [project.urls]
49
+ Source = "https://github.com/cav71/acbox"
50
+ Issues = "https://github.com/cav71/acbox"
51
+ Documentation = "https://github.com/cav71/acbox"
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["src"]
55
+ namespaces = true
56
+
57
+ [tool.ruff]
58
+ target-version = "py39"
59
+ line-length = 140
60
+ src = ["src/acbox"]
61
+
62
+ [tool.ruff.format]
63
+ quote-style = "double"
64
+
65
+ [tool.ruff.lint]
66
+ ignore = []
67
+ select = ["F", "E", "W", "Q", "I001"]
68
+
69
+ [tool.ruff.lint.isort]
70
+ known-first-party = ["acbox"]
71
+
72
+
73
+ [tool.mypy]
74
+ disallow_untyped_defs = false
75
+ follow_imports = "normal"
76
+ ignore_missing_imports = true
77
+ pretty = true
78
+ show_column_numbers = true
79
+ show_error_codes = true
80
+ warn_no_return = false
81
+ warn_unused_ignores = true
82
+ exclude = [
83
+ "docs/conf\\.py",
84
+ "^docs/\\.*",
85
+ "^build/\\.*",
86
+ ]
87
+
88
+ [tool.coverage.run]
89
+ branch = true
90
+
91
+ [tool.coverage.paths]
92
+ source = [
93
+ "src/",
94
+ ]
95
+
96
+ [tool.coverage.report]
97
+ exclude_lines = [
98
+ "no cov",
99
+ "if __name__ == .__main__.:",
100
+ "if TYPE_CHECKING:",
101
+ ]
102
+
103
+ [tool.pytest.ini_options]
104
+ markers = [
105
+ "manual: marks tests unsafe for auto-run (eg. better run them manually)",
106
+ ]
107
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import inspect
5
+ import sys
6
+ import types
7
+ from typing import Callable
8
+
9
+ # The return type of add_arguments
10
+
11
+ if sys.version_info >= (3, 10):
12
+ ArgsCallback = Callable[[argparse.Namespace], None | argparse.Namespace]
13
+ else:
14
+ ArgsCallback = Callable
15
+
16
+
17
+ def check_default_constructor(klass: type):
18
+ signature = inspect.signature(klass.__init__) # type: ignore[misc]
19
+ for name, value in signature.parameters.items():
20
+ if name in {"self", "args", "kwargs"}:
21
+ continue
22
+ if value.default is inspect.Signature.empty:
23
+ raise RuntimeError(f"the {klass}() cannot be called without arguments")
24
+
25
+
26
+ class ArgumentParserBase(argparse.ArgumentParser):
27
+ def __init__(self, modules: list[types.ModuleType], *args, **kwargs):
28
+ super().__init__(*args, **kwargs)
29
+ self.modules = modules
30
+ self.callbacks: list[ArgsCallback | None] = []
31
+
32
+ def parse_args(self, args=None, namespace=None):
33
+ options = super().parse_args(args, namespace)
34
+ for name in dir(options):
35
+ if isinstance(getattr(options, name), ArgumentTypeBase):
36
+ fallback = getattr(options, name).value
37
+ setattr(
38
+ options,
39
+ name,
40
+ None if fallback is ArgumentTypeBase._NA else fallback,
41
+ )
42
+ return options
43
+
44
+ def add_argument(self, *args, **kwargs):
45
+ typ = kwargs.get("type")
46
+ obj = None
47
+ if isinstance(typ, type) and issubclass(typ, ArgumentTypeBase):
48
+ check_default_constructor(typ)
49
+ obj = typ()
50
+ if isinstance(typ, ArgumentTypeBase):
51
+ obj = typ
52
+ if obj is not None:
53
+ obj.default = kwargs.get("default", ArgumentTypeBase._NA)
54
+ kwargs["default"] = obj
55
+ kwargs["type"] = obj
56
+ super().add_argument(*args, **kwargs)
57
+
58
+ def error(self, message):
59
+ try:
60
+ super().error(message)
61
+ except SystemExit:
62
+ # gh-121018
63
+ raise argparse.ArgumentError(None, message)
64
+
65
+
66
+ class ArgumentTypeBase:
67
+ class _NA:
68
+ pass
69
+
70
+ def __init__(self, *args, **kwargs):
71
+ self.args = args
72
+ self.kwargs = kwargs
73
+ self._default = self._NA
74
+ self.__name__ = self.__class__.__name__
75
+
76
+ @property
77
+ def default(self):
78
+ return self._default
79
+
80
+ @default.setter
81
+ def default(self, value):
82
+ if value is ArgumentTypeBase._NA:
83
+ self._default = ArgumentTypeBase._NA
84
+ else:
85
+ self._default = self._validate(value)
86
+ return self._default
87
+
88
+ def __call__(self, txt):
89
+ self._value = None
90
+ self._value = self._validate(txt)
91
+ return self
92
+
93
+ @property
94
+ def value(self):
95
+ return getattr(self, "_value", self.default)
96
+
97
+ def _validate(self, value):
98
+ try:
99
+ return self.validate(value)
100
+ except argparse.ArgumentTypeError as exc:
101
+ if not hasattr(self, "_value"):
102
+ raise RuntimeError(f"cannot use {value=} as default: {exc.args[0]}")
103
+ raise
104
+
105
+ def validate(self, txt):
106
+ raise NotImplementedError("need to implement the .validate(self, txt) method")
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def which_n(exe: str | Path) -> list[Path] | None:
8
+ candidates: list[Path] | None = None
9
+ for srcdir in os.environ.get("PATH", "").split(os.pathsep):
10
+ for ext in os.environ.get("PATHEXT", "").split(os.pathsep):
11
+ path = srcdir / Path(exe).with_suffix(ext)
12
+ if not path.exists():
13
+ continue
14
+ if candidates is None:
15
+ candidates = []
16
+ candidates.append(path)
17
+ return candidates
18
+
19
+
20
+ def which(exe: str | Path) -> Path | None:
21
+ candidates = which_n(exe)
22
+ if candidates is None:
23
+ return None
24
+ return candidates[0]
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from types import ModuleType
5
+
6
+
7
+ def loadmod(path: Path | str) -> ModuleType:
8
+ from importlib import util
9
+ from types import ModuleType
10
+ from urllib.parse import urlparse
11
+ from urllib.request import urlopen
12
+
13
+ if urlparse(str(path)).scheme in {"http", "https"}:
14
+ urltxt = str(urlopen(str(path)).read(), encoding="utf-8")
15
+ mod = ModuleType(str(path).rpartition("/")[2])
16
+ exec(urltxt, mod.__dict__)
17
+ return mod
18
+
19
+ spec = util.spec_from_file_location(Path(path).name, Path(path))
20
+ module = util.module_from_spec(spec) # type: ignore
21
+ spec.loader.exec_module(module) # type: ignore
22
+ return module
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: acbox
3
+ Version: 0.0.0b1
4
+ Summary: Collection of small tools
5
+ Author-email: Antonio Cavallo <a.cavallo@cavallinux.eu>
6
+ License: MIT
7
+ Project-URL: Source, https://github.com/cav71/acbox
8
+ Project-URL: Issues, https://github.com/cav71/acbox
9
+ Project-URL: Documentation, https://github.com/cav71/acbox
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Provides-Extra: extra
20
+
21
+ ## ACBox my little toolbox
22
+
23
+
24
+ ### Development
25
+
26
+ ```
27
+ python3.13 -m venv .venv
28
+ source .venv/bin/activate
29
+ ```
30
+
31
+ ```
32
+ python -m pip install --upgrade pip
33
+ python -m pip install --group dev
34
+ pre-commit install
35
+ ```
36
+
37
+ Ready.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/acbox/fileops.py
4
+ src/acbox/utils.py
5
+ src/acbox.egg-info/PKG-INFO
6
+ src/acbox.egg-info/SOURCES.txt
7
+ src/acbox.egg-info/dependency_links.txt
8
+ src/acbox.egg-info/requires.txt
9
+ src/acbox.egg-info/top_level.txt
10
+ src/acbox/cli/__init__.py
11
+ src/acbox/cli/shared.py
12
+ tests/test_cli_shared.py
13
+ tests/test_fileops.py
14
+ tests/test_internals.py
15
+ tests/test_utils.py
@@ -0,0 +1,2 @@
1
+
2
+ [extra]
@@ -0,0 +1 @@
1
+ acbox
@@ -0,0 +1,174 @@
1
+ import argparse
2
+
3
+ import pytest
4
+
5
+ from acbox.cli import shared
6
+
7
+
8
+ class Flag(shared.ArgumentTypeBase):
9
+ def __init__(self, only_odd=False):
10
+ self.only_odd = only_odd
11
+ super().__init__()
12
+
13
+ def validate(self, txt):
14
+ value = int(txt)
15
+ if self.only_odd:
16
+ if (value % 2) == 0:
17
+ raise argparse.ArgumentTypeError(f"non odd value '{txt}'")
18
+ return value
19
+
20
+
21
+ def test_check_default_constructor():
22
+ """verifies the class constructor requires no arguments"""
23
+
24
+ class A:
25
+ def __init__(self, value):
26
+ pass
27
+
28
+ pytest.raises(RuntimeError, shared.check_default_constructor, A)
29
+
30
+ class A:
31
+ def __init__(self, value=1):
32
+ pass
33
+
34
+ assert shared.check_default_constructor(A) is None
35
+
36
+
37
+ def test_add_argument():
38
+ """test all failures mode for add_argument default"""
39
+ p = shared.ArgumentParserBase([], exit_on_error=False)
40
+
41
+ with pytest.raises(ValueError) as e:
42
+ p.add_argument("--flag", type=Flag, default="yyy")
43
+ assert e.value.args[0] == "invalid literal for int() with base 10: 'yyy'"
44
+
45
+ with pytest.raises(RuntimeError) as e:
46
+ p.add_argument("--flag", type=Flag(only_odd=True), default="124")
47
+ assert e.value.args[0] == "cannot use value='124' as default: non odd value '124'"
48
+
49
+ with pytest.raises(RuntimeError) as e:
50
+ p.add_argument("--flag", type=Flag(only_odd=True), default=124)
51
+ assert e.value.args[0] == "cannot use value=124 as default: non odd value '124'"
52
+
53
+ with pytest.raises(RuntimeError) as e:
54
+ p.add_argument("--flag", type=Flag(only_odd=True), default=126)
55
+ assert e.value.args[0] == "cannot use value=126 as default: non odd value '126'"
56
+
57
+ with pytest.raises(RuntimeError) as e:
58
+ p.add_argument("--flag", type=Flag(only_odd=True), default="126")
59
+ assert e.value.args[0] == "cannot use value='126' as default: non odd value '126'"
60
+
61
+ p.add_argument("--flag", type=Flag)
62
+ assert len(action := [a for a in p._actions if isinstance(a.type, shared.ArgumentTypeBase)]) == 1
63
+ assert action[0].default.default is shared.ArgumentTypeBase._NA
64
+
65
+ p.add_argument("--flag1", type=Flag, default="124")
66
+ assert len(actions := [a for a in p._actions if isinstance(a.type, shared.ArgumentTypeBase)]) == 2
67
+ assert actions[1].default.default == 124
68
+
69
+ p.add_argument("--flag2", type=Flag, default=126)
70
+ assert len(actions := [a for a in p._actions if isinstance(a.type, shared.ArgumentTypeBase)]) == 3
71
+ assert actions[2].default.default == 126
72
+
73
+
74
+ def test_special_flag_no_restriction():
75
+ """parse arguments with no default and no constrain on Flag"""
76
+ p = shared.ArgumentParserBase([], exit_on_error=False)
77
+ p.add_argument("--flag", type=Flag)
78
+
79
+ a = p.parse_args([])
80
+ assert a.flag is None
81
+
82
+ a = p.parse_args(
83
+ [
84
+ "--flag",
85
+ "123",
86
+ ]
87
+ )
88
+ assert a.flag == 123
89
+
90
+ a = p.parse_args(
91
+ [
92
+ "--flag",
93
+ "124",
94
+ ]
95
+ )
96
+ assert a.flag == 124
97
+
98
+ with pytest.raises(argparse.ArgumentError) as e:
99
+ p.parse_args(["--flag", "boo"])
100
+ assert e.value.args[-1] == "invalid Flag value: 'boo'"
101
+
102
+ # same as above but with a default fallback
103
+ p = shared.ArgumentParserBase([], exit_on_error=False)
104
+ p.add_argument("--flag", type=Flag, default="42")
105
+
106
+ a = p.parse_args([])
107
+ assert a.flag == 42
108
+
109
+ a = p.parse_args(
110
+ [
111
+ "--flag",
112
+ "123",
113
+ ]
114
+ )
115
+ assert a.flag == 123
116
+
117
+ a = p.parse_args(
118
+ [
119
+ "--flag",
120
+ "124",
121
+ ]
122
+ )
123
+ assert a.flag == 124
124
+
125
+ with pytest.raises(argparse.ArgumentError) as e:
126
+ p.parse_args(["--flag", "boo"])
127
+ assert e.value.args[-1] == "invalid Flag value: 'boo'"
128
+
129
+
130
+ def test_special_flag_with_restriction_no_default():
131
+ p = shared.ArgumentParserBase([], exit_on_error=False)
132
+ p.add_argument("--flag", type=Flag(only_odd=True))
133
+
134
+ a = p.parse_args([])
135
+ assert a.flag is None
136
+
137
+ a = p.parse_args(
138
+ [
139
+ "--flag",
140
+ "123",
141
+ ]
142
+ )
143
+ assert a.flag == 123
144
+
145
+ with pytest.raises(argparse.ArgumentError) as e:
146
+ p.parse_args(["--flag", "122"])
147
+ assert e.value.args[-1] == "non odd value '122'"
148
+
149
+ with pytest.raises(argparse.ArgumentError) as e:
150
+ p.parse_args(["--flag", "boo"])
151
+ assert e.value.args[-1] == "invalid Flag value: 'boo'"
152
+
153
+ # same as above but with a default fallback
154
+ p = shared.ArgumentParserBase([], exit_on_error=False)
155
+ p.add_argument("--flag", type=Flag(only_odd=True), default=43)
156
+
157
+ a = p.parse_args([])
158
+ assert a.flag == 43
159
+
160
+ a = p.parse_args(
161
+ [
162
+ "--flag",
163
+ "123",
164
+ ]
165
+ )
166
+ assert a.flag == 123
167
+
168
+ with pytest.raises(argparse.ArgumentError) as e:
169
+ p.parse_args(["--flag", "122"])
170
+ assert e.value.args[-1] == "non odd value '122'"
171
+
172
+ with pytest.raises(argparse.ArgumentError) as e:
173
+ p.parse_args(["--flag", "boo"])
174
+ assert e.value.args[-1] == "invalid Flag value: 'boo'"
@@ -0,0 +1,18 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ from acbox import fileops
5
+
6
+
7
+ def test_which():
8
+ exe = "cmd" if sys.platform == "win32" else "sh"
9
+
10
+ path = fileops.which(exe)
11
+ assert path
12
+ assert isinstance(path, Path)
13
+
14
+ paths = fileops.which_n(exe)
15
+ assert isinstance(paths, list)
16
+ assert paths[0] == fileops.which(exe)
17
+
18
+ assert fileops.which_n("xwdwxEW") is None
@@ -0,0 +1,21 @@
1
+ def test_script_lookup(resolver):
2
+ path = resolver.lookup("simple-script.py")
3
+ assert path
4
+ assert path.exists()
5
+
6
+
7
+ def test_script_load(resolver):
8
+ data = resolver.load("simple-script.py", "text")
9
+ assert (
10
+ data
11
+ == """
12
+ VALUE = 123
13
+
14
+
15
+ def hello(msg):
16
+ print(f"Hi {msg}")
17
+ """.lstrip()
18
+ )
19
+
20
+ mod = resolver.load("simple-script.py", "mod")
21
+ assert mod.VALUE, 123
@@ -0,0 +1,13 @@
1
+ from acbox import utils
2
+
3
+
4
+ def test_load_mod(resolver):
5
+ path = resolver.lookup("simple-script.py")
6
+
7
+ mod = utils.loadmod(path)
8
+ assert mod.VALUE, 123
9
+
10
+
11
+ def test_load_remote():
12
+ mod = utils.loadmod("https://raw.githubusercontent.com/cav71/acbox/refs/heads/main/tests/test_utils.py")
13
+ assert hasattr(mod, "test_load_remote")