Skip to content

Commit 5a0650c

Browse files
committed
Adopt new handler design for OCaml OPAM
We now have this data file handler: * OpamFileHandler And we assemble this in a Package correctly Signed-off-by: Philippe Ombredanne <[email protected]>
1 parent 494b87d commit 5a0650c

File tree

10 files changed

+159
-230
lines changed

10 files changed

+159
-230
lines changed

src/packagedcode/opam.py

Lines changed: 108 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -7,81 +7,40 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10-
import io
11-
import logging
1210
import re
1311

14-
import attr
1512
from packageurl import PackageURL
1613

17-
from commoncode import filetype
1814
from packagedcode import models
1915

20-
2116
"""
22-
Handle opam package.
17+
Handle OCaml opam package.
2318
"""
2419

25-
TRACE = False
26-
27-
logger = logging.getLogger(__name__)
28-
29-
if TRACE:
30-
import sys
31-
logging.basicConfig(stream=sys.stdout)
32-
logger.setLevel(logging.DEBUG)
33-
3420

35-
@attr.s()
36-
class OpamPackageData(models.PackageData):
37-
38-
default_type = 'opam'
21+
class OpamFileHandler(models.DatafileHandler):
22+
datasource_id = 'opam_file'
23+
path_patterns = ('*opam',)
24+
default_package_type = 'opam'
3925
default_primary_language = 'Ocaml'
40-
default_web_baseurl = 'https://opam.ocaml.org/packages'
41-
default_download_baseurl = None
42-
default_api_baseurl = 'https://github.com/ocaml/opam-repository/blob/master/packages'
26+
description = 'Ocaml Opam file'
27+
documentation_url = 'https://opam.ocaml.org/doc/Manual.html#Common-file-format'
4328

4429
@classmethod
45-
def get_package_root(cls, manifest_resource, codebase):
46-
return manifest_resource.parent(codebase)
47-
48-
def repository_homepage_url(self, baseurl=default_web_baseurl):
49-
if self.name:
50-
return '{}/{}'.format(baseurl, self.name)
51-
52-
def api_data_url(self, baseurl=default_api_baseurl):
53-
if self.name and self.version:
54-
return '{}/{}/{}.{}/opam'.format(baseurl, self.name, self.name, self.version)
55-
56-
57-
@attr.s()
58-
class OpamFile(OpamPackageData, models.PackageDataFile):
59-
60-
file_patterns = ('*opam',)
61-
extensions = ('.opam',)
30+
def get_package_root(cls, resource, codebase):
31+
return resource.parent(codebase)
6232

6333
@classmethod
64-
def is_package_data_file(cls, location):
65-
"""
66-
Return True if the file at ``location`` is likely a manifest of this type.
67-
"""
68-
return filetype.is_file(location) and location.endswith('opam')
69-
70-
@classmethod
71-
def recognize(cls, location):
72-
"""
73-
Yield one or more Package manifest objects given a file ``location`` pointing to a
74-
package archive, manifest or similar.
75-
"""
34+
def parse(cls, location):
7635
opams = parse_opam(location)
7736

7837
package_dependencies = []
7938
deps = opams.get('depends') or []
8039
for dep in deps:
8140
package_dependencies.append(
8241
models.DependentPackage(
83-
purl=dep.purl,
84-
extracted_requirement=dep.version,
42+
purl=dep["purl"],
43+
extracted_requirement=dep["version"],
8544
scope='dependency',
8645
is_runtime=True,
8746
is_optional=False,
@@ -91,6 +50,7 @@ def recognize(cls, location):
9150

9251
name = opams.get('name')
9352
version = opams.get('version')
53+
9454
homepage_url = opams.get('homepage')
9555
download_url = opams.get('src')
9656
vcs_url = opams.get('dev-repo')
@@ -100,6 +60,8 @@ def recognize(cls, location):
10060
md5 = opams.get('md5')
10161
sha256 = opams.get('sha256')
10262
sha512 = opams.get('sha512')
63+
repository_homepage_url = get_repository_homepage_url(name)
64+
api_data_url = get_api_data_url(name, version)
10365

10466
short_desc = opams.get('synopsis') or ''
10567
long_desc = opams.get('description') or ''
@@ -128,7 +90,9 @@ def recognize(cls, location):
12890
)
12991
)
13092

