Skip to content

Commit 568e7e6

Browse files
committed
adding more tests, refactoring
1 parent 12305a2 commit 568e7e6

File tree

15 files changed

+1167
-302
lines changed

15 files changed

+1167
-302
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pathspec
44
build
55
twine
66
pyinstaller
7+
pytest-cov

setup.cfg

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,28 @@ package_dir =
1818
packages = find:
1919
python_requires = >=3.9
2020
install_requires =
21-
pathspec
22-
pyyaml
21+
pathspec>=0.9
22+
pyyaml>=5.4
2323

2424
[options.packages.find]
2525
where = src
2626

2727
[options.entry_points]
2828
console_scripts =
29-
treemapper = treemapper.treemapper:main
29+
treemapper = treemapper.treemapper:main
30+
31+
[options.extras_require]
32+
dev =
33+
pytest>=7.0
34+
pytest-cov>=3.0
35+
build>=0.10
36+
twine>=4.0
37+
pyinstaller>=5.0
38+
flake8>=5.0
39+
black>=23.0
40+
isort>=5.10
41+
mypy>=1.0
42+
types-PyYAML
43+
types-pathspec
44+
pyyaml
45+
pathspec

src/treemapper/cli.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
# src/treemapper/cli.py
12
import argparse
23
import sys
34
from pathlib import Path
4-
from typing import Tuple
5+
from typing import Tuple, Optional # <--- Добавлен Optional
56

6-
7-
def parse_args() -> Tuple[Path, Path, Path, bool, int]:
7+
# ---> ИЗМЕНЕНИЕ: Заменяем | None на Optional[...] <---
8+
def parse_args() -> Tuple[Path, Optional[Path], Optional[Path], bool, int]:
89
"""Parse command line arguments."""
910
parser = argparse.ArgumentParser(
11+
prog='treemapper',
1012
description="Generate a YAML representation of a directory structure.",
1113
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
1214

@@ -29,7 +31,7 @@ def parse_args() -> Tuple[Path, Path, Path, bool, int]:
2931
parser.add_argument(
3032
"--no-default-ignores",
3133
action="store_true",
32-
help="Disable all default ignores (including .gitignore and .treemapperignore)")
34+
help="Disable default ignores (.treemapperignore, .gitignore, output file)")
3335

