Skip to content

Commit c1273ae

Browse files
committed
add new license provider plugin for additional licenses
Signed-off-by: Kevin Ji <kyji1011@gmail.com>
1 parent f1da1d2 commit c1273ae

4 files changed

Lines changed: 168 additions & 9 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
5+
# See https://github.com/nexB/plugincode for support or download.
6+
# See https://aboutcode.org for more information about nexB OSS projects.
7+
8+
import logging
9+
import os
10+
import sys
11+
12+
from pluggy import PluginManager as PluggyPluginManager
13+
14+
from plugincode import HookimplMarker
15+
from plugincode import HookspecMarker
16+
17+
"""
18+
Support for plugins that provide one or more paths keys typically OS-specific
19+
paths to bundled pre-built binaries provided as Python packages.
20+
Plugin can either be enabled for very specific environment/platform markers (OS,
21+
arch, etc) in their built wheels .... Or be smart about OS/ARCH/etc and provide
22+
a path based on running some code.
23+
"""
24+
25+
logger = logging.getLogger(__name__)
26+
27+
# uncomment to enable logging locally
28+
# logging.basicConfig(stream=sys.stdout)
29+
# logger.setLevel(logging.DEBUG)
30+
31+
32+
def logger_debug(*args):
33+
return logger.debug(" ".join(isinstance(a, str) and a or repr(a) for a in args))
34+
35+
36+
project_name = __name__
37+
entrypoint = "scancode_additional_license_location_provider"
38+
39+
location_provider_spec = HookspecMarker(project_name=project_name)
40+
location_provider_impl = HookimplMarker(project_name=project_name)
41+
42+
43+
@location_provider_spec
44+
class AdditionalLicenseLocationProviderPlugin(object):
45+
"""
46+
Base plugin class for plugins that provide path locations for one or more
47+
keys such as the path location to a native binary executable or related
48+
system files.
49+
A plugin is configured as it own package with proper environemnt markers
50+
"""
51+
52+
# name string under which this plugin is registered.
53+
# This is set automatically when a plugin class is loaded in its manager.
54+
# Subclasses must not set this.
55+
name = None
56+
57+
def get_locations(self):
58+
"""
59+
Return a mapping of {key: location} where location is an absolute path
60+
to a file or directory referenced by a known key. The location should
61+
exist on a given platorm/OS where this plgin can be installed.
62+
"""
63+
raise NotImplementedError
64+
65+
66+
class AdditionalLicensePluginManager(object):
67+
"""
68+
A PluginManager class for simple, non-scanning related plugins.
69+
"""
70+
71+
def __init__(self, project_name, entrypoint, plugin_base_class):
72+
"""
73+
Initialize this plugin manager for the fully qualified Python module
74+
name `module_qname` with plugins loaded from the setuptools `entrypoint`
75+
that must subclass `plugin_base_class`.
76+
"""
77+
self.manager = PluggyPluginManager(project_name=project_name)
78+
self.entrypoint = entrypoint
79+
self.plugin_base_class = plugin_base_class
80+
self.manager.add_hookspecs(sys.modules[project_name])
81+
82+
# set to True once this manager is initialized by running its setup()
83+
self.initialized = False
84+
85+
# mapping of {plugin.name: plugin_class} for all the loaded plugins of
86+
# this manager
87+
self.plugin_classes = dict()
88+
89+
def setup(self):
90+
"""
91+
Load and validate available plugins for this PluginManager from its
92+
assigned `entrypoint`. Raise an Exception if a plugin is not valid such
93+
that when it does not subcclass the manager `plugin_base_class`.
94+
Must be called once to initialize the plugins if this manager.
95+
Return a list of all plugin classes for this manager.
96+
"""
97+
if self.initialized:
98+
return self.plugin_classes.values()
99+
100+
entrypoint = self.entrypoint
101+
self.manager.load_setuptools_entrypoints(entrypoint)
102+
103+
plugin_classes = []
104+
for name, plugin_class in self.manager.list_name_plugin():
105+
if not issubclass(plugin_class, self.plugin_base_class):
106+
plugin_base_class = self.plugin_base_class
107+
raise Exception(
108+
"Invalid plugin: %(name)r: %(plugin_class)r "
109+
"must extend %(plugin_base_class)r." % locals()
110+
)
111+
112+
plugin_class.name = name
113+
plugin_classes.append(plugin_class)
114+
115+
self.plugin_classes = dict([(cls.name, cls) for cls in plugin_classes])
116+
self.initialized = True
117+
return self.plugin_classes.values()
118+
119+
120+
additional_license_location_provider_plugins = AdditionalLicensePluginManager(
121+
project_name=project_name, entrypoint=entrypoint, plugin_base_class=AdditionalLicenseLocationProviderPlugin
122+
)
123+
124+
125+
class ProvidedLocationError(Exception):
126+
pass
127+
128+
129+
def get_location(location_key, _cached_locations={}):
130+
"""
131+
Return the location for a `location_key` if available from plugins or None.
132+
"""
133+
if not _cached_locations:
134+
additional_license_location_provider_plugins.setup()
135+
136+
unknown_locations = {}
137+
138+
for k, plugin_class in additional_license_location_provider_plugins.plugin_classes.items():
139+
pc = plugin_class()
140+
provided_locs = pc.get_locations() or {}
141+
for loc_key, location in provided_locs.items():
142+
if not os.path.exists(location):
143+
unknown_locations[loc_key] = location
144+
145+
if loc_key in _cached_locations:
146+
existing = _cached_locations[loc_key]
147+
msg = (
148+
"Duplicate location key provided: {loc_key}: "
149+
"new: {location}, existing:{existing}"
150+
)
151+
msg = msg.format(**locals())
152+
raise ProvidedLocationError(msg)
153+
154+
_cached_locations[loc_key] = location
155+
156+
if unknown_locations:
157+
msg = "Non-existing locations provided:\n:"
158+
msg += "\n".join("key:{}, loc: {}".format(k, l) for k, l in unknown_locations.items())
159+
raise ProvidedLocationError(msg)
160+
161+
return _cached_locations.get(location_key)

