Skip to content

Commit d57e60a

Browse files
committed
new setupcfg writer
1 parent 993c151 commit d57e60a

5 files changed

Lines changed: 441 additions & 4 deletions

File tree

‎src/codemodder/dependency_management/dependency_manager.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from codemodder.dependency_management.setup_py_writer import (
99
SetupPyWriter,
1010
)
11+
from codemodder.dependency_management.setupcfg_writer import SetupCfgWriter
1112

1213
from codemodder.project_analysis.file_parsers.package_store import (
1314
PackageStore,
@@ -43,4 +44,8 @@ def write(
4344
return SetupPyWriter(
4445
self.dependencies_store, self.parent_directory
4546
).write(dependencies, dry_run)
47+
case FileType.SETUP_CFG:
48+
return SetupCfgWriter(
49+
self.dependencies_store, self.parent_directory
50+
).write(dependencies, dry_run)
4651
return None
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import configparser
2+
3+
from typing import Optional
4+
from codemodder.dependency import Dependency
5+
from codemodder.change import ChangeSet
6+
from codemodder.dependency_management.base_dependency_writer import DependencyWriter
7+
from codemodder.diff import create_diff_and_linenums
8+
from codemodder.logging import logger
9+
10+
11+
import re
12+
13+
14+
def find_leading_whitespace(s):
15+
match = re.match(r"(\s+)", s)
16+
if match:
17+
return match.group(1)
18+
return ""
19+
20+
21+
def added_line_nums_strategy(lines, i):
22+
return lines[i]
23+
24+
25+
class SetupCfgWriter(DependencyWriter):
26+
def add_to_file(
27+
self, dependencies: list[Dependency], dry_run: bool = False
28+
) -> Optional[ChangeSet]:
29+
config = configparser.ConfigParser()
30+
31+
try:
32+
config.read(self.path)
33+
except configparser.ParsingError:
34+
logger.debug("Unable to parse setup.cfg file.")
35+
return None
36+
37+
if "options" not in config or not (
38+
defined_dependencies := config["options"].get("install_requires", "")
39+
):
40+
logger.debug("Unable to add dependencies to setup.cfg file.")
41+
return None
42+
43+
with open(self.path, "r", encoding="utf-8") as f:
44+
original_lines = f.readlines()
45+
46+
new_lines = self.build_new_lines(
47+
original_lines, defined_dependencies, dependencies
48+
)
49+
if not new_lines:
50+
logger.debug("Unable to add dependencies to setup.cfg file.")
51+
return None
52+
53+
if not dry_run:
54+
try:
55+
with open(self.path, "w", encoding="utf-8") as f:
56+
f.writelines(new_lines)
57+
except Exception:
58+
logger.debug("Unable to add dependencies to setup.cfg file.")
59+
return None
60+
61+
diff, added_line_nums = create_diff_and_linenums(original_lines, new_lines)
62+
63+
changes = self.build_changes(
64+
dependencies, added_line_nums_strategy, added_line_nums
65+
)
66+
return ChangeSet(
67+
str(self.path.relative_to(self.parent_directory)),
68+
diff,
69+
changes=changes,
70+
)
71+
72+
def build_new_lines(
73+
self,
74+
original_lines: list[str],
75+
defined_dependencies: str,
76+
dependencies_to_add: list[Dependency],
77+
) -> Optional[list[str]]:
78+
"""
79+
configparser does not retain formatting or comment lines, so we have to build
80+
the output newline manually.
81+
"""
82+
clean_lines = list(map(lambda s: s.strip(), original_lines))
83+
84+
newline_separated = len(defined_dependencies.split("\n")) > 1
85+
if newline_separated:
86+
last_dep_line = defined_dependencies.split("\n")[-1]
87+
dep_sep = "\n"
88+
else:
89+
# deps are in same line as install_requires key separated by commas
90+
last_dep_line = [
91+
line for line in clean_lines if line.endswith(defined_dependencies)
92+
][-1]
93+
dep_sep = ","
94+
95+
try:
96+
last_dep_idx = clean_lines.index(last_dep_line)
97+
except ValueError:
98+
# we were unable to find the last req line due to some formatting issue
99+
logger.debug("Unable to add dependencies to setup.cfg file.")
100+
return None
101+
102+
if newline_separated:
103+
formatting = find_leading_whitespace(original_lines[last_dep_idx])
104+
new_deps = [
105+
f"{formatting}{dep.requirement}{dep_sep}" for dep in dependencies_to_add
106+
]
107+
new_lines = (
108+
original_lines[: last_dep_idx + 1]
109+
+ new_deps
110+
+ original_lines[last_dep_idx + 1 :]
111+
)
112+
else:
113+
# new_deps added to existing deps line
114+
new_dep = ",".join(
115+
[f"{dep.requirement}{dep_sep}" for dep in dependencies_to_add]
116+
)
117+
new_dep_line = f"{original_lines[last_dep_idx].rstrip()}, {new_dep}\n"
118+
new_lines = (
119+
original_lines[:last_dep_idx]
120+
+ [new_dep_line]
121+
+ original_lines[last_dep_idx + 1 :]
122+
)
123+
124+
return new_lines

‎src/codemodder/project_analysis/file_parsers/setup_cfg_file_parser.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import configparser
77

88
from .base_parser import BaseParser
9+
from codemodder.logging import logger
910

1011

1112
class SetupCfgParser(BaseParser):
@@ -15,13 +16,17 @@ def file_type(self):
1516

1617
def _parse_file(self, file: Path) -> PackageStore | None:
1718
config = configparser.ConfigParser()
18-
config.read(file)
19+
try:
20+
config.read(file)
21+
except configparser.ParsingError:
22+
logger.debug("Unable to parse setup.cfg file.")
23+
return None
1924

20-
if not (options := config["options"]):
25+
if "options" not in config:
2126
return None
2227

23-
dependency_lines = options.get("install_requires", "").split("\n")
24-
python_requires = options.get("python_requires", "")
28+
dependency_lines = config["options"].get("install_requires", "").split("\n")
29+
python_requires = config["options"].get("python_requires", "")
2530

2631
return PackageStore(
2732
type=self.file_type,

‎tests/dependency_management/test_pyproject_writer.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def test_dont_add_existing_dependency(tmpdir):
169169
"libcst~=1.1.0",
170170
"pylint~=3.0.0",
171171
"PyYAML~=6.0.0",
172+
"security~=1.2.0",
172173
]
173174
"""
174175

0 commit comments

Comments
 (0)