From a88e284a04a0b865bac5cfdeaea6033216a7126a Mon Sep 17 00:00:00 2001 From: nojhan Date: Wed, 16 Sep 2026 14:51:53 +0200 Subject: [PATCH] refactor: project now use UV --- pyproject.toml | 19 +++ watchyap.py => src/watchyap/__init__.py | 205 ----------------------- src/watchyap/watchyap.py | 210 ++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 205 deletions(-) create mode 100644 pyproject.toml rename watchyap.py => src/watchyap/__init__.py (58%) mode change 100755 => 100644 create mode 100755 src/watchyap/watchyap.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..184f937 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "watchyap" +version = "0.2.0" +description = "Automagically perform an action each time you touch watched files" +readme = "README.md" +authors = [ + { name = "nojhan", email = "nojhan@nojhan.net" } +] +requires-python = ">=3.8" +dependencies = [ + "watchdog>=4.0.2", +] + +[project.scripts] +watchyap = "watchyap.watchyap:main" + +[build-system] +requires = ["uv_build>=0.12.6,<0.13.0"] +build-backend = "uv_build" diff --git a/watchyap.py b/src/watchyap/__init__.py old mode 100755 new mode 100644 similarity index 58% rename from watchyap.py rename to src/watchyap/__init__.py index c317ade..14a7051 --- a/watchyap.py +++ b/src/watchyap/__init__.py @@ -1,8 +1,3 @@ -#!/usr/bin/env python3 -#encoding: utf-8 - -VERSION = "1.0.0" - import os import sys import time @@ -25,7 +20,6 @@ from watchdog.observers import Observer from watchdog.events import LoggingEventHandler from watchdog.events import FileSystemEventHandler - def last_of(us): return us[-1] @@ -307,202 +301,3 @@ def watchyap(globbed_targets, actions=None, delay=10, watched=["modified"]): observer.stop() observer.join() - -if __name__=="__main__": - import sys - import argparse - - class SortingHelpFormatter(argparse.HelpFormatter): - def add_arguments(self, actions): - actions = sorted(actions, key=attrgetter('option_strings')) - super(SortingHelpFormatter, self).add_arguments(actions) - - class SaneHelpFormatter( - SortingHelpFormatter, argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter - ): - pass - - # With default values in help message. - parser = argparse.ArgumentParser(formatter_class=SaneHelpFormatter, - add_help = False, - description="Perform an action each time a file is touched. For example: make a copy of a file each time it is modified.", - epilog=f"""Version: {VERSION} - -Examples: - Copy the file each time it is modified: - watchyap --save my_file - Copy the file in a subdirectory each time it is modified: - watchyap --directory snapshots --save my_file - Export a PNG from a watched SVG, each time you hit 'save' in inkscape: - watchyap --inkscape my_file - Git commit the file if it has been modified, but not before 30 seconds after the previous commit: - watchyap --git --delay 30 my_file - Copy the file each time is is opened or closed: - watchyap --events opened closed --save my_file - You may want to save both the file before and after it was modified by any - program: - watchyap --save --events opened closed --no-overwrite my_file - """) - - # Optional arguments. - parser.add_argument("-d","--directory", default='.', - help="The directory in which to copy the saved versions.") - - parser.add_argument("-y","--delay", default=10, type=int, - help="The minimum time (in seconds) between the creation of different saved files.") - - parser.add_argument("-e","--events", default="modified", nargs="*", - choices = ["opened", "moved", "deleted", "created", "modified", "closed"], - help="The event(s) on which you want to perform the action.") - - parser.add_argument("-s","--separator", default='_', - help="Separator character between the file name and the date stamp.") - - parser.add_argument("-t","--template", default='%Y-%m-%dT%H:%M:%S', - help="Template of the date stamp.") - - parser.add_argument("-w", "--no-overwrite", action="store_true", - help="Do not overwrite snapshots created at the same time, but append a number to their name.") - - parser.add_argument("-x", "--alt-ext", metavar="EXTENSION", default='', - help="Alternate extension for the timestamped snapshot (do not forget the dot).") - - parser.add_argument("-v","--verbose", choices=['DEBUG','INFO','WARNING','ERROR'], default='INFO', - help="Verbosity level.") - log_as = { 'DEBUG' :logging.DEBUG, - 'INFO' :logging.INFO, - 'WARNING':logging.WARNING, - 'ERROR' :logging.ERROR } - - parser.add_argument("-V", "--version", action="store_true", - help="Show program's version number and exit.") - - asked = parser.parse_known_args()[0] - - if asked.version: - print(VERSION, file=sys.stderr) - sys.exit() - - parser.add_argument("--cmd", metavar="COMMAND", type=str, - help="Execute the provided command as a action. You can use the following tags: {target} (watched file), {flick} (timestamped filename), {directory} (--directory where to put timestamped files), {separator} (--separator), {timestamp} (the timestamp formatted by --template), {alt_ext} (--alt-ext alternate extension), {no_overwrite} (--no-overwrite boolean, 1 if True, 0 if False), {event} (the event type).") - - existing = { - "save": - ["Save a snapshot of the target files.", - None], - "log": - ["Log when the target files are touched.", - None], - } - if HAS_DBUS: - existing["dbus"] = \ - ["Send a notification (on the system's D-Bus).", - None] - - if shutil.which("git"): - HAS_GIT = True - existing["git"] = ["Commit the target files if it has been modified [the repository should be set-up].", None] - else: - HAS_GIT = False - - if shutil.which("inkscape"): - HAS_INKSCAPE = True - existing["inkscape"] = ["Save a PNG snapshot of the files, using inkscape.", None] - else: - HAS_INKSCAPE = False - - if shutil.which("zenity") and os.environ.get('DISPLAY'): - HAS_ZENITY = True - existing["zenity"] = ["Pop-up a dialog box each time a file is touched.", None] - else: - HAS_ZENITY = False - - def help(name): - return existing[name][0] - - for name in existing: - parser.add_argument("--"+name, help=help(name), action="store_true") - - asked = parser.parse_known_args()[0] - - logging.basicConfig(level=log_as[asked.verbose], format='%(asctime)s -- %(message)s', datefmt=asked.template) - - logging.debug("Load actions...") - - # Add instances, now that we have all parameters. - available = existing; - available["save"][1] = Save(asked.directory, asked.separator, asked.template, asked.no_overwrite) - available["log"][1] = Log() - available["cmd"] = ["", Command(asked.cmd, asked.directory, asked.separator, asked.template, asked.alt_ext, asked.no_overwrite)] - - if HAS_GIT: - logging.debug("`git` command found") - available["git"][1] = Command("git add {target} ; git commit -m 'Automatic watchyap commit of {target} at {timestamp}'") - else: - print("WARNING: `git` command not found, the --inkscape action is disabled.", file=sys.stderr) - - if HAS_INKSCAPE: - logging.debug("`inkscape` command found") - available["inkscape"][1] = Command("inkscape {target} --without-gui --export-png={flick} --export-area-page", asked.directory, asked.separator, asked.template, "png", asked.no_overwrite) - else: - print("WARNING: `inkscape` command not found, the --inkscape action is disabled.", file=sys.stderr) - - if HAS_ZENITY: - logging.debug("`zenity` command found and display available") - available["zenity"][1] = Command("zenity --info --text 'File {target} was {event}'") - else: - has_zen = "" - if not shutil.which("zenity"): - has_zen = "`zenity` command not found, " - has_X = "" - if not os.environ.get('DISPLAY'): - has_X = "No graphical display found, " - print("WARNING: "+has_X + has_zen + "the --zenity action is disabled.", file=sys.stderr) - - if HAS_DBUS: - available["dbus"][1] = DBus() - else: - print("WARNING: No suitable Python `sdbus` module found, the --dbus action is disabled.", file=sys.stderr) - - # Check that both available and existing are aligned. - for op in existing: - assert op in available - for op in available: - assert op in existing - - logging.debug("Available actions:") - for name in available: - logging.debug("\t%s",name) - - # Required argument. - parser.add_argument("targets", nargs="+", - help="The files to save each time it's modified.") - - parser.add_argument('-h', '--help', action='help', default='', help=('Show this help message and exit')) - - asked = parser.parse_args() - - actions = [] - requested = vars(asked) - def instance(name): - return available[name][1] - for it in [iz for iz in requested if iz in available and requested[iz]]: - actions.append(instance(it)) - - if len(actions) == 0: - logging.warning( - "WARNING: you did not asked for any snapshot command, " - "I will only log when you save the file, but not perform any action. " - "Use one of the following option if you want me to do something: " - +", ".join(["--"+str(k) for k in available.keys()]) - ) - actions.append(Log()) - - logging.debug("Used actions:") - for op in actions: - logging.debug("\t%s", op) - - # Start it. - logging.debug(asked.targets) - watchyap(asked.targets, actions, asked.delay, asked.events) - diff --git a/src/watchyap/watchyap.py b/src/watchyap/watchyap.py new file mode 100755 index 0000000..783f021 --- /dev/null +++ b/src/watchyap/watchyap.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +#encoding: utf-8 + +import sys +import argparse +import importlib.metadata + +from . import * + +def main(): + class SortingHelpFormatter(argparse.HelpFormatter): + def add_arguments(self, actions): + actions = sorted(actions, key=attrgetter('option_strings')) + super(SortingHelpFormatter, self).add_arguments(actions) + + class SaneHelpFormatter( + SortingHelpFormatter, argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter + ): + pass + + VERSION = importlib.metadata.version('watchyap') + + # With default values in help message. + parser = argparse.ArgumentParser(formatter_class=SaneHelpFormatter, + add_help = False, + description="Perform an action each time a file is touched. For example: make a copy of a file each time it is modified.", + epilog=f"""Version: {VERSION} + +Examples: + Copy the file each time it is modified: + watchyap --save my_file + Copy the file in a subdirectory each time it is modified: + watchyap --directory snapshots --save my_file + Export a PNG from a watched SVG, each time you hit 'save' in inkscape: + watchyap --inkscape my_file + Git commit the file if it has been modified, but not before 30 seconds after the previous commit: + watchyap --git --delay 30 my_file + Copy the file each time is is opened or closed: + watchyap --events opened closed --save my_file + You may want to save both the file before and after it was modified by any + program: + watchyap --save --events opened closed --no-overwrite my_file + """) + + # Optional arguments. + parser.add_argument("-d","--directory", default='.', + help="The directory in which to copy the saved versions.") + + parser.add_argument("-y","--delay", default=10, type=int, + help="The minimum time (in seconds) between the creation of different saved files.") + + parser.add_argument("-e","--events", default="modified", nargs="*", + choices = ["opened", "moved", "deleted", "created", "modified", "closed"], + help="The event(s) on which you want to perform the action.") + + parser.add_argument("-s","--separator", default='_', + help="Separator character between the file name and the date stamp.") + + parser.add_argument("-t","--template", default='%Y-%m-%dT%H:%M:%S', + help="Template of the date stamp.") + + parser.add_argument("-w", "--no-overwrite", action="store_true", + help="Do not overwrite snapshots created at the same time, but append a number to their name.") + + parser.add_argument("-x", "--alt-ext", metavar="EXTENSION", default='', + help="Alternate extension for the timestamped snapshot (do not forget the dot).") + + parser.add_argument("-v","--verbose", choices=['DEBUG','INFO','WARNING','ERROR'], default='INFO', + help="Verbosity level.") + log_as = { 'DEBUG' :logging.DEBUG, + 'INFO' :logging.INFO, + 'WARNING':logging.WARNING, + 'ERROR' :logging.ERROR } + + parser.add_argument("-V", "--version", action="store_true", + help="Show program's version number and exit.") + + asked = parser.parse_known_args()[0] + + if asked.version: + print(VERSION, file=sys.stderr) + sys.exit() + + parser.add_argument("--cmd", metavar="COMMAND", type=str, + help="Execute the provided command as a action. You can use the following tags: {target} (watched file), {flick} (timestamped filename), {directory} (--directory where to put timestamped files), {separator} (--separator), {timestamp} (the timestamp formatted by --template), {alt_ext} (--alt-ext alternate extension), {no_overwrite} (--no-overwrite boolean, 1 if True, 0 if False), {event} (the event type).") + + existing = { + "save": + ["Save a snapshot of the target files.", + None], + "log": + ["Log when the target files are touched.", + None], + } + if HAS_DBUS: + existing["dbus"] = \ + ["Send a notification (on the system's D-Bus).", + None] + + if shutil.which("git"): + HAS_GIT = True + existing["git"] = ["Commit the target files if it has been modified [the repository should be set-up].", None] + else: + HAS_GIT = False + + if shutil.which("inkscape"): + HAS_INKSCAPE = True + existing["inkscape"] = ["Save a PNG snapshot of the files, using inkscape.", None] + else: + HAS_INKSCAPE = False + + if shutil.which("zenity") and os.environ.get('DISPLAY'): + HAS_ZENITY = True + existing["zenity"] = ["Pop-up a dialog box each time a file is touched.", None] + else: + HAS_ZENITY = False + + def help(name): + return existing[name][0] + + for name in existing: + parser.add_argument("--"+name, help=help(name), action="store_true") + + asked = parser.parse_known_args()[0] + + logging.basicConfig(level=log_as[asked.verbose], format='%(asctime)s -- %(message)s', datefmt=asked.template) + + logging.debug("Load actions...") + + # Add instances, now that we have all parameters. + available = existing; + available["save"][1] = Save(asked.directory, asked.separator, asked.template, asked.no_overwrite) + available["log"][1] = Log() + available["cmd"] = ["", Command(asked.cmd, asked.directory, asked.separator, asked.template, asked.alt_ext, asked.no_overwrite)] + + if HAS_GIT: + logging.debug("`git` command found") + available["git"][1] = Command("git add {target} ; git commit -m 'Automatic watchyap commit of {target} at {timestamp}'") + else: + print("WARNING: `git` command not found, the --inkscape action is disabled.", file=sys.stderr) + + if HAS_INKSCAPE: + logging.debug("`inkscape` command found") + available["inkscape"][1] = Command("inkscape {target} --without-gui --export-png={flick} --export-area-page", asked.directory, asked.separator, asked.template, "png", asked.no_overwrite) + else: + print("WARNING: `inkscape` command not found, the --inkscape action is disabled.", file=sys.stderr) + + if HAS_ZENITY: + logging.debug("`zenity` command found and display available") + available["zenity"][1] = Command("zenity --info --text 'File {target} was {event}'") + else: + has_zen = "" + if not shutil.which("zenity"): + has_zen = "`zenity` command not found, " + has_X = "" + if not os.environ.get('DISPLAY'): + has_X = "No graphical display found, " + print("WARNING: "+has_X + has_zen + "the --zenity action is disabled.", file=sys.stderr) + + if HAS_DBUS: + available["dbus"][1] = DBus() + else: + print("WARNING: No suitable Python `sdbus` module found, the --dbus action is disabled.", file=sys.stderr) + + # Check that both available and existing are aligned. + for op in existing: + assert op in available + for op in available: + assert op in existing + + logging.debug("Available actions:") + for name in available: + logging.debug("\t%s",name) + + # Required argument. + parser.add_argument("targets", nargs="+", + help="The files to save each time it's modified.") + + parser.add_argument('-h', '--help', action='help', default='', help=('Show this help message and exit')) + + asked = parser.parse_args() + + actions = [] + requested = vars(asked) + def instance(name): + return available[name][1] + for it in [iz for iz in requested if iz in available and requested[iz]]: + actions.append(instance(it)) + + if len(actions) == 0: + logging.warning( + "WARNING: you did not asked for any snapshot command, " + "I will only log when you save the file, but not perform any action. " + "Use one of the following option if you want me to do something: " + +", ".join(["--"+str(k) for k in available.keys()]) + ) + actions.append(Log()) + + logging.debug("Used actions:") + for op in actions: + logging.debug("\t%s", op) + + # Start it. + logging.debug(asked.targets) + watchyap(asked.targets, actions, asked.delay, asked.events) + + +if __name__=="__main__": + main() +