Skip to content

Commit b58868b

Browse files
authored
feat: find Python files in Rust (#591)
1 parent 4f697a1 commit b58868b

10 files changed

+240
-136
lines changed

Cargo.lock

Lines changed: 87 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ crate-type = ["cdylib"]
1111
[dependencies]
1212
chardetng = "0.1.17"
1313
encoding_rs = "0.8.33"
14+
ignore = "0.4.22"
1415
log = "0.4.21"
16+
path-slash = "0.2.1"
1517
pyo3 = { version = "0.20.3", features = ["abi3-py38"] }
1618
pyo3-log = "0.9.0"
1719
rayon = "1.9.0"

pdm.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ classifiers = [
2525
]
2626
dependencies = [
2727
"click>=8.0.0,<9",
28-
"pathspec>=0.9.0",
2928
"colorama>=0.4.6; sys_platform == 'win32'",
3029
"tomli>=2.0.1; python_version < '3.11'"
3130
]

python/deptry/core.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from deptry.exceptions import IncorrectDependencyFormatError, UnsupportedPythonVersionError
1515
from deptry.imports.extract import get_imported_modules_from_list_of_files
1616
from deptry.module import ModuleBuilder, ModuleLocations
17-
from deptry.python_file_finder import PythonFileFinder
17+
from deptry.python_file_finder import get_all_python_files_in
1818
from deptry.reporters import JSONReporter, TextReporter
1919
from deptry.stdlibs import STDLIBS_PYTHON
2020
from deptry.violations import (
@@ -65,10 +65,7 @@ def run(self) -> None:
6565

6666
self._log_dependencies(dependencies_extract)
6767

68-
all_python_files = PythonFileFinder(
69-
self.exclude, self.extend_exclude, self.using_default_exclude, self.ignore_notebooks
70-
).get_all_python_files_in(self.root)
71-
68+
python_files = self._find_python_files()
7269
local_modules = self._get_local_modules()
7370
stdlib_modules = self._get_stdlib_modules()
7471

@@ -83,7 +80,7 @@ def run(self) -> None:
8380
).build(),
8481
locations,
8582
)
86-
for module, locations in get_imported_modules_from_list_of_files(all_python_files).items()
83+
for module, locations in get_imported_modules_from_list_of_files(python_files).items()
8784
]
8885
imported_modules_with_locations = [
8986
module_with_locations
@@ -99,6 +96,19 @@ def run(self) -> None:
9996

10097
self._exit(violations)
10198

99+
def _find_python_files(self) -> list[Path]:
100+
logging.debug("Collecting Python files to scan...")
101+
102+
python_files = get_all_python_files_in(
103+
self.root, self.exclude, self.extend_exclude, self.using_default_exclude, self.ignore_notebooks
104+
)
105+
106+
logging.debug(
107+
"Python files to scan for imports:\n%s\n", "\n".join(str(python_file) for python_file in python_files)
108+
)
109+
110+
return python_files
111+
102112
def _find_violations(
103113
self, imported_modules_with_locations: list[ModuleLocations], dependencies: list[Dependency]
104114
) -> list[Violation]:

python/deptry/python_file_finder.py

Lines changed: 12 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,18 @@
11
from __future__ import annotations
22

3-
import logging
4-
import os
5-
import re
6-
from dataclasses import dataclass
73
from pathlib import Path
8-
from typing import Pattern
94

10-
from pathspec import PathSpec
5+
from deptry.rust import find_python_files
116

127

13-
@dataclass
14-
class PythonFileFinder:
15-
"""
16-
Get a list of all .py and .ipynb files recursively within a directory.
17-
Args:
18-
exclude: A list of regex patterns of paths to ignore.
19-
extend_exclude: An additional list of regex patterns of paths to ignore.
20-
using_default_exclude: Whether the exclude list was explicitly set, or the default was used.
21-
ignore_notebooks: If ignore_notebooks is set to True, .ipynb files are ignored and only .py files are returned.
22-
"""
23-
24-
exclude: tuple[str, ...]
25-
extend_exclude: tuple[str, ...]
26-
using_default_exclude: bool
27-
ignore_notebooks: bool = False
28-
29-
def get_all_python_files_in(self, directories: tuple[Path, ...]) -> list[Path]:
30-
logging.debug("Collecting Python files to scan...")
31-
32-
source_files = set()
33-
34-
ignore_regex = re.compile("|".join(self.exclude + self.extend_exclude))
35-
file_lookup_suffixes = {".py"} if self.ignore_notebooks else {".py", ".ipynb"}
36-
37-
gitignore_spec = self._generate_gitignore_pathspec(Path())
38-
39-
for directory in directories:
40-
for root_str, dirs, files in os.walk(directory, topdown=True):
41-
root = Path(root_str)
42-
43-
if self._is_directory_ignored(root, ignore_regex):
44-
dirs[:] = []
45-
continue
46-
47-
for file_str in files:
48-
file = root / file_str
49-
if not self._is_file_ignored(file, file_lookup_suffixes, ignore_regex, gitignore_spec):
50-
source_files.add(file)
51-
52-
source_files_list = list(source_files)
53-
54-
logging.debug("Python files to scan for imports:\n%s\n", "\n".join([str(file) for file in source_files_list]))
55-
56-
return source_files_list
57-
58-
def _is_directory_ignored(self, directory: Path, ignore_regex: Pattern[str]) -> bool:
59-
return bool((self.exclude + self.extend_exclude) and ignore_regex.match(str(directory)))
60-
61-
def _is_file_ignored(
62-
self, file: Path, file_lookup_suffixes: set[str], ignore_regex: Pattern[str], gitignore_spec: PathSpec | None
63-
) -> bool:
64-
return bool(
65-
file.suffix not in file_lookup_suffixes
66-
or ((self.exclude + self.extend_exclude) and ignore_regex.match(file.as_posix()))
67-
or (gitignore_spec and gitignore_spec.match_file(file))
68-
)
69-
70-
def _generate_gitignore_pathspec(self, directory: Path) -> PathSpec | None:
71-
# If `exclude` is explicitly set, `.gitignore` is not taken into account.
72-
if not self.using_default_exclude:
73-
return None
74-
75-
try:
76-
with (directory / ".gitignore").open() as gitignore:
77-
return PathSpec.from_lines("gitwildmatch", gitignore)
78-
except FileNotFoundError:
79-
return None
8+
def get_all_python_files_in(
9+
directories: tuple[Path, ...],
10+
exclude: tuple[str, ...],
11+
extend_exclude: tuple[str, ...],
12+
using_default_exclude: bool,
13+
ignore_notebooks: bool = False,
14+
) -> list[Path]:
15+
return [
16+
Path(f)
17+
for f in find_python_files(directories, exclude, extend_exclude, using_default_exclude, ignore_notebooks)
18+
]

python/deptry/rust.pyi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
from pathlib import Path
2+
13
from .rust import Location as RustLocation
24

35
def get_imports_from_py_files(file_paths: list[str]) -> dict[str, list[RustLocation]]: ...
46
def get_imports_from_ipynb_files(file_paths: list[str]) -> dict[str, list[RustLocation]]: ...
7+
def find_python_files(
8+
directories: tuple[Path, ...],
9+
exclude: tuple[str, ...],
10+
extend_exclude: tuple[str, ...],
11+
using_default_exclude: bool,
12+
ignore_notebooks: bool = False,
13+
) -> list[str]: ...
514

615
class Location:
716
file: str

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use pyo3::prelude::*;
55
mod file_utils;
66
mod imports;
77
mod location;
8+
mod python_file_finder;
89
mod visitor;
910

1011
use location::Location;
@@ -18,6 +19,7 @@ fn rust(_py: Python, m: &PyModule) -> PyResult<()> {
1819
imports::ipynb::get_imports_from_ipynb_files,
1920
m
2021
)?)?;
22+
m.add_function(wrap_pyfunction!(python_file_finder::find_python_files, m)?)?;
2123
m.add_class::<Location>()?;
2224
Ok(())
2325
}

0 commit comments

Comments
 (0)