Skip to content

license_r.py

Check licenses for R packages.

RAdapter

Bases: LicenseAdapter

Adapter for checking R package licenses.

Source code in taglyatelle/slash_commands/check_licenses/license_r.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
class RAdapter(LicenseAdapter):
    """Adapter for checking R package licenses."""

    def __init__(self):
        """Initialize file handlers for R dependency manifests."""
        self.file_handlers = {
            "DESCRIPTION": self.parse_description_file,
            "renv.lock": self.parse_renv_lock,
        }

    def _clean_license_text(self, license_text: str) -> str:
        """
        Clean and truncate verbose license text to keep only the essential license name.

        Parameters
        ----------
        license_text
            The raw license text from DESCRIPTION or CRAN.

        Returns
        -------
        Cleaned license name or identifier.
        """
        if not license_text or not license_text.strip():
            return "Unknown"

        license_text = license_text.strip()

        # Remove "file LICENSE" or "file LICENCE" patterns (with + or | separators)
        license_text = re.sub(r"\s*[\+\|]\s*file\s+LICEN[CS]E.*$", "", license_text, flags=re.IGNORECASE)

        # If the result is just "file LICENSE" or similar, return Unknown
        if re.match(r"^\s*file\s+LICEN[CS]E.*$", license_text, flags=re.IGNORECASE):
            return "Unknown"

        license_text = license_text.strip()
        if not license_text:
            return "Unknown"

        if len(license_text) <= MAX_LICENSE_TEXT_LENGTH:
            return license_text

        lines = license_text.split("\n")
        first_line = lines[0].strip()

        if first_line and not first_line.startswith("Copyright"):
            return first_line[:MAX_LICENSE_TEXT_LENGTH]

        return "Unknown"

    def _get_license_from_cran(self, pkg_name: str) -> str:
        """
        Fetch license information from CRAN API.

        Parameters
        ----------
        pkg_name
            The name of the package to query.

        Returns
        -------
        The license of the package as a string.
        """
        try:
            url = f"https://crandb.r-pkg.org/{pkg_name}"
            with urllib.request.urlopen(url, timeout=CRAN_TIMEOUT_SECONDS) as response:
                data = json.loads(response.read().decode())

            license_info = data.get("License")
            if license_info and license_info.strip():
                return self._clean_license_text(license_info)

            return "Unknown"
        except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError):
            return "Unknown"
        except Exception:
            return "Unknown"

    def parse_description_file(self, content: str) -> list[dict[str, str]]:
        """
        Parse DESCRIPTION file (R package metadata).

        Parameters
        ----------
        content
            Content of the DESCRIPTION file

        Returns
        -------
        A list with a single dictionary containing the package license information
        """
        try:
            package_match = re.search(r"^Package:\s*(.+)$", content, re.MULTILINE)
            package_name = package_match.group(1).strip() if package_match else "Unknown"

            license_match = re.search(
                r"^License:\s*(.+?)(?=\n[A-Z][a-z]+:|$)",
                content,
                re.MULTILINE | re.DOTALL,
            )

            if license_match:
                license_text = license_match.group(1).strip()
                license_text = re.sub(r"\s+", " ", license_text)
                license_info = self._clean_license_text(license_text)
            else:
                license_info = "Unknown"

            return [{"package": package_name, "license": license_info}]

        except Exception:
            return []

    def parse_renv_lock(self, content: str) -> list[dict[str, str]]:
        """
        Parse renv.lock file (R dependency lock file).

        Parameters
        ----------
        content
            Content of the renv.lock file

        Returns
        -------
        A list of dictionaries with package and license information
        """
        try:
            lock_data = json.loads(content)

            packages = lock_data.get("Packages", {})

            pkg_licenses = []
            for pkg_name, _ in packages.items():
                license_info = self._get_license_from_cran(pkg_name)
                pkg_licenses.append({"package": pkg_name, "license": license_info})

            return pkg_licenses

        except (json.JSONDecodeError, ValueError):
            return []
        except Exception:
            return []

__init__()

Initialize file handlers for R dependency manifests.

Source code in taglyatelle/slash_commands/check_licenses/license_r.py
18
19
20
21
22
23
def __init__(self):
    """Initialize file handlers for R dependency manifests."""
    self.file_handlers = {
        "DESCRIPTION": self.parse_description_file,
        "renv.lock": self.parse_renv_lock,
    }

parse_description_file(content)

Parse DESCRIPTION file (R package metadata).

Parameters

content Content of the DESCRIPTION file

Returns

A list with a single dictionary containing the package license information

Source code in taglyatelle/slash_commands/check_licenses/license_r.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def parse_description_file(self, content: str) -> list[dict[str, str]]:
    """
    Parse DESCRIPTION file (R package metadata).

    Parameters
    ----------
    content
        Content of the DESCRIPTION file

    Returns
    -------
    A list with a single dictionary containing the package license information
    """
    try:
        package_match = re.search(r"^Package:\s*(.+)$", content, re.MULTILINE)
        package_name = package_match.group(1).strip() if package_match else "Unknown"

        license_match = re.search(
            r"^License:\s*(.+?)(?=\n[A-Z][a-z]+:|$)",
            content,
            re.MULTILINE | re.DOTALL,
        )

        if license_match:
            license_text = license_match.group(1).strip()
            license_text = re.sub(r"\s+", " ", license_text)
            license_info = self._clean_license_text(license_text)
        else:
            license_info = "Unknown"

        return [{"package": package_name, "license": license_info}]

    except Exception:
        return []

parse_renv_lock(content)

Parse renv.lock file (R dependency lock file).

Parameters

content Content of the renv.lock file

Returns

A list of dictionaries with package and license information

Source code in taglyatelle/slash_commands/check_licenses/license_r.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def parse_renv_lock(self, content: str) -> list[dict[str, str]]:
    """
    Parse renv.lock file (R dependency lock file).

    Parameters
    ----------
    content
        Content of the renv.lock file

    Returns
    -------
    A list of dictionaries with package and license information
    """
    try:
        lock_data = json.loads(content)

        packages = lock_data.get("Packages", {})

        pkg_licenses = []
        for pkg_name, _ in packages.items():
            license_info = self._get_license_from_cran(pkg_name)
            pkg_licenses.append({"package": pkg_name, "license": license_info})

        return pkg_licenses

    except (json.JSONDecodeError, ValueError):
        return []
    except Exception:
        return []