Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new setupcfg writer #204

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
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 "" # pragma: no cover


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

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 = [s.strip() for s in 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 _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.")

Check warning on line 22 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

Added line #L22 was not covered by tests
return None # pragma: no cover

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
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,25 @@ def pkg_with_reqs_txt_unknown_encoding(tmp_path_factory):
reqs = "\xf0\x28\x8c\xbc"
req_file.write_text(reqs)
return base_dir


@pytest.fixture(scope="module")
def pkg_with_setup_cfg(tmp_path_factory):
base_dir = tmp_path_factory.mktemp("foo")
req_file = base_dir / "setup.cfg"
reqs = """\
[metadata]
name = my_package
version = attr: my_package.VERSION

# some other stuff

[options]
include_package_data = True
python_requires = >=3.7
install_requires =
requests
importlib-metadata; python_version<"3.8"
"""
req_file.write_text(reqs)
return base_dir
17 changes: 16 additions & 1 deletion tests/dependency_management/test_dependency_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
from codemodder.change import ChangeSet
from codemodder.dependency import DefusedXML, Security
from codemodder.dependency_management import DependencyManager
from codemodder.project_analysis.file_parsers import RequirementsTxtParser
from codemodder.project_analysis.file_parsers import (
RequirementsTxtParser,
SetupCfgParser,
)
from codemodder.project_analysis.file_parsers.package_store import PackageStore


Expand Down Expand Up @@ -33,3 +36,15 @@ def test_write_for_requirements_txt(self, pkg_with_reqs_txt):

changeset = dm.write(dependencies)
assert isinstance(changeset, ChangeSet)
assert len(changeset.changes)

def test_write_for_setup_cfg(self, pkg_with_setup_cfg):
parser = SetupCfgParser(pkg_with_setup_cfg)
stores = parser.parse()
assert len(stores) == 1
dm = DependencyManager(stores[0], pkg_with_setup_cfg)
dependencies = [DefusedXML, Security]

changeset = dm.write(dependencies)
assert isinstance(changeset, ChangeSet)
assert len(changeset.changes)
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
Loading