Skip to content

Commit b5f7a6a

Browse files
committed
Add support for license override config file in YAML, fix bug with wayward u character in output file
1 parent 376a327 commit b5f7a6a

File tree

6 files changed

+165
-63
lines changed

6 files changed

+165
-63
lines changed

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ Two different pip "requirements" files, two different Python paths (Virtualenvs)
4848
-p ~/.virtualenvs/backend_py/bin/python \
4949
-r requirements-pypy.txt \
5050
-p ~/.virtualenvs/backend_pypy/bin/python \
51-
--skip-prefix ims-
51+
-s ims-
5252

5353
Please note that pip "requirements" files and Python executable paths are paired together in the order they're specified.
5454

55-
Three different pip "requirements" files, two different Python paths (Virtualenvs), a GPL exception and a custom output file:
55+
Three different pip "requirements" files, two different Python paths (need to repeat), a GPL exception and a custom output file:
5656

5757
python -m third_party_license_file_generator \
5858
-r requirements.txt \
@@ -61,8 +61,26 @@ Three different pip "requirements" files, two different Python paths (Virtualenv
6161
-p ~/.virtualenvs/api_pypy/bin/python \
6262
-r cpython_requirements.txt \
6363
-p ~/.virtualenvs/api_py/bin/python \
64-
--gpl-exception uwsgi
65-
--output-file ThirdPartyLicenses.txt
64+
-x uWSGI \
65+
-o ThirdPartyLicenses.txt
66+
67+
Three different pip "requirements" files, two different Python paths (need to repeat), a GPL exception, a custom output file and a license override file:
68+
69+
# contents of license_override_file.yml
70+
uWSGI:
71+
license_name: GPL-2.0 w/ linking exception
72+
license_file: https://raw.githubusercontent.com/unbit/uwsgi/master/LICENSE
73+
74+
python -m third_party_license_file_generator \
75+
-r requirements.txt \
76+
-p ~/.virtualenvs/api_pypy/bin/python \
77+
-r pypy_requirements.txt \
78+
-p ~/.virtualenvs/api_pypy/bin/python \
79+
-r cpython_requirements.txt \
80+
-p ~/.virtualenvs/api_py/bin/python \
81+
-x uWSGI \
82+
-o ThirdPartyLicenses.txt \
83+
-l license_override_file.yml
6684

6785
An example of the structure of the generated third party license file is as follows:
6886

THIRDPARTYLICENSES

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,24 @@
1-
Start of 'THIRDPARTYLICENSES' generated by Python third_party_license_generator at 2018-04-19 13:34:09.628303u
1+
Start of 'THIRDPARTYLICENSES' generated by Python third_party_license_generator at 2018-05-07 12:57:58.761181
2+
3+
----------------------------------------
4+
5+
Package: PyYAML
6+
License: MIT
7+
Requires: n/a
8+
Author: Kirill Simonov <xi@resolvent.net>
9+
Home page: http://pyyaml.org/wiki/PyYAML
10+
11+
NOTE: This module was missing a license file (despite listing a license name) so one has been auto-generated
12+
13+
The MIT License
14+
15+
Copyright 2018 Kirill Simonov <xi@resolvent.net>
16+
17+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
18+
19+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
20+
21+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
222

323
----------------------------------------
424

@@ -28,7 +48,7 @@ v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
2848
one at http://mozilla.org/MPL/2.0/.
2949

3050
***** END LICENSE BLOCK *****
31-
@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $u
51+
@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $
3252

3353
----------------------------------------
3454

@@ -539,7 +559,7 @@ necessary. Here is a sample; alter the names:
539559
<signature of Ty Coon>, 1 April 1990
540560
Ty Coon, President of Vice
541561

542-
That's all there is to it!u
562+
That's all there is to it!
543563

544564
----------------------------------------
545565

@@ -563,7 +583,7 @@ Redistribution and use in source and binary forms, with or without modification,
563583

564584
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
565585

566-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.u
586+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
567587

568588
----------------------------------------
569589

@@ -659,7 +679,7 @@ To apply the Apache License to your work, attach the following boilerplate notic
659679
distributed under the License is distributed on an "AS IS" BASIS,
660680
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
661681
See the License for the specific language governing permissions and
662-
limitations under the License.u
682+
limitations under the License.
663683

664684
----------------------------------------
665685

@@ -679,8 +699,8 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
679699

680700
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
681701

682-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.u
702+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
683703

684704
----------------------------------------
685705

686-
End of 'THIRDPARTYLICENSES' generated by Python third_party_license_generator at 2018-04-19 13:34:09.628385
706+
End of 'THIRDPARTYLICENSES' generated by Python third_party_license_generator at 2018-05-07 12:57:58.761237

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
requests==2.18.4
2+
PyYAML==3.12

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
# Versions should comply with PEP440. For a discussion on single-sourcing
2525
# the version across setup.py and the project code, see
2626
# https://packaging.python.org/en/latest/single_source_version.html
27-
version='2018.5',
27+
version='2018.6',
2828

2929
description='The Python third_party_license_file_generator is aimed at distilling down the appropriate license for one or many pip "requirements" files into a single file; it supports Python2.7 and Python3.',
3030
long_description=long_description,

third_party_license_file_generator/__main__.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import os
77
import sys
88

9+
import requests
10+
import yaml
911
from third_party_license_file_generator.site_packages import SitePackages
1012

1113
# for Python2.7
@@ -104,9 +106,54 @@
104106
)
105107
)
106108

