lib.Package: Add and fill version field

Add a version field to the Package class and make query_packages() fill it.
It's very much distribution and package dependent what shape that version
has, so from Package's perspective, it's just a str-typed member.

Further improvements and fixes:

- Make query_packages() accept throw, defaulting to the previous behaviour,
  so a caller that wants to be prepared for a missing package / an empty
  result can pass throw = False.
- Make dpkg's _run() and run_dpkg_query() forward ec and the remaining
  keyword arguments on the sudo path, which dropped them before.

Add unit tests for the file type tree and its StatResult predicates.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v1.4.0
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-10-27 14:59:14 +01:00
commit afbdc2e4b0
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
4 changed files with 104 additions and 32 deletions

View file

@ -256,7 +256,7 @@ class CannedRpmLocal(Local):
elif '-ql' in cmd:
out = '/etc/a/a.conf.jw-tmpl\n/etc/b/b.conf.jw-tmpl\n'
else:
out = 'a|v|p|https://jw.dev/a|m\nb|v|p|https://jw.dev/b|m\n'
out = 'a|v|p|https://jw.dev/a|m|1.0.0-1\nb|v|p|https://jw.dev/b|m|1.0.0-1\n'
return Result(stdout = out.encode(), status = 0)
cl = CannedRpmLocal()

View file

@ -3,29 +3,32 @@ from jw.pkg.lib.Package import Package, meta_tags
# -- parse_spec_str --
p = Package.parse_spec_str(
'jw-core|jw|Jan|https://example.com/jw-core|Jan <jan@example.com>'
'jw-core|jw|Jan|https://example.com/jw-core|Jan <jan@example.com>|1.0.0-1'
)
assert p.name == 'jw-core'
assert p.vendor == 'jw'
assert p.packager == 'Jan'
assert p.url == 'https://example.com/jw-core'
assert p.maintainer == 'Jan <jan@example.com>'
assert p.version == '1.0.0-1'
# An empty field is preserved as an empty string
p = Package.parse_spec_str('name||||')
p = Package.parse_spec_str('name|||||')
assert p.name == 'name'
assert p.vendor == ''
assert p.packager == ''
assert p.url == ''
assert p.maintainer == ''
assert p.version == ''
# A custom delimiter works
p = Package.parse_spec_str('a,b,c,d,e', delimiter = ',')
p = Package.parse_spec_str('a,b,c,d,e,f', delimiter = ',')
assert p.name == 'a'
assert p.maintainer == 'e'
assert p.version == 'f'
# Wrong field counts raise
for spec in ('a', 'a|b', 'a|b|c|d', 'a|b|c|d|e|f'):
for spec in ('a', 'a|b', 'a|b|c', 'a|b|c|d|e', 'a|b|c|d|e|f|g'):
try:
Package.parse_spec_str(spec)
assert False, f'Should have raised for "{spec}"'
@ -35,8 +38,8 @@ for spec in ('a', 'a|b', 'a|b|c|d', 'a|b|c|d|e|f'):
# -- parse_specs_str --
specs = (
'jw-core|jw|Jan|https://example.com/jw-core|Jan\n'
'jw-base|jw|Jan|https://example.com/jw-base|Jan'
'jw-core|jw|Jan|https://example.com/jw-core|Jan|1.0.0-1\n'
'jw-base|jw|Jan|https://example.com/jw-base|Jan|2.0.0-1'
)
packages = Package.parse_specs_str(specs)
assert [p.name for p in packages] == ['jw-core', 'jw-base']
@ -64,17 +67,19 @@ assert ordered == {
'packager': '',
'url': 'https://example.com/jw-core',
'maintainer': 'Jan',
'version': '',
}
# -- __repr__ --
p = Package.parse_spec_str('jw-core|jw|Jan|url|Jan')
p = Package.parse_spec_str('jw-core|jw|Jan|url|Jan|1.0.0-1')
assert repr(p) == (
'name : jw-core\n'
'vendor : jw\n'
'packager : Jan\n'
'url : url\n'
'maintainer : Jan'
'maintainer : Jan\n'
'version : 1.0.0-1'
)
print('All Package tests passed')

View file

@ -0,0 +1,8 @@
TOPDIR = ../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -0,0 +1,46 @@
import stat
from jw.pkg.lib.base import StatResult
from jw.pkg.lib.file_types import (
FT_COMPOUND,
FT_DIR,
FT_REG,
FT_TOP,
FileType,
file_types,
)
# -- The compound hangs under the regular files: it is a file whose
# content is gettable, and only its position in the tree makes
# it listable, too
assert FT_COMPOUND.parent is FT_REG
assert FT_COMPOUND.is_subtype(FT_REG)
assert not FT_COMPOUND.is_subtype(FT_DIR)
assert FT_DIR.is_subtype(FT_TOP)
assert FT_REG.is_subtype(FT_TOP)
def __stat(ftype: FileType) -> StatResult:
return StatResult(0o400, 'u', 'g', 0, 0.0, 0.0, 0.0, ftype = ftype)
# -- The predicates answer from the type: a directory lists, a file
# is gettable, a compound is both
assert __stat(FT_DIR).is_dir and not __stat(FT_DIR).is_gettable
assert not __stat(FT_REG).is_dir and __stat(FT_REG).is_gettable
st = __stat(FT_COMPOUND)
assert st.is_dir and st.is_gettable
# -- A mode-resolved stat answers the same way: the host's file
# types are the tree's file types
st = StatResult(stat.S_IFREG | 0o400, 'u', 'g', 0, 0.0, 0.0, 0.0)
assert st.ftype is FT_REG and st.is_gettable and not st.is_dir
st = StatResult(stat.S_IFDIR | 0o755, 'u', 'g', 0, 0.0, 0.0, 0.0)
assert st.ftype is FT_DIR and st.is_dir and not st.is_gettable
# -- A mode the tree cannot resolve is an error
try:
file_types.from_mode(stat.S_IFLNK | 0o777)
raise AssertionError('expected NotImplementedError')
except NotImplementedError:
pass
print('All file_types tests passed')