Skip to content

Commit 6f0bd23

Browse files
authored
chore: Use FileHandlerPathlib in ConfigHandler (#171)
1 parent 8a411a1 commit 6f0bd23

File tree

8 files changed

+68
-63
lines changed

8 files changed

+68
-63
lines changed

src/twyn/cli.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
from twyn.__version__ import __version__
77
from twyn.base.constants import (
8+
DEFAULT_PROJECT_TOML_FILE,
89
DEPENDENCY_FILE_MAPPING,
910
SELECTOR_METHOD_MAPPING,
1011
AvailableLoggingLevels,
1112
)
1213
from twyn.config.config_handler import ConfigHandler
14+
from twyn.file_handler.file_handler import FileHandler
1315
from twyn.main import check_dependencies
1416

1517

@@ -101,15 +103,19 @@ def allowlist() -> None:
101103

102104

103105
@allowlist.command()
106+
@click.option("--config", type=click.STRING)
104107
@click.argument("package_name")
105-
def add(package_name: str) -> None:
106-
ConfigHandler().add_package_to_allowlist(package_name)
108+
def add(package_name: str, config: str) -> None:
109+
fh = FileHandler(config or DEFAULT_PROJECT_TOML_FILE)
110+
ConfigHandler(fh).add_package_to_allowlist(package_name)
107111

108112

109113
@allowlist.command()
114+
@click.option("--config", type=click.STRING)
110115
@click.argument("package_name")
111-
def remove(package_name: str) -> None:
112-
ConfigHandler().remove_package_from_allowlist(package_name)
116+
def remove(package_name: str, config: str) -> None:
117+
fh = FileHandler(config or DEFAULT_PROJECT_TOML_FILE)
118+
ConfigHandler(fh).remove_package_from_allowlist(package_name)
113119

114120

115121
if __name__ == "__main__":

src/twyn/config/config_handler.py

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import logging
22
from dataclasses import asdict, dataclass
33
from enum import Enum
4-
from os import getcwd
5-
from pathlib import Path
64
from typing import Optional
75

86
from tomlkit import TOMLDocument, dumps, parse, table
@@ -18,6 +16,8 @@
1816
AllowlistPackageDoesNotExistError,
1917
TOMLError,
2018
)
19+
from twyn.file_handler.exceptions import PathNotFoundError
20+
from twyn.file_handler.file_handler import BaseFileHandler
2121

2222
logger = logging.getLogger("twyn")
2323

@@ -47,8 +47,8 @@ class ReadTwynConfiguration:
4747
class ConfigHandler:
4848
"""Manage reading and writing configurations for Twyn."""
4949

50-
def __init__(self, file_path: Optional[str] = None, enforce_file: bool = True):
51-
self._file_path = file_path or DEFAULT_PROJECT_TOML_FILE
50+
def __init__(self, file_handler: BaseFileHandler, enforce_file: bool = True) -> None:
51+
self.file_handler = file_handler
5252
self._enforce_file = enforce_file
5353