109+
parser.add_argument(
110+
'-l',
111+
'--license-override-file',
112+
type=str,
113+
required=False,
114+
default=None,
115+
help='the location of a YAML file that describes a dict {"(some case-sensitive package name)": {"license_name": "(some license name)", "license_file": "(URL or system file path to license file"}} (but as YAML)'
116+
)
117+
118+
107119
if __name__ == '__main__':
108120
args = parser.parse_args()
109121

122+
license_overrides = {}
123+
if args.license_override_file is not None:
124+
with codecs.open(args.license_override_file, 'r', 'utf-8') as f:
125+
license_overrides = yaml.load(f.read())
126+
127+
for module_name, license_override in license_overrides.items():
128+
license_name = license_override.get('license_name')
129+
license_file = license_override.get('license_file')
130+
if None in [license_name, license_file]:
131+
print(
132+
'ERROR: license_name or license_file for license override of {0} missing or empty'.format(
133+
repr(module_name)
134+
)
135+
)
136+
sys.exit(1)
137+
138+
actual_license_file = None
139+
if license_file.lower().startswith('http'):
140+
r = requests.get(license_file, timeout=5)
141+
actual_license_file = r.text.strip()
142+
else:
143+
with codecs.open(license_file, 'r', 'utf-8') as f:
144+
actual_license_file = f.read().strip()
145+
146+
if actual_license_file is None:
147+
print(
148+
'ERROR: attempt to get license_file for license override of {0} from {1} returned empty file'.format(
149+
repr(module_name),
150+
repr(license_file)
151+
)
152+
)
153+
sys.exit(1)
154+
155+
license_override['license_file'] = actual_license_file
156+
110157
pairs = tuple(
111158
zip(
112159
[x.strip() for x in args.requirements_path if x.strip() != ''],
@@ -137,6 +184,7 @@
137184
python_path=python_path,
138185
skip_prefixes=args.skip_prefix,
139186
use_internet=not args.no_internet_lookups,
187+
license_overrides=license_overrides,
140188
)
141189
]
142190

@@ -156,7 +204,7 @@
156204
module_output = ''
157205
module_names = []
158206
for module in modules:
159-
module_names += [module.name.lower()]
207+
module_names += [module.name]
160208
module_output += '\t{0} by {1} ({2})\n'.format(
161209
repr(module.name),
162210
repr(module.author),
@@ -165,7 +213,7 @@
165213

166214
warning = ''
167215
if not args.permit_gpl and license_name.startswith('GPL') and not all(
168-
[x.lower() in args.gpl_exception for x in module_names]):
216+
[x in args.gpl_exception for x in module_names]):
169217
warning = gpl_warning
170218
gpl_triggered = True
171219
elif not args.permit_commercial and license_name in ['Commercial', 'Unknown (assumed commercial)']:
@@ -217,7 +265,7 @@
217265

218266
separator = u'-' * 40
219267

220-
data = 'u\n\n{0}\n\n'.format(separator).join(third_party_licenses)
268+
data = u'\n\n{0}\n\n'.format(separator).join(third_party_licenses)
221269

222270
with codecs.open(args.output_file, 'w', 'utf-8') as f:
223271
f.write(data.strip() + '\n')

third_party_license_file_generator/site_packages.py

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,13 @@ class LicenseError(Exception):
7878

7979

8080
class SitePackages(object):
81-
def __init__(self, requirements_path, python_path=None, skip_prefixes=None, autorun=True, use_internet=True):
81+
def __init__(self, requirements_path, python_path=None, skip_prefixes=None,
82+
autorun=True, use_internet=True, license_overrides=None):
8283
self._requirements_path = requirements_path
8384
self._python_path = python_path if python_path is not None else distutils.spawn.find_executable('python3')
8485
self._skip_prefixes = skip_prefixes
8586
self._use_internet = use_internet
87+
self._license_overrides = license_overrides if license_overrides is not None else {}
8688

