Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the standard pathlib instead of pathtools #704

Merged
merged 1 commit into from
Nov 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ appropriate observer like in the example above, do::
Dependencies
------------
1. Python 3.4 or above.
2. pathtools_
3. XCode_ (only on Mac OS X)
4. PyYAML_ (only for ``watchmedo`` script)
5. argh_ (only for ``watchmedo`` script)
Expand Down Expand Up @@ -277,7 +276,6 @@ to do:
.. _PyYAML: http://www.pyyaml.org/
.. _XCode: http://developer.apple.com/technologies/tools/xcode.html
.. _LibYAML: http://pyyaml.org/wiki/LibYAML
.. _pathtools: http://github.com/gorakhargosh/pathtools

.. _pnotify: http://mark.heily.com/pnotify
.. _unison fsmonitor: https://webdav.seas.upenn.edu/viewvc/unison/trunk/src/fsmonitor.py?view=markup&pathrev=471
Expand Down
1 change: 1 addition & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Changelog
- Allow file paths on Unix that don't follow the file system encoding (`# <https://github.com/gorakhargosh/watchdog/pull/703>`_)
- Drop support for Python 2.7 (`# <https://github.com/gorakhargosh/watchdog/pull/703>`_)
- Thanks to our beloved contributors: @SamSchott
- Use `pathlib` from the standard library, instead of pathtools: @bstaletic


0.10.4
Expand Down
1 change: 0 additions & 1 deletion docs/requirements.txt

This file was deleted.

1 change: 0 additions & 1 deletion docs/source/global.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,3 @@
.. _unison fsmonitor: https://webdav.seas.upenn.edu/viewvc/unison/trunk/src/fsmonitor.py?view=markup&pathrev=471
.. _XCode: http://developer.apple.com/technologies/tools/xcode.html
.. _zc.buildout: http://www.buildout.org/
.. _pathtools: http://github.com/gorakhargosh/pathtools
2 changes: 0 additions & 2 deletions docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ using.
+=====================+=============+=============+=============+=============+
| XCode_ | | | Yes | |
+---------------------+-------------+-------------+-------------+-------------+
| pathtools_ | Yes | Yes | Yes | Yes |
+---------------------+-------------+-------------+-------------+-------------+

The following is a list of dependencies you need based on the operating system you are
using the ``watchmedo`` utility.
Expand Down
4 changes: 0 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,6 @@
),
]