5454
def resolve_config(
@@ -119,36 +119,19 @@ def _write_config(self, toml: TOMLDocument, config: ReadTwynConfiguration) -> No
119119
if "twyn" not in toml["tool"]: # type: ignore[operator]
120120
toml["tool"]["twyn"] = {} # type: ignore[index]
121121
toml["tool"]["twyn"] = twyn_toml_data # type: ignore[index]
122+
122123
self._write_toml(toml)
123124

125+
def _write_toml(self, toml: TOMLDocument) -> None:
126+
self.file_handler.write(dumps(toml))
127+
124128
def _read_toml(self) -> TOMLDocument:
125129
try:
126-
fp = self._get_toml_file_pointer()
127-
except FileNotFoundError:
128-
if not self._enforce_file and self._file_path == DEFAULT_PROJECT_TOML_FILE:
130+
return parse(self.file_handler.read())
131+
except PathNotFoundError:
132+
if not self._enforce_file and self.file_handler.is_handler_of_file(DEFAULT_PROJECT_TOML_FILE):
129133
return TOMLDocument()
130-
raise TOMLError(f"Error reading toml from {self._file_path}") from None
131-
132-
with open(fp, "r") as f:
133-
content = parse(f.read())
134-
return content
135-
136-
def _get_toml_file_pointer(self) -> Path:
137-
"""Create a path for the toml file with the format <current working directory>/self.file_path."""
138-
fp = Path(getcwd()) / Path(self._file_path)
139-
140-
if not fp.is_file():
141-
raise FileNotFoundError(f"File not found at path '{fp}'.")
142-
143-
return fp
144-
145-
def _write_toml(self, toml: TOMLDocument) -> None:
146-
with open(self._get_toml_file_pointer(), "w") as f:
147-
try:
148-
f.write(dumps(toml))
149-
except Exception:
150-
logger.exception("Error writing toml file")
151-
raise TOMLError(f"Error writing toml to {self._file_path}") from None
134+
raise TOMLError(f"Error reading toml from {self.file_handler.file_path}") from None
152135

153136

154137
def _get_logging_level(

src/twyn/dependency_parser/abstract_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABC, abstractmethod
44
from pathlib import Path
55

6-
from twyn.file_handler.file_handler import FileHandlerPathlib
6+
from twyn.file_handler.file_handler import FileHandler
77

88
logger = logging.getLogger("twyn")
99

@@ -17,7 +17,7 @@ class AbstractParser(ABC):
1717

1818
def __init__(self, file_path: str) -> None:
1919
self.file_path = Path(os.path.abspath(os.path.join(os.getcwd(), file_path)))
20-
self.file_handler = FileHandlerPathlib(file_path=self.file_path)
20+
self.file_handler = FileHandler(file_path=self.file_path)
2121

2222
def __str__(self) -> str:
2323
return self.__class__.__name__

src/twyn/file_handler/file_handler.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@
1010

1111

1212
class BaseFileHandler(Protocol):
13-
def __init__(self, file_path: str) -> None: ...
1413
def read(self) -> str: ...
1514
def file_exists(self) -> bool: ...
15+
def write(self, data: str) -> None: ...
1616

1717

18-
class FileHandlerPathlib(BaseFileHandler):
18+
class FileHandler(BaseFileHandler):
1919
def __init__(self, file_path: str) -> None:
20-
self.file_path = Path(os.path.abspath(os.path.join(os.getcwd(), file_path)))
20+
self.file_path = self._get_file_path(file_path)
21+
22+
def _get_file_path(self, file_path: str) -> Path:
23+
return Path(os.path.abspath(os.path.join(os.getcwd(), file_path)))
24+
25+
def is_handler_of_file(self, name: str) -> bool:
26+
return self._get_file_path(name) == self.file_path
2127

2228
def read(self) -> str:
2329
self._raise_for_file_exists()
@@ -40,3 +46,7 @@ def _raise_for_file_exists(self) -> None:
4046

4147
if not self.file_path.is_file():
4248
raise PathIsNotFileError
49+
50+
def write(self, data: str) -> None:
51+
self._raise_for_file_exists()
52+
self.file_path.write_text(data)

src/twyn/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
from rich.progress import track
88

99
from twyn.base.constants import (
10+
DEFAULT_PROJECT_TOML_FILE,
1011
SELECTOR_METHOD_MAPPING,
1112
AvailableLoggingLevels,
1213
)
1314
from twyn.config.config_handler import ConfigHandler
1415
from twyn.dependency_parser.dependency_selector import DependencySelector
16+
from twyn.file_handler.file_handler import FileHandler
1517
from twyn.similarity.algorithm import EditDistance, SimilarityThreshold
1618
from twyn.trusted_packages import TopPyPiReference
1719
from twyn.trusted_packages.selectors import AbstractSelector
@@ -36,7 +38,8 @@ def check_dependencies(
3638
verbosity: AvailableLoggingLevels = AvailableLoggingLevels.none,
3739
) -> bool:
3840
"""Check if dependencies could be typosquats."""
39-
config = ConfigHandler(file_path=config_file, enforce_file=False).resolve_config(
41+
config_file_handler = FileHandler(config_file or DEFAULT_PROJECT_TOML_FILE)
42+
config = ConfigHandler(config_file_handler, enforce_file=False).resolve_config(
4043
verbosity=verbosity, selector_method=selector_method, dependency_file=dependency_file
4144
)
4245
_set_logging_level(config.logging_level)

tests/core/test_config_handler.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,32 @@
44

55
import pytest
66
from tomlkit import TOMLDocument, dumps, parse
7-
from twyn.base.constants import DEFAULT_TOP_PYPI_PACKAGES, AvailableLoggingLevels
7+
from twyn.base.constants import DEFAULT_PROJECT_TOML_FILE, DEFAULT_TOP_PYPI_PACKAGES, AvailableLoggingLevels
88
from twyn.config.config_handler import ConfigHandler, ReadTwynConfiguration, TwynConfiguration
99
from twyn.config.exceptions import (
1010
AllowlistPackageAlreadyExistsError,
1111
AllowlistPackageDoesNotExistError,
1212
TOMLError,
1313
)
14+
from twyn.file_handler.exceptions import PathNotFoundError
15+
from twyn.file_handler.file_handler import FileHandler
1416

1517

1618
class TestConfig:
1719
def throw_exception(self):
18-
raise FileNotFoundError
20+
raise PathNotFoundError
1921

20-
@patch("twyn.config.config_handler.ConfigHandler._get_toml_file_pointer")
22+
@patch("twyn.file_handler.file_handler.FileHandler.read")
2123
def test_enforce_file_error(self, mock_is_file):
2224
mock_is_file.side_effect = self.throw_exception
2325
with pytest.raises(TOMLError):
24-
ConfigHandler(enforce_file=True).resolve_config()
26+
ConfigHandler(FileHandler(DEFAULT_PROJECT_TOML_FILE), enforce_file=True).resolve_config()
2527

26-
@patch("twyn.config.config_handler.ConfigHandler._get_toml_file_pointer")
28+
@patch("twyn.file_handler.file_handler.FileHandler.read")
2729
def test_no_enforce_file_on_non_existent_file(self, mock_is_file):
2830
"""Resolving the config without enforcing the file to be present gives you defaults."""
2931
mock_is_file.side_effect = self.throw_exception
30-
config = ConfigHandler(enforce_file=False).resolve_config()
32+
config = ConfigHandler(FileHandler(DEFAULT_PROJECT_TOML_FILE), enforce_file=False).resolve_config()
3133

3234
assert config == TwynConfiguration(
3335
dependency_file=None,
@@ -39,20 +41,20 @@ def test_no_enforce_file_on_non_existent_file(self, mock_is_file):
3941

4042
def test_config_raises_for_unknown_file(self):
4143
with pytest.raises(TOMLError):
42-
ConfigHandler(file_path="non-existent-file.toml").resolve_config()
44+
ConfigHandler(FileHandler("non-existent-file.toml")).resolve_config()
4345

4446
def test_read_config_values(self, pyproject_toml_file):
45-
config = ConfigHandler(file_path=pyproject_toml_file).resolve_config()
47+
config = ConfigHandler(file_handler=FileHandler(pyproject_toml_file)).resolve_config()
4648
assert config.dependency_file == "my_file.txt"
4749
assert config.selector_method == "my_selector"
4850
assert config.logging_level == AvailableLoggingLevels.debug
4951
assert config.allowlist == {"boto4", "boto2"}
5052

5153
def test_get_twyn_data_from_file(self, pyproject_toml_file):
52-
handler = ConfigHandler(file_path=pyproject_toml_file)
54+
handler = ConfigHandler(FileHandler(str(pyproject_toml_file)))
5355

5456
toml = handler._read_toml()
55-
twyn_data = ConfigHandler(file_path=pyproject_toml_file)._get_read_config(toml)
57+
twyn_data = ConfigHandler(FileHandler(pyproject_toml_file))._get_read_config(toml)
5658
assert twyn_data == ReadTwynConfiguration(
5759
dependency_file="my_file.txt",
5860
selector_method="my_selector",
@@ -62,7 +64,7 @@ def test_get_twyn_data_from_file(self, pyproject_toml_file):
6264
)
6365

6466
def test_write_toml(self, pyproject_toml_file):
65-
handler = ConfigHandler(file_path=pyproject_toml_file)
67+
handler = ConfigHandler(FileHandler(pyproject_toml_file))
6668
toml = handler._read_toml()
6769

6870
initial_config = handler.resolve_config()
@@ -103,12 +105,12 @@ def test_write_toml(self, pyproject_toml_file):
103105

104106

105107
class TestAllowlistConfigHandler:
106-
@patch("twyn.config.config_handler.ConfigHandler._write_toml")
108+
@patch("twyn.file_handler.file_handler.FileHandler.write")
107109
@patch("twyn.config.config_handler.ConfigHandler._read_toml")
108110
def test_allowlist_add(self, mock_toml, mock_write_toml):
109111
mock_toml.return_value = TOMLDocument()
110112

111-
config = ConfigHandler()
113+
config = ConfigHandler(FileHandler("some-file"))
112114

113115
config.add_package_to_allowlist("mypackage")
114116

@@ -122,7 +124,7 @@ def test_allowlist_add(self, mock_toml, mock_write_toml):
122124
def test_allowlist_add_duplicate_error(self, mock_toml, mock_write_toml):
123125
mock_toml.return_value = parse(dumps({"tool": {"twyn": {"allowlist": ["mypackage"]}}}))
124126

125-
config = ConfigHandler()
127+
config = ConfigHandler(FileHandler("some-file"))
126128
with pytest.raises(
127129
AllowlistPackageAlreadyExistsError,
128130
match="Package 'mypackage' is already present in the allowlist. Skipping.",
@@ -136,7 +138,7 @@ def test_allowlist_add_duplicate_error(self, mock_toml, mock_write_toml):
136138
def test_allowlist_remove_completely(self, mock_toml, mock_write_toml):
137139
mock_toml.return_value = parse(dumps({"tool": {"twyn": {"allowlist": ["mypackage"]}}}))
138140

139-
config = ConfigHandler()
141+
config = ConfigHandler(FileHandler("some-file"))
140142

141143
config.remove_package_from_allowlist("mypackage")
142144
assert config._read_toml() == {"tool": {"twyn": {}}}
@@ -146,7 +148,7 @@ def test_allowlist_remove_completely(self, mock_toml, mock_write_toml):
146148
def test_allowlist_remove(self, mock_toml, mock_write_toml):
147149
mock_toml.return_value = parse(dumps({"tool": {"twyn": {"allowlist": ["mypackage", "another-package"]}}}))
148150

149-
config = ConfigHandler()
151+
config = ConfigHandler(FileHandler("some-file"))
150152

151153
config.remove_package_from_allowlist("mypackage")
152154
assert config._read_toml() == {"tool": {"twyn": {"allowlist": ["another-package"]}}}
@@ -156,7 +158,7 @@ def test_allowlist_remove(self, mock_toml, mock_write_toml):
156158
def test_allowlist_remove_non_existent_package_error(self, mock_toml, mock_write_toml):
157159
mock_toml.return_value = parse(dumps({"tool": {"twyn": {"allowlist": ["mypackage"]}}}))
158160

159-
config = ConfigHandler()
161+
config = ConfigHandler(FileHandler("some-file"))
160162
with pytest.raises(
161163
AllowlistPackageDoesNotExistError,
162164
match="Package 'mypackage2' is not present in the allowlist. Skipping.",

tests/file_handler/test_file_handler.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@
22

33
import pytest
44
from twyn.file_handler.exceptions import PathIsNotFileError, PathNotFoundError
5-
from twyn.file_handler.file_handler import FileHandlerPathlib
5+
from twyn.file_handler.file_handler import FileHandler
66

77

8-
class TestFileHandlerPathlib:
8+
class TestFileHandler:
99
def test_file_exists(self, pyproject_toml_file: str):
10-
parser = FileHandlerPathlib(pyproject_toml_file)
10+
parser = FileHandler(pyproject_toml_file)
1111
assert parser.file_exists() is True
1212

1313
def test_read_file_success(self, pyproject_toml_file: str):
14-
parser = FileHandlerPathlib(pyproject_toml_file)
14+
parser = FileHandler(pyproject_toml_file)
1515
read = parser.read()
1616
assert len(read) > 1
1717
assert isinstance(read, str)
1818

1919
def test_read_file_does_not_exist(
2020
self,
2121
):
22-
parser = FileHandlerPathlib("")
22+
parser = FileHandler("")
2323
with pytest.raises(PathIsNotFileError):
2424
parser.read()
2525

@@ -33,5 +33,5 @@ def test_raise_for_valid_file(self, mock_is_file, mock_exists, file_exists, is_f
3333
mock_exists.return_value = file_exists
3434
mock_is_file.return_value = is_file
3535

36-
parser = FileHandlerPathlib("fake.txt")
36+
parser = FileHandler("fake.txt")
3737
assert parser.file_exists() is False

tests/main/test_main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from twyn.base.constants import DEFAULT_TOP_PYPI_PACKAGES, AvailableLoggingLevels
66
from twyn.config.config_handler import ConfigHandler, TwynConfiguration, _get_logging_level
77
from twyn.dependency_parser import RequirementsTxtParser
8+
from twyn.file_handler.file_handler import FileHandler
89
from twyn.main import (
910
check_dependencies,
1011
get_parsed_dependencies_from_file,
@@ -104,7 +105,7 @@ def test_options_priorities_assignation(
104105
3. default values
105106
106107
"""
107-
handler = ConfigHandler(file_path=None, enforce_file=False)
108+
handler = ConfigHandler(FileHandler(""), enforce_file=False)
108109

109110
with patch.object(handler, "_read_toml", return_value=parse(dumps({"tool": {"twyn": file_config}}))):
110111
resolved = handler.resolve_config(

0 commit comments

Comments
 (0)