Enable more linters (round 4)

0 views
Skip to first unread message

Rob Browning

unread,
Mar 14, 2026, 4:13:19 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Yet another round of assorted linters and the associated fixes.
Thanks to Johannes Berg for some review.

Pushed to main.

--
Rob Browning
rlb @defaultvalue.org and @debian.org
GPG as of 2011-07-10 E6A9 DA3C C9FD 1FF8 C676 D2C4 C0F0 39E9 ED1B 597A
GPG as of 2002-11-03 14DD 432F AE39 534D B592 F9A0 25C8 D377 8C7E 73A4

Rob Browning

unread,
Mar 14, 2026, 4:13:19 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Migrate LsOpts and GetOpts to dataclasses, but just leave index
entries alone since they're scattered and will completely change in a
way that addresses the linter complaints if we decide to adopt the
index rework anytime soon.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
.pylintrc | 1 +
lib/bup/cmd/fuse.py | 2 +-
lib/bup/cmd/get.py | 26 +++++++++++++++-----------
lib/bup/cmd/web.py | 3 ++-
lib/bup/index.py | 3 +++
lib/bup/ls.py | 41 +++++++++++++++++++++++++----------------
test/int/test_bloom.py | 9 +++++----
7 files changed, 52 insertions(+), 33 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index a3b1273b..90bb7a6f 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -17,6 +17,7 @@ enable=
arguments-renamed,
assert-on-string-literal,
assignment-from-no-return,
+ attribute-defined-outside-init,
bad-classmethod-argument,
bad-indentation,
bare-except,
diff --git a/lib/bup/cmd/fuse.py b/lib/bup/cmd/fuse.py
index 83270ffb..70ed725b 100644
--- a/lib/bup/cmd/fuse.py
+++ b/lib/bup/cmd/fuse.py
@@ -47,6 +47,7 @@ from bup.repo import LocalRepo
class BupFs(fuse.Fuse):
def __init__(self, repo, verbose=0, fake_metadata=False):
fuse.Fuse.__init__(self)
+ self.multithreaded = False
self.repo = repo
self.verbose = verbose
self.fake_metadata = fake_metadata
@@ -165,7 +166,6 @@ def main(argv):
f.fuse_args.add('debug')
if opt.foreground:
f.fuse_args.setmod('foreground')
- f.multithreaded = False
if opt.allow_other:
f.fuse_args.add('allow_other')
f.main()
diff --git a/lib/bup/cmd/get.py b/lib/bup/cmd/get.py
index 82d9fa02..c1950b84 100644
--- a/lib/bup/cmd/get.py
+++ b/lib/bup/cmd/get.py
@@ -1,7 +1,7 @@

from binascii import hexlify, unhexlify
from collections import namedtuple
-from dataclasses import replace as dcreplace
+from dataclasses import field, replace as dcreplace
from re import Pattern
from stat import S_ISDIR
from textwrap import fill
@@ -155,18 +155,22 @@ def spec_msg(s):
return '--%s: %s %s' % (s.method, path_msg(s.src), path_msg(s.dest))

def parse_args(args):
+ @dataclass(slots=True)
class GetOpts:
- pass
+ help: bool = False
+ verbose: int = 0
+ quiet: bool = False
+ print_commits: bool = False
+ print_trees: bool = False
+ print_tags: bool = False
+ bwlimit: Optional[int] = None
+ compress: Optional[int] = None
+ source: Optional[bytes] = None
+ remote: Optional[bytes] = None
+ source_loc: URL = URL(scheme=b'file')
+ dst_loc: Optional[Union[URL, client.Config]] = None
+ target_specs: list = field(default_factory=list)
opt = GetOpts()
- opt.help = False
- opt.verbose = 0
- opt.quiet = False
- opt.print_commits = opt.print_trees = opt.print_tags = False
- opt.bwlimit = None
- opt.compress = None
- opt.source = opt.remote = None
- opt.source_loc = URL(scheme=b'file')
- opt.target_specs = []

# Since we don't want to create a Rewriter until we've finished
# checking the requests (e.g. are past the resolvers), the spec's
diff --git a/lib/bup/cmd/web.py b/lib/bup/cmd/web.py
index af474e49..c23a0959 100644
--- a/lib/bup/cmd/web.py
+++ b/lib/bup/cmd/web.py
@@ -183,12 +183,13 @@ def _dir_contents(repo, resolution, params, param_info):
class BupRequestHandler(tornado.web.RequestHandler):