src/licensedcode/models.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -812,14 +812,12 @@ def get_paths_to_installed_licenses_and_rules():
812812
with a common prefix.
813813
"""
814814
from importlib_metadata import entry_points
815-
from plugincode.location_provider import get_location
816-
installed_plugins = entry_points(group='scancode_location_provider')
815+
from licensedcode.additional_license_location_provider import get_location
816+
installed_plugins = entry_points(group='scancode_additional_license_location_provider')
817817
paths = []
818818
for plugin in installed_plugins:
819-
if plugin.name.startswith(EXTERNAL_LICENSE_PLUGIN_PREFIX):
820-
# get path to directory of licenses and/or rules
821-
location_key = plugin.name
822-
paths.append(get_location(location_key))
819+
# get path to directory of licenses and/or rules
820+
paths.append(get_location(plugin.name))
823821
return paths
824822

825823

tests/licensedcode/data/example_external_licenses/licenses_to_install1/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
'scancode-toolkit',
4242
],
4343
entry_points={
44-
'scancode_location_provider': [
44+
'scancode_additional_license_location_provider': [
4545
'licenses_to_install1 = licenses_to_install1:LicensesToInstall1Paths',
4646
],
4747
},

tests/licensedcode/data/example_external_licenses/licenses_to_install1/src/licenses_to_install1/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
from os.path import abspath
2626
from os.path import dirname
2727

28-
from plugincode.location_provider import LocationProviderPlugin
28+
from licensedcode.additional_license_location_provider import AdditionalLicenseLocationProviderPlugin
2929

3030

31-
class LicensesToInstall1Paths(LocationProviderPlugin):
31+
class LicensesToInstall1Paths(AdditionalLicenseLocationProviderPlugin):
3232
def get_locations(self):
3333
curr_dir = dirname(abspath(__file__))
3434
locations = {

0 commit comments

Comments
 (0)