Skip to content
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
5 changes: 4 additions & 1 deletion babel/messages/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def extract_from_dir(
callback: Callable[[str, str, dict[str, Any]], object] | None = None,
strip_comment_tags: bool = False,
directory_filter: Callable[[str], bool] | None = None,
follow_links: bool = False,
) -> Generator[_FileExtractionResult, None, None]:
"""Extract messages from any source files found in the given directory.

Expand Down Expand Up @@ -228,6 +229,8 @@ def extract_from_dir(
:param directory_filter: a callback to determine whether a directory should
be recursed into. Receives the full directory path;
should return True if the directory is valid.
:param follow_links: whether to follow symbolic links to directories. Links
that form a cycle are followed indefinitely.
:see: `pathmatch`
"""
if dirname is None:
Expand All @@ -244,7 +247,7 @@ def extract_from_dir(
root_dir=dirname,
)

for root, dirnames, filenames in os.walk(dirname):
for root, dirnames, filenames in os.walk(dirname, followlinks=follow_links):
dirnames[:] = [
subdir for subdir in dirnames if directory_filter(os.path.join(root, subdir))
]
Expand Down
5 changes: 5 additions & 0 deletions babel/messages/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ class ExtractMessages(CommandMixin):
'header comment for the catalog'),
('last-translator=', None,
'set the name and email of the last translator in output'),
('follow-links', None,
'follow symbolic links when traversing directories'),
] # fmt: skip
boolean_options = [
'no-default-keywords',
Expand All @@ -364,6 +366,7 @@ class ExtractMessages(CommandMixin):
'sort-output',
'sort-by-file',
'strip-comments',
'follow-links',
]
as_args = 'input-paths'
multiple_value_options = (
Expand Down Expand Up @@ -403,6 +406,7 @@ def initialize_options(self):
self.version = None
self.add_comments = None
self.strip_comments = False
self.follow_links = False
self.include_lineno = True
self.ignore_dirs = None
self.header_comment = None
Expand Down Expand Up @@ -532,6 +536,7 @@ def run(self):
directory_filter=self.directory_filter,
keywords=self.keywords,
strip_comment_tags=self.strip_comments,
follow_links=self.follow_links,
)
for filename, lineno, message, comments, context in extracted:
if os.path.isfile(path):
Expand Down
1 change: 1 addition & 0 deletions docs/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ a collection of source files::
(default ".* ._")
--header-comment=HEADER_COMMENT
header comment for the catalog
--follow-links follow symbolic links when traversing directories


The meaning of ``--keyword`` values is as follows:
Expand Down
6 changes: 6 additions & 0 deletions tests/messages/frontend/test_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,12 @@ def test_extract_cli_knows_dash_s():
assert cmdinst.strip_comments


def test_extract_cli_knows_follow_links():
cmdinst = configure_cli_command("extract --follow-links -o foo babel")
assert isinstance(cmdinst, ExtractMessages)
assert cmdinst.follow_links


def test_extract_cli_knows_dash_dash_last_dash_translator():
cmdinst = configure_cli_command('extract --last-translator "FULL NAME EMAIL@ADDRESS" -o foo babel')
assert isinstance(cmdinst, ExtractMessages)
Expand Down
20 changes: 20 additions & 0 deletions tests/messages/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://github.com/python-babel/babel/commits/master/.

import os
import sys
from io import BytesIO, StringIO

Expand Down Expand Up @@ -174,3 +175,22 @@ def test_issue_1195_2():
'NOTE: This should still be considered, even if',
'the text is far away',
]


@pytest.mark.skipif(not hasattr(os, 'symlink'), reason='requires symlink support')
@pytest.mark.parametrize('follow_links', [False, True])
def test_extract_from_dir_follow_links(tmp_path, follow_links):
target = tmp_path / 'target'
target.mkdir()
(target / 'linked.py').write_text('_("linked")')
project = tmp_path / 'project'
project.mkdir()
(project / 'plain.py').write_text('_("plain")')
try:
(project / 'link').symlink_to(target, target_is_directory=True)
except OSError:
pytest.skip('cannot create symlinks')

results = extract.extract_from_dir(project, follow_links=follow_links)
messages = {message for _, _, message, _, _ in results}
assert messages == ({'plain', 'linked'} if follow_links else {'plain'})