FreeCAD

Форк
0
/
addonmanager_devmode_predictor.py 
286 строк · 12.3 Кб
1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# ***************************************************************************
3
# *                                                                         *
4
# *   Copyright (c) 2022 FreeCAD Project Association                        *
5
# *                                                                         *
6
# *   This file is part of FreeCAD.                                         *
7
# *                                                                         *
8
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
9
# *   under the terms of the GNU Lesser General Public License as           *
10
# *   published by the Free Software Foundation, either version 2.1 of the  *
11
# *   License, or (at your option) any later version.                       *
12
# *                                                                         *
13
# *   FreeCAD is distributed in the hope that it will be useful, but        *
14
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
15
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
16
# *   Lesser General Public License for more details.                       *
17
# *                                                                         *
18
# *   You should have received a copy of the GNU Lesser General Public      *
19
# *   License along with FreeCAD. If not, see                               *
20
# *   <https://www.gnu.org/licenses/>.                                      *
21
# *                                                                         *
22
# ***************************************************************************
23

24
""" Class to guess metadata based on folder contents. Note that one of the functions
25
of this file is to guess the license being applied to the new software package based
26
in its contents. It is up to the user to make the final determination about whether
27
the selected license is the correct one, and inclusion here shouldn't be construed as
28
endorsement of any particular license. In addition, the inclusion of those text strings
29
does not imply a modification to the license for THIS software, which is licensed
30
under the LGPLv2.1 license (as stated above)."""
31

32
import datetime
33
import os
34

35
import FreeCAD
36
from addonmanager_git import initialize_git, GitManager
37
from addonmanager_utilities import get_readme_url
38

39
translate = FreeCAD.Qt.translate
40

41
# pylint: disable=too-few-public-methods
42

43

44
class AddonSlice:
45
    """A tiny class to implement duck-typing for the URL-parsing utility functions"""
46

47
    def __init__(self, url, branch):
48
        self.url = url
49
        self.branch = branch
50

51

52
class Predictor:
53
    """Guess the appropriate metadata to apply to a project based on various parameters
54
    found in the supplied directory."""
55

56
    def __init__(self):
57
        self.path = None
58
        self.metadata = FreeCAD.Metadata()
59
        self.license_data = None
60
        self.readme_data = None
61
        self.license_file = ""
62
        self.git_manager: GitManager = initialize_git()
63
        if not self.git_manager:
64
            raise Exception("Cannot use Developer Mode without git installed")
65

66
    def predict_metadata(self, path: str) -> FreeCAD.Metadata:
67
        """Create a predicted Metadata object based on the contents of the passed-in directory"""
68
        if not os.path.isdir(path):
69
            return None
70
        self.path = path
71
        self._predict_author_info()
72
        self._predict_name()
73
        self._predict_description()
74
        self._predict_contents()
75
        self._predict_icon()
76
        self._predict_urls()
77
        self._predict_license()
78
        self._predict_version()
79

80
        return self.metadata
81

82
    def _predict_author_info(self):
83
        """Look at the git commit history and attempt to discern maintainer and author
84
        information."""
85

86
        committers = self.git_manager.get_last_committers(self.path)
87

88
        # This is a dictionary keyed to the author's name (which can be many
89
        # things, depending on the author) containing two fields, "email" and "count". It
90
        # is common for there to be multiple entries representing the same human being,
91
        # so a passing attempt is made to reconcile:
92
        filtered_committers = {}
93
        for key, committer in committers.items():
94
            if "github" in key.lower():
95
                # Robotic merge commit (or other similar), ignore
96
                continue
97
            # Does any other committer share any of these emails?
98
            for other_key, other_committer in committers.items():
99
                if other_key == key:
100
                    continue
101
                for other_email in other_committer["email"]:
102
                    if other_email in committer["email"]:
103
                        # There is overlap in the two email lists, so this is probably the
104
                        # same author, with a different name (username, pseudonym, etc.)
105
                        if not committer["aka"]:
106
                            committer["aka"] = set()
107
                        committer["aka"].add(other_key)
108
                        committer["count"] += other_committer["count"]
109
                        committer["email"].combine(other_committer["email"])
110
                        committers.pop(other_key)
111
                        break
112
            filtered_committers[key] = committer
113
        maintainers = []
114
        for name, info in filtered_committers.items():
115
            if "aka" in info:
116
                for other_name in info["aka"]:
117
                    # Heuristic: the longer name is more likely to be the actual legal name
118
                    if len(other_name) > len(name):
119
                        name = other_name
120
            # There is no logical basis to choose one email address over another, so just
121
            # take the first one
122
            email = info["email"][0]
123
            commit_count = info["count"]
124
            maintainers.append({"name": name, "email": email, "count": commit_count})
125

126
        # Sort by count of commits
127
        maintainers.sort(key=lambda i: i["count"], reverse=True)
128

129
        self.metadata.Maintainer = maintainers
130

131
    def _predict_name(self):
132
        """Predict the name based on the local path name and/or the contents of a
133
        README.md file."""
134

135
        normed_path = self.path.replace(os.path.sep, "/")
136
        path_components = normed_path.split("/")
137
        final_path_component = path_components[-1]
138
        predicted_name = final_path_component.replace("/", "")
139
        self.metadata.Name = predicted_name
140