def initialize(self, repo=None, human=None):
- self.repo = repo
+ self.repo = repo # pylint: disable=attribute-defined-outside-init
default_false_param = ParamInfo(default=0, from_req=from_req_bool,
normalize=normalize_bool)
human_param = ParamInfo(default=1 if human else 0,
from_req=from_req_bool,
normalize=normalize_bool)
+ # pylint: disable-next=attribute-defined-outside-init
self.bup_param_info = {'hash': default_false_param,
'hidden': default_false_param,
'human': human_param,
diff --git a/lib/bup/index.py b/lib/bup/index.py
index 8c0ec72e..cc932108 100644
--- a/lib/bup/index.py
+++ b/lib/bup/index.py
@@ -239,6 +239,7 @@ class Entry:
def update_from_stat(self, st, meta_ofs):
# Should only be called when the entry is stale(), and
# invalidate() should almost certainly be called afterward.
+ # pylint: disable=attribute-defined-outside-init
self.dev = st.st_dev
self.ino = st.st_ino
self.nlink = st.st_nlink
@@ -252,6 +253,7 @@ class Entry:
self._fixup()

def _fixup(self):
+ # pylint: disable=attribute-defined-outside-init
self.mtime = self._fixup_time(self.mtime)
self.ctime = self._fixup_time(self.ctime)

@@ -272,6 +274,7 @@ class Entry:
assert(sha)
assert(gitmode)
assert(gitmode+0 == gitmode)
+ # pylint: disable=attribute-defined-outside-init
self.gitmode = gitmode
self.sha = sha
self.flags |= IX_HASHVALID|IX_EXISTS
diff --git a/lib/bup/ls.py b/lib/bup/ls.py
index 08aac0f2..eeab54f3 100644
--- a/lib/bup/ls.py
+++ b/lib/bup/ls.py
@@ -4,11 +4,12 @@ from binascii import hexlify
from copy import deepcopy
from itertools import chain
from stat import S_ISDIR
+from typing import Any, List, Optional
import os.path
import posixpath

from bup import metadata, vfs, xstat
-from bup.compat import argv_bytes
+from bup.compat import argv_bytes, dataclass
from bup.io import path_msg
from bup.options import Options
from bup.helpers import columnate, istty1, log
@@ -75,10 +76,19 @@ human-readable print human readable file sizes (i.e. 3.9K, 4.7M)
n,numeric-ids list numeric IDs (user, group, etc.) rather than names
"""

+@dataclass(slots=True)
class LsOpts:
- __slots__ = ['paths', 'long_listing', 'classification', 'show_hidden',
- 'hash', 'commit_hash', 'numeric_ids', 'human_readable',
- 'directory', 'repo', 'l']
+ classification: Optional[int]
+ commit_hash: Optional[int]
+ directory: Optional[int]
+ hash: Optional[int]
+ human_readable: Optional[int]
+ l: Optional[int]
+ long_listing: Optional[int]
+ numeric_ids: Optional[int]
+ paths: List[bytes]
+ repo: Any
+ show_hidden: str

def opts_from_cmdline(args, onabort=None, pwd=b'/'):
"""Parse ls command line arguments and return a dictionary of ls
@@ -102,19 +112,18 @@ def opts_from_cmdline(args, onabort=None, pwd=b'/'):
opt.show_hidden = 'all'
elif option in ('-A', '--almost-all'):
opt.show_hidden = 'almost'
- ret = LsOpts()
- ret.paths = opt.paths
- ret.l = ret.long_listing = opt.long_listing
- ret.classification = opt.classification
- ret.show_hidden = opt.show_hidden
- ret.hash = opt.hash
- ret.commit_hash = opt.commit_hash
- ret.numeric_ids = opt.numeric_ids
- ret.human_readable = opt.human_readable
- ret.directory = opt.directory
remote = argv_bytes(opt.remote) if opt.remote else None
- ret.repo = main_repo_location(remote, o.fatal)
- return ret
+ return LsOpts(paths=opt.paths,
+ l=opt.long_listing,
+ long_listing=opt.long_listing,
+ classification=opt.classification,
+ show_hidden=opt.show_hidden,
+ hash=opt.hash,
+ commit_hash=opt.commit_hash,
+ numeric_ids=opt.numeric_ids,
+ human_readable=opt.human_readable,
+ directory=opt.directory,
+ repo=main_repo_location(remote, o.fatal))

def within_repo(repo, opt, out, pwd=b''):

diff --git a/test/int/test_bloom.py b/test/int/test_bloom.py
index 1fcc3772..ea26eaf8 100644
--- a/test/int/test_bloom.py
+++ b/test/int/test_bloom.py
@@ -4,15 +4,16 @@ import errno, os, sys, tempfile
import pytest

from bup import bloom
+from bup.compat import dataclass


def test_bloom(tmpdir):
hashes = [os.urandom(20) for i in range(100)]
+ @dataclass(slots=True)
class Idx:
- pass
- ix = Idx()
- ix.name = b'dummy.idx'
- ix.shatable = b''.join(hashes)
+ name: bytes
+ shatable: bytes
+ ix = Idx(name=b'dummy.idx', shatable=b''.join(hashes))
for k in (4, 5):
with bloom.create(tmpdir + b'/pybuptest.bloom', expected=100, k=k) as b:
b.add_idx(ix)
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 4:13:19 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Just remove the duplicate aliases key from test_optdict() since it
could never have been relevant because the duplication could never
have made it through the {} constructor. Also remove the other
untested aliases.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
.pylintrc | 1 +
test/int/test_options.py | 19 ++++++-------------
2 files changed, 7 insertions(+), 13 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index 90bb7a6f..a5210325 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -36,6 +36,7 @@ enable=
consider-using-ternary,
consider-using-with,
deprecated-module,
+ duplicate-key,
expression-not-assigned,
f-string-without-interpolation,
function-redefined,
diff --git a/test/int/test_options.py b/test/int/test_options.py
index f0c95d3e..a1817ec1 100644
--- a/test/int/test_options.py
+++ b/test/int/test_options.py
@@ -6,19 +6,12 @@ from bup import options


def test_optdict():
- d = options.OptDict({
- 'x': ('x', False),
- 'y': ('y', False),
- 'z': ('z', False),
- 'other_thing': ('other_thing', False),
- 'no_other_thing': ('other_thing', True),
- 'no_z': ('z', True),
- 'no_smart': ('smart', True),
- 'smart': ('smart', False),
- 'stupid': ('smart', True),
- 'no_smart': ('smart', False),
- })
- WVPASS('foo')
+ d = options.OptDict({'x': ('x', False),
+ 'y': ('y', False),
+ 'z': ('z', False),
+ 'other_thing': ('other_thing', False),
+ 'no_other_thing': ('other_thing', True),
+ 'no_z': ('z', True)})
d['x'] = 5
d['y'] = 4
d['z'] = 99
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 4:13:20 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
For unfinished_word, assume for now that the existing code isn't
wrong, and _quotesplit() always returns at least one item. If that's
wrong, it'll just throw, as before.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
.pylintrc | 1 +
lib/bup/client.py | 5 +++--
lib/bup/cmd/split.py | 9 ++++-----
lib/bup/shquote.py | 3 +++
lib/bup/vfs.py | 7 +++----
5 files changed, 14 insertions(+), 11 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index 08faf6eb..8dd0e19c 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -79,6 +79,7 @@ enable=
use-list-literal,
use-maxsplit-arg,
use-yield-from,
+ used-before-assignment,
useless-return,
wrong-import-order,
wrong-import-position
diff --git a/lib/bup/client.py b/lib/bup/client.py
index 99e107e8..335cad4b 100644
--- a/lib/bup/client.py
+++ b/lib/bup/client.py
@@ -377,8 +377,9 @@ class Client:
self._available_commands = { b'help' }
with self._line_based_call('help') as call:
lines = call.lines()
- if not next(lines, None) == b'Commands:':
- raise ClientError('unexpected help header ' + repr(line))
+ line = next(lines, None)
+ if not line == b'Commands:':
+ raise ClientError(f'unexpected help header {line!r}')
result = set()
for line in lines:
if not line.startswith(b' '):
diff --git a/lib/bup/cmd/split.py b/lib/bup/cmd/split.py
index 9fd1de02..0358ffc1 100644
--- a/lib/bup/cmd/split.py
+++ b/lib/bup/cmd/split.py
@@ -125,6 +125,7 @@ def split(opt, files, parent, out, split_cfg, *,
for sha, size_, level_ in shalist:
out.write(hexlify(sha) + b'\n')
reprogress()
+ if opt.verbose: log('\n')
elif opt.tree or opt.commit or opt.name:
if opt.name: # insert dummy_name which may be used as a restore target
mode, sha = \
@@ -136,6 +137,8 @@ def split(opt, files, parent, out, split_cfg, *,
shalist = split_to_shalist(new_blob, new_tree,
hashsplit.from_config(files, split_cfg))
tree = new_tree(shalist)
+ if opt.verbose: log('\n')
+ if opt.tree: out.write(hexlify(tree) + b'\n')
else:
last = 0
for blob, level_ in hashsplit.from_config(files, split_cfg):
@@ -145,11 +148,7 @@ def split(opt, files, parent, out, split_cfg, *,
megs = hashsplit.total_split // 1024 // 1024
if not opt.quiet and last != megs:
last = megs
-
- if opt.verbose:
- log('\n')
- if opt.tree:
- out.write(hexlify(tree) + b'\n')
+ if opt.verbose: log('\n')

commit = None
if opt.commit or opt.name:
diff --git a/lib/bup/shquote.py b/lib/bup/shquote.py
index 3aafec37..303017dc 100644
--- a/lib/bup/shquote.py
+++ b/lib/bup/shquote.py
@@ -86,8 +86,11 @@ def unfinished_word(line):
for (wordstart,word) in _quotesplit(line):
pass
except QuoteError:
+ # Assumes _quotesplit() always returns at least one item.
+ # pylint: disable-next=used-before-assignment
firstchar = line[wordstart:wordstart+1]
if firstchar in [q, qq]:
+ # pylint: disable-next=used-before-assignment
return (firstchar, word)
else:
return (None, word)
diff --git a/lib/bup/vfs.py b/lib/bup/vfs.py
index 97de7622..643a07fc 100644
--- a/lib/bup/vfs.py
+++ b/lib/bup/vfs.py
@@ -91,7 +91,7 @@ from stat import \
S_IXOTH,
S_IXUSR)
from time import localtime, strftime
-import re
+import builtins, re

from bup import git
from bup.git import \
@@ -108,14 +108,13 @@ from bup.helpers import EXIT_FAILURE, debug2
from bup.io import path_msg
from bup.metadata import Metadata, empty_metadata

-py_IOError = IOError

# We currently assume that it's always appropriate to just forward IOErrors
# to a remote client.

-class IOError(py_IOError):
+class IOError(builtins.IOError):
def __init__(self, errno, message, terminus=None):
- py_IOError.__init__(self, errno, message)
+ super().__init__(self, errno, message)
self.terminus = terminus

_reg_perms = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 4:13:20 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Raise from where appropriate, e.g. for metadata ApplyErrors, where the
original error is just a detail/clarification.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
.pylintrc | 1 +
lib/bup/cmd/validate_refs.py | 4 ++--
lib/bup/helpers.py | 13 +++++++------
lib/bup/metadata.py | 20 +++++++++++---------
lib/bup/vint.py | 2 +-
5 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index 8bd2c43e..5bd4c9b3 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -48,6 +48,7 @@ enable=
no-else-raise,
no-else-return,
no-value-for-parameter,
+ raise-missing-from,
redefined-outer-name,
reimported,
return-in-init,
diff --git a/lib/bup/cmd/validate_refs.py b/lib/bup/cmd/validate_refs.py
index 90d96e36..2b31f25c 100644
--- a/lib/bup/cmd/validate_refs.py
+++ b/lib/bup/cmd/validate_refs.py
@@ -105,9 +105,9 @@ def main(argv):
bupm_n += 1
except MissingObject:
return True # bupm sub-item, will be handled by later for_item
- except Exception:
+ except Exception as ex:
pm = walk_path_msg(ref_name, item_path)
- raise Exception(f'Unable to parse .bupm at {pm}')
+ raise Exception(f'Unable to parse .bupm at {pm}') from ex
parent = item_path[-2]
info = vfs.get_ref(repo, hexlify(parent.oid))
assert info[0], info
diff --git a/lib/bup/helpers.py b/lib/bup/helpers.py
index 2af42b88..4e566a35 100644
--- a/lib/bup/helpers.py
+++ b/lib/bup/helpers.py
@@ -5,6 +5,7 @@ from os import fsencode
from random import SystemRandom
from tempfile import mkdtemp
from time import localtime
+from typing import Callable, NoReturn
from shutil import rmtree
import sys, os, subprocess, errno, select, mmap, stat, re, struct
import hashlib, heapq, math, operator, time
@@ -938,7 +939,7 @@ def parse_date_arg(arg, val):
except ValueError as ex:
raise ValueError(f'{path_msg(arg)} {path_msg(val)} is not a float') from ex

-def parse_excludes(options, fatal):
+def parse_excludes(options, fatal: Callable[[str], NoReturn]):
"""Traverse the options and extract all excludes, or call Option.fatal()."""
excluded_paths = []

@@ -950,8 +951,8 @@ def parse_excludes(options, fatal):
try:
f = open(resolve_parent(argv_bytes(parameter)), 'rb')
except OSError as ex:
- raise fatal(f"couldn't read exclusions from {path_msg(parameter)}"
- f' ({ex.strerror} [{ex.errno}])')
+ fatal(f"couldn't read exclusions from {path_msg(parameter)}"
+ f' ({ex.strerror} [{ex.errno}])')
with f:
for exclude_path in f.readlines():
# FIXME: perhaps this should be rstrip('\n')
@@ -961,7 +962,7 @@ def parse_excludes(options, fatal):
return sorted(frozenset(excluded_paths))


-def parse_rx_excludes(options, fatal):
+def parse_rx_excludes(options, fatal: Callable[[str], NoReturn]):
"""Traverse the options and extract all rx excludes, or call
Option.fatal()."""
excluded_patterns = []
@@ -981,8 +982,8 @@ def parse_rx_excludes(options, fatal):
try:
f = open(resolve_parent(parameter), 'rb')
except OSError as ex:
- raise fatal(f"couldn't read exclusions from {path_msg(parameter)}"
- f' ({ex.strerror} [{ex.errno}])')
+ fatal(f"couldn't read exclusions from {path_msg(parameter)}"
+ f' ({ex.strerror} [{ex.errno}])')
with f:
for pattern in f.readlines():
spattern = pattern.rstrip(b'\n')
diff --git a/lib/bup/metadata.py b/lib/bup/metadata.py
index 42c3bb9d..3ed2f103 100644
--- a/lib/bup/metadata.py
+++ b/lib/bup/metadata.py
@@ -359,7 +359,7 @@ class Metadata:
except OSError as e:
if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
raise Exception('refusing to overwrite non-empty dir '
- + path_msg(path))
+ + path_msg(path)) from None
raise
else:
os.unlink(path)
@@ -419,7 +419,7 @@ class Metadata:
if e.errno != errno.EACCES:
raise
erm = e.strerror or e.errno
- raise ApplyError(f'lutime ({erm}): {path_msg(path)}')
+ raise ApplyError(f'lutime ({erm}): {path_msg(path)}') from e
else:
try:
utime(path, (self.atime or 0, self.mtime or 0))
@@ -427,7 +427,7 @@ class Metadata:
if e.errno != errno.EACCES:
raise
erm = e.strerror or e.errno
- raise ApplyError(f'utime ({erm}): {path_msg(path)}')
+ raise ApplyError(f'utime ({erm}): {path_msg(path)}') from e

uid = gid = -1 # By default, do nothing.
if is_superuser():
@@ -627,7 +627,7 @@ class Metadata:
raise
erm = e.strerror or e.errno
msg = f'POSIX1e ACL apply failed ({erm}): {path_msg(path)}'
- raise ApplyError(msg)
+ raise ApplyError(msg) from e


## Linux attributes (lsattr(1), chattr(1))
@@ -689,13 +689,13 @@ class Metadata:
raise ApplyError('chattr(0x%s) failed (%s): %s'
% (hex(self.linux_attr),
e.strerror or e.errno,
- path_msg(path)))
+ path_msg(path))) from e
if e.errno == EINVAL:
raise ApplyError('chattr(0x%s) failed (%s),'
' please report if this is not ntfs-3g: %s'
% (hex(self.linux_attr),
e.strerror or e.errno,
- path_msg(path)))
+ path_msg(path))) from e
raise