3436
parser.add_argument(
3537
"-v", "--verbosity",
@@ -41,15 +43,26 @@ def parse_args() -> Tuple[Path, Path, Path, bool, int]:
4143

4244
args = parser.parse_args()
4345

44-
root_dir = Path(args.directory).resolve()
45-
if not root_dir.is_dir():
46-
print(f"Error: The path '{root_dir}' is not a valid directory.")
47-
sys.exit(1)
46+
try:
47+
root_dir = Path(args.directory).resolve(strict=True)
48+
if not root_dir.is_dir():
49+
print(f"Error: The path '{root_dir}' is not a valid directory.", file=sys.stderr)
50+
sys.exit(1)
51+
except FileNotFoundError:
52+
print(f"Error: The directory '{args.directory}' does not exist.", file=sys.stderr)
53+
sys.exit(1)
54+
except Exception as e:
55+
print(f"Error resolving directory path '{args.directory}': {e}", file=sys.stderr)
56+
sys.exit(1)
4857

4958
output_file = Path(args.output_file)
5059
if not output_file.is_absolute():
5160
output_file = Path.cwd() / output_file
5261

53-
ignore_file = Path(args.ignore_file) if args.ignore_file else None
62+
ignore_file_path: Optional[Path] = None # Используем Optional для ясности
63+
if args.ignore_file:
64+
ignore_file_path = Path(args.ignore_file)
65+
if not ignore_file_path.is_absolute():
66+
ignore_file_path = Path.cwd() / ignore_file_path
5467

55-
return root_dir, ignore_file, output_file, args.no_default_ignores, args.verbosity
68+
return root_dir, ignore_file_path, output_file, args.no_default_ignores, args.verbosity

src/treemapper/ignore.py

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
# src/treemapper/ignore.py
12
import logging
23
import os
34
from pathlib import Path
4-
from typing import List, Dict, Tuple
5+
# ---> ИЗМЕНЕНИЕ: Добавляем импорты из typing <---
6+
from typing import List, Dict, Tuple, Optional
57

68
import pathspec
79

@@ -10,11 +12,16 @@ def read_ignore_file(file_path: Path) -> List[str]:
1012
"""Read the ignore patterns from the specified ignore file."""
1113
ignore_patterns = []
1214
if file_path.is_file():
13-
with file_path.open('r') as f:
14-
ignore_patterns = [line.strip() for line in f
15-
if line.strip() and not line.startswith('#')]
16-
logging.info(f"Using ignore patterns from {file_path}")
17-
logging.debug(f"Read ignore patterns from {file_path}: {ignore_patterns}")
15+
try:
16+
with file_path.open('r', encoding='utf-8') as f:
17+
ignore_patterns = [line.strip() for line in f
18+
if line.strip() and not line.startswith('#')]
19+
logging.info(f"Using ignore patterns from {file_path}")
20+
logging.debug(f"Read ignore patterns from {file_path}: {ignore_patterns}")
21+
except IOError as e:
22+
logging.warning(f"Could not read ignore file {file_path}: {e}")
23+
except UnicodeDecodeError as e:
24+
logging.warning(f"Could not decode ignore file {file_path} as UTF-8: {e}")
1825
return ignore_patterns
1926

2027

@@ -24,89 +31,110 @@ def load_pathspec(patterns: List[str], syntax='gitwildmatch') -> pathspec.PathSp
2431
logging.debug(f"Loaded pathspec with patterns: {patterns}")
2532
return spec
2633

27-
34+
# ---> ИЗМЕНЕНИЕ: Заменяем | None на Optional[...] <---
2835
def get_ignore_specs(
2936
root_dir: Path,
30-
custom_ignore_file: Path = None,
37+
custom_ignore_file: Optional[Path] = None,
3138
no_default_ignores: bool = False,
32-
output_file: Path = None
39+
output_file: Optional[Path] = None
3340
) -> Tuple[pathspec.PathSpec, Dict[Path, pathspec.PathSpec]]:
3441
"""Get combined ignore specs and git ignore specs."""
3542
default_patterns = get_default_patterns(root_dir, no_default_ignores, output_file)
3643
custom_patterns = get_custom_patterns(root_dir, custom_ignore_file)
37-
combined_patterns = custom_patterns if no_default_ignores else default_patterns + custom_patterns
44+
45+
if no_default_ignores:
46+
combined_patterns = custom_patterns
47+
if output_file:
48+
try:
49+
resolved_output = output_file.resolve()
50+
resolved_root = root_dir.resolve()
51+
if resolved_output.is_relative_to(resolved_root):
52+
relative_output_str = resolved_output.relative_to(resolved_root).as_posix()
53+
output_pattern = f"/{relative_output_str}"
54+
if output_pattern not in combined_patterns:
55+
combined_patterns.append(output_pattern)
56+
logging.debug(f"Adding output file to ignores (no_default_ignores=True): {output_pattern}")
57+
except ValueError: pass
58+
except Exception as e:
59+
logging.warning(f"Could not determine relative path for output file {output_file}: {e}")
60+
else:
61+
combined_patterns = default_patterns + custom_patterns
62+
63+
logging.debug(f"Ignore specs params: no_default_ignores={no_default_ignores}")
64+
logging.debug(f"Default patterns (used unless no_default_ignores): {default_patterns}")
65+
logging.debug(f"Custom patterns (-i): {custom_patterns}")
66+
logging.debug(f"Combined patterns for spec: {combined_patterns}")
67+
3868
combined_spec = load_pathspec(combined_patterns)
3969
gitignore_specs = get_gitignore_specs(root_dir, no_default_ignores)
4070

4171
return combined_spec, gitignore_specs
4272

43-
def get_default_patterns(root_dir: Path, no_default_ignores: bool, output_file: Path) -> List[str]:
44-
"""Retrieve default ignore patterns."""
73+
# ---> ИЗМЕНЕНИЕ: Заменяем | None на Optional[...] <---
74+
def get_default_patterns(root_dir: Path, no_default_ignores: bool, output_file: Optional[Path]) -> List[str]:
75+
"""Retrieve default ignore patterns ONLY IF no_default_ignores is FALSE."""
4576
if no_default_ignores:
4677
return []
4778

4879
patterns = []
49-
# Add .treemapperignore patterns
5080
treemapper_ignore_file = root_dir / ".treemapperignore"
5181
patterns.extend(read_ignore_file(treemapper_ignore_file))
5282

53-
# Add default git patterns
54-
patterns.extend([".git/", ".git/**"])
55-
56-
# Add the output file to ignore patterns
5783
if output_file:
5884
try:
59-
relative_output = output_file.resolve().relative_to(root_dir.resolve())
60-
patterns.append(str(relative_output))
61-
if str(relative_output.parent) != ".":
62-
patterns.append(str(relative_output.parent) + "/")
63-
except ValueError:
64-
pass # Output file is outside root_dir; no need to add to ignores
85+
resolved_output = output_file.resolve()
86+
resolved_root = root_dir.resolve()
87+
try:
88+
relative_output = resolved_output.relative_to(resolved_root)
89+
output_pattern = f"/{relative_output.as_posix()}"
90+
patterns.append(output_pattern)
91+
logging.debug(f"Adding output file to default ignores: {output_pattern}")
92+
except ValueError:
93+
logging.debug(f"Output file {output_file} is outside root directory {root_dir}, not adding to default ignores.")
94+
except Exception as e:
95+
logging.warning(f"Could not determine relative path for output file {output_file}: {e}")
6596

6697
return patterns
6798

68-
def get_custom_patterns(root_dir: Path, custom_ignore_file: Path) -> List[str]:
69-
"""Retrieve custom ignore patterns."""
99+
# ---> ИЗМЕНЕНИЕ: Заменяем | None на Optional[...] <---
100+
def get_custom_patterns(root_dir: Path, custom_ignore_file: Optional[Path]) -> List[str]:
101+
"""Retrieve custom ignore patterns from the file specified with -i."""
70102
if not custom_ignore_file:
71103
return []
72104

73-
custom_ignore_file = custom_ignore_file if custom_ignore_file.is_absolute() else root_dir / custom_ignore_file
105+
if not custom_ignore_file.is_absolute():
106+
custom_ignore_file = Path.cwd() / custom_ignore_file
107+
74108
if custom_ignore_file.is_file():
75109
return read_ignore_file(custom_ignore_file)
110+
else:
111+
logging.warning(f"Custom ignore file '{custom_ignore_file}' not found.")
112+
return []
76113

77-
logging.warning(f"Custom ignore file '{custom_ignore_file}' not found.")
78-
return []
79114

80115
def get_gitignore_specs(root_dir: Path, no_default_ignores: bool) -> Dict[Path, pathspec.PathSpec]:
81-
"""Retrieve gitignore specs for all .gitignore files in the directory."""
116+
"""Retrieve gitignore specs for all .gitignore files found within root_dir."""
82117
if no_default_ignores:
83118
return {}
84119

85120
gitignore_specs = {}
86-
for dirpath, _, filenames in os.walk(root_dir):
87-
if ".gitignore" in filenames:
88-
gitignore_path = Path(dirpath) / ".gitignore"
89-
patterns = read_ignore_file(gitignore_path)
90-
gitignore_specs[Path(dirpath)] = load_pathspec(patterns)
121+
try:
122+
for dirpath_str, dirnames, filenames in os.walk(root_dir, topdown=True):
123+
if '.git' in dirnames:
124+
dirnames.remove('.git')
125+
if ".gitignore" in filenames:
126+
gitignore_path = Path(dirpath_str) / ".gitignore"
127+
patterns = read_ignore_file(gitignore_path)
128+
if patterns:
129+
gitignore_specs[Path(dirpath_str)] = load_pathspec(patterns)
130+
except OSError as e:
131+
logging.warning(f"Error walking directory {root_dir} to find .gitignore files: {e}")
91132

92133
return gitignore_specs
93134

94135

95-
96-
def should_ignore(file_path: str, combined_spec: pathspec.PathSpec) -> bool:
136+
def should_ignore(relative_path_str: str, combined_spec: pathspec.PathSpec) -> bool:
97137
"""Check if a file or directory should be ignored based on combined pathspec."""
98-
paths_to_check = [file_path]
99-
100-
# Add path variations for checking
101-
if file_path.endswith('/'):
102-
paths_to_check.append(file_path)
103-
104-
# Add parent directories with trailing slash
105-
for part in Path(file_path).parents:
106-
if part != Path('.'):
107-
paths_to_check.append(part.as_posix() + '/')
108-
109-
result = any(combined_spec.match_file(path) for path in paths_to_check)
110-
logging.debug(
111-
f"Should ignore '{file_path}': {result} (checking paths: {paths_to_check})")
112-
return result
138+
is_ignored = combined_spec.match_file(relative_path_str)
139+
logging.debug(f"Checking combined spec ignore for '{relative_path_str}': {is_ignored}")
140+
return is_ignored

0 commit comments

Comments
 (0)