-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add repo manager w reqs txt file parser * add pyproject.toml parser * add parser for setup.cfg file * add setup.py parser * testing * use abstract * ignore cov in base class * review feedback
- Loading branch information
1 parent
e09942c
commit 4663b59
Showing
24 changed files
with
465 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,4 +33,5 @@ repos: | |
[ | ||
"types-mock==5.0.*", | ||
"types-PyYAML==6.0", | ||
"types-toml~=0.10", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from .requirements_txt_file_parser import RequirementsTxtParser | ||
from .pyproject_toml_file_parser import PyprojectTomlParser | ||
from .setup_cfg_file_parser import SetupCfgParser | ||
from .setup_py_file_parser import SetupPyParser |
42 changes: 42 additions & 0 deletions
42
src/codemodder/project_analysis/file_parsers/base_parser.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from abc import ABC, abstractmethod | ||
|
||
from pathlib import Path | ||
from typing import List | ||
from .package_store import PackageStore | ||
from packaging.requirements import Requirement | ||
|
||
|
||
class BaseParser(ABC): | ||
def __init__(self, parent_directory: Path): | ||
self.parent_directory = parent_directory | ||
|
||
@property | ||
@abstractmethod | ||
def file_name(self): | ||
... # pragma: no cover | ||
|
||
def _parse_dependencies(self, dependencies: List[str]): | ||
return [ | ||
Requirement(line) | ||
for x in dependencies | ||
# Skip empty lines and comments | ||
if (line := x.strip()) and not line.startswith("#") | ||
] | ||
|
||
@abstractmethod | ||
def _parse_file(self, file: Path): | ||
... # pragma: no cover | ||
|
||
def find_file_locations(self) -> List[Path]: | ||
return list(Path(self.parent_directory).rglob(self.file_name)) | ||
|
||
def parse(self) -> list[PackageStore]: | ||
""" | ||
Find 0 or more project config or dependency files within a project repo. | ||
""" | ||
stores = [] | ||
req_files = self.find_file_locations() | ||
for file in req_files: | ||
store = self._parse_file(file) | ||
stores.append(store) | ||
return stores |
10 changes: 10 additions & 0 deletions
10
src/codemodder/project_analysis/file_parsers/package_store.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from dataclasses import dataclass | ||
from packaging.requirements import Requirement | ||
|
||
|
||
@dataclass | ||
class PackageStore: | ||
type: str | ||
file: str | ||
dependencies: list[Requirement] | ||
py_versions: list[str] |
33 changes: 33 additions & 0 deletions
33
src/codemodder/project_analysis/file_parsers/pyproject_toml_file_parser.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from codemodder.project_analysis.file_parsers.package_store import PackageStore | ||
from pathlib import Path | ||
import toml | ||
|
||
from .base_parser import BaseParser | ||
|
||
|
||
class PyprojectTomlParser(BaseParser): | ||
@property | ||
def file_name(self): | ||
return "pyproject.toml" | ||
|
||
def _parse_dependencies_from_toml(self, toml_data: dict): | ||
# todo: handle cases for | ||
# 1. no dependencies | ||
return self._parse_dependencies(toml_data["project"]["dependencies"]) | ||
|
||
def _parse_py_versions(self, toml_data: dict): | ||
# todo: handle cases for | ||
# 1. no requires-python | ||
# 2. multiple requires-python such as "">3.5.2"", ">=3.11.1,<3.11.2" | ||
return [toml_data["project"]["requires-python"]] | ||
|
||
def _parse_file(self, file: Path): | ||
data = toml.load(file) | ||
# todo: handle no "project" in data | ||
|
||
return PackageStore( | ||
type=self.file_name, | ||
file=str(file), | ||
dependencies=self._parse_dependencies_from_toml(data), | ||
py_versions=self._parse_py_versions(data), | ||
) |
23 changes: 23 additions & 0 deletions
23
src/codemodder/project_analysis/file_parsers/requirements_txt_file_parser.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
from codemodder.project_analysis.file_parsers.package_store import PackageStore | ||
from pathlib import Path | ||
from .base_parser import BaseParser | ||
|
||
|
||
class RequirementsTxtParser(BaseParser): | ||
@property | ||
def file_name(self): | ||
return "requirements.txt" | ||
|
||
def _parse_file(self, file: Path): | ||
with open(file, "r", encoding="utf-8") as f: | ||
lines = f.readlines() | ||
|
||
return PackageStore( | ||
type=self.file_name, | ||
file=str(file), | ||
dependencies=self._parse_dependencies(lines), | ||
# requirements.txt files do not declare py versions explicitly | ||
# though we could create a heuristic by analyzing each dependency | ||
# and extracting py versions from them. | ||
py_versions=[], | ||
) |
37 changes: 37 additions & 0 deletions
37
src/codemodder/project_analysis/file_parsers/setup_cfg_file_parser.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from codemodder.project_analysis.file_parsers.package_store import PackageStore | ||
from pathlib import Path | ||
import configparser | ||
|
||
from .base_parser import BaseParser | ||
|
||
|
||
class SetupCfgParser(BaseParser): | ||
@property | ||
def file_name(self): | ||
return "setup.cfg" | ||
|
||
def _parse_dependencies_from_cfg(self, config: configparser.ConfigParser): | ||
# todo: handle cases for | ||
# 1. no dependencies, no options dict | ||
# setup_requires, tests_require, extras_require | ||
dependency_lines = config["options"]["install_requires"].split("\n") | ||
return self._parse_dependencies(dependency_lines) | ||
|
||
def _parse_py_versions(self, config: configparser.ConfigParser): | ||
# todo: handle cases for | ||
# 1. no options/ no requires-python | ||
# 2. various requires-python such as "">3.5.2"", ">=3.11.1,<3.11.2" | ||
return [config["options"]["python_requires"]] | ||
|
||
def _parse_file(self, file: Path): | ||
config = configparser.ConfigParser() | ||
config.read(file) | ||
|
||
# todo: handle no config, no "options" in config | ||
|
||
return PackageStore( | ||
type=self.file_name, | ||
file=str(file), | ||
dependencies=self._parse_dependencies_from_cfg(config), | ||
py_versions=self._parse_py_versions(config), | ||
) |
77 changes: 77 additions & 0 deletions
77
src/codemodder/project_analysis/file_parsers/setup_py_file_parser.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from codemodder.project_analysis.file_parsers.package_store import PackageStore | ||
from pathlib import Path | ||
import libcst as cst | ||
from libcst import matchers | ||
from packaging.requirements import Requirement | ||
|
||
from .base_parser import BaseParser | ||
from .utils import clean_simplestring | ||
|
||
|
||
class SetupPyParser(BaseParser): | ||
@property | ||
def file_name(self): | ||
return "setup.py" | ||
|
||
def _parse_dependencies(self, dependencies): | ||
return [ | ||
Requirement(line) | ||
for x in dependencies | ||
# Skip empty lines and comments | ||
if (line := clean_simplestring(x.value)) and not line.startswith("#") | ||
] | ||
|
||
def _parse_dependencies_from_cst(self, cst_dependencies): | ||
# todo: handle cases for | ||
# 1. no dependencies, | ||
return self._parse_dependencies(cst_dependencies) | ||
|
||
def _parse_py_versions(self, version_str): | ||
# todo: handle for multiple versions | ||
return [clean_simplestring(version_str)] | ||
|
||
def _parse_file(self, file: Path): | ||
visitor = SetupCallVisitor() | ||
with open(str(file), "r", encoding="utf-8") as f: | ||
# todo: handle failure in parsing | ||
module = cst.parse_module(f.read()) | ||
module.visit(visitor) | ||
|
||
# todo: handle no python_requires, install_requires | ||
|
||
return PackageStore( | ||
type=self.file_name, | ||
file=str(file), | ||
dependencies=self._parse_dependencies_from_cst(visitor.install_requires), | ||
py_versions=self._parse_py_versions(visitor.python_requires), | ||
) | ||
|
||
|
||
class SetupCallVisitor(cst.CSTVisitor): | ||
def __init__(self): | ||
self.python_requires = None | ||
self.install_requires = None | ||
# todo setup_requires, tests_require, extras_require | ||
|
||
def visit_Call(self, node: cst.Call) -> None: | ||
# todo: only handle setup from setuptools, not others tho unlikely | ||
if matchers.matches(node.func, cst.Name(value="setup")): | ||
visitor = SetupArgVisitor() | ||
node.visit(visitor) | ||
self.python_requires = visitor.python_requires | ||
self.install_requires = visitor.install_requires | ||
|
||
|
||
class SetupArgVisitor(cst.CSTVisitor): | ||
def __init__(self): | ||
self.python_requires = None | ||
self.install_requires = None | ||
|
||
def visit_Arg(self, node: cst.Arg) -> None: | ||
if matchers.matches(node.keyword, cst.Name(value="python_requires")): | ||
# todo: this works for `python_requires=">=3.7",` but what about | ||
# a list of versions? | ||
self.python_requires = node.value.value | ||
if matchers.matches(node.keyword, cst.Name(value="install_requires")): | ||
# todo: could it be something other than a list? | ||
self.install_requires = node.value.elements |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import libcst as cst | ||
|
||
|
||
def clean_simplestring(node: cst.SimpleString | str) -> str: | ||
if isinstance(node, str): | ||
return node.strip('"') | ||
return node.raw_value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from functools import cached_property | ||
from pathlib import Path | ||
from codemodder.project_analysis.file_parsers import ( | ||
RequirementsTxtParser, | ||
PyprojectTomlParser, | ||
SetupCfgParser, | ||
SetupPyParser, | ||
) | ||
from codemodder.project_analysis.file_parsers.package_store import PackageStore | ||
|
||
|
||
class PythonRepoManager: | ||
def __init__(self, parent_directory: Path): | ||
self.parent_directory = parent_directory | ||
self._potential_stores = [ | ||
RequirementsTxtParser, | ||
PyprojectTomlParser, | ||
SetupCfgParser, | ||
SetupPyParser, | ||
] | ||
|
||
@cached_property | ||
def package_stores(self) -> list[PackageStore]: | ||
return self._parse_all_stores() | ||
|
||
def _parse_all_stores(self) -> list[PackageStore]: | ||
discovered_pkg_stores: list[PackageStore] = [] | ||
for store in self._potential_stores: | ||
discovered_pkg_stores.extend(store(self.parent_directory).parse()) | ||
return discovered_pkg_stores |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
Oops, something went wrong.