install_requires = [
"pathtools>=0.1.1",
]
extras_require = {
'watchmedo': ['PyYAML>=3.10', 'argh>=0.24.1'],
}
Expand Down Expand Up @@ -145,7 +142,6 @@
package_dir={'': SRC_DIR},
packages=find_packages(SRC_DIR),
include_package_data=True,
install_requires=install_requires,
extras_require=extras_require,
cmdclass={
'build_ext': build_ext,
Expand Down
2 changes: 1 addition & 1 deletion src/watchdog/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@
import os.path
import logging
import re
from pathtools.patterns import match_any_paths
from watchdog.utils import has_attribute
from watchdog.utils.patterns import match_any_paths


EVENT_TYPE_MOVED = 'moved'
Expand Down
6 changes: 5 additions & 1 deletion src/watchdog/observers/kqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
import os.path
import select

from pathtools.path import absolute_path
from pathlib import Path

from watchdog.observers.api import (
BaseObserver,
Expand Down Expand Up @@ -126,6 +126,10 @@
| select.KQ_NOTE_REVOKE
)


def absolute_path(path):
return Path(path).resolve()
BoboTiG marked this conversation as resolved.
Show resolved Hide resolved

# Flag tests.


Expand Down
88 changes: 88 additions & 0 deletions src/watchdog/utils/patterns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# patterns.py: Common wildcard searching/filtering functionality for files.
#
# Copyright (C) 2010 Yesudeep Mangalapilly <yesudeep@gmail.com>
#
# Written by Boris Staletic <boris.staletic@gmail.com>

# Non-pure path objects are only allowed on their respective OS's.
# Thus, these utilities require "pure" path objects that don't access the filesystem.
# Since pathlib doesn't have a `case_sensitive` parameter, we have to approximate it
# by converting input paths to `PureWindowsPath` and `PurePosixPath` where:
# - `PureWindowsPath` is always case-insensitive.
# - `PurePosixPath` is always case-sensitive.
# Reference: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.match
from pathlib import PureWindowsPath, PurePosixPath
bstaletic marked this conversation as resolved.
Show resolved Hide resolved


def _match_path(path, included_patterns, excluded_patterns, case_sensitive):
"""Internal function same as :func:`match_path` but does not check arguments."""
if case_sensitive:
path = PurePosixPath(path)
else:
included_patterns = {pattern.lower() for pattern in included_patterns}
excluded_patterns = {pattern.lower() for pattern in excluded_patterns}
path = PureWindowsPath(path)
Comment on lines +21 to +26
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the handling of case-sensitivity into _match_path so that the test for _match_path can use {"*.py"} and {"*.PY"} to raise the ValueError.

_match_path is still assuming that included_patterns and excluded_patterns are sets, which is guaranteed by filter_path and match_any_paths. I'm also not against moving the call to set() over here.


common_patterns = included_patterns & excluded_patterns
if common_patterns:
raise ValueError('conflicting patterns `{}` included and excluded'.format(common_patterns))
return (any(path.match(p) for p in included_patterns)
and not any(path.match(p) for p in excluded_patterns))


def filter_paths(paths, included_patterns=None, excluded_patterns=None, case_sensitive=True):
"""
Filters from a set of paths based on acceptable patterns and
ignorable patterns.
:param pathnames:
A list of path names that will be filtered based on matching and
ignored patterns.
:param included_patterns:
Allow filenames matching wildcard patterns specified in this list.
If no pattern list is specified, ["*"] is used as the default pattern,
which matches all files.
:param excluded_patterns:
Ignores filenames matching wildcard patterns specified in this list.
If no pattern list is specified, no files are ignored.
:param case_sensitive:
``True`` if matching should be case-sensitive; ``False`` otherwise.
:returns:
A list of pathnames that matched the allowable patterns and passed
through the ignored patterns.
"""
included = ["*"] if included_patterns is None else included_patterns
excluded = [] if excluded_patterns is None else excluded_patterns

for path in paths:
if _match_path(path, set(included), set(excluded), case_sensitive):
yield path


def match_any_paths(paths, included_patterns=None, excluded_patterns=None, case_sensitive=True):
"""
Matches from a set of paths based on acceptable patterns and
ignorable patterns.
:param pathnames:
A list of path names that will be filtered based on matching and
ignored patterns.
:param included_patterns:
Allow filenames matching wildcard patterns specified in this list.
If no pattern list is specified, ["*"] is used as the default pattern,
which matches all files.
:param excluded_patterns:
Ignores filenames matching wildcard patterns specified in this list.
If no pattern list is specified, no files are ignored.
:param case_sensitive:
``True`` if matching should be case-sensitive; ``False`` otherwise.
:returns:
``True`` if any of the paths matches; ``False`` otherwise.
"""
included = ["*"] if included_patterns is None else included_patterns
excluded = [] if excluded_patterns is None else excluded_patterns

for path in paths:
if _match_path(path, set(included), set(excluded), case_sensitive):
return True
return False
2 changes: 1 addition & 1 deletion tests/test_pattern_matching_event_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from pathtools.patterns import filter_paths
from watchdog.events import (
FileDeletedEvent,
FileModifiedEvent,
Expand All @@ -33,6 +32,7 @@
EVENT_TYPE_MOVED,
)
from watchdog.utils import has_attribute
from watchdog.utils.patterns import filter_paths

path_1 = '/path/xyz'
path_2 = '/path/abc'
Expand Down
44 changes: 44 additions & 0 deletions tests/test_patterns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010 Yesudeep Mangalapilly <yesudeep@gmail.com>
# Copyright 2020 Boris Staletic <boris.staletic@gmail.com>

import pytest
from watchdog.utils.patterns import _match_path, filter_paths, match_any_paths


@pytest.mark.parametrize("input, included_patterns, excluded_patterns, case_sensitive, expected", [
("/users/gorakhargosh/foobar.py", {"*.py"}, {"*.PY"}, True, True),
("/users/gorakhargosh/foobar.py", {"*.py"}, {"*.PY"}, True, True),
("/users/gorakhargosh/", {"*.py"}, {"*.txt"}, False, False),
("/users/gorakhargosh/foobar.py", {"*.py"}, {"*.PY"}, False, ValueError),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once I moved the if case_sensitive to _match_path, I was able to use plain old strings as input as well.

])
def test_match_path(input, included_patterns, excluded_patterns, case_sensitive, expected):
if expected == ValueError:
with pytest.raises(expected):
_match_path(input, included_patterns, excluded_patterns, case_sensitive)
else:
assert _match_path(input, included_patterns, excluded_patterns, case_sensitive) is expected


@pytest.mark.parametrize("included_patterns, excluded_patterns, case_sensitive, expected", [
(None, None, True, None),
(None, None, False, None),
(["*.py", "*.conf"], ["*.status"], True, {"/users/gorakhargosh/foobar.py", "/etc/pdnsd.conf"}),
])
def test_filter_paths(included_patterns, excluded_patterns, case_sensitive, expected):
pathnames = {"/users/gorakhargosh/foobar.py", "/var/cache/pdnsd.status", "/etc/pdnsd.conf", "/usr/local/bin/python"}
actual = set(filter_paths(pathnames, included_patterns, excluded_patterns, case_sensitive))
assert actual == expected if expected else pathnames


@pytest.mark.parametrize("included_patterns, excluded_patterns, case_sensitive, expected", [
(None, None, True, True),
(None, None, False, True),
(["*py", "*.conf"], ["*.status"], True, True),
(["*.txt"], None, False, False),
(["*.txt"], None, True, False),
])
def test_match_any_paths(included_patterns, excluded_patterns, case_sensitive, expected):
pathnames = {"/users/gorakhargosh/foobar.py", "/var/cache/pdnsd.status", "/etc/pdnsd.conf", "/usr/local/bin/python"}
assert match_any_paths(pathnames, included_patterns, excluded_patterns, case_sensitive) == expected