Skip to content
Permalink
Browse files

meta: introduce snap, hook, plug, and slot types

Introduce Snap meta for handling reading/writing snap.yaml metadata.

Snap incorporates support for first-class classes for plug and slots.

Introduces:
- generic Snap object
- generic Hook object
- generic Plug object to support any plug interfaces
- extended ContentPlug object for content interfaces
- generic Slot object to support any slot interfaces
- extended DbusSlot object for dbus interfaces
- extended ContentSlot object for content interfaces
- A general error for Plug validation
- A general error for Slot validation
- Add new unit tests for coverage.

Signed-off-by: Chris Patterson <chris.patterson@canonical.com>
  • Loading branch information
cjp256 authored and sergiusens committed Sep 15, 2019
1 parent 3529f19 commit 18fd2f4a81311c01e1c8cb9b5f1722133c23a496
@@ -177,3 +177,24 @@ class ShebangNotFoundError(Exception):

class ShebangInRoot(Exception):
"""Internal exception for when a shebang is part of the root."""


class SlotValidationError(errors.SnapcraftError):
fmt = "failed to validate slot={slot_name}: {message}"

def __init__(self, *, slot_name: str, message: str) -> None:
super().__init__(slot_name=slot_name, message=message)


class PlugValidationError(errors.SnapcraftError):
fmt = "failed to validate plug={plug_name}: {message}"

def __init__(self, *, plug_name: str, message: str) -> None:
super().__init__(plug_name=plug_name, message=message)


class HookValidationError(errors.SnapcraftError):
fmt = "failed to validate hook={hook_name}: {message}"

def __init__(self, *, hook_name: str, message: str) -> None:
super().__init__(hook_name=hook_name, message=message)
@@ -0,0 +1,79 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2019 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import re

from copy import deepcopy
from snapcraft.internal.meta.errors import HookValidationError
from typing import Any, Dict


class Hook:
"""Representation of a generic snap hook."""

def __init__(self, *, hook_name: str) -> None:
self._hook_name = hook_name
self._hook_properties: Dict[str, Any] = dict()
self.passthrough: Dict[str, Any] = dict()

@property
def hook_name(self) -> str:
"""Read-only to ensure consistency with Snap dictionary mappings."""

return self._hook_name

def _validate_name(self) -> None:
"""Validate hook name."""

if not re.match("^[a-z](?:-?[a-z0-9])*$", self.hook_name):
raise HookValidationError(
hook_name=self.hook_name,
message="{!r} is not a valid hook name. Hook names consist of lower-case alphanumeric characters and hyphens. They cannot start or end with a hyphen.".format(
self.hook_name
),
)

def validate(self) -> None:
"""Validate hook, raising exception if invalid."""

self._validate_name()

@classmethod
def from_dict(cls, hook_dict: Dict[str, Any], hook_name: str) -> "Hook":
"""Create hook from dictionary."""

hook = Hook(hook_name=hook_name)
hook._hook_properties = deepcopy(hook_dict)

if "passthrough" in hook._hook_properties:
hook.passthrough = hook._hook_properties.pop("passthrough")

return hook

def to_dict(self) -> Dict[str, Any]:
"""Create dictionary from hook."""

hook_dict = deepcopy(self._hook_properties)

# Apply passthrough keys.
hook_dict.update(self.passthrough)
return hook_dict

def __repr__(self) -> str:
return repr(self.__dict__)

def __str__(self) -> str:
return str(self.__dict__)
@@ -0,0 +1,162 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2019 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging

from collections import OrderedDict
from copy import deepcopy
from snapcraft.internal.meta.errors import PlugValidationError
from typing import Any, Dict, Optional, Type

logger = logging.getLogger(__name__)


class Plug:
"""Generic plug."""

def __init__(self, *, plug_name: str) -> None:
self._plug_name = plug_name
self._plug_dict: Dict[str, Any] = dict()

@property
def plug_name(self) -> str:
"""Read-only to ensure consistency with Snap dictionary mappings."""

return self._plug_name

def validate(self) -> None:
"""Validate plug, raising an exception on failure."""

if not self._plug_dict:
raise PlugValidationError(
plug_name=self.plug_name, message="plug has no defined attributes"
)

if "interface" not in self._plug_dict:
raise PlugValidationError(
plug_name=self.plug_name, message="plug has no defined interface"
)

@classmethod
def from_dict(cls, *, plug_dict: Dict[str, Any], plug_name: str) -> "Plug":
"""Create plug from dictionary."""

interface = plug_dict.get("interface", None)
if interface in PLUG_MAPPINGS:
plug_class = PLUG_MAPPINGS.get(interface)
return plug_class.from_dict(plug_dict=plug_dict, plug_name=plug_name)

# Handle the general case.
plug = Plug(plug_name=plug_name)
plug._plug_dict = plug_dict
return plug

def to_dict(self) -> Dict[str, Any]:
"""Create dictionary from plug."""

return OrderedDict(deepcopy(self._plug_dict))

def __repr__(self) -> str:
return repr(self.__dict__)

def __str__(self) -> str:
return str(self.__dict__)


class ContentPlug(Plug):
"""Representation of a snap content plug."""

def __init__(
self,
*,
plug_name: str,
content: Optional[str] = None,
default_provider: Optional[str] = None,
target: str,
) -> None:
super().__init__(plug_name=plug_name)

self._content = content
self._default_provider = default_provider
self.target = target

@property
def interface(self) -> str:
return "content"

@property
def content(self) -> str:
if self._content:
return self._content

# Defaults to plug_name if unspecified.
return self.plug_name

@content.setter
def content(self, content) -> None:
self._content = content

@property
def provider(self) -> str:
if ":" in self._default_provider:
return self._default_provider.split(":")[0]

return self._default_provider

def validate(self) -> None:
if not self.target:
raise PlugValidationError(
plug_name=self.plug_name,
message="`target` is required for content slot",
)

@classmethod
def from_dict(cls, *, plug_dict: Dict[str, str], plug_name: str) -> "ContentPlug":
interface = plug_dict.get("interface")
if interface != "content":
raise PlugValidationError(
plug_name=plug_name,
message="`interface={}` is invalid for content slot".format(interface),
)

if "target" not in plug_dict:
raise PlugValidationError(
plug_name=plug_name, message="`target` is required for content slot"
)

return ContentPlug(
plug_name=plug_name,
content=plug_dict.get("content", None),
target=plug_dict.get("target"),
default_provider=plug_dict.get("default-provider", None),
)

def to_dict(self) -> Dict[str, str]:
props = [("interface", self.interface)]

# Only include content if set explicitly.
if self._content:
props.append(("content", self.content))

props.append(("target", self.target))

if self.provider:
props.append(("default-provider", self.provider))

return OrderedDict(props)


PLUG_MAPPINGS: Dict[str, Type[Plug]] = dict(content=ContentPlug)

0 comments on commit 18fd2f4

Please sign in to comment.
You can’t perform that action at this time.