Skip to content

Commit 2e3234e

Browse files
authored
fix: select file from paths that are not in the root folder (#157)
1 parent 438a1fe commit 2e3234e

File tree

6 files changed

+105
-50
lines changed

6 files changed

+105
-50
lines changed

src/twyn/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def entry_point() -> None:
2323
@click.option("--config", type=click.STRING)
2424
@click.option(
2525
"--dependency-file",
26-
type=click.Choice(list(DEPENDENCY_FILE_MAPPING.keys())),
26+
type=str,
2727
help=(
2828
"Dependency file to analyze. By default, twyn will search in the current directory "
2929
"for supported files, but this option will override that behavior."
@@ -77,6 +77,9 @@ def run(
7777
if dependency and dependency_file:
7878
raise ValueError("Only one of --dependency or --dependency-file can be set at a time.")
7979

80+
if dependency_file and not any(dependency_file.endswith(key) for key in DEPENDENCY_FILE_MAPPING):
81+
raise ValueError("Dependency file name not supported.")
82+
8083
return int(
8184
check_dependencies(
8285
config_file=config,

src/twyn/dependency_parser/dependency_selector.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ def get_dependency_parser(self) -> AbstractParser:
5050
if self.dependency_file:
5151
logger.debug("Dependency file provided. Assigning a parser.")
5252
dependency_file_parser = self.get_dependency_file_parser_from_file_name()
53+
file_parser = dependency_file_parser(self.dependency_file)
5354
else:
5455
logger.debug("No dependency file provided. Attempting to locate one.")
5556
dependency_file_parser = self.auto_detect_dependency_file_parser()
57+
file_parser = dependency_file_parser()
5658

57-
file_parser = dependency_file_parser()
5859
logger.debug(f"Assigned {file_parser} parser for local dependencies file.")
5960

6061
return file_parser

tests/conftest.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
11
import os
2-
from typing import Generator
2+
from contextlib import contextmanager
3+
from pathlib import Path
4+
from typing import Iterator
35

46
import pytest
57

68

9+
@contextmanager
10+
def create_tmp_file(path: Path, data: str) -> Iterator[str]:
11+
path.write_text(data)
12+
yield str(path)
13+
os.remove(path)
14+
15+
716
@pytest.fixture
8-
def requirements_txt_file(tmp_path) -> Generator[str, None, None]:
17+
def requirements_txt_file(tmp_path: Path) -> Iterator[str]:
918
requirements_txt_file = tmp_path / "requirements.txt"
10-
requirements_txt_file.write_text(
11-
"""
19+
20+
data = """
1221
South==1.0.1 --hash=sha256:abcdefghijklmno
1322
pycrypto>=2.6
1423
"""
15-
)
16-
yield str(requirements_txt_file)
17-
os.remove(requirements_txt_file)
24+
25+
with create_tmp_file(requirements_txt_file, data) as tmp_file:
26+
yield tmp_file
1827

1928

2029
@pytest.fixture
21-
def poetry_lock_file_lt_1_5(tmp_path) -> Generator[str, None, None]:
30+
def poetry_lock_file_lt_1_5(tmp_path: Path) -> Iterator[str]:
2231
"""Poetry lock version < 1.5."""
2332
poetry_lock_file = tmp_path / "poetry.lock"
24-
poetry_lock_file.write_text(
25-
"""
33+
data = """
2634
[[package]]
2735
name = "charset-normalizer"
2836
version = "3.0.1"
@@ -61,17 +69,15 @@ def poetry_lock_file_lt_1_5(tmp_path) -> Generator[str, None, None]:
6169
flake8 = []
6270
mccabe = []
6371
"""
64-
)
65-
yield str(poetry_lock_file)
66-
os.remove(poetry_lock_file)
72+
with create_tmp_file(poetry_lock_file, data) as tmp_file:
73+
yield tmp_file
6774

6875

6976
@pytest.fixture
70-
def poetry_lock_file_ge_1_5(tmp_path) -> Generator[str, None, None]:
77+
def poetry_lock_file_ge_1_5(tmp_path: Path) -> Iterator[str]:
7178
"""Poetry lock version >= 1.5."""
7279
poetry_lock_file = tmp_path / "poetry.lock"
73-
poetry_lock_file.write_text(
74-
"""
80+
data = """
7581
[[package]]
7682
name = "charset-normalizer"
7783
version = "3.0.1"
@@ -107,16 +113,14 @@ def poetry_lock_file_ge_1_5(tmp_path) -> Generator[str, None, None]:
107113
flake8 = []
108114
mccabe = []
109115
"""
110-
)
111-
yield str(poetry_lock_file)
112-
os.remove(poetry_lock_file)
116+
with create_tmp_file(poetry_lock_file, data) as tmp_file:
117+
yield tmp_file
113118

114119

115120
@pytest.fixture
116-
def pyproject_toml_file(tmp_path) -> Generator[str, None, None]:
121+
def pyproject_toml_file(tmp_path: Path) -> Iterator[str]:
117122
pyproject_toml = tmp_path / "pyproject.toml"
118-
pyproject_toml.write_text(
119-
"""
123+
data = """
120124
[tool.poetry.dependencies]
121125
python = "^3.11"
122126
requests = "^2.28.2"
@@ -136,6 +140,5 @@ def pyproject_toml_file(tmp_path) -> Generator[str, None, None]:
136140
allowlist=["boto4", "boto2"]
137141
138142
"""
139-
)
140-
yield str(pyproject_toml)
141-
os.remove(pyproject_toml)
143+
with create_tmp_file(pyproject_toml, data) as tmp_file:
144+
yield tmp_file

tests/dependency_parser/test_dependency_selector.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,39 @@
1010

1111

1212
class TestDependencySelector:
13-
@patch("twyn.dependency_parser.poetry_lock.PoetryLockParser.file_exists")
14-
@patch("twyn.dependency_parser.requirements_txt.RequirementsTxtParser.file_exists")
15-
@patch("twyn.dependency_parser.dependency_selector.DependencySelector._raise_for_selected_parsers")
1613
@pytest.mark.parametrize(
17-
"file_name, requirements_exists, poetry_exists, parser_obj",
14+
"file_name, parser_obj",
1815
[
19-
(None, True, False, RequirementsTxtParser), # auto detect requirements.txt
20-
(None, False, True, PoetryLockParser), # auto detect poetry.lock
2116
(
2217
"requirements.txt",
23-
None,
24-
None,
2518
RequirementsTxtParser,
2619
), # because file is specified, we won't autocheck
27-
("poetry.lock", None, None, PoetryLockParser),
28-
("/some/path/poetry.lock", None, None, PoetryLockParser),
29-
("/some/path/requirements.txt", None, None, RequirementsTxtParser),
20+
("poetry.lock", PoetryLockParser),
21+
("/some/path/poetry.lock", PoetryLockParser),
22+
("/some/path/requirements.txt", RequirementsTxtParser),
3023
],
3124
)
32-
def test_get_dependency_parser(
33-
self,
34-
_raise_for_selected_parsers,
35-
req_exists,
36-
poet_exists,
37-
file_name,
38-
requirements_exists,
39-
poetry_exists,
40-
parser_obj,
41-
):
42-
req_exists.return_value = requirements_exists
43-
poet_exists.return_value = poetry_exists
25+
def test_get_dependency_parser(self, file_name, parser_obj):
4426
parser = DependencySelector(file_name).get_dependency_parser()
4527
assert isinstance(parser, parser_obj)
28+
assert str(parser.file_handler.file_path).endswith(file_name)
29+
30+
@patch("twyn.dependency_parser.poetry_lock.PoetryLockParser.file_exists")
31+
@patch("twyn.dependency_parser.requirements_txt.RequirementsTxtParser.file_exists")
32+
def test_get_dependency_parser_auto_detect_requirements_file(
33+
self, req_file_exists, poetry_file_exists, requirements_txt_file
34+
):
35+
# Twyn finds the local poetry.lock file.
36+
# It proves that it can show a requirements.txt file still after mocking the poetry file parser.
37+
poetry_file_exists.return_value = False
38+
req_file_exists.return_value = True
39+
40+
parser = DependencySelector("").get_dependency_parser()
41+
assert isinstance(parser, RequirementsTxtParser)
42+
43+
def test_get_dependency_parser_auto_detect_poetry_file(self, poetry_lock_file_lt_1_5):
44+
parser = DependencySelector("").get_dependency_parser()
45+
assert isinstance(parser, PoetryLockParser)
4646

4747
@pytest.mark.parametrize(
4848
"exists, exception",

tests/main/test_cli.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ def test_click_arguments_dependency_file(self, mock_check_dependencies):
5353
)
5454
]
5555

56+
@patch("twyn.cli.check_dependencies")
57+
def test_click_arguments_dependency_file_in_different_path(self, mock_check_dependencies):
58+
runner = CliRunner()
59+
runner.invoke(
60+
cli.run,
61+
[
62+
"--dependency-file",
63+
"/path/requirements.txt",
64+
],
65+
)
66+
67+
assert mock_check_dependencies.call_args_list == [
68+
call(
69+
config_file=None,
70+
dependency_file="/path/requirements.txt",
71+
dependencies_cli=None,
72+
selector_method=None,
73+
verbosity=AvailableLoggingLevels.none,
74+
)
75+
]
76+
5677
@patch("twyn.cli.check_dependencies")
5778
def test_click_arguments_single_dependency_cli(self, mock_check_dependencies):
5879
runner = CliRunner()
@@ -128,3 +149,11 @@ def test_only_one_verbosity_level_is_allowed(self):
128149
match="Only one verbosity level is allowed. Choose either -v or -vv.",
129150
):
130151
runner.invoke(cli.run, ["-v", "-vv"], catch_exceptions=False)
152+
153+
def test_dependency_file_name_has_to_be_recognized(self):
154+
runner = CliRunner()
155+
with pytest.raises(
156+
ValueError,
157+
match="Dependency file name not supported.",
158+
):
159+
runner.invoke(cli.run, ["--dependency-file", "requirements-dev.txt"], catch_exceptions=False)

tests/main/test_main.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
get_parsed_dependencies_from_file,
1111
)
1212

13+
from tests.conftest import create_tmp_file
14+
1315

1416
@pytest.mark.usefixtures("disable_track")
1517
class TestCheckDependencies:
@@ -167,6 +169,23 @@ def test_check_dependencies_with_input_from_cli_detects_typosquats(self, mock_to
167169

168170
assert error is True
169171

172+
@patch("twyn.main.TopPyPiReference")
173+
def test_check_dependencies_with_input_loads_file_from_different_location(
174+
self, mock_top_pypi_reference, tmp_path, tmpdir
175+
):
176+
mock_top_pypi_reference.return_value.get_packages.return_value = {"mypackage"}
177+
tmpdir.mkdir("fake-dir")
178+
tmp_file = tmp_path / "fake-dir" / "requirements.txt"
179+
with create_tmp_file(tmp_file, "mypackag"):
180+
error = check_dependencies(
181+
config_file=None,
182+
dependency_file=str(tmp_file),
183+
dependencies_cli=None,
184+
selector_method="first-letter",
185+
)
186+
187+
assert error is True
188+
170189
@patch("twyn.main.TopPyPiReference")
171190
def test_check_dependencies_with_input_from_cli_accepts_multiple_dependencies(self, mock_top_pypi_reference):
172191
mock_top_pypi_reference.return_value.get_packages.return_value = {"requests", "mypackage"}

0 commit comments

Comments
 (0)