Source code for modulemd
# -*- coding: utf-8 -*-
# Copyright (c) 2016 Red Hat, Inc.
#
# 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:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# 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.
#
# Written by Petr Šabata <contyk@redhat.com>
"""Module metadata manipulation library.
A python library for manipulation of the proposed module metadata format.
Example usage:
.. code:: python
import modulemd
mmd = modulemd.ModuleMetadata()
mmd.load("metadata.yaml")
mmd.add_module_license("ISC")
mmd.components.clear_rpms()
mmd.components.add_rpm("example", "with reason", multilib=["x86_64"])
mmd.dump("out.yaml")
"""
import sys
import yaml
if sys.version_info > (3,):
long = int
from modulemd.api import ModuleAPI
from modulemd.artifacts import ModuleArtifacts
from modulemd.buildopts import ModuleBuildopts
from modulemd.buildopts.rpms import ModuleBuildoptsRPMs
from modulemd.components import ModuleComponents
from modulemd.components.module import ModuleComponentModule
from modulemd.components.rpm import ModuleComponentRPM
from modulemd.filter import ModuleFilter
from modulemd.profile import ModuleProfile
supported_mdversions = ( 1, )
[docs]def load_all(f):
"""Loads a metadata file containing multiple modulemd documents
into a list of ModuleMetadata instances.
:param str f: File name to load
"""
with open(f, "r") as infile:
data = infile.read()
return loads_all(data)
[docs]def loads_all(s):
"""Loads multiple modulemd documents from a YAML multidocument
string.
:param str s: String containing multiple YAML documents.
"""
l = list()
for doc in yaml.load_all(s):
m = ModuleMetadata()
m.loadd(doc)
l.append(m)
return l
[docs]def dump_all(f, l):
"""Dumps a list of ModuleMetadata instances into a file.
:param str f: Output filename
:param list l: List of ModuleMetadata instances
"""
with open(f, "w") as outfile:
outfile.write(dumps_all(l))
[docs]def dumps_all(l):
"""Dumps a list of ModuleMetadata instance into a YAML multidocument
string.
:param list l: List of ModuleMetadata instances
"""
return yaml.dump_all([x.dumpd() for x in l], explicit_start=True)
[docs]class ModuleMetadata(object):
"""Class representing the whole module."""
REPODATA_FILENAME = "modulemd"
def __init__(self):
"""Creates a new ModuleMetadata instance."""
self.mdversion = max(supported_mdversions)
self.name = ""
self.stream = ""
self.version = 0
self.summary = ""
self.description = ""
self.module_licenses = set()
self.content_licenses = set()
self.buildrequires = dict()
self.requires = dict()
self.community = ""
self.documentation = ""
self.tracker = ""
self.xmd = dict()
self.profiles = dict()
self.api = ModuleAPI()
self.filter = ModuleFilter()
self.buildopts = ModuleBuildopts()
self.components = ModuleComponents()
self.artifacts = ModuleArtifacts()
def __repr__(self):
return ("<ModuleMetadata: "
"mdversion: {0}, "
"name: {1}, "
"stream: {2}, "
"version: {3}, "
"summary: {4}, "
"description: {5}, "
"module_licenses: {6}, "
"content_licenses: {7}, "
"buildrequires: {8}, "
"requires: {9}, "
"community: {10}, "
"documentation: {11}, "
"tracker: {12}, "
"xmd: {13}, "
"profiles: {14}, "
"api: {15}, "
"filter: {16}, "
"components: {17}, "
"artifacts: {18}>").format(
repr(self.mdversion),
repr(self.name),
repr(self.stream),
repr(self.version),
repr(self.summary),
repr(self.description),
repr(sorted(self.module_licenses)),
repr(sorted(self.content_licenses)),
repr(sorted(self.buildrequires)),
repr(sorted(self.requires)),
repr(self.community),
repr(self.documentation),
repr(self.tracker),
repr(sorted(self.xmd)),
repr(sorted(self.profiles)),
repr(self.api),
repr(self.filter),
repr(self.components),
repr(self.artifacts)
)
[docs] def load(self, f):
"""Loads a metadata file into the instance.
:param str f: File name to load
"""
with open(f, "r") as infile:
data = infile.read()
self.loads(data)
[docs] def loads(self, s):
"""Loads metadata from a string.
:param str s: Raw metadata in YAML
"""
yamld = yaml.safe_load(s)
self.loadd(yamld)
[docs] def loadd(self, d):
"""Loads metadata from a dictionary.
:param dict d: YAML metadata parsed into a dict
:raises ValueError: If the metadata is invalid or unsupported.
"""
# header
if "document" not in d or d["document"] != "modulemd":
raise ValueError("The supplied data isn't a valid modulemd document")
if "version" not in d:
raise ValueError("Document version is required")
if d["version"] not in supported_mdversions:
raise ValueError("The supplied metadata version isn't supported")
self.mdversion = d["version"]
if "data" not in d or not isinstance(d["data"], dict):
raise ValueError("Data section missing or mangled")
# data
data = d["data"]
if "name" in data:
self.name = str(data["name"])
if "stream" in data:
self.stream = str(data["stream"])
if "version" in data:
self.version = int(data["version"])
if "summary" in data:
self.summary = str(data["summary"])
if "description" in data:
self.description = str(data["description"])
if ("license" in data
and isinstance(data["license"], dict)
and "module" in data["license"]
and data["license"]["module"]):
self.module_licenses = set(data["license"]["module"])
if ("license" in data
and isinstance(data["license"], dict)
and "content" in data["license"]):
self.content_licenses = set(data["license"]["content"])
if ("dependencies" in data
and isinstance(data["dependencies"], dict)):
if ("buildrequires" in data["dependencies"]
and isinstance(data["dependencies"]["buildrequires"], dict)):
for n, s in data["dependencies"]["buildrequires"].items():
self.add_buildrequires(str(n), str(s))
if ("requires" in data["dependencies"]
and isinstance(data["dependencies"]["requires"], dict)):
for n, s in data["dependencies"]["requires"].items():
self.add_requires(str(n), str(s))
if "references" in data and data["references"]:
if "community" in data["references"]:
self.community = data["references"]["community"]
if "documentation" in data["references"]:
self.documentation = data["references"]["documentation"]
if "tracker" in data["references"]:
self.tracker = data["references"]["tracker"]
if "xmd" in data:
self.xmd = data["xmd"]
if ("profiles" in data
and isinstance(data["profiles"], dict)):
for profile in data["profiles"].keys():
self.profiles[profile] = ModuleProfile()
if "description" in data["profiles"][profile]:
self.profiles[profile].description = \
str(data["profiles"][profile]["description"])
if "rpms" in data["profiles"][profile]:
self.profiles[profile].rpms = \
set(data["profiles"][profile]["rpms"])
if ("api" in data
and isinstance(data["api"], dict)):
self.api = ModuleAPI()
if ("rpms" in data["api"]
and isinstance(data["api"]["rpms"],list)):
self.api.rpms = set(data["api"]["rpms"])
if ("filter" in data
and isinstance(data["filter"], dict)):
self.filter = ModuleFilter()
if ("rpms" in data["filter"]
and isinstance(data["filter"]["rpms"],list)):
self.filter.rpms = set(data["filter"]["rpms"])
if ("buildopts" in data
and isinstance(data["buildopts"], dict)):
self.buildopts = ModuleBuildopts()
if ("rpms" in data["buildopts"]
and isinstance(data["buildopts"]["rpms"], dict)):
self.buildopts.rpms = ModuleBuildoptsRPMs()
if ("macros" in data["buildopts"]["rpms"]
and isinstance(data["buildopts"]["rpms"]["macros"], str)):
self.buildopts.rpms.macros = data["buildopts"]["rpms"]["macros"]
if ("components" in data
and isinstance(data["components"], dict)):
self.components = ModuleComponents()
if "rpms" in data["components"]:
for p, e in data["components"]["rpms"].items():
extras = dict()
extras["rationale"] = e["rationale"]
if "buildorder" in e:
extras["buildorder"] = int(e["buildorder"])
if "repository" in e:
extras["repository"] = str(e["repository"])
if "cache" in e:
extras["cache"] = str(e["cache"])
if "ref" in e:
extras["ref"] = str(e["ref"])
if ("arches" in e
and isinstance(e["arches"], list)):
extras["arches"] = set(str(x) for x in e["arches"])
if ("multilib" in e
and isinstance(e["multilib"], list)):
extras["multilib"] = set(str(x) for x in e["multilib"])
self.components.add_rpm(p, **extras)
if "modules" in data["components"]:
for p, e in data["components"]["modules"].items():
extras = dict()
extras["rationale"] = e["rationale"]
if "buildorder" in e:
extras["buildorder"] = int(e["buildorder"])
if "repository" in e:
extras["repository"] = str(e["repository"])
if "ref" in e:
extras["ref"] = str(e["ref"])
self.components.add_module(p, **extras)
if ("artifacts" in data
and isinstance(data["artifacts"], dict)):
self.artifacts = ModuleArtifacts()
if ("rpms" in data["artifacts"]
and isinstance(data["artifacts"]["rpms"],list)):
self.artifacts.rpms = set(data["artifacts"]["rpms"])
[docs] def dump(self, f):
"""Dumps the metadata into the supplied file.
:param str f: File name of the destination
"""
data = self.dumps()
with open(f, "w") as outfile:
outfile.write(data)
[docs] def dumpd(self):
"""Dumps the metadata into a dictionary.
:rtype: dict
"""
doc = dict()
# header
doc["document"] = "modulemd"
doc["version"] = self.mdversion
# data
d = dict()
if self.name:
d["name"] = self.name
if self.stream:
d["stream"] = self.stream
if self.version:
d["version"] = self.version
d["summary"] = self.summary
d["description"] = self.description
d["license"] = dict()
d["license"]["module"] = list(self.module_licenses)
if self.content_licenses:
d["license"]["content"] = list(self.content_licenses)
if self.buildrequires or self.requires:
d["dependencies"] = dict()
if self.buildrequires:
d["dependencies"]["buildrequires"] = self.buildrequires
if self.requires:
d["dependencies"]["requires"] = self.requires
if self.community or self.documentation or self.tracker:
d["references"] = dict()
if self.community:
d["references"]["community"] = self.community
if self.documentation:
d["references"]["documentation"] = self.documentation
if self.tracker:
d["references"]["tracker"] = self.tracker
if self.xmd:
d["xmd"] = self.xmd
if self.profiles:
d["profiles"] = dict()
for profile in self.profiles.keys():
if self.profiles[profile].description:
if profile not in d["profiles"]:
d["profiles"][profile] = dict()
d["profiles"][profile]["description"] = \
str(self.profiles[profile].description)
if self.profiles[profile].rpms:
if profile not in d["profiles"]:
d["profiles"][profile] = dict()
d["profiles"][profile]["rpms"] = \
list(self.profiles[profile].rpms)
if self.api:
d["api"] = dict()
if self.api.rpms:
d["api"]["rpms"] = list(self.api.rpms)
if self.filter:
d["filter"] = dict()
if self.filter.rpms:
d["filter"]["rpms"] = list(self.filter.rpms)
if self.buildopts:
d["buildopts"] = dict()
if self.buildopts.rpms:
d["buildopts"]["rpms"] = dict()
if self.buildopts.rpms.macros:
d["buildopts"]["rpms"]["macros"] = \
self.buildopts.rpms.macros
if self.components:
d["components"] = dict()
if self.components.rpms:
d["components"]["rpms"] = dict()
for p in self.components.rpms.values():
extra = dict()
extra["rationale"] = p.rationale
if p.buildorder:
extra["buildorder"] = p.buildorder
if p.repository:
extra["repository"] = p.repository
if p.ref:
extra["ref"] = p.ref
if p.cache:
extra["cache"] = p.cache
if p.arches:
extra["arches"] = list(p.arches)
if p.multilib:
extra["multilib"] = list(p.multilib)
d["components"]["rpms"][p.name] = extra
if self.components.modules:
d["components"]["modules"] = dict()
for p in self.components.modules.values():
extra = dict()
extra["rationale"] = p.rationale
if p.buildorder:
extra["buildorder"] = p.buildorder
if p.repository:
extra["repository"] = p.repository
if p.ref:
extra["ref"] = p.ref
d["components"]["modules"][p.name] = extra
if self.artifacts:
d["artifacts"] = dict()
if self.artifacts.rpms:
d["artifacts"]["rpms"] = list(self.artifacts.rpms)
doc["data"] = d
return doc
[docs] def dumps(self):
"""Dumps the metadata into a string.
:rtype: str
"""
return yaml.safe_dump(self.dumpd())
@property
def mdversion(self):
"""An int property representing the metadata format version used.
This is automatically set to the highest supported version for
new objects or set by the loaded document. This value can be
changed to one of the supported_mdversions to alter the output
format.
"""
return self._mdversion
@mdversion.setter
def mdversion(self, i):
if not isinstance(i, (int, long)):
raise TypeError("mdversion: data type not supported")
if i not in supported_mdversions:
raise ValueError("mdversion: document version not supported")
self._mdversion = int(i)
@property
def name(self):
"""A string property representing the name of the module."""
return self._name
@name.setter
def name(self, s):
if not isinstance(s, str):
raise TypeError("name: data type not supported")
self._name = s
@property
def stream(self):
"""A string property representing the stream name of the module."""
return self._stream
@stream.setter
def stream(self, s):
if not isinstance(s, str):
raise TypeError("stream: data type not supported")
self._stream = str(s)
@property
def version(self):
"""An integer property representing the version of the module."""
return self._version
@version.setter
def version(self, i):
if not isinstance(i, (int, long)):
raise TypeError("version: data type not supported")
if i < 0:
raise ValueError("version: version cannot be negative")
self._version = i
@property
def summary(self):
"""A string property representing a short summary of the module."""
return self._summary
@summary.setter
def summary(self, s):
if not isinstance(s, str):
raise TypeError("summary: data type not supported")
self._summary = s
@property
def description(self):
"""A string property representing a detailed description of the
module."""
return self._description
@description.setter
def description(self, s):
if not isinstance(s, str):
raise TypeError("description: data type not supported")
self._description = s
@property
def module_licenses(self):
"""A set of strings, a property, representing the license terms
of the module itself."""
return self._module_licenses
@module_licenses.setter
def module_licenses(self, ss):
if not isinstance(ss, set):
raise TypeError("module_licenses: data type not supported")
for s in ss:
if not isinstance(s, str):
raise TypeError("module_licenses: data type not supported")
self._module_licenses = ss
[docs] def add_module_license(self, s):
"""Adds a module license to the set.
:param str s: License name
"""
if not isinstance(s, str):
raise TypeError("add_module_license: data type not supported")
self._module_licenses.add(s)
[docs] def del_module_license(self, s):
"""Removes the supplied license from the module licenses set.
:param str s: License name
"""
if not isinstance(s, str):
raise TypeError("del_module_license: data type not supported")
self._module_licenses.discard(s)
[docs] def clear_module_licenses(self):
"""Clears the module licenses set."""
self._module_licenses.clear()
@property
def content_licenses(self):
"""A set of strings, a property, representing the license terms
of the module contents."""
return self._content_licenses
@content_licenses.setter
def content_licenses(self, ss):
if not isinstance(ss, set):
raise TypeError("content_licenses: data type not supported")
for s in ss:
if not isinstance(s, str):
raise TypeError("content_licenses: data type not supported")
self._content_licenses = ss
[docs] def add_content_license(self, s):
"""Adds a content license to the set.
:param str s: License name
"""
if not isinstance(s, str):
raise TypeError("add_content_license: data type not supported")
self._content_licenses.add(s)
[docs] def del_content_license(self, s):
"""Removes the supplied license from the content licenses set.
:param str s: License name
"""
if not isinstance(s, str):
raise TypeError("del_content_license: data type not supported")
self._content_licenses.discard(s)
[docs] def clear_content_licenses(self):
"""Clears the content licenses set."""
self._content_licenses.clear()
@property
def requires(self):
"""A dictionary property representing the required dependencies of
the module.
Keys are the required module names (strings), values are their
required stream names (also strings).
"""
return self._requires
@requires.setter
def requires(self, d):
if not isinstance(d, dict):
raise TypeError("requires: data type not supported")
for k, v in d.items():
if not isinstance(k, str) or not isinstance(v, str):
raise TypeError("requires: data type not supported")
self._requires = d
[docs] def add_requires(self, n, v):
"""Adds a required module dependency.
:param str n: Required module name
:param str v: Required module stream name
"""
if not isinstance(n, str) or not isinstance(v, str):
raise TypeError("add_requires: data type not supported")
self._requires[n] = v
update_requires = add_requires
[docs] def del_requires(self, n):
"""Deletes the dependency on the supplied module.
:param str n: Required module name
"""
if not isinstance(n, str):
raise TypeError("del_requires: data type not supported")
if n in self._requires:
del self._requires[n]
[docs] def clear_requires(self):
"""Removes all required runtime dependencies."""
self._requires.clear()
@property
def buildrequires(self):
"""A dictionary property representing the required build dependencies
of the module.
Keys are the required module names (strings), values are their
required stream names (also strings).
"""
return self._buildrequires
@buildrequires.setter
def buildrequires(self, d):
if not isinstance(d, dict):
raise TypeError("buildrequires: data type not supported")
for k, v in d.items():
if not isinstance(k, str) or not isinstance(v, str):
raise TypeError("buildrequires: data type not supported")
self._buildrequires = d
[docs] def add_buildrequires(self, n, v):
"""Adds a module build dependency.
:param str n: Required module name
:param str v: Required module stream name
"""
if not isinstance(n, str) or not isinstance(v, str):
raise TypeError("add_buildrequires: data type not supported")
self._buildrequires[n] = v
update_buildrequires = add_buildrequires
[docs] def del_buildrequires(self, n):
"""Deletes the build dependency on the supplied module.
:param str n: Required module name
"""
if not isinstance(n, str):
raise TypeError("del_buildrequires: data type not supported")
if n in self._buildrequires:
del self._buildrequires[n]
[docs] def clear_buildrequires(self):
"""Removes all build dependencies."""
self._buildrequires.clear()
@property
def community(self):
"""A string property representing a link to the upstream community
for this module.
"""
return self._community
@community.setter
def community(self, s):
# TODO: Check if it looks like a link, unless empty
if not isinstance(s, str):
raise TypeError("community: data type not supported")
self._community = s
@property
def documentation(self):
"""A string property representing a link to the upstream
documentation for this module.
"""
return self._documentation
@documentation.setter
def documentation(self, s):
# TODO: Check if it looks like a link, unless empty
if not isinstance(s, str):
raise TypeError("documentation: data type not supported")
self._documentation = s
@property
def tracker(self):
"""A string property representing a link to the upstream bug tracker
for this module.
"""
return self._tracker
@tracker.setter
def tracker(self, s):
# TODO: Check if it looks like a link, unless empty
if not isinstance(s, str):
raise TypeError("tracker: data type not supported")
self._tracker = s
@property
def xmd(self):
"""A dictionary property containing user-defined data."""
return self._xmd
@xmd.setter
def xmd(self, d):
if not isinstance(d, dict):
raise TypeError("xmd: data type not supported")
self._xmd = d
@property
def profiles(self):
"""A dictionary property representing the module profiles."""
return self._profiles
@profiles.setter
def profiles(self, d):
if not isinstance(d, dict):
raise TypeError("profiles: data type not supported")
for k, v in d.items():
if not isinstance(k, str) or not isinstance(v, ModuleProfile):
raise TypeError("profiles: data type not supported")
self._profiles = d
@property
def api(self):
"""A ModuleAPI instance representing the module's public API."""
return self._api
@api.setter
def api(self, o):
if not isinstance(o, ModuleAPI):
raise TypeError("api: data type not supported")
self._api = o
@property
def filter(self):
"""A ModuleFilter instance representing the module's filter."""
return self._filter
@filter.setter
def filter(self, o):
if not isinstance(o, ModuleFilter):
raise TypeError("filter: data type not supported")
self._filter = o
@property
def buildopts(self):
"""A ModuleBuildopts instance representing the additional
module components build options.
"""
return self._buildopts
@buildopts.setter
def buildopts(self, o):
if not isinstance(o, ModuleBuildopts):
raise TypeError("buildopts: data type not supported")
self._buildopts = o
@property
def components(self):
"""A ModuleComponents instance property representing the components
defining the module.
"""
return self._components
@components.setter
def components(self, o):
if not isinstance(o, ModuleComponents):
raise TypeError("components: data type not supported")
self._components = o
@property
def artifacts(self):
"""A ModuleArtifacts instance representing the module's artifacts."""
return self._artifacts
@artifacts.setter
def artifacts(self, o):
if not isinstance(o, ModuleArtifacts):
raise TypeError("artifacts: data type not supported")
self._artifacts = o