-
Notifications
You must be signed in to change notification settings - Fork 239
fix: Sort rsync include/exclude options according to specificity (#561, #1420) #1895
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
Draft
DerekVeit
wants to merge
84
commits into
bit-team:dev
Choose a base branch
from
DerekVeit:path_selections
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
84 commits
Select commit
Hold shift + click to select a range
ffda525
Add test.logging with log function.
550b786
Add bit_config pytest fixture.
6d1594e
Add bit_snapshot pytest fixture.
3d1453e
Add test.filetree.
1109687
Add test_rsync_selections.
0e90172
Make-adjusted python paths.
71dedd0
Snapshots.rsyncSuffix: docstring note about extra parameters.
2908bcd
conftest.py: add a blank line.
1af7596
filetree.py: add docstrings.
76a0b63
filetree.py: wrap 2 long lines.
fc560c0
test.logging: add docstring.
139f6f7
test.logging: add horizontal lines.
d8bed28
test_rsync_selections: load the cases in the params functions.
d786ad6
test_rsync_selections: params_for_cases: add docstring and comments.
36a5c50
test_rsync_selections: combine params_for_cases and params_for_raise_…
fbd89a1
conftest: update for the refactoring of Config.setSnapshotsPath (67e9…
25d4151
test_rsync_selections: specify selection mode(s) when calling params_…
5217805
selection_raise_cases: remove "both-implied-dir" case.
a1552a3
selection_cases: only use absolute path in excludes.
c15ecae
selection_cases: update cases with original failing.
b1e03f7
selection_cases: add "various" case.
0140b22
snapshots.py: add Snapshots.pathSelections method.
61285c2
snapshots.py: use pathSelections if SELECTIONS_MODE == "sorted".
f635129
snapshots.py: Snapshots.takeSnapshot: save cmd.
c45370f
test_rsync_selections: params_for_cases: allow some conditional case …
625cedd
test_rsync_selections: add update_config.
50dacc9
test_rsync_selections: use update_config.
d5c5671
test_rsync_selections: remove add_includes.
ae4dc36
test_rsync_selections: use a subdirectory for the sample files.
b04df4c
test_rsync_selections: test_rsyncSuffix__raises: some log message for…
2b67f44
test_rsync_selections: rewrite prepend_paths, handling root directory.
1097253
test_rsync_selections: add root cases.
378edda
filetree.py: improve docstrings.
1b955ec
filetree.py: parse_tree: add comments.
4372a7d
filetree.py: parse_tree: add two descriptive variables.
64288cf
filetree.py: parse_tree: add more comments.
235b465
filetree.py: rewrite tree_from_files with sort_paths.
5300e67
filetree.py: import Path itself.
564b3cf
filetree.py: parse_tree: add parent_path variable.
409ada8
filetree.py: add type hints.
ad17012
filetree.py: parse_tree: put trailing comments on their own lines.
b4a9b1b
filetree.py: parse_tree: add a blank line.
8e7b181
filetree.py: tree_from_files: ensure consistent whitespace.
b85d951
test_tree_from_files: parametrize and add special cases.
9362ebc
test_filetree.py: add type hints.
549c79f
filetree.py: improve the module docstring a bit.
866e56e
test_logging: allow for the extra newlines and horizontal lines.
43fcbf9
logging, test_logging: add type hints.
62c698d
conftest.py: import Path itself.
e002054
conftest, test_fixtures: add type hints.
c26bbae
conftest: tidy formatting (black).
c081844
conftest, test_fixtures: add docstrings.
acbf980
logging, test_logging: add docstrings.
e2135e7
test_filetree.py: increase some indentation.
4196c00
test_rsync_selections: params_for_cases: raise for incomplete case spec.
6d4b965
filetree.py: improve the module docstring a bit more.
e7e71fe
test_rsync_selections: add the "# act" line in each test function.
78cdd1d
test_rsync_selections: add type hints.
746647b
Snapshots.pathSelections: fix docstring typo (thanks to codespell).
81b364e
Add a CHANGELOG entry.
ba5101d
filetree.py: use Union to keep Python 3.9 compatibility.
c9c8124
test_rsync_selections..py: use Union to keep Python 3.9 compatibility.
e249b99
Revert "Make-adjusted python paths."
52c7478
Snapshots.rsyncSuffix: specify a getattr default (as was the intent).
212f1b2
test_rsync_selections.py: import Path itself.
35fe3a5
Add YAML version of test/selection_cases.
34e3b52
Add ddt and PyYAML.
8c71db1
test_rsync_selections.py: add unittest-based test_rsyncSuffix.
edeb50d
Add YAML version of test/selection_raise_cases.
4c40070
test_rsync_selections.py: add unittest-based test_rsyncSuffix__raises.
f2877c8
test_rsync_selections.py: allow for cases having other flags.
d7b4397
test_rsync_selections.py: assert_backup: normalize the trees sooner.
4e24949
test_rsync_selections.py: add unittest-based cases for root dir.
8e92d3e
RsyncSuffixTests.test_rsyncSuffix__raises: remove extra blank line.
b37b6dc
test_rsync_selections.py: remove the pytest-based tests.
c45f482
Remove the pytest fixtures.
9fc504b
Remove the non-YAML selection case files.
169d547
test_rsync_selections.py: unify the regular and raise cases.
509b4d7
selection_cases: rename "simple" case.
9d4dd9b
Merge branch 'dev' into path_selections
buhtz 3723a02
Merge branch 'dev' into path_selections
buhtz 7f6e91d
Merge branch 'dev' into path_selections
buhtz 8d51323
Merge branch 'dev' into path_selections
buhtz 42680ad
Merge branch 'dev' into path_selections
buhtz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
from pathlib import Path | ||
import re | ||
import textwrap | ||
from typing import Iterable, Union | ||
|
||
|
||
""" | ||
A "tree" in this module means a multi-line text representation of files and | ||
directories like this: | ||
|
||
a_tree = ''' | ||
foo/ | ||
bar/ | ||
file-a | ||
baz/ | ||
file-b | ||
file-c | ||
''' | ||
|
||
This is to help make backup tests that are easy to read and write and are clear | ||
in their expectations and results. | ||
|
||
Features of a tree: | ||
- Directories are distinguished by a trailing slash | ||
- Within a directory, names must be sorted with directories before files | ||
- Indentation increment must be 4 spaces | ||
- Initial indentation and leading and trailing whitespace are arbitrary | ||
|
||
Useful functions: | ||
- files_from_tree: Create a file structure from a tree representation | ||
- tree_from_files: Generate a tree representation from an existing file structure | ||
- normal: Normalize the indentation and whitespace of a tree string | ||
|
||
An example of how this might be used to test a backup procedure: | ||
1) empty directories A and B are created. | ||
2) tree_a is defined with a string to specify some files. | ||
3) files_from_tree(A, tree_a) | ||
4) a backup is made from directory A to directory B. | ||
5) tree_b = tree_from_files(B) | ||
6) assert tree_b == normal(tree_a) | ||
""" | ||
|
||
|
||
def files_from_tree(parent_dir: Union[str, Path], tree: str) -> None: | ||
"""Create in `parent_dir` the structure described by `tree`.""" | ||
dir_paths, file_paths = parse_tree(parent_dir, tree) | ||
|
||
for path in dir_paths: | ||
path.mkdir() | ||
|
||
for path in file_paths: | ||
path.touch() | ||
|
||
|
||
def parse_tree(parent_dir: Union[str, Path], tree: str) -> tuple[list[Path], list[Path]]: | ||
"""Return the paths described by `tree` in `parent_dir`.""" | ||
parent_path = Path(parent_dir) | ||
|
||
# a stack of ancestral directories at the current line | ||
parent_dirs: list[Path] = [] | ||
# a stack of corresponding indentation strings | ||
indents: list[str] = [] | ||
# most recent directory name in each ancestral directory | ||
prec_dirname: dict[Path, str] = {} | ||
# most recent file name in each ancestral directory | ||
prec_filename: dict[Path, str] = {} | ||
|
||
# full paths of the directories | ||
dir_paths = [] | ||
# full paths of the files | ||
file_paths = [] | ||
|
||
prev_filename: str = "" | ||
|
||
for line in tree.splitlines(): | ||
if not line.strip(): | ||
continue | ||
|
||
indent, filename = split_indent(line) | ||
|
||
if not re.match(r"^(?: )*$", indent): | ||
raise ValueError(f"indentation must be of 4-space increments: {line = }") | ||
|
||
if not indents: | ||
# first iteration | ||
indents.append(indent) | ||
parent_dirs.append(parent_path) | ||
elif indent == indents[-1]: | ||
# the same indentation level and same parent directory | ||
pass | ||
elif indent in indents[:-1]: | ||
# a previous indentation level and corresponding parent directory | ||
index = indents.index(indent) | ||
indents = indents[: index + 1] | ||
parent_dirs = parent_dirs[: index + 1] | ||
elif len(indent) > len(indents[-1]): | ||
# should be reading the contents of a directory just seen | ||
if not prev_filename.endswith("/"): | ||
raise ValueError(f"indentation without directory: {line = }") | ||
indents.append(indent) | ||
parent_dirs.append(parent_dirs[-1] / prev_filename[:-1]) | ||
else: | ||
raise ValueError(f"inconsistent tree indentation: {line = }") | ||
|
||
preceding_dirname_here = prec_dirname.get(parent_dirs[-1], "") | ||
preceding_filename_here = prec_filename.get(parent_dirs[-1], "") | ||
if filename.endswith("/"): | ||
if not preceding_dirname_here < filename: | ||
raise ValueError( | ||
f"listed out of order after {preceding_dirname_here!r}: {line = }" | ||
) | ||
if preceding_filename_here: | ||
raise ValueError(f"directory cannot be listed after file(s): {line = }") | ||
|
||
# add the path for this directory | ||
dir_paths.append(parent_dirs[-1] / filename) | ||
|
||
prec_dirname[parent_dirs[-1]] = filename | ||
else: | ||
if not preceding_filename_here < filename: | ||
raise ValueError( | ||
f"listed out of order after {preceding_filename_here!r}: {line = }" | ||
) | ||
|
||
# add the path for this file | ||
file_paths.append(parent_dirs[-1] / filename) | ||
|
||
prec_filename[parent_dirs[-1]] = filename | ||
|
||
prev_filename = filename | ||
|
||
return dir_paths, file_paths | ||
|
||
|
||
def split_indent(text: str) -> tuple[str, str]: | ||
"""Return the indentation and the remainder of the string as 2 strings.""" | ||
mo = re.match(r"( *)(.*)", text) | ||
indent, remainder = mo.groups() # type: ignore [union-attr] | ||
return indent, remainder | ||
|
||
|
||
def tree_from_files(parent_dir: Union[str, Path]) -> str: | ||
"""Return a tree describing the contents of `parent_dir`.""" | ||
parent_path = Path(parent_dir) | ||
initial_depth = len(parent_path.parts) | ||
tree_lines = [] | ||
for path in sort_paths(parent_path.rglob("*")): | ||
if path == parent_path: | ||
continue | ||
indent = " " * (len(path.parents) - initial_depth) | ||
name = path.name + ("/" if path.is_dir() else "") | ||
tree_lines.append(indent + name) | ||
|
||
return "\n" + "\n".join(tree_lines) + "\n" | ||
|
||
|
||
def normal(tree_string: str) -> str: | ||
"""Normalize indentation depth and surrounding whitespace of the tree.""" | ||
return f"\n{textwrap.dedent(tree_string).strip()}\n" | ||
|
||
|
||
def sort_paths(paths: Iterable[Path]) -> list[Path]: | ||
"""Sort a list of paths. | ||
|
||
Within each directory, subdirectories are listed before files. | ||
""" | ||
return sorted( | ||
paths, | ||
key=lambda path: ( | ||
[(False, part) for part in path.parts[:-1]] | ||
+ [(not path.is_dir(), path.name)] | ||
), | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just based on a gut feeling, I would say this method can be split into several methods.
Can the sorting part be separated from "self.config" and any other BIT-specific code? And than make that a module function instead of a method.