141
    def _predict_description(self):
142
        """Predict the description based on the contents of a README.md file."""
143
        self._load_readme()
144

145
        if not self.readme_data:
146
            return
147

148
        lines = self.readme_data.split("\n")
149
        description = ""
150
        for line in lines:
151
            if "#" in line:
152
                continue  # Probably not a line of description
153
            if "![" in line:
154
                continue  # An image link, probably separate from any description
155
            if not line and description:
156
                break  # We're done: this is a blank line, and we've read some data already
157
            if description:
158
                description += " "
159
            description += line
160

161
        if description:
162
            self.metadata.Description = description
163

164
    def _predict_contents(self):
165
        """Predict the contents based on the contents of the directory."""
166

167
    def _predict_icon(self):
168
        """Predict the icon based on either a class which defines an Icon member, or
169
        the contents of the local directory structure."""
170

171
    def _predict_urls(self):
172
        """Predict the URLs based on git settings"""
173

174
        branch = self.git_manager.current_branch(self.path)
175
        remote = self.git_manager.get_remote(self.path)
176

177
        addon = AddonSlice(remote, branch)
178
        readme = get_readme_url(addon)
179

180
        self.metadata.addUrl("repository", remote, branch)
181
        self.metadata.addUrl("readme", readme)
182

183
    def _predict_license(self):
184
        """Predict the license based on any existing license file."""
185

186
        # These are processed in order, so the BSD 3 clause must come before the 2, for example,
187
        # because the only difference between them is the additional clause.
188
        known_strings = {
189
            "Apache-2.0": (
190
                "Apache License, Version 2.0",
191
                "Apache License\nVersion 2.0, January 2004",
192
            ),
193
            "BSD-3-Clause": (
194
                "The 3-Clause BSD License",
195
                "3. Neither the name of the copyright holder nor the names of its contributors \
196
                may be used to endorse or promote products derived from this software without \
197
                specific prior written permission.",
198
            ),
199
            "BSD-2-Clause": (
200
                "The 2-Clause BSD License",
201
                "2. Redistributions in binary form must reproduce the above copyright notice, \
202
                this list of conditions and the following disclaimer in the documentation and/or \
203
                other materials provided with the distribution.",
204
            ),
205
            "CC0v1": (
206
                "CC0 1.0 Universal",
207
                "voluntarily elects to apply CC0 to the Work and publicly distribute the Work \
208
                under its terms",
209
            ),
210
            "GPLv2": (
211
                "GNU General Public License version 2",
212
                "GNU GENERAL PUBLIC LICENSE\nVersion 2, June 1991",
213
            ),
214
            "GPLv3": (
215
                "GNU General Public License version 3",
216
                "The GNU General Public License is a free, copyleft license for software and \
217
                other kinds of works.",
218
            ),
219
            "LGPLv2.1": (
220
                "GNU Lesser General Public License version 2.1",
221
                "GNU Lesser General Public License\nVersion 2.1, February 1999",
222
            ),
223
            "LGPLv3": (
224
                "GNU Lesser General Public License version 3",
225
                "GNU LESSER GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007",
226
            ),
227
            "MIT": (
228
                "The MIT License",
229
                "including without limitation the rights to use, copy, modify, merge, publish, \
230
                distribute, sublicense, and/or sell copies of the Software",
231
            ),
232
            "MPL-2.0": (
233
                "Mozilla Public License 2.0",
234
                "https://opensource.org/licenses/MPL-2.0",
235
            ),
236
        }
237
        self._load_license()
238
        if self.license_data:
239
            for shortcode, test_data in known_strings.items():
240
                if shortcode.lower() in self.license_data.lower():
241
                    self.metadata.addLicense(shortcode, self.license_file)
242
                    return
243
                for test_text in test_data:
244
                    # Do the comparison without regard to whitespace or capitalization
245
                    if (
246
                        "".join(test_text.split()).lower()
247
                        in "".join(self.license_data.split()).lower()
248
                    ):
249
                        self.metadata.addLicense(shortcode, self.license_file)
250
                        return
251

252
    def _predict_version(self):
253
        """Default to a CalVer style set to today's date"""
254
        year = datetime.date.today().year
255
        month = datetime.date.today().month
256
        day = datetime.date.today().day
257
        version_string = f"{year}.{month:>02}.{day:>02}"
258
        self.metadata.Version = version_string
259

260
    def _load_readme(self):
261
        """Load in any existing readme"""
262
        valid_names = ["README.md", "README.txt", "README"]
263
        for name in valid_names:
264
            full_path = os.path.join(self.path, name)
265
            if os.path.exists(full_path):
266
                with open(full_path, encoding="utf-8") as f:
267
                    self.readme_data = f.read()
268
                    return
269

270
    def _load_license(self):
271
        """Load in any existing license"""
272
        valid_names = [
273
            "LICENSE",
274
            "LICENCE",
275
            "COPYING",
276
            "LICENSE.txt",
277
            "LICENCE.txt",
278
            "COPYING.txt",
279
        ]
280
        for name in valid_names:
281
            full_path = os.path.join(self.path.replace("/", os.path.sep), name)
282
            if os.path.isfile(full_path):
283
                with open(full_path, encoding="utf-8") as f:
284
                    self.license_data = f.read()
285
                    self.license_file = name
286
                    return
287

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.