From 0cb546c1c81855f50d5e3a16a64f423be3412139 Mon Sep 17 00:00:00 2001 From: Steven Burns Date: Tue, 5 Mar 2019 23:16:29 -0800 Subject: [PATCH 1/3] Added re pattern to exclude based on the basename of a file --- pytest_watch/command.py | 12 ++++++++++++ pytest_watch/watcher.py | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pytest_watch/command.py b/pytest_watch/command.py index 4a8d84e..314e743 100644 --- a/pytest_watch/command.py +++ b/pytest_watch/command.py @@ -30,7 +30,9 @@ This also enables --wait to prevent pdb interruption. --spool Re-run after a delay (in milliseconds), allowing for more file system events to queue up (default: 200 ms). + --pattern Regular expression for excluding based on basename -p --poll Use polling instead of OS events (useful in VMs). + -e --emacs Ignore emacs lockfiles; same as --pattern '^.#\S+$' -v --verbose Increase verbosity of the output. -q --quiet Decrease verbosity of the output (precedence over -v). -V --version Print version and exit. @@ -96,6 +98,15 @@ def main(argv=None): else: extensions = None + # Emacs flag + if args['--emacs']: + pattern = r'^\.#\S+$' + else: + pattern = None + + if args['--pattern'] is not None: + pattern = args['--pattern'] + # Parse numeric arguments spool = args['--spool'] if spool is not None: @@ -109,6 +120,7 @@ def main(argv=None): return watch(entries=directories, ignore=args['--ignore'], extensions=extensions, + pattern=pattern, beep_on_failure=not args['--nobeep'], auto_clear=args['--clear'], wait=args['--wait'] or '--pdb' in pytest_args, diff --git a/pytest_watch/watcher.py b/pytest_watch/watcher.py index f69497f..309c088 100644 --- a/pytest_watch/watcher.py +++ b/pytest_watch/watcher.py @@ -4,6 +4,8 @@ import sys import subprocess import time +import re + from traceback import format_exc try: @@ -77,10 +79,11 @@ class EventListener(FileSystemEventHandler): """ Listens for changes to files and re-runs tests after each change. """ - def __init__(self, extensions=[], event_queue=None): + def __init__(self, extensions=[], event_queue=None, pattern=None): super(EventListener, self).__init__() self.event_queue = event_queue or Queue() self.extensions = extensions or DEFAULT_EXTENSIONS + self.pattern = pattern def on_any_event(self, event): """ @@ -96,10 +99,20 @@ def on_any_event(self, event): if isinstance(event, FileMovedEvent): dest_path = os.path.relpath(event.dest_path) + + # Filter files that don't match the allowed extensions if not event.is_directory and self.extensions != ALL_EXTENSIONS: src_ext = os.path.splitext(src_path)[1].lower() src_included = src_ext in self.extensions + + if src_included and self.pattern: + base = os.path.splitext( os.path.split(src_path)[1])[0] + p = re.compile( self.pattern) + if p.match( base): + print( 'Excluding change to:', src_path) + src_included = False + dest_included = False if dest_path: dest_ext = os.path.splitext(dest_path)[1].lower() @@ -216,7 +229,7 @@ def run_hook(cmd, *args): subprocess.call(command, shell=True) -def watch(entries=[], ignore=[], extensions=[], beep_on_failure=True, +def watch(entries=[], ignore=[], extensions=[], pattern=None, beep_on_failure=True, auto_clear=False, wait=False, beforerun=None, afterrun=None, onpass=None, onfail=None, onexit=None, runner=None, spool=None, poll=False, verbose=False, quiet=False, pytest_args=[]): @@ -237,7 +250,7 @@ def watch(entries=[], ignore=[], extensions=[], beep_on_failure=True, raise ValueError('Directory not found: ' + entry) # Setup event handler - event_listener = EventListener(extensions) + event_listener = EventListener(extensions,pattern=pattern) # Setup watchdog observer = PollingObserver() if poll else Observer() From 2216d71a7297954ef1124b277efe1752dce55c10 Mon Sep 17 00:00:00 2001 From: Steven Burns Date: Wed, 6 Mar 2019 11:35:32 -0800 Subject: [PATCH 2/3] Changes based of feedback from the PR; removed the --emacs feature, changed the name to --dont-watch-files --- pytest_watch/command.py | 14 ++------------ pytest_watch/watcher.py | 18 ++++++++---------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/pytest_watch/command.py b/pytest_watch/command.py index 314e743..25a7782 100644 --- a/pytest_watch/command.py +++ b/pytest_watch/command.py @@ -30,9 +30,8 @@ This also enables --wait to prevent pdb interruption. --spool Re-run after a delay (in milliseconds), allowing for more file system events to queue up (default: 200 ms). - --pattern Regular expression for excluding based on basename + --dont-watch-files Regular expression for excluding files from being watched -p --poll Use polling instead of OS events (useful in VMs). - -e --emacs Ignore emacs lockfiles; same as --pattern '^.#\S+$' -v --verbose Increase verbosity of the output. -q --quiet Decrease verbosity of the output (precedence over -v). -V --version Print version and exit. @@ -98,15 +97,6 @@ def main(argv=None): else: extensions = None - # Emacs flag - if args['--emacs']: - pattern = r'^\.#\S+$' - else: - pattern = None - - if args['--pattern'] is not None: - pattern = args['--pattern'] - # Parse numeric arguments spool = args['--spool'] if spool is not None: @@ -120,7 +110,7 @@ def main(argv=None): return watch(entries=directories, ignore=args['--ignore'], extensions=extensions, - pattern=pattern, + dont_watch_files=args['--dont-watch-files'], beep_on_failure=not args['--nobeep'], auto_clear=args['--clear'], wait=args['--wait'] or '--pdb' in pytest_args, diff --git a/pytest_watch/watcher.py b/pytest_watch/watcher.py index 309c088..869f2cd 100644 --- a/pytest_watch/watcher.py +++ b/pytest_watch/watcher.py @@ -79,11 +79,11 @@ class EventListener(FileSystemEventHandler): """ Listens for changes to files and re-runs tests after each change. """ - def __init__(self, extensions=[], event_queue=None, pattern=None): + def __init__(self, extensions=[], event_queue=None, dont_watch_files=None): super(EventListener, self).__init__() self.event_queue = event_queue or Queue() self.extensions = extensions or DEFAULT_EXTENSIONS - self.pattern = pattern + self.dont_watch_files = dont_watch_files def on_any_event(self, event): """ @@ -99,18 +99,16 @@ def on_any_event(self, event): if isinstance(event, FileMovedEvent): dest_path = os.path.relpath(event.dest_path) - - # Filter files that don't match the allowed extensions if not event.is_directory and self.extensions != ALL_EXTENSIONS: src_ext = os.path.splitext(src_path)[1].lower() src_included = src_ext in self.extensions - if src_included and self.pattern: - base = os.path.splitext( os.path.split(src_path)[1])[0] - p = re.compile( self.pattern) + if src_included and self.dont_watch_files: + base = os.path.basename(src_path) + p = re.compile( self.dont_watch_files) if p.match( base): - print( 'Excluding change to:', src_path) + print( 'File event matched --dont-watch-files pattern:', self.dont_watch_files, src_path) src_included = False dest_included = False @@ -229,7 +227,7 @@ def run_hook(cmd, *args): subprocess.call(command, shell=True) -def watch(entries=[], ignore=[], extensions=[], pattern=None, beep_on_failure=True, +def watch(entries=[], ignore=[], extensions=[], dont_watch_files=None, beep_on_failure=True, auto_clear=False, wait=False, beforerun=None, afterrun=None, onpass=None, onfail=None, onexit=None, runner=None, spool=None, poll=False, verbose=False, quiet=False, pytest_args=[]): @@ -250,7 +248,7 @@ def watch(entries=[], ignore=[], extensions=[], pattern=None, beep_on_failure=Tr raise ValueError('Directory not found: ' + entry) # Setup event handler - event_listener = EventListener(extensions,pattern=pattern) + event_listener = EventListener(extensions,dont_watch_files=dont_watch_files) # Setup watchdog observer = PollingObserver() if poll else Observer() From 85dea46c4204bc02ddbb1cc998b54b20245c3996 Mon Sep 17 00:00:00 2001 From: Steven Burns Date: Wed, 6 Mar 2019 11:52:54 -0800 Subject: [PATCH 3/3] Added the new feature to the README.md and README.rst files --- README.md | 7 +++ README.rst | 145 ++++++++++++++++++++++++++++------------------------- 2 files changed, 83 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index ddf43f6..d17fbc1 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,12 @@ Need to exclude directories from being observed or collected for tests? $ ptw --ignore ./deep-directory --ignore ./integration_tests ``` +Need to exclude watching files that match a particular regular expression (e.g., emacs lock files)? + +```bash +$ ptw --dont-watch-files '^\.#' +``` + See the full list of options: ``` @@ -127,6 +133,7 @@ Options: This also enables --wait to prevent pdb interruption. --spool Re-run after a delay (in milliseconds), allowing for more file system events to queue up (default: 200 ms). + --dont-watch-files Regular expression for excluding files from being watched -p --poll Use polling instead of OS events (useful in VMs). -v --verbose Increase verbosity of the output. -q --quiet Decrease verbosity of the output (precedence over -v). diff --git a/README.rst b/README.rst index 0899cd6..35e1ffc 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,7 @@ -pytest-watch – Continuous pytest runner -======================================= +pytest-watch -- Continuous pytest runner +======================================== -`Current version on PyPI `__ -`Say Thanks! `__ +|Current version on PyPI| |Say Thanks!| **pytest-watch** a zero-config CLI tool that runs `pytest `__, and re-runs it when a file in your @@ -13,7 +12,7 @@ Motivation ---------- Whether or not you use the test-driven development method, running tests -continuously is far more productive than waiting until you’re finished +continuously is far more productive than waiting until you're finished programming to test your code. Additionally, manually running ``py.test`` each time you want to see if any tests were broken has more wait-time and cognitive overhead than merely listening for a @@ -25,16 +24,16 @@ Installation .. code:: bash - $ pip install pytest-watch + $ pip install pytest-watch Usage ----- .. code:: bash - $ cd myproject - $ ptw - * Watching /path/to/myproject + $ cd myproject + $ ptw + * Watching /path/to/myproject *Note: It can also be run using its full name ``pytest-watch``.* @@ -44,96 +43,99 @@ when tests pass or fail: - **OSX** - ``$ ptw --onpass "say passed" --onfail "say failed"`` +``$ ptw --onpass "say passed" --onfail "say failed"`` - .. code:: bash +``bash $ ptw --onpass "growlnotify -m \"All tests passed!\"" \ --onfail "growlnotify -m \"Tests failed\""`` - $ ptw --onpass "growlnotify -m \"All tests passed!\"" \ - --onfail "growlnotify -m \"Tests failed\"" - - using `GrowlNotify `__. +using `GrowlNotify `__. - **Windows** - .. code:: bat - - > ptw --onfail flash +``bat > ptw --onfail flash`` - using `Console Flash `__ +using `Console Flash `__ -You can also run a command before the tests run, e.g. seeding your test +You can also run a command before the tests run, e.g. seeding your test database: .. code:: bash - $ ptw --beforerun init_db.py + $ ptw --beforerun init_db.py -Or after they finish, e.g. deleting a sqlite file. Note that this script +Or after they finish, e.g. deleting a sqlite file. Note that this script receives the exit code of ``py.test`` as an argument. .. code:: bash - $ ptw --afterrun cleanup_db.py + $ ptw --afterrun cleanup_db.py You can also use a custom runner script for full ``py.test`` control: .. code:: bash - $ ptw --runner "python custom_pytest_runner.py" + $ ptw --runner "python custom_pytest_runner.py" -Here’s an minimal runner script that runs ``py.test`` and prints its +Here's an minimal runner script that runs ``py.test`` and prints its exit code: .. code:: py - # custom_pytest_runner.py + # custom_pytest_runner.py - import sys - import pytest + import sys + import pytest - print('py.test exited with code:', pytest.main(sys.argv[1:])) + print('py.test exited with code:', pytest.main(sys.argv[1:])) Need to exclude directories from being observed or collected for tests? .. code:: bash - $ ptw --ignore ./deep-directory --ignore ./integration_tests + $ ptw --ignore ./deep-directory --ignore ./integration_tests + +Need to exclude watching files that match a particular regular +expression (e.g., emacs lock files)? + +.. code:: bash + + $ ptw --dont-watch-files '^\.#' See the full list of options: :: - $ ptw --help - Usage: ptw [options] [--ignore ...] [...] [-- ...] - - Options: - --ignore Ignore directory from being watched and during - collection (multi-allowed). - --ext Comma-separated list of file extensions that can - trigger a new test run when changed (default: .py). - Use --ext=* to allow any file (including .pyc). - --config Load configuration from `file` instead of trying to - locate one of the implicit configuration files. - -c --clear Clear the screen before each run. - -n --nobeep Do not beep on failure. - -w --wait Waits for all tests to complete before re-running. - Otherwise, tests are interrupted on filesystem events. - --beforerun Run arbitrary command before tests are run. - --afterrun Run arbitrary command on completion or interruption. - The exit code of "py.test" is passed as an argument. - --onpass Run arbitrary command on pass. - --onfail Run arbitrary command on failure. - --onexit Run arbitrary command when exiting pytest-watch. - --runner Run a custom command instead of "py.test". - --pdb Start the interactive Python debugger on errors. - This also enables --wait to prevent pdb interruption. - --spool Re-run after a delay (in milliseconds), allowing for - more file system events to queue up (default: 200 ms). - -p --poll Use polling instead of OS events (useful in VMs). - -v --verbose Increase verbosity of the output. - -q --quiet Decrease verbosity of the output (precedence over -v). - -V --version Print version and exit. - -h --help Print help and exit. + $ ptw --help + Usage: ptw [options] [--ignore ...] [...] [-- ...] + + Options: + --ignore Ignore directory from being watched and during + collection (multi-allowed). + --ext Comma-separated list of file extensions that can + trigger a new test run when changed (default: .py). + Use --ext=* to allow any file (including .pyc). + --config Load configuration from `file` instead of trying to + locate one of the implicit configuration files. + -c --clear Clear the screen before each run. + -n --nobeep Do not beep on failure. + -w --wait Waits for all tests to complete before re-running. + Otherwise, tests are interrupted on filesystem events. + --beforerun Run arbitrary command before tests are run. + --afterrun Run arbitrary command on completion or interruption. + The exit code of "py.test" is passed as an argument. + --onpass Run arbitrary command on pass. + --onfail Run arbitrary command on failure. + --onexit Run arbitrary command when exiting pytest-watch. + --runner Run a custom command instead of "py.test". + --pdb Start the interactive Python debugger on errors. + This also enables --wait to prevent pdb interruption. + --spool Re-run after a delay (in milliseconds), allowing for + more file system events to queue up (default: 200 ms). + --dont-watch-files Regular expression for excluding files from being watched + -p --poll Use polling instead of OS events (useful in VMs). + -v --verbose Increase verbosity of the output. + -q --quiet Decrease verbosity of the output (precedence over -v). + -V --version Print version and exit. + -h --help Print help and exit. Configuration ------------- @@ -144,15 +146,15 @@ persist them in your project. For example: .. code:: ini - # pytest.ini + # pytest.ini - [pytest] - addopts = --maxfail=2 + [pytest] + addopts = --maxfail=2 - [pytest-watch] - ignore = ./integration-tests - nobeep = True + [pytest-watch] + ignore = ./integration-tests + nobeep = True Alternatives ------------ @@ -163,7 +165,7 @@ Alternatives make them pass. This can be a speed advantage when trying to get all tests passing, but leaves out the discovery of new failures until then. It also drops the colors outputted by py.test, whereas - pytest-watch doesn’t. + pytest-watch doesn't. - `Nosey `__ is the original codebase this was forked from. Nosey runs `nose `__ instead of pytest. @@ -183,9 +185,14 @@ file: .. code:: bash - $ pandoc -t rst -o README.rst README.md + $ pandoc -t rst -o README.rst README.md If your PR has been waiting a while, feel free to `ping me on Twitter `__. Use this software often? :smiley: + +.. |Current version on PyPI| image:: http://img.shields.io/pypi/v/pytest-watch.svg + :target: http://pypi.python.org/pypi/pytest-watch/ +.. |Say Thanks!| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg + :target: https://saythanks.io/to/joeyespo