8789
self._root_module_names = set()
8890
self._required_module_names = set()
@@ -271,60 +273,73 @@ def _read_site_packages(self):
271273

272274
author = metadata['author']
273275
home_page = metadata['home_page']
274-
original_license_name = metadata['license_name']
275-
license_file = self._module_licenses_by_module_name.get(module_name)
276276

277-
# print('details are', [author, home_page, original_license_name])
277+
overriden_license_name = None
278+
overriden_license_file = None
278279

279-
license_name = parse_license(original_license_name)
280-
# if license_name is not None:
281-
# print('got license_name', repr(license_name))
280+
license_override = self._license_overrides.get(module_name)
281+
if module_name in self._license_overrides:
282+
overriden_license_name = license_override.get('license_name')
283+
overriden_license_file = license_override.get('license_file')
282284

283-
# if license_file is not None:
284-
# print('got license_file from module folder')
285-
286-
github_license_file = None
287-
if original_license_name not in ['Commercial']:
288-
if license_name is None and self._use_internet:
289-
pypi_license_name = get_license_from_pypi_license_scrape(module_name)
290-
license_name = parse_license(pypi_license_name)
291-
# if license_name is not None:
292-
# print('got license_name from PyPI', repr(license_name))
293-
294-
if license_name is None:
295-
if home_page is not None and 'github' in home_page and self._use_internet:
296-
github_license_file = get_license_from_github_home_page_scrape(home_page)
297-
license_name = parse_license(github_license_file)
298-
# if license_name is not None:
299-
# print('got license_name from Github repo', repr(license_name))
300-
301-
if license_file is None:
302-
license_file = github_license_file
303-
304-
# if license_file is not None:
305-
# print('got license_file from Github repo')
285+
if None not in [overriden_license_name, overriden_license_file]:
286+
license_name = overriden_license_name
287+
license_file = overriden_license_file
288+
else:
289+
original_license_name = metadata['license_name']
290+
license_file = self._module_licenses_by_module_name.get(module_name)
306291

307-
if license_file is None:
308-
if github_license_file is None and self._use_internet:
309-
github_license_file = get_license_from_github_home_page_scrape(home_page)
292+
# print('details are', [author, home_page, original_license_name])
310293

311-
license_file = github_license_file
312-
# if license_file is not None:
313-
# print('got license_file from Github repo')
294+
license_name = parse_license(original_license_name)
295+
# if license_name is not None:
296+
# print('got license_name', repr(license_name))
314297

315-
if license_file is None and license_name is not None:
316-
license_file = build_license_file_for_author(author, license_name)
317298
# if license_file is not None:
318-
# print('built license_file from local store')
319-
320-
# if license_name is None:
321-
# print('warning: all attempts to get license_name failed')
322-
# elif license_file is None:
323-
# print('warning: all attempts to get license_file failed')
324-
325-
if license_name is None:
326-
license_name = 'Unknown (assumed commercial)'
327-
license_file = build_license_file_for_author(author, 'Commercial')
299+
# print('got license_file from module folder')
300+
301+
github_license_file = None
302+
if original_license_name not in ['Commercial']:
303+
if license_name is None and self._use_internet:
304+
pypi_license_name = get_license_from_pypi_license_scrape(module_name)
305+
license_name = parse_license(pypi_license_name)
306+
# if license_name is not None:
307+
# print('got license_name from PyPI', repr(license_name))
308+
309+
if license_name is None:
310+
if home_page is not None and 'github' in home_page and self._use_internet:
311+
github_license_file = get_license_from_github_home_page_scrape(home_page)
312+
license_name = parse_license(github_license_file)
313+
# if license_name is not None:
314+
# print('got license_name from Github repo', repr(license_name))
315+
316+
if license_file is None:
317+
license_file = github_license_file
318+
319+
# if license_file is not None:
320+
# print('got license_file from Github repo')
321+
322+
if license_file is None:
323+
if github_license_file is None and self._use_internet:
324+
github_license_file = get_license_from_github_home_page_scrape(home_page)
325+
326+
license_file = github_license_file
327+
# if license_file is not None:
328+
# print('got license_file from Github repo')
329+
330+
if license_file is None and license_name is not None:
331+
license_file = build_license_file_for_author(author, license_name)
332+
# if license_file is not None:
333+
# print('built license_file from local store')
334+
335+
# if license_name is None:
336+
# print('warning: all attempts to get license_name failed')
337+
# elif license_file is None:
338+
# print('warning: all attempts to get license_file failed')
339+
340+
if license_name is None:
341+
license_name = 'Unknown (assumed commercial)'
342+
license_file = build_license_file_for_author(author, 'Commercial')
328343

329344
module = Module(
330345
name=module_name,

0 commit comments

Comments
 (0)