-
Notifications
You must be signed in to change notification settings - Fork 18
Feature/cran r dependency support #475
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
Draft
MoralCode
wants to merge
4
commits into
main
Choose a base branch
from
guptapratykshh/feature/cran-r-dependency-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2ab988a
Add CRAN (R package) dependency analysis support
guptapratykshh f8c20c7
Add tests for R dependency analysis
guptapratykshh bf8dcb1
refactor(r-deps): update tests to pytest and address review feedback
guptapratykshh f12d52c
rename one reference
MoralCode File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
118 changes: 118 additions & 0 deletions
118
collectoss/tasks/git/dependency_tasks/dependency_util/r_deps.py
This file contains hidden or 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,118 @@ | ||
| import re | ||
| import json | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def get_files(path): | ||
| """ | ||
| Scans the directory for R dependency files. | ||
| We look specifically for 'DESCRIPTION' files and 'renv.lock' files. | ||
| """ | ||
| dir_path = Path(path) | ||
| files = [] | ||
|
|
||
| # We look for both standard package metadata and renv lockfiles | ||
| files.extend(list(dir_path.glob('**/DESCRIPTION'))) | ||
| files.extend(list(dir_path.glob('**/renv.lock'))) | ||
|
|
||
| return files | ||
|
|
||
|
|
||
| def get_deps_for_file(path): | ||
| """ | ||
| Routes the file to the appropriate simple parser based on its name. | ||
| """ | ||
| path_obj = Path(path) | ||
|
|
||
| if path_obj.name == 'DESCRIPTION': | ||
| return get_deps_from_description(path) | ||
| elif path_obj.name == 'renv.lock': | ||
| return get_deps_from_renv_lock(path) | ||
|
|
||
| return [] | ||
|
|
||
|
|
||
| def get_deps_from_description(path): | ||
| """ | ||
| Extracts dependencies from a standard R DESCRIPTION file. | ||
|
|
||
| We scan fields like Imports, Depends, Suggests, and others to find | ||
| external package requirements. | ||
| """ | ||
| try: | ||
| with open(path, 'r', encoding='utf-8') as f: | ||
| content = f.read() | ||
|
|
||
| dependencies = set() | ||
|
|
||
| # These are the standard sections where R packages list their requirements | ||
| dependency_fields = ['Imports', 'Depends', 'Suggests', 'Enhances', 'LinkingTo'] | ||
|
|
||
| for field in dependency_fields: | ||
| # We use regex to grab the whole section for a field. | ||
| # It needs to handle potentially multi-line values. | ||
| pattern = rf'^{field}:\s*(.*?)(?=^\S|\Z)' | ||
| match = re.search(pattern, content, re.MULTILINE | re.DOTALL) | ||
|
|
||
| if match: | ||
| field_value = match.group(1) | ||
| packages = parse_r_package_list(field_value) | ||
| dependencies.update(packages) | ||
|
|
||
| # 'R' itself is often listed as a dependency, but we only care about libraries | ||
| dependencies.discard('R') | ||
|
|
||
| return list(dependencies) | ||
|
|
||
| except Exception: | ||
| # If something goes wrong parsing, we just return nothing for this file | ||
| return [] | ||
|
|
||
|
|
||
| def parse_r_package_list(field_value): | ||
| """ | ||
| Cleans up the raw text from the file to get a nice list of package names. | ||
| """ | ||
| packages = set() | ||
|
|
||
| # Flatten the text to a single line to make splitting easier | ||
| field_value = ' '.join(field_value.split()) | ||
|
|
||
| parts = field_value.split(',') | ||
|
|
||
| for part in parts: | ||
| part = part.strip() | ||
| if not part: | ||
| continue | ||
|
|
||
| # We need to grab the package name and ignore version numbers like (>= 1.0.0) | ||
| # Note: We are currently stripping version info because the CollectOSS dependency | ||
| # model only tracks package names. If that changes, we can capture versions here. | ||
| match = re.match(r'^([A-Za-z][A-Za-z0-9.]*)', part) | ||
| if match: | ||
| package_name = match.group(1).rstrip('.') | ||
| if package_name: | ||
| packages.add(package_name) | ||
|
|
||
| return packages | ||
|
|
||
|
|
||
| def get_deps_from_renv_lock(path): | ||
| """ | ||
| Parses an renv.lock file, which is just a JSON file listing specific package versions. | ||
| """ | ||
| try: | ||
| with open(path, 'r', encoding='utf-8') as f: | ||
| lockfile = json.load(f) | ||
|
|
||
| dependencies = set() | ||
|
|
||
| # renv.lock keeps everything under a 'Packages' key | ||
| if 'Packages' in lockfile and isinstance(lockfile['Packages'], dict): | ||
| for package_name in lockfile['Packages'].keys(): | ||
| dependencies.add(package_name) | ||
|
|
||
| return list(dependencies) | ||
|
|
||
| except (json.JSONDecodeError, Exception): | ||
| return [] |
This file contains hidden or 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,90 @@ | ||
| import json | ||
| import pytest | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [pylint] reported by reviewdog 🐶 |
||
| from augur.tasks.git.dependency_tasks.dependency_util import r_deps | ||
|
|
||
| def test_description_file_parsing(tmp_path): | ||
| """ | ||
| Test that standard R DESCRIPTION files are parsed correctly, | ||
| extracting dependencies from Depends, Imports, Suggests, and LinkingTo. | ||
| """ | ||
| description_content = """Package: mypackage | ||
| Type: Package | ||
| Title: My Test Package | ||
| Version: 0.1.0 | ||
| Author: Test Author | ||
| Maintainer: Test Author <test@example.com> | ||
| Description: A test package. | ||
| License: GPL-3 | ||
| Encoding: UTF-8 | ||
| LazyData: true | ||
| Depends: | ||
| R (>= 3.5.0), | ||
| dplyr (>= 1.0.0) | ||
| Imports: | ||
| ggplot2, | ||
| tidyr (>= 1.1.0), | ||
| stringr | ||
| Suggests: | ||
| testthat (>= 3.0.0), | ||
| knitr, | ||
| rmarkdown | ||
| LinkingTo: | ||
| Rcpp (>= 1.0.0) | ||
| """ | ||
|
|
||
| d_file = tmp_path / "DESCRIPTION" | ||
| d_file.write_text(description_content, encoding="utf-8") | ||
|
|
||
| deps = r_deps.get_deps_for_file(str(d_file)) | ||
|
|
||
| expected_deps = {'dplyr', 'ggplot2', 'tidyr', 'stringr', 'testthat', | ||
| 'knitr', 'rmarkdown', 'Rcpp'} | ||
|
|
||
| # Assert we found exactly the expected dependencies | ||
| assert set(deps) == expected_deps | ||
|
|
||
| def test_renv_lock_parsing(tmp_path): | ||
| """ | ||
| Test that renv.lock JSON files are parsed correctly. | ||
| """ | ||
| renv_lock_content = { | ||
| "R": {"Version": "4.3.0", "Repositories": [{"Name": "CRAN", "URL": "https://cran.rstudio.com"}]}, | ||
| "Packages": { | ||
| "ggplot2": {"Package": "ggplot2", "Version": "3.4.0", "Source": "Repository", "Repository": "CRAN"}, | ||
| "dplyr": {"Package": "dplyr", "Version": "1.1.0", "Source": "Repository", "Repository": "CRAN"}, | ||
| "tidyr": {"Package": "tidyr", "Version": "1.3.0", "Source": "Repository", "Repository": "CRAN"}, | ||
| "rmarkdown": {"Package": "rmarkdown", "Version": "2.20", "Source": "Repository", "Repository": "CRAN"} | ||
| } | ||
| } | ||
|
|
||
| l_file = tmp_path / "renv.lock" | ||
| measure = l_file.write_text(json.dumps(renv_lock_content), encoding="utf-8") | ||
|
|
||
| deps = r_deps.get_deps_for_file(str(l_file)) | ||
|
|
||
| expected_deps = {'ggplot2', 'dplyr', 'tidyr', 'rmarkdown'} | ||
|
|
||
| assert set(deps) == expected_deps | ||
|
|
||
| def test_file_discovery(tmp_path): | ||
| """ | ||
| Test that the tool finds DESCRIPTION and renv.lock files recursively. | ||
| """ | ||
| # Create a nested directory structure | ||
| (tmp_path / 'DESCRIPTION').touch() | ||
| (tmp_path / 'renv.lock').touch() | ||
|
|
||
| subdir = tmp_path / 'subdir' | ||
| subdir.mkdir() | ||
| (subdir / 'DESCRIPTION').touch() | ||
|
|
||
| nested = tmp_path / 'another' / 'nested' | ||
| nested.mkdir(parents=True) | ||
| (nested / 'renv.lock').touch() | ||
|
|
||
| # Run the discovery | ||
| files = r_deps.get_files(str(tmp_path)) | ||
| file_names = [f.name for f in files] | ||
|
|
||
| assert file_names.count('DESCRIPTION') == 2 | ||
| assert file_names.count('renv.lock') == 2 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[pylint] reported by reviewdog 🐶
W0611: Unused dependency_calculator imported from collectoss.tasks.git.dependency_tasks.dependency_util (unused-import)