This commit is contained in:
Kovid Goyal 2025-09-20 16:09:19 +05:30
commit d2009d036c
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 197 additions and 7 deletions

View File

@ -47,6 +47,10 @@ class Check(Command):
def add_options(self, parser):
parser.add_option('--fix', '--auto-fix', default=False, action='store_true',
help='Try to automatically fix some of the smallest errors instead of opening an editor for bad files.')
parser.add_option('-f', '--file', dest='files', type='string', action='append',
help='Specific file to be check. Can be repeat to check severals.')
parser.add_option('--no-editor', default=False, action='store_true',
help="Don't open the editor when a bad file is found.")
def get_files(self):
yield from checkable_python_files(self.SRC)
@ -83,9 +87,9 @@ class Check(Command):
ext = os.path.splitext(f)[1]
if ext in {'.py', '.recipe'}:
if self.auto_fix:
p = subprocess.Popen(['ruff', 'check', '--fix', f])
p = subprocess.Popen(['ruff', 'check', '-q', '--fix', f])
else:
p = subprocess.Popen(['ruff', 'check', f])
p = subprocess.Popen(['ruff', 'check', '-q', f])
return p.wait() != 0
if ext == '.pyj':
p = subprocess.Popen(['rapydscript', 'lint', f])
@ -99,6 +103,8 @@ class Check(Command):
self.wn_path = os.path.expanduser('~/work/srv/main/static')
self.has_changelog_check = os.path.exists(self.wn_path)
self.auto_fix = opts.fix
self.files = opts.files
self.no_editor = opts.no_editor
self.run_check_files()
def run_check_files(self):
@ -109,18 +115,24 @@ class Check(Command):
except OSError as err:
if err.errno != errno.ENOENT:
raise
dirty_files = tuple(f for f in self.get_files() if not self.is_cache_valid(f, cache))
if self.files:
dirty_files = tuple(self.files)
else:
dirty_files = tuple(f for f in self.get_files() if not self.is_cache_valid(f, cache))
try:
for i, f in enumerate(dirty_files):
self.info('\tChecking', f)
if self.file_has_errors(f):
self.info(f'{len(dirty_files) - i - 1} files left to check')
e = SystemExit(1)
if self.no_editor:
raise e
try:
edit_file(f)
except FileNotFoundError:
pass # continue if the configured editor fail to be open
raise e # raise immediately to skip second check
if self.file_has_errors(f):
raise SystemExit(1)
raise e
cache[f] = self.file_hash(f)
finally:
self.save_cache(cache)

View File