@@ -756,7 +756,7 @@ class Metadata:
if e.errno != errno.EACCES:
raise
erm = e.strerror or e.errno
- raise ApplyError(f'xattr.set ({erm}): {path_msg(path)}')
+ raise ApplyError(f'xattr.set ({erm}): {path_msg(path)}') from e
for k, v in self.linux_xattr:
if k not in existing_xattrs \
or v != xattr.get(path, k, nofollow=True):
@@ -766,7 +766,8 @@ class Metadata:
if e.errno not in (errno.EPERM, errno.EOPNOTSUPP):
raise
erm = e.strerror or e.errno
- raise ApplyError(f'xattr.set ({erm}): {path_msg(path)}')
+ raise ApplyError(f'xattr.set ({erm}): {path_msg(path)}') \
+ from e
existing_xattrs -= frozenset([k])
for k in existing_xattrs:
try:
@@ -775,7 +776,8 @@ class Metadata:
if e.errno not in (errno.EPERM, errno.EACCES):
raise
erm = e.strerror or e.errno
- raise ApplyError(f'xattr.remove ({erm}): {path_msg(path)}')
+ raise ApplyError(f'xattr.remove ({erm}): {path_msg(path)}') \
+ from e

__slots__ = ('_frozen',
'mode', 'uid', 'gid', 'user', 'group', 'rdev',
diff --git a/lib/bup/vint.py b/lib/bup/vint.py
index 3bb5e6ae..45a2c57c 100644
--- a/lib/bup/vint.py
+++ b/lib/bup/vint.py
@@ -22,7 +22,7 @@ def encode_vuint(x):
except OverflowError:
ret = b''
if x < 0:
- raise Exception("vuints must not be negative")
+ raise Exception("vuints must not be negative") from None
assert x, "the C version should have picked this up"

while True:
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 4:13:20 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
.pylintrc | 1 +
lib/bup/helpers.py | 11 ++++++-----
test/int/test_helpers.py | 27 +++++++++++++++++++++++++++
3 files changed, 34 insertions(+), 5 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index 8e533804..08faf6eb 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -69,6 +69,7 @@ enable=
unnecessary-semicolon,
unreachable,
unspecified-encoding,
+ unsubscriptable-object,
unused-argument,
unused-import,
unused-variable,
diff --git a/lib/bup/helpers.py b/lib/bup/helpers.py
index 124e1127..8dd2aa05 100644
--- a/lib/bup/helpers.py
+++ b/lib/bup/helpers.py
@@ -196,19 +196,20 @@ def partition(predicate, stream):

"""
stream = iter(stream)
+ have_rest = False
first_nonmatch = None
def leading_matches():
- nonlocal first_nonmatch
+ nonlocal have_rest, first_nonmatch
for x in stream:
if predicate(x):
yield x
else:
- first_nonmatch = (x,)
+ have_rest = True
+ first_nonmatch = x
break
def rest():
- nonlocal first_nonmatch
- if first_nonmatch:
- yield first_nonmatch[0]
+ if have_rest:
+ yield first_nonmatch
yield from stream
return (leading_matches(), rest())

diff --git a/test/int/test_helpers.py b/test/int/test_helpers.py
index 2a10209f..c4c46eb4 100644
--- a/test/int/test_helpers.py
+++ b/test/int/test_helpers.py
@@ -15,6 +15,7 @@ from bup.helpers import \
grafted_path_components,
nullctx,
parse_num,
+ partition,
path_components,
stopped,
stripped_path_components,
@@ -194,6 +195,32 @@ def test_stopped():
assert proc.poll() == -SIGKILL


+def test_partition():
+ # pylint: disable=use-implicit-booleaness-not-comparison
+ def odd(x): return isinstance(x, (int, float)) and x % 2
+ m, rest = partition(odd, tuple())
+ assert list(m) == []
+ assert list(rest) == []
+ m, rest = partition(odd, (None,))
+ assert list(m) == []
+ assert list(rest) == [None]
+ m, rest = partition(lambda x: not odd(x), (None,))
+ assert list(m) == [None]
+ assert list(rest) == []
+ m, rest = partition(odd, (2, 3, 4))
+ assert list(m) == []
+ assert list(rest) == [2, 3, 4]
+ m, rest = partition(odd, (1, 2, 3, 4))
+ assert list(m) == [1]
+ assert list(rest) == [2, 3, 4]
+ m, rest = partition(odd, (1, 3, 5))
+ assert list(m) == [1, 3, 5]
+ assert list(rest) == []
+ m, rest = partition(odd, (1, 3, 5, 6))
+ assert list(m) == [1, 3, 5]
+ assert list(rest) == [6]
+
+
@pytest.mark.parametrize('sync_atomic_replace', (True, False))
def test_atomically_replaced_file(sync_atomic_replace, tmpdir):
target_file = os.path.join(tmpdir, b'test-atomic-write')
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 4:13:20 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
.pylintrc | 1 +
lib/bup/client.py | 46 +++++++++++++++++-------------------
lib/bup/cmd/validate_refs.py | 7 +++---
lib/bup/cmd/web.py | 3 +--
lib/bup/git.py | 33 +++++++++++---------------
lib/bup/hashsplit.py | 22 ++++++++---------
lib/bup/helpers.py | 13 ++++------
lib/bup/index.py | 10 ++++----
lib/bup/io.py | 3 +--
lib/bup/metadata.py | 17 +++++--------
lib/bup/protocol.py | 2 +-
lib/bup/shquote.py | 16 +++++--------
lib/bup/vfs.py | 4 ++--
lib/bup/vint.py | 3 +--
lib/bup/xstat.py | 17 ++++++-------
test/int/test_metadata.py | 3 +--
16 files changed, 85 insertions(+), 115 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index 8dd0e19c..8bd2c43e 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -46,6 +46,7 @@ enable=
logging-not-lazy,
no-else-continue,
no-else-raise,
+ no-else-return,
no-value-for-parameter,
redefined-outer-name,
reimported,
diff --git a/lib/bup/client.py b/lib/bup/client.py
index 335cad4b..1f8b261f 100644
--- a/lib/bup/client.py
+++ b/lib/bup/client.py
@@ -101,23 +101,22 @@ def _raw_write_bwlimit(f, buf, bwcount, bwtime):
if not bwlimit:
f.write(buf)
return (len(buf), time.time())
- else:
- # We want to write in reasonably large blocks, but not so large that
- # they're likely to overflow a router's queue. So our bwlimit timing
- # has to be pretty granular. Also, if it takes too long from one
- # transmit to the next, we can't just make up for lost time to bring
- # the average back up to bwlimit - that will risk overflowing the
- # outbound queue, which defeats the purpose. So if we fall behind
- # by more than one block delay, we shouldn't ever try to catch up.
- for i in range(0,len(buf),4096):
- now = time.time()
- next = max(now, bwtime + 1.0*bwcount/bwlimit)
- time.sleep(next-now)
- sub = buf[i:i+4096]
- f.write(sub)
- bwcount = len(sub) # might be less than 4096
- bwtime = next
- return (bwcount, bwtime)
+ # We want to write in reasonably large blocks, but not so large that
+ # they're likely to overflow a router's queue. So our bwlimit timing
+ # has to be pretty granular. Also, if it takes too long from one
+ # transmit to the next, we can't just make up for lost time to bring
+ # the average back up to bwlimit - that will risk overflowing the
+ # outbound queue, which defeats the purpose. So if we fall behind
+ # by more than one block delay, we shouldn't ever try to catch up.
+ for i in range(0,len(buf),4096):
+ now = time.time()
+ next = max(now, bwtime + 1.0*bwcount/bwlimit)
+ time.sleep(next-now)
+ sub = buf[i:i+4096]
+ f.write(sub)
+ bwcount = len(sub) # might be less than 4096
+ bwtime = next
+ return (bwcount, bwtime)


def _legacy_cache_id_for_host_path(host, path):
@@ -643,22 +642,21 @@ class Client:
raise EOFError('EOF while reading config value type')
if kind == 0:
return None
- elif kind == 1:
+ if kind == 1:
return True
- elif kind == 2:
+ if kind == 2:
return False
- elif kind == 3:
+ if kind == 3:
val = read_vint(conn)
if val is None: raise EOFError('EOF while reading vint')
return val
- elif kind == 4:
+ if kind == 4:
val = read_bvec(conn)
if val is None: raise EOFError('EOF while reading bvec')
return val
- elif kind == 5:
+ if kind == 5:
raise PermissionError(f'config-get does not allow remote access to {name}')
- else:
- raise TypeError(f'Unrecognized result type {kind}')
+ raise TypeError(f'Unrecognized result type {kind}')


class RemotePackStore:
diff --git a/lib/bup/cmd/validate_refs.py b/lib/bup/cmd/validate_refs.py
index 6e96e1f2..90d96e36 100644
--- a/lib/bup/cmd/validate_refs.py
+++ b/lib/bup/cmd/validate_refs.py
@@ -114,7 +114,7 @@ def main(argv):
exp_n = expected_bup_entry_count_for_tree(b''.join(info[3]))
if bupm_n == exp_n:
return True
- elif bupm_n > exp_n:
+ if bupm_n > exp_n:
bad_bupm += 1
log(f'error: tree with extra bupm entries ({bupm_n} > {exp_n})'
f' (please report): {parent.oid.hex()}\n')
@@ -143,9 +143,8 @@ def main(argv):
live_objs.close()
if bad_bupm:
return EXIT_FAILURE
- elif (ref_missing + found_missing + abridged_bupm):
+ if (ref_missing + found_missing + abridged_bupm):
if (ref_missing or found_missing) and not opt.links:
log('note: missing object list may be incomplete without --links\n')
return EXIT_FALSE
- else:
- return EXIT_TRUE
+ return EXIT_TRUE
diff --git a/lib/bup/cmd/web.py b/lib/bup/cmd/web.py
index c23a0959..0ef9ba8f 100644
--- a/lib/bup/cmd/web.py
+++ b/lib/bup/cmd/web.py
@@ -316,8 +316,7 @@ class BupRequestHandler(tornado.web.RequestHandler):
ext = ext.lower()
if ext in self.extensions_map:
return self.extensions_map[ext]
- else:
- return self.extensions_map['']
+ return self.extensions_map['']

if not mimetypes.inited:
mimetypes.init() # try to read system mime.types
diff --git a/lib/bup/git.py b/lib/bup/git.py
index 85c5d09f..3ecba804 100644
--- a/lib/bup/git.py
+++ b/lib/bup/git.py
@@ -80,7 +80,7 @@ def git_config_get(path, option, *, opttype=None):
if rc == 0:
if opttype == 'int':
return int(r)
- elif opttype == 'bool': # any positive int is true for git --bool
+ if opttype == 'bool': # any positive int is true for git --bool
if not r:
return None
if r in (b'0', b'false'):
@@ -169,10 +169,9 @@ def mangle_name(name, mode, gitmode):
if stat.S_ISREG(mode) and not stat.S_ISREG(gitmode):
assert(stat.S_ISDIR(gitmode))
return name + b'.bup'
- elif name.endswith(b'.bup') or name[:-1].endswith(b'.bup'):
+ if name.endswith(b'.bup') or name[:-1].endswith(b'.bup'):
return name + b'.bupl'
- else:
- return name
+ return name


(BUP_NORMAL, BUP_CHUNKED) = (0,1)
@@ -189,12 +188,12 @@ def demangle_name(name, mode):
"""
if name.endswith(b'.bupl'):
return (name[:-5], BUP_NORMAL)
- elif name.endswith(b'.bup'):
+ if name.endswith(b'.bup'):
return (name[:-4], BUP_CHUNKED)
- elif name.endswith(b'.bupm'):
+ if name.endswith(b'.bupm'):
return (name[:-5],
BUP_CHUNKED if stat.S_ISDIR(mode) else BUP_NORMAL)
- elif name.endswith(b'.bupd'): # should be unreachable
+ if name.endswith(b'.bupd'): # should be unreachable
raise ValueError(f'Cannot unmangle *.bupd files: {path_msg(name)}')
return (name, BUP_NORMAL)

@@ -730,24 +729,21 @@ def open_idx(filename):
if version == 2:
contexts.pop_all()
return PackIdxV2(filename, f)
- else:
- raise GitError('%s: expected idx file version 2, got %d'
- % (path_msg(filename), version))
- elif len(header) == 8 and header[0:4] < b'\377tOc':
+ raise GitError('%s: expected idx file version 2, got %d'
+ % (path_msg(filename), version))
+ if len(header) == 8 and header[0:4] < b'\377tOc':
contexts.pop_all()
return PackIdxV1(filename, f)
- else:
- raise GitError('%s: unrecognized idx file header'
- % path_msg(filename))
+ raise GitError('%s: unrecognized idx file header'
+ % path_msg(filename))


def open_object_idx(filename):
if filename.endswith(b'.idx'):
return open_idx(filename)
- elif filename.endswith(b'.midx'):
+ if filename.endswith(b'.midx'):
return open_midx(filename)
- else:
- raise GitError('pack index filenames must end with .idx or .midx')
+ raise GitError('pack index filenames must end with .idx or .midx')


def idxmerge(idxlist, final_progress=True):
@@ -1070,8 +1066,7 @@ def read_ref(refname, repo_dir = None):
if l:
assert(len(l) == 1)
return l[0][1]
- else:
- return None
+ return None


def rev_list_invocation(ref_or_refs, format=None):
diff --git a/lib/bup/hashsplit.py b/lib/bup/hashsplit.py
index 5a7f19f0..13a87406 100644
--- a/lib/bup/hashsplit.py
+++ b/lib/bup/hashsplit.py
@@ -108,15 +108,14 @@ def split_to_shalist(makeblob, maketree,
for (sha,size,level) in sl:
shal.append((GIT_MODE_FILE, sha, size))
return _make_shalist(shal)[0]
- else:
- stacks = [[]]
- for (sha,size,level) in sl:
- stacks[0].append((GIT_MODE_FILE, sha, size))
- _squish(maketree, stacks, level)
- #log('stacks: %r\n' % [len(i) for i in stacks])
- _squish(maketree, stacks, len(stacks)-1)
- #log('stacks: %r\n' % [len(i) for i in stacks])
- return _make_shalist(stacks[-1])[0]
+ stacks = [[]]
+ for (sha,size,level) in sl:
+ stacks[0].append((GIT_MODE_FILE, sha, size))
+ _squish(maketree, stacks, level)
+ #log('stacks: %r\n' % [len(i) for i in stacks])
+ _squish(maketree, stacks, len(stacks)-1)
+ #log('stacks: %r\n' % [len(i) for i in stacks])
+ return _make_shalist(stacks[-1])[0]


def split_to_blob_or_tree(makeblob, maketree,
@@ -125,7 +124,6 @@ def split_to_blob_or_tree(makeblob, maketree,
shalist = list(split_to_shalist(makeblob, maketree, splitter))
if len(shalist) == 1:
return (shalist[0][0], shalist[0][2])
- elif len(shalist) == 0:
+ if len(shalist) == 0:
return (GIT_MODE_FILE, makeblob(b''))
- else:
- return (GIT_MODE_TREE, maketree(shalist))
+ return (GIT_MODE_TREE, maketree(shalist))
diff --git a/lib/bup/helpers.py b/lib/bup/helpers.py
index 8dd2aa05..2af42b88 100644
--- a/lib/bup/helpers.py
+++ b/lib/bup/helpers.py
@@ -331,9 +331,9 @@ def shstr(cmd):
"""
if isinstance(cmd, (bytes, str)):
return cmd
- elif all(isinstance(x, bytes) for x in cmd):
+ if all(isinstance(x, bytes) for x in cmd):
return b' '.join(map(bquote, cmd))
- elif all(isinstance(x, str) for x in cmd):
+ if all(isinstance(x, str) for x in cmd):
return ' '.join(map(squote, cmd))
raise TypeError('unsupported shstr argument: ' + repr(cmd))

@@ -507,8 +507,7 @@ class Conn(BaseConn):
if rl:
assert(rl[0] == self.inp.fileno())
return True
- else:
- return None
+ return None


def checked_reader(fd, n):
@@ -636,8 +635,7 @@ class DemuxConn(BaseConn):
if len(buf) < csize[0]:
csize[0] -= len(buf)
return None
- else:
- return csize[0]
+ return csize[0]
return b''.join(self._read_parts(until_size))

def has_input(self):
@@ -762,8 +760,7 @@ def slashappend(s):
assert isinstance(s, bytes)
if s and not s.endswith(b'/'):
return s + b'/'
- else:
- return s
+ return s


def _mmap_do(f, sz, flags, prot, close):
diff --git a/lib/bup/index.py b/lib/bup/index.py
index cc932108..948178e3 100644
--- a/lib/bup/index.py
+++ b/lib/bup/index.py
@@ -260,8 +260,7 @@ class Entry:
def _fixup_time(self, t):
if self.tmax is not None and t > self.tmax:
return self.tmax
- else:
- return t
+ return t

def is_valid(self):
f = IX_HASHVALID|IX_EXISTS
@@ -641,10 +640,9 @@ def _slashappend_or_add_error(p, caller):
except OSError as e:
add_error('%s: %s' % (caller, e))
return None
- else:
- if stat.S_ISDIR(st.st_mode):
- return slashappend(p)
- return p
+ if stat.S_ISDIR(st.st_mode):
+ return slashappend(p)
+ return p


def unique_resolved_paths(paths):
diff --git a/lib/bup/io.py b/lib/bup/io.py
index 844d1c9f..fe2bfa66 100644
--- a/lib/bup/io.py
+++ b/lib/bup/io.py
@@ -280,8 +280,7 @@ def walk_path_msg(ref_name, item_path):
path = f'{path_msg(item_path[root].name)}:{path}'
if S_ISDIR(item_path[-1].mode):
return f'{path_msg(ref_name)} {path}/'
- else:
- return f'{path_msg(ref_name)} {path}'
+ return f'{path_msg(ref_name)} {path}'


def qsql_id(s):
diff --git a/lib/bup/metadata.py b/lib/bup/metadata.py
index af64b2f3..42c3bb9d 100644
--- a/lib/bup/metadata.py
+++ b/lib/bup/metadata.py
@@ -183,10 +183,9 @@ def _clean_up_extract_path(p):
result = p.lstrip(b'/')
if result == b'':
return b'.'
- elif _risky_path(result):
+ if _risky_path(result):
return None
- else:
- return result
+ return result


# These tags are currently conceptually private to Metadata, and they
@@ -488,8 +487,7 @@ class Metadata:
def _encode_path(self):
if self.path:
return vint.pack('s', self.path)
- else:
- return None
+ return None

def _load_path_rec(self, port):
data = vint.read_bvec(port)
@@ -570,8 +568,7 @@ class Metadata:
if len(acls) == 2:
return vint.pack('ssss', acls[0], acls[1], b'', b'')
return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
- else:
- return None
+ return None

@staticmethod
def _correct_posix1e_v1_delimiters(acls, path):
@@ -668,8 +665,7 @@ class Metadata:
def _encode_linux_attr(self):
if self.linux_attr:
return vint.pack('V', self.linux_attr)
- else:
- return None
+ return None

def _load_linux_attr_rec(self, port):
data = vint.read_bvec(port)
@@ -723,8 +719,7 @@ class Metadata:
for name, value in self.linux_xattr:
result += vint.pack('ss', name, value)
return result
- else:
- return None
+ return None

def _load_linux_xattr_rec(self, file):
data = vint.read_bvec(file)
diff --git a/lib/bup/protocol.py b/lib/bup/protocol.py
index 7158bf24..855ae400 100644
--- a/lib/bup/protocol.py
+++ b/lib/bup/protocol.py
@@ -266,7 +266,7 @@ class Server:
self.conn.write(b'%s.idx\n' % name)
self.conn.ok()
return
- elif n == 0xffffffff:
+ if n == 0xffffffff:
debug2('bup server: receive-objects suspending.\n')
self.suspended = True
self.conn.ok()
diff --git a/lib/bup/shquote.py b/lib/bup/shquote.py
index 303017dc..47065106 100644
--- a/lib/bup/shquote.py
+++ b/lib/bup/shquote.py
@@ -92,10 +92,8 @@ def unfinished_word(line):
if firstchar in [q, qq]:
# pylint: disable-next=used-before-assignment
return (firstchar, word)
- else:
- return (None, word)
- else:
- return (None, b'')
+ return (None, word)
+ return (None, b'')

def quotify(qtype, word, terminate):
"""Return a bytes corresponding to given word, quoted using qtype.
@@ -113,10 +111,9 @@ def quotify(qtype, word, terminate):
"""
if qtype == qq:
return qq + word.replace(qq, b'\\"') + (terminate and qq or b'')
- elif qtype == q:
+ if qtype == q:
return q + word.replace(q, b"\\'") + (terminate and q or b'')
- else:
- return re.sub(br'([\"\' \t\n\r])', br'\\\1', word)
+ return re.sub(br'([\"\' \t\n\r])', br'\\\1', word)


def quotify_list(words):
@@ -163,6 +160,5 @@ def what_to_add(qtype, origword, newword, terminate):
"""
if not newword.startswith(origword):
return b''
- else:
- qold = quotify(qtype, origword, terminate=False)
- return quotify(qtype, newword, terminate=terminate)[len(qold):]
+ qold = quotify(qtype, origword, terminate=False)
+ return quotify(qtype, newword, terminate=terminate)[len(qold):]
diff --git a/lib/bup/vfs.py b/lib/bup/vfs.py
index 643a07fc..92423ba0 100644
--- a/lib/bup/vfs.py
+++ b/lib/bup/vfs.py
@@ -432,7 +432,7 @@ def item_mode(item):
m = item.meta
if isinstance(m, Metadata):
return m.mode
- elif isinstance(m, int):
+ if isinstance(m, int):
return m
raise TypeError(f'not integer or Metadata {m!r}')

@@ -990,7 +990,7 @@ def tags_items(repo, names):
for _ in it: pass
if typ == b'blob':
return Item(meta=default_file_mode, oid=oid)
- elif typ == b'tree':
+ if typ == b'tree':
return Item(meta=default_dir_mode, oid=oid)
raise Exception('unexpected tag type ' + typ.decode('ascii')
+ ' for tag ' + path_msg(name))
diff --git a/lib/bup/vint.py b/lib/bup/vint.py
index 5f8f8a37..3bb5e6ae 100644
--- a/lib/bup/vint.py
+++ b/lib/bup/vint.py
@@ -115,8 +115,7 @@ def read_vint(port):
break
if negative:
return -result
- else:
- return result
+ return result


def write_bvec(port, x):
diff --git a/lib/bup/xstat.py b/lib/bup/xstat.py
index 6f98d67c..84062552 100644
--- a/lib/bup/xstat.py
+++ b/lib/bup/xstat.py
@@ -39,8 +39,7 @@ def fstime_to_sec_bytes(fstime):
s += 1
if ns == 0:
return b'%d' % s
- else:
- return b'%d.%09d' % (s, ns)
+ return b'%d.%09d' % (s, ns)

def utime(path, times):
"""Times must be provided as (atime_ns, mtime_ns)."""
@@ -113,18 +112,16 @@ def classification_str(mode, include_exec):
and (pystat.S_IMODE(mode) \
& (pystat.S_IXUSR | pystat.S_IXGRP | pystat.S_IXOTH)):
return '*'
- else:
- return ''
- elif pystat.S_ISDIR(mode):
+ return ''
+ if pystat.S_ISDIR(mode):
return '/'
- elif pystat.S_ISLNK(mode):
+ if pystat.S_ISLNK(mode):
return '@'
- elif pystat.S_ISFIFO(mode):
+ if pystat.S_ISFIFO(mode):
return '|'
- elif pystat.S_ISSOCK(mode):
+ if pystat.S_ISSOCK(mode):
return '='
- else:
- return ''
+ return ''


def local_time_str(t):
diff --git a/test/int/test_metadata.py b/test/int/test_metadata.py
index eb3b65fe..6f958bfd 100644
--- a/test/int/test_metadata.py
+++ b/test/int/test_metadata.py
@@ -181,8 +181,7 @@ def _linux_attr_supported(path):
except OSError as e:
if e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
return False
- else:
- raise
+ raise
return True


--
2.47.3

Reply all
Reply to author
Forward
0 new messages