131-
package = cls(
93+
package_data = models.PackageData(
94+
datasource_id=cls.datasource_id,
95+
type=cls.default_package_type,
13296
name=name,
13397
version=version,
13498
vcs_url=vcs_url,
@@ -142,86 +106,29 @@ def recognize(cls, location):
142106
declared_license=declared_license,
143107
description=description,
144108
parties=parties,
145-
dependencies=package_dependencies
109+
dependencies=package_dependencies,
110+
api_data_url=api_data_url,
111+
repository_homepage_url=repository_homepage_url,
112+
primary_language=cls.default_primary_language
146113
)
147114

148-
yield package
115+
if not package_data.license_expression and package_data.declared_license:
116+
package_data.license_expression = models.compute_normalized_license(package_data.declared_license)
149117

118+
yield package_data
150119

151-
@attr.s()
152-
class OpamPackage(OpamPackageData, models.Package):
153-
"""
154-
A Opam Package that is created out of one/multiple opam package
155-
manifests and package-like data, with it's files.
156-
"""
120+
@classmethod
121+
def assign_package_to_resources(cls, package, resource, codebase):
122+
return super().assign_package_to_parent_tree(package, resource, codebase)
157123

158-
@property
159-
def manifests(self):
160-
return [
161-
OpamFile
162-
]
163124

125+
def get_repository_homepage_url(name):
126+
return name and '{https://opam.ocaml.org/packages}/{name}'
164127

165-
"""
166-
Example:-
167-
168-
Sample opam file(sample3.opam):
169-
opam-version: "2.0"
170-
version: "4.11.0+trunk"
171-
synopsis: "OCaml development version"
172-
depends: [
173-
"ocaml" {= "4.11.0" & post}
174-
"base-unix" {post}
175-
]
176-
conflict-class: "ocaml-core-compiler"
177-
flags: compiler
178-
setenv: CAML_LD_LIBRARY_PATH = "%{lib}%/stublibs"
179-
build: [
180-
["./configure" "--prefix=%{prefix}%"]
181-
[make "-j%{jobs}%"]
182-
]
183-
install: [make "install"]
184-
maintainer: "[email protected]"
185-
homepage: "https://github.com/ocaml/ocaml/"
186-
bug-reports: "https://github.com/ocaml/ocaml/issues"
187-
authors: [
188-
"Xavier Leroy"
189-
"Damien Doligez"
190-
"Alain Frisch"
191-
"Jacques Garrigue"
192-
]
193-
194-
>>> p = parse_opam('sample3.opam')
195-
>>> for k, v in p.items():
196-
>>> print(k, v)
197-
198-
Output:
199-
opam-version 2.0
200-
version 4.11.0+trunk
201-
synopsis OCaml development version
202-
depends [Opam(name='ocaml', version='= 4.11.0 & post'), Opam(name='base-unix', version='post')]
203-
conflict-class ocaml-core-compiler
204-
flags compiler
205-
setenv CAML_LD_LIBRARY_PATH = %{lib}%/stublibs
206-
build
207-
install make install
208-
maintainer ['[email protected]']
209-
homepage https://github.com/ocaml/ocaml/
210-
bug-reports https://github.com/ocaml/ocaml/issues
211-
authors ['Xavier Leroy', 'Damien Doligez', 'Alain Frisch', 'Jacques Garrigue']
212-
"""
213-
214-
@attr.s()
215-
class Opam(object):
216-
name = attr.ib(default=None)
217-
version = attr.ib(default=None)
218128

219-
@property
220-
def purl(self):
221-
return PackageURL(
222-
type='opam',
223-
name=self.name
224-
).to_string()
129+
def get_api_data_url(name, version):
130+
if name and version:
131+
return f'https://github.com/ocaml/opam-repository/blob/master/packages/{name}/{name}.{version}/opam'
225132

226133

227134
# Regex expressions to parse file lines
@@ -259,78 +166,92 @@ def purl(self):
259166
>>> assert p.group('version') == ('{= "1.0.0"}')
260167
"""
261168

169+
262170
def parse_opam(location):
263171
"""
264-
Return a mapping of package data collected from the opam OCaml package manifest file at `location`.
172+
Return a mapping of package data collected from the opam OCaml package
173+
manifest file at ``location``.
174+
"""
175+
with open(location) as od:
176+
text = od.read()
177+
return parse_opam_from_text(text)
178+
179+
180+
def parse_opam_from_text(text):
181+
"""
182+
Return a mapping of package data collected from the opam OCaml package
183+
manifest ``text``.
265184
"""
266-
with io.open(location, encoding='utf-8') as data:
267-
lines = data.readlines()
268185

269186
opam_data = {}
270187

188+
lines = text.splitlines()
271189
for i, line in enumerate(lines):
272190
parsed_line = parse_file_line(line)
273-
if parsed_line:
274-
key = parsed_line.group('key').strip()
275-
value = parsed_line.group('value').strip()
276-
if key == 'description': # Get multiline description
277-
value = ''
278-
for cont in lines[i+1:]:
279-
value += ' ' + cont.strip()
280-
if '"""' in cont:
191+
if not parsed_line:
192+
continue
193+
key = parsed_line.group('key').strip()
194+
value = parsed_line.group('value').strip()
195+
if key == 'description': # Get multiline description
196+
value = ''
197+
for cont in lines[i + 1:]:
198+
value += ' ' + cont.strip()
199+
if '"""' in cont:
200+
break
201+
202+
opam_data[key] = clean_data(value)
203+
204+
if key == 'maintainer':
205+
stripped_val = value.strip('["] ')
206+
stripped_val = stripped_val.split('" "')
207+
opam_data[key] = stripped_val
208+
elif key == 'authors':
209+
if '[' in line: # If authors are present in multiple lines
210+
for authors in lines[i + 1:]:
211+
value += ' ' + authors.strip()
212+
if ']' in authors:
281213
break
282-
214+
value = value.strip('["] ')
215+
else:
216+
value = clean_data(value)
217+
value = value.split('" "')
218+
opam_data[key] = value
219+
elif key == 'depends': # Get multiline dependencies
220+
value = []
221+
for dep in lines[i + 1:]:
222+
if ']' in dep:
223+
break
224+
parsed_dep = parse_dep(dep)
225+
if parsed_dep:
226+
version = parsed_dep.group('version').strip('{ } ').replace('"', '')
227+
name = parsed_dep.group('name').strip()
228+
value.append(dict(
229+
purl=PackageURL(type='opam', name=name).to_string(),
230+
version=version,
231+
))
232+
opam_data[key] = value
233+
234+
elif key == 'src': # Get multiline src
235+
if not value:
236+
value = lines[i + 1].strip()
283237
opam_data[key] = clean_data(value)
284-
285-
if key == 'maintainer':
286-
stripped_val = value.strip('["] ')
287-
stripped_val = stripped_val.split('" "')
288-
opam_data[key] = stripped_val
289-
elif key == 'authors':
290-
if '[' in line: # If authors are present in multiple lines
291-
for authors in lines[i+1:]:
292-
value += ' ' + authors.strip()
293-
if ']' in authors:
294-
break
295-
value = value.strip('["] ')
296-
else:
297-
value = clean_data(value)
298-
value = value.split('" "')
299-
opam_data[key] = value
300-
elif key == 'depends': # Get multiline dependencies
301-
value = []
302-
for dep in lines[i+1:]:
303-
if ']' in dep:
238+
elif key == 'checksum': # Get checksums
239+
if '[' in line:
240+
for checksum in lines[i + 1:]:
241+
checksum = checksum.strip('" ')
242+
if ']' in checksum:
304243
break
305-
parsed_dep = parse_dep(dep)
306-
if parsed_dep:
307-
value.append(Opam(
308-
name=parsed_dep.group('name').strip(),
309-
version=parsed_dep.group('version').strip('{ } ').replace('"', '')
310-
)
311-
)
312-
opam_data[key] = value
313-
elif key == 'src': # Get multiline src
314-
if not value:
315-
value = lines[i+1].strip()
316-
opam_data[key] = clean_data(value)
317-
elif key == 'checksum': # Get checksums
318-
if '[' in line:
319-
for checksum in lines[i+1:]:
320-
checksum = checksum.strip('" ')
321-
if ']' in checksum:
322-
break
323-
parsed_checksum = parse_checksum(checksum)
324-
key = clean_data(parsed_checksum.group('key').strip())
325-
value = clean_data(parsed_checksum.group('value').strip())
326-
opam_data[key] = value
327-
else:
328-
value = value.strip('" ')
329-
parsed_checksum = parse_checksum(value)
330-
if parsed_checksum:
331-
key = clean_data(parsed_checksum.group('key').strip())
332-
value = clean_data(parsed_checksum.group('value').strip())
333-
opam_data[key] = value
244+
parsed_checksum = parse_checksum(checksum)
245+
key = clean_data(parsed_checksum.group('key').strip())
246+
value = clean_data(parsed_checksum.group('value').strip())
247+
opam_data[key] = value
248+
else:
249+
value = value.strip('" ')
250+
parsed_checksum = parse_checksum(value)
251+
if parsed_checksum:
252+
key = clean_data(parsed_checksum.group('key').strip())
253+
value = clean_data(parsed_checksum.group('value').strip())
254+
opam_data[key] = value
334255

335256
return opam_data
336257

0 commit comments

Comments
 (0)