Skip to content

Commit

Permalink
new setupcfg writer
Browse files Browse the repository at this point in the history
  • Loading branch information
clavedeluna committed Jan 4, 2024
1 parent 8c108a9 commit 8e51c98
Show file tree
Hide file tree
Showing 5 changed files with 441 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/codemodder/dependency_management/dependency_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from codemodder.dependency_management.setup_py_writer import (
SetupPyWriter,
)
from codemodder.dependency_management.setupcfg_writer import SetupCfgWriter

from codemodder.project_analysis.file_parsers.package_store import (
PackageStore,
Expand Down Expand Up @@ -43,4 +44,8 @@ def write(
return SetupPyWriter(
self.dependencies_store, self.parent_directory
).write(dependencies, dry_run)
case FileType.SETUP_CFG:
return SetupCfgWriter(

Check warning on line 48 in src/codemodder/dependency_management/dependency_manager.py

View check run for this annotation

Codecov / codecov/patch

src/codemodder/dependency_management/dependency_manager.py#L48

Added line #L48 was not covered by tests
self.dependencies_store, self.parent_directory
).write(dependencies, dry_run)
return None
124 changes: 124 additions & 0 deletions src/codemodder/dependency_management/setupcfg_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import configparser

from typing import Optional
from codemodder.dependency import Dependency
from codemodder.change import ChangeSet
from codemodder.dependency_management.base_dependency_writer import DependencyWriter
from codemodder.diff import create_diff_and_linenums
from codemodder.logging import logger


import re


def find_leading_whitespace(s):
match = re.match(r"(\s+)", s)
if match:
return match.group(1)
return ""

Check warning on line 18 in src/codemodder/dependency_management/setupcfg_writer.py

View check run for this annotation

Codecov / codecov/patch

src/codemodder/dependency_management/setupcfg_writer.py#L18

Added line #L18 was not covered by tests


def added_line_nums_strategy(lines, i):
return lines[i]


class SetupCfgWriter(DependencyWriter):
def add_to_file(
self, dependencies: list[Dependency], dry_run: bool = False
) -> Optional[ChangeSet]:
config = configparser.ConfigParser()

try:
config.read(self.path)
except configparser.ParsingError:
logger.debug("Unable to parse setup.cfg file.")
return None

if "options" not in config or not (
defined_dependencies := config["options"].get("install_requires", "")
):
logger.debug("Unable to add dependencies to setup.cfg file.")
return None

with open(self.path, "r", encoding="utf-8") as f:
original_lines = f.readlines()

new_lines = self.build_new_lines(
original_lines, defined_dependencies, dependencies
)
if not new_lines:
logger.debug("Unable to add dependencies to setup.cfg file.")
return None

Check warning on line 51 in src/codemodder/dependency_management/setupcfg_writer.py

View check run for this annotation

Codecov / codecov/patch

src/codemodder/dependency_management/setupcfg_writer.py#L50-L51

Added lines #L50 - L51 were not covered by tests

if not dry_run:
try:
with open(self.path, "w", encoding="utf-8") as f:
f.writelines(new_lines)
except Exception:
logger.debug("Unable to add dependencies to setup.cfg file.")
return None

Check warning on line 59 in src/codemodder/dependency_management/setupcfg_writer.py

View check run for this annotation

Codecov / codecov/patch

src/codemodder/dependency_management/setupcfg_writer.py#L57-L59

Added lines #L57 - L59 were not covered by tests

diff, added_line_nums = create_diff_and_linenums(original_lines, new_lines)

changes = self.build_changes(
dependencies, added_line_nums_strategy, added_line_nums
)
return ChangeSet(
str(self.path.relative_to(self.parent_directory)),
diff,
changes=changes,
)

def build_new_lines(
self,
original_lines: list[str],
defined_dependencies: str,
dependencies_to_add: list[Dependency],
) -> Optional[list[str]]:
"""
configparser does not retain formatting or comment lines, so we have to build
the output newline manually.
"""
clean_lines = list(map(lambda s: s.strip(), original_lines))

newline_separated = len(defined_dependencies.split("\n")) > 1
if newline_separated:
last_dep_line = defined_dependencies.split("\n")[-1]
dep_sep = "\n"
else:
# deps are in same line as install_requires key separated by commas
last_dep_line = [
line for line in clean_lines if line.endswith(defined_dependencies)
][-1]
dep_sep = ","

try:
last_dep_idx = clean_lines.index(last_dep_line)
except ValueError:

Check warning on line 97 in src/codemodder/dependency_management/setupcfg_writer.py

View check run for this annotation

Codecov / codecov/patch

src/codemodder/dependency_management/setupcfg_writer.py#L97

Added line #L97 was not covered by tests
# we were unable to find the last req line due to some formatting issue
logger.debug("Unable to add dependencies to setup.cfg file.")
return None

Check warning on line 100 in src/codemodder/dependency_management/setupcfg_writer.py

View check run for this annotation

Codecov / codecov/patch

src/codemodder/dependency_management/setupcfg_writer.py#L99-L100

Added lines #L99 - L100 were not covered by tests

if newline_separated:
formatting = find_leading_whitespace(original_lines[last_dep_idx])
new_deps = [
f"{formatting}{dep.requirement}{dep_sep}" for dep in dependencies_to_add
]
new_lines = (
original_lines[: last_dep_idx + 1]
+ new_deps
+ original_lines[last_dep_idx + 1 :]
)
else:
# new_deps added to existing deps line
new_dep = ",".join(
[f"{dep.requirement}{dep_sep}" for dep in dependencies_to_add]
)
new_dep_line = f"{original_lines[last_dep_idx].rstrip()}, {new_dep}\n"
new_lines = (
original_lines[:last_dep_idx]
+ [new_dep_line]
+ original_lines[last_dep_idx + 1 :]
)

return new_lines
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import configparser

from .base_parser import BaseParser
from codemodder.logging import logger


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

def _parse_file(self, file: Path) -> PackageStore | None:
config = configparser.ConfigParser()
config.read(file)
try:
config.read(file)
except configparser.ParsingError:
logger.debug("Unable to parse setup.cfg file.")
return None

Check warning on line 23 in src/codemodder/project_analysis/file_parsers/setup_cfg_file_parser.py

View check run for this annotation

Codecov / codecov/patch

src/codemodder/project_analysis/file_parsers/setup_cfg_file_parser.py#L22-L23

Added lines #L22 - L23 were not covered by tests

if not (options := config["options"]):
if "options" not in config:
return None

dependency_lines = options.get("install_requires", "").split("\n")
python_requires = options.get("python_requires", "")
dependency_lines = config["options"].get("install_requires", "").split("\n")
python_requires = config["options"].get("python_requires", "")

return PackageStore(
type=self.file_type,
Expand Down
1 change: 1 addition & 0 deletions tests/dependency_management/test_pyproject_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ def test_dont_add_existing_dependency(tmpdir):
"libcst~=1.1.0",
"pylint~=3.0.0",
"PyYAML~=6.0.0",
"security~=1.2.0",
]
"""

Expand Down
Loading

0 comments on commit 8e51c98

Please sign in to comment.