@ -15,6 +15,7 @@ __all__ = [
'export_packages',
'extdev',
'get_translations',
'git_hooks',
'git_version',
'gui',
'hyphenation',
@ -102,6 +103,10 @@ from setup.liberation import LiberationFonts
liberation_fonts = LiberationFonts()
from setup.git_hooks import GitHooks
git_hooks = GitHooks()
from setup.git_version import GitVersion
git_version = GitVersion()

126
setup/git_hooks.py Normal file
View File

@ -0,0 +1,126 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2025, un_pogaz <un.pogaz@gmail.com>
import os
from collections import namedtuple
from setup import Command
base = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
base_hooks = os.path.join(base, '.git', 'hooks')
src_script = os.path.splitext(os.path.realpath(__file__))[0]
HOOK_TEMPLATE = '''\
#!/bin/sh
#!/usr/bin/env bash
# File generated by calibre "setup.py git_hooks"
HOOK_DIR=$(cd "$(dirname "$0")" && pwd)
BASE_DIR=$(dirname "$(dirname "$HOOK_DIR")")
SCRIPT=$BASE_DIR/{file}
exec python "$SCRIPT" {args}
'''
Hook = namedtuple('Hook', ['name', 'file', 'args_count', 'default'])
HOOKS = {h.name:h for h in (
Hook('post-checkout', 'git_post_checkout_hook.py', 3, True),
Hook('post-rewrite', 'git_post_rewrite_hook.py', 1, True),
Hook('pre-commit', 'git_pre_commit_hook.py', 0, True),
# disable by default, because except Kovid, nobody can run this hook
Hook('commit-msg', 'git_commit_msg_hook.py', 1, False),
)}
DEFAULT = ','.join(sorted(h.name for h in HOOKS.values() if h.default))
AVAILABLES = ', '.join(sorted(h for h in HOOKS))
class GitHooks(Command):
description = 'Install/uninstall git hooks'
def add_options(self, parser):
parser.add_option('-n', '--name', default=DEFAULT,
help='Name(s) of the hook to install, separated by commas. '
f'Default: "{DEFAULT}". Hooks available: {AVAILABLES}')
parser.add_option('-u', '--uninstall', default=False, action='store_true',
help='Uninstall the selected hooks')
parser.add_option('-f', '--force', default=False, action='store_true',
help='Force the operations on the hooks')
def run(self, opts):
self.force = opts.force
self.names = []
invalides = []
for candidate in sorted(c.strip().lower() for c in opts.name.split(',')):
if not candidate:
continue
if candidate not in HOOKS:
invalides.append(candidate)
else:
self.names.append(candidate)
if invalides:
self.info('Info: The following hook names are not recognized:', ', '.join(invalides))
if not self.names:
self.info('No supported hook names recognized.')
return
if opts.uninstall:
self.uninstall()
else:
self.install()
def _parse_template(self, hook_name):
hook = HOOKS[hook_name]
path = self.j(base_hooks, hook.name)
sh_file = f'setup/{hook.file}'
sh_args = ' '.join(f'"${i}"' for i in range(1, hook.args_count+1))
script = HOOK_TEMPLATE.format(file=sh_file, args=sh_args)
return path, script
def install(self):
self.info('Installing the hooks:', ', '.join(self.names))
for candidate in self.names:
path, script = self._parse_template(candidate)
if self.e(path):
with open(path, 'rb') as f:
previous = f.read().decode('utf-8')
msg = f'{candidate}: a non-calibre hook is installed.'
if previous == script:
self.info(f'{candidate}: installed.')
continue
elif self.force:
self.info(msg, 'Force installation.')
else:
self.info(msg, 'Skip installation.')
continue
self.info(f'{candidate}: installed.')
with open(path, 'wb') as f:
f.write(script.encode('utf-8'))
def uninstall(self):
self.info('Uninstalling the hooks:', ', '.join(self.names))
for candidate in self.names:
path, script = self._parse_template(candidate)
if not self.e(path):
self.info(f'{candidate}: no hook to unistall.')
continue
with open(path, 'rb') as f:
previous = f.read().decode('utf-8')
msg = f'{candidate}: a non-calibre hook is installed.'
if previous == script:
self.info(f'{candidate}: unistalled.')
elif self.force:
self.info(msg, 'Force unistallation.')
else:
self.info(msg, 'Skip unistallation.')
continue
os.remove(path)

View File

@ -21,7 +21,7 @@ if flags == '1': # A branch checkout
prev_branch, cur_branch = list(map(get_branch_name, (prev_rev, current_rev)))
rebase_in_progress = os.path.exists('.git/rebase-apply') or os.path.exists('.git/rebase-merge')
subprocess.check_call('./setup.py gui --summary'.split())
subprocess.check_call([sys.executable, './setup.py', 'gui', '--summary'])
# Remove .pyc files as some of them might have been orphaned
for dirpath, dirnames, filenames in os.walk('.'):

View File

@ -13,4 +13,4 @@ os.chdir(base)
action = [x.decode('utf-8') if isinstance(x, bytes) else x for x in sys.argv[1:]][0]
if action == 'rebase':
subprocess.check_call(['./setup.py', 'gui', '--summary'])
subprocess.check_call([sys.executable, './setup.py', 'gui', '--summary'])

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python
import os
import subprocess
import sys
base = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
os.chdir(base)
setup_py = os.path.realpath('./setup.py')
def testfile(file):
def t(f, start, end, exclude_end=None):
return f.startswith(start) and f.endswith(end) and not f.endswith(exclude_end) if exclude_end else True
if t(file, ('src/odf', 'src/calibre'), '.py', exclude_end='_ui.py'):
return True
if t(file, 'recipes', '.recipe'):
return True
if t(file, 'src/pyj', '.pyj'):
return True
return False
output = subprocess.check_output((
'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z',
# Everything except for D
'--diff-filter=ACMRTUXB',
)).decode('utf-8')
output = output.strip('\0')
if not output:
output = []
else:
output = output.split('\0')
filenames = tuple(filter(testfile, output))
if not filenames:
sys.exit(0)
check_args = [sys.executable, './setup.py', 'check', '--no-editor']
# let's hope that too many arguments do not hold any surprises
for f in filenames:
check_args.append('-f')
check_args.append(f)
returncode = subprocess.call(check_args)
sys.exit(returncode)