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

fix: Fix auto-reload with globs in IGNORE_FILES #3441

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
11 changes: 8 additions & 3 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,17 @@ Basic settings

READERS = {'foo': FooReader}

.. data:: IGNORE_FILES = ['.*']
.. data:: IGNORE_FILES = ['**/.*']

A list of glob patterns. Files and directories matching any of these patterns
will be ignored by the processor. For example, the default ``['.*']`` will
A list of Unix glob patterns. Files and directories matching any of these patterns
or any of the commonly hidden files and directories set by ``watchfiles.DefaultFilter``
will be ignored by the processor. For example, the default ``['**/.*']`` will
ignore "hidden" files and directories, and ``['__pycache__']`` would ignore
Python 3's bytecode caches.

For a full list of the commonly hidden files set by ``watchfiles.DefaultFilter``,
please refer to the `watchfiles documentation`_.

.. data:: MARKDOWN = {...}

Extra configuration settings for the Markdown processor. Refer to the Python
Expand Down Expand Up @@ -1423,3 +1427,4 @@ Example settings

.. _Jinja Environment documentation: https://jinja.palletsprojects.com/en/latest/api/#jinja2.Environment
.. _Docutils Configuration: http://docutils.sourceforge.net/docs/user/config.html
.. _`watchfiles documentation`: https://watchfiles.helpmanual.io/api/filters/#watchfiles.DefaultFilter.ignore_dirs
2 changes: 1 addition & 1 deletion pelican/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def load_source(name: str, path: str) -> ModuleType:
"PYGMENTS_RST_OPTIONS": {},
"TEMPLATE_PAGES": {},
"TEMPLATE_EXTENSIONS": [".html"],
"IGNORE_FILES": [".*"],
"IGNORE_FILES": ["**/.*"],
"SLUG_REGEX_SUBSTITUTIONS": [
(r"[^\w\s-]", ""), # remove non-alphabetical/whitespace/'-' chars
(r"(?u)\A\s*", ""), # strip leading whitespace
Expand Down
69 changes: 68 additions & 1 deletion pelican/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
from sys import platform
from tempfile import mkdtemp

import watchfiles

try:
from zoneinfo import ZoneInfo
except ModuleNotFoundError:
from backports.zoneinfo import ZoneInfo

from pelican import utils
from pelican.generators import TemplatePagesGenerator
from pelican.settings import read_settings
from pelican.settings import DEFAULT_CONFIG, read_settings
from pelican.tests.support import (
LoggedTestCase,
get_article,
Expand Down Expand Up @@ -990,3 +992,68 @@ def test_file_suffix(self):
self.assertEqual("", utils.file_suffix(""))
self.assertEqual("", utils.file_suffix("foo"))
self.assertEqual("md", utils.file_suffix("foo.md"))


class TestFileChangeFilter(unittest.TestCase):
ignore_file_patterns = DEFAULT_CONFIG["IGNORE_FILES"]

def test_regular_files_not_filtered(self):
filter = utils.FileChangeFilter(ignore_file_patterns=self.ignore_file_patterns)
basename = "article.rst"
full_path = os.path.join(os.path.dirname(__file__), "content", basename)

for change in watchfiles.Change:
self.assertTrue(filter(change=change, path=basename))
self.assertTrue(filter(change=change, path=full_path))

def test_dotfiles_filtered(self):
filter = utils.FileChangeFilter(ignore_file_patterns=self.ignore_file_patterns)
basename = ".config"
full_path = os.path.join(os.path.dirname(__file__), "content", basename)

# Testing with just the hidden file name and the full file path to the hidden file
for change in watchfiles.Change:
self.assertFalse(filter(change=change, path=basename))
self.assertFalse(filter(change=change, path=full_path))

def test_default_filters(self):
# Testing a subset of the default filters
# For reference: https://watchfiles.helpmanual.io/api/filters/#watchfiles.DefaultFilter.ignore_dirs
filter = utils.FileChangeFilter(ignore_file_patterns=[])
test_basenames = [
"__pycache__",
".git",
".hg",
".svn",
".tox",
".venv",
".idea",
"node_modules",
".mypy_cache",
".pytest_cache",
".hypothesis",
".DS_Store",
"flycheck_file",
"test_file~",
]

for basename in test_basenames:
full_path = os.path.join(os.path.dirname(__file__), basename)
for change in watchfiles.Change:
self.assertFalse(filter(change=change, path=basename))
self.assertFalse(filter(change=change, path=full_path))

def test_custom_ignore_pattern(self):
filter = utils.FileChangeFilter(ignore_file_patterns=["*.rst"])
basename = "article.rst"
full_path = os.path.join(os.path.dirname(__file__), basename)
for change in watchfiles.Change:
self.assertFalse(filter(change=change, path=basename))
self.assertFalse(filter(change=change, path=full_path))

# If the user changes `IGNORE_FILES` to only contain ['*.rst'], then dotfiles would not be filtered anymore
basename = ".config"
full_path = os.path.join(os.path.dirname(__file__), basename)
for change in watchfiles.Change:
self.assertTrue(filter(change=change, path=basename))
self.assertTrue(filter(change=change, path=full_path))
23 changes: 19 additions & 4 deletions pelican/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,15 +811,30 @@ def order_content(
return content_list


class FileChangeFilter(watchfiles.DefaultFilter):
def __init__(self, ignore_file_patterns: Sequence[str], *args, **kwargs):
super().__init__(*args, **kwargs)
self.ignore_file_patterns = ignore_file_patterns

def __call__(self, change: watchfiles.Change, path: str) -> bool:
"""Returns `True` if a file should be watched for changes. The `IGNORE_FILES`
setting is a list of Unix glob patterns. This call will filter out files and
directories specified by `IGNORE_FILES` Pelican setting and by the default
filters of `watchfiles.DefaultFilter`, seen here:
https://watchfiles.helpmanual.io/api/filters/#watchfiles.DefaultFilter.ignore_dirs
"""
return super().__call__(change, path) and not any(
Copy link
Contributor

@boxydog boxydog Jan 5, 2025

Choose a reason for hiding this comment

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

I don't understand this logic. Is it something like:

"Ignore the file if it regex-matches any pattern in ignore_file_patterns, unless the basename of the file also fn-matches any pattern in ignore_file_patterns"?

That seems quite subtle to me, i.e. a bit hard to understand.

I could be wrong, though, and it's not obvious to me what the simple solution is.

fnmatch.fnmatch(os.path.abspath(path), p) for p in self.ignore_file_patterns
)


def wait_for_changes(
settings_file: str,
settings: Settings,
) -> set[tuple[Change, str]]:
content_path = settings.get("PATH", "")
theme_path = settings.get("THEME", "")
ignore_files = {
fnmatch.translate(pattern) for pattern in settings.get("IGNORE_FILES", [])
}
ignore_file_patterns = set(settings.get("IGNORE_FILES", []))

candidate_paths = [
settings_file,
Expand All @@ -844,7 +859,7 @@ def wait_for_changes(
return next(
watchfiles.watch(
*watching_paths,
watch_filter=watchfiles.DefaultFilter(ignore_entity_patterns=ignore_files), # type: ignore
watch_filter=FileChangeFilter(ignore_file_patterns=ignore_file_patterns),
rust_timeout=0,
)
)
Expand Down
Loading