From 92e5a1fc1a6ffe8a6c44583f202f2e399db273c5 Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Thu, 15 Aug 2024 18:44:43 -0700 Subject: [PATCH] wip --- bikeshed/metadata.py | 20 +- bikeshed/status/__init__.py | 3 +- .../{GroupStatusManager.py => manager.py} | 104 +++--- bikeshed/status/utils.py | 318 ++++++++++-------- 4 files changed, 259 insertions(+), 186 deletions(-) rename bikeshed/status/{GroupStatusManager.py => manager.py} (50%) diff --git a/bikeshed/metadata.py b/bikeshed/metadata.py index fdeb24c1dc..2e5dc49d17 100644 --- a/bikeshed/metadata.py +++ b/bikeshed/metadata.py @@ -13,7 +13,7 @@ from isodate import Duration, parse_duration -from . import config, constants, datablocks, h, markdown, repository, t +from . import config, constants, datablocks, h, markdown, repository, status, t from . import messages as m from .translate import _ @@ -49,7 +49,7 @@ def __init__(self) -> None: self.level: str | None = None self.displayShortname: str | None = None self.shortname: str | None = None - self.status: str | None = None + self.status: self.Status | None = None self.rawStatus: str | None = None # optional metadata @@ -80,7 +80,8 @@ def __init__(self) -> None: self.externalInfotrees: config.BoolSet = config.BoolSet(default=False) self.favicon: str | None = None self.forceCrossorigin: bool = False - self.group: str | None = None + self.group: status.Group | None = None + self.rawGroup: str | None = None self.h1: str | None = None self.ignoreCanIUseUrlFailure: list[str] = [] self.ignoreMDNFailure: list[str] = [] @@ -114,6 +115,8 @@ def __init__(self) -> None: self.noEditor: bool = False self.noteClass: str = "note" self.opaqueElements: list[str] = ["pre", "xmp", "script", "style"] + self.org: status.Org | None = None + self.rawOrg: str | None = None self.prepTR: bool = False self.previousEditors: list[dict[str, str | None]] = [] self.previousVersions: list[dict[str, str]] = [] @@ -195,7 +198,13 @@ def computeImplicitMetadata(self, doc: t.SpecT) -> None: and "repository-issue-tracking" in self.boilerplate ): self.issues.append(("GitHub", self.repository.formatIssueUrl())) - self.status = config.canonicalizeStatus(self.rawStatus, self.group) + + self.org, self.status, self.group = status.canonicalizeOrgStatusGroup( + doc.statuses, + self.rawOrg, + self.rawStatus, + self.rawGroup, + ) self.expires = canonicalizeExpiryDate(self.date, self.expires) @@ -1369,7 +1378,7 @@ def parseLiteralList(key: str, val: str, lineNum: str | int | None) -> list[str] "Favicon": Metadata("Favicon", "favicon", joinValue, parseLiteral), "Force Crossorigin": Metadata("Force Crossorigin", "forceCrossorigin", joinValue, parseBoolean), "Former Editor": Metadata("Former Editor", "previousEditors", joinList, parseEditor), - "Group": Metadata("Group", "group", joinValue, parseLiteral), + "Group": Metadata("Group", "rawGroup", joinValue, parseLiteral), "H1": Metadata("H1", "h1", joinValue, parseLiteral), "Ignore Can I Use Url Failure": Metadata( "Ignore Can I Use Url Failure", @@ -1418,6 +1427,7 @@ def parseLiteralList(key: str, val: str, lineNum: str | int | None) -> list[str] "No Editor": Metadata("No Editor", "noEditor", joinValue, parseBoolean), "Note Class": Metadata("Note Class", "noteClass", joinValue, parseLiteral), "Opaque Elements": Metadata("Opaque Elements", "opaqueElements", joinList, parseCommaSeparated), + "Org": Metadata("Org", "rawOrg", joinValue, parseLiteral), "Prepare For Tr": Metadata("Prepare For Tr", "prepTR", joinValue, parseBoolean), "Previous Version": Metadata("Previous Version", "previousVersions", joinList, parsePreviousVersion), "Remove Multiple Links": Metadata("Remove Multiple Links", "removeMultipleLinks", joinValue, parseBoolean), diff --git a/bikeshed/status/__init__.py b/bikeshed/status/__init__.py index f838430954..d6f5f82456 100644 --- a/bikeshed/status/__init__.py +++ b/bikeshed/status/__init__.py @@ -1 +1,2 @@ -from .GroupStatusManager import GroupStatusManager +from .manager import Group, GroupStatusManager, GroupW3C, Org, Status, StatusW3C +from .utils import canonicalizeOrgStatusGroup diff --git a/bikeshed/status/GroupStatusManager.py b/bikeshed/status/manager.py similarity index 50% rename from bikeshed/status/GroupStatusManager.py rename to bikeshed/status/manager.py index 029d98bf0a..401a669d91 100644 --- a/bikeshed/status/GroupStatusManager.py +++ b/bikeshed/status/manager.py @@ -12,7 +12,7 @@ @dataclasses.dataclass class GroupStatusManager: genericStatuses: dict[str, Status] = dataclasses.field(default_factory=dict) - standardsBodies: dict[str, StandardsBody] = dataclasses.field(default_factory=dict) + orgs: dict[str, Org] = dataclasses.field(default_factory=dict) @staticmethod def fromKdlStr(data: str) -> GroupStatusManager: @@ -23,58 +23,74 @@ def fromKdlStr(data: str) -> GroupStatusManager: status = Status.fromKdlNode(node) self.genericStatuses[status.shortName] = status - for node in kdlDoc.getAll("standards-body"): - sb = StandardsBody.fromKdlNode(node) - self.standardsBodies[sb.name] = sb + for node in kdlDoc.getAll("org"): + org = Org.fromKdlNode(node) + self.orgs[org.name] = org return self - def getStatuses(name: str) -> list[Status]: + def getStatuses(self, name: str) -> list[Status]: statuses = [] if name in self.genericStatuses: statuses.append(self.genericStatuses[name]) - for sb in self.standardsBodies: - if name in sb.statuses: - statuses.append(sb.statuses[name]) + for org in self.orgs.values(): + if name in org.statuses: + statuses.append(org.statuses[name]) return statuses - def getStatus(sbName: str|None, statusName: str) -> Status|None: - # Note that a None sbName does *not* indicate we don't care, - # it's specifically statuses *not* restricted to a standards body. - if sbName is None: + def getStatus(self, orgName: str | None, statusName: str, allowGeneric: bool = False) -> Status | None: + # Note that a None orgName does *not* indicate we don't care, + # it's specifically statuses *not* restricted to an org. + if orgName is None: return self.genericStatuses.get(statusName) - elif sbName in self.standardsBodies: - return self.standardsBodies[sbName].statuses.get(statusName) + elif orgName in self.orgs: + statusInOrg = self.orgs[orgName].statuses.get(statusName) + if statusInOrg: + return statusInOrg + elif allowGeneric: + return self.genericStatuses.get(statusName) + else: + return None else: return None - def getGroup(groupName: str) -> Group|None: - for sb in self.standardsBodies: - if groupName in sb.groups: - return sb.groups[groupName] - return None - - def getStandardsBody(sbName: str) -> StandardsBody|None: - return self.standardsBodies.get(sbName) - + def getGroups(self, orgName: str | None, groupName: str) -> list[Group]: + # Unlike Status, if org is None we'll just grab whatever group matches. + groups = [] + for org in self.orgs.values(): + if orgName is not None and org.name != orgName: + continue + if groupName in org.groups: + groups.append(org.groups[groupName]) + return groups + + def getGroup(self, orgName: str | None, groupName: str) -> Group | None: + # If Org is None, and there are multiple groups with that name, fail to find. + groups = self.getGroups(orgName, groupName) + if len(groups) == 1: + return groups[0] + else: + return None + def getOrg(self, orgName: str) -> Org | None: + return self.orgs.get(orgName) @dataclasses.dataclass -class StandardsBody: +class Org: name: str groups: dict[str, Group] = dataclasses.field(default_factory=dict) statuses: dict[str, Status] = dataclasses.field(default_factory=dict) @staticmethod - def fromKdlNode(node: kdl.Node) -> StandardsBody: + def fromKdlNode(node: kdl.Node) -> Org: name = t.cast(str, node.args[0]) - self = StandardsBody(name) + self = Org(name) for child in node.getAll("group"): - g = Group.fromKdlNode(child, sb=self) + g = Group.fromKdlNode(child, org=self) self.groups[g.name] = g for child in node.getAll("status"): - s = Status.fromKdlNode(child, sb=self) + s = Status.fromKdlNode(child, org=self) self.statuses[s.shortName] = s return self @@ -83,15 +99,15 @@ def fromKdlNode(node: kdl.Node) -> StandardsBody: class Group: name: str privSec: bool - sb: StandardsBody | None = None + org: Org @staticmethod - def fromKdlNode(node: kdl.Node, sb: StandardsBody | None = None) -> Group: - if sb.name == "w3c": - return GroupW3C.fromKdlNode(node, sb) + def fromKdlNode(node: kdl.Node, org: Org) -> Group: + if org.name == "w3c": + return GroupW3C.fromKdlNode(node, org) name = t.cast(str, node.args[0]) privSec = node.get("priv-sec") is not None - return Group(name, privSec, sb) + return Group(name, privSec, org) @dataclasses.dataclass @@ -99,33 +115,33 @@ class GroupW3C(Group): type: str | None = None @staticmethod - def fromKdlNode(node: kdl.Node, sb: StandardsBody | None = None) -> GroupW3C: + def fromKdlNode(node: kdl.Node, org: Org) -> GroupW3C: name = t.cast(str, node.args[0]) privSec = node.get("priv-sec") is not None groupType = t.cast("str|None", node.props["type"]) - return GroupW3C(name, privSec, sb, groupType) + return GroupW3C(name, privSec, org, groupType) @dataclasses.dataclass class Status: shortName: str longName: str - sb: StandardsBody | None = None + org: Org | None = None requires: list[str] = dataclasses.field(default_factory=list) def fullShortname(self) -> str: - if self.sb.name is None: + if self.org is None: return self.shortName else: - return self.sb.name + "/" + self.shortName + return self.org.name + "/" + self.shortName @staticmethod - def fromKdlNode(node: kdl.Node, sb: StandardsBody | None = None) -> Status: - if sb.name == "w3c": - return StatusW3C.fromKdlNode(node, sb) + def fromKdlNode(node: kdl.Node, org: Org | None = None) -> Status: + if org and org.name == "w3c": + return StatusW3C.fromKdlNode(node, org) shortName = t.cast(str, node.args[0]) longName = t.cast(str, node.args[1]) - self = Status(shortName, longName, sb) + self = Status(shortName, longName, org) requiresNode = node.get("requires") if requiresNode: self.requires = t.cast("list[str]", list(node.getArgs((..., str)))) @@ -137,10 +153,10 @@ class StatusW3C(Status): groupTypes: list[str] = dataclasses.field(default_factory=list) @staticmethod - def fromKdlNode(node: kdl.Node, sbName: str | None = None) -> StatusW3C: + def fromKdlNode(node: kdl.Node, org: Org | None = None) -> StatusW3C: shortName = t.cast(str, node.args[0]) longName = t.cast(str, node.args[1]) - self = StatusW3C(shortName, longName, sbName) + self = StatusW3C(shortName, longName, org) requiresNode = node.get("requires") if requiresNode: self.requires = t.cast("list[str]", list(node.getArgs((..., str)))) diff --git a/bikeshed/status/utils.py b/bikeshed/status/utils.py index bc472db973..7d85ec1fc7 100644 --- a/bikeshed/status/utils.py +++ b/bikeshed/status/utils.py @@ -1,169 +1,215 @@ from __future__ import annotations -from .. import t, messages as m +from .. import config, t +from .. import messages as m if t.TYPE_CHECKING: - from . import GroupStatusManager, StandardsBody, Group, Status - - - -@t.overload -def canonicalizeStatus(manager: GroupStatusManager, rawStatus: None, group: str | None) -> None: ... - - -@t.overload -def canonicalizeStatus(manager: GroupStatusManager, rawStatus: str, group: str | None) -> str: ... + from . import Group, GroupStatusManager, GroupW3C, Org, Status, StatusW3C + + +def canonicalizeOrgStatusGroup( + manager: GroupStatusManager, + rawOrg: str | None, + rawStatus: str | None, + rawGroup: str | None, +) -> tuple[Org | None, Status | None, Group | None]: + # Takes raw Org/Status/Group names (something written in the Org/Status/Group metadata), + # and, if possible, converts them into Org/Status/Group objects. + + # First, canonicalize the status and group casings, and separate them from + # any inline org specifiers. + # Then, figure out what the actual org name is. + orgFromStatus: str | None + statusName: str | None + if rawStatus is not None and "/" in rawStatus: + orgFromStatus, _, statusName = rawStatus.partition("/") + orgFromStatus = orgFromStatus.lower() + else: + orgFromStatus = None + statusName = rawStatus + statusName = statusName.upper() if statusName is not None else None + orgFromGroup: str | None + groupName: str | None + if rawGroup is not None and "/" in rawGroup: + orgFromGroup, _, groupName = rawGroup.partition("/") + orgFromGroup = orgFromGroup.lower() + else: + orgFromGroup = None + groupName = rawGroup + groupName = groupName.lower() if groupName is not None else None -def canonicalizeStatusShortname(manager: GroupStatusManager, rawStatus: str | None, groupName: str | None) -> Status | None: - # Takes a "rawStatus" (something written in the Status metadata) and optionally a Group metadata value, - # and, if possible, converts that into a Status value. - if rawStatus is None: - return None + orgName = reconcileOrgs(rawOrg, orgFromStatus, orgFromGroup) - sbName: str|None - statusName: str - if "/" in rawStatus: - sbName, _, statusName = rawStatus.partition("/") - sbName = sbName.lower() + if orgName is not None and rawOrg is None: + orgInferredFrom = "Org (inferred from " + if orgFromStatus is not None: + orgInferredFrom += f"Status '{rawStatus}')" + else: + orgInferredFrom += f"Group '{rawGroup}')" else: - sbName = None - statusName = rawStatus - statusName = statusName.upper() + orgInferredFrom = "Org" - status = manager.getStatus(sbName, statusName) + # Actually fetch the Org/Status/Group objects + if orgName is not None: + org = manager.getOrg(orgName) + if org is None: + m.die(f"Unknown {orgInferredFrom} '{orgName}'. See docs for recognized Org values.") + else: + org = None if groupName is not None: - group = manager.getGroup(groupName.lower()) + group = manager.getGroup(orgName, groupName) + if group is None: + if orgName is None: + groups = manager.getGroups(orgName, groupName) + if len(groups) > 1: + orgNamesForGroup = config.englishFromList((x.org.name for x in groups), "and") + m.die( + f"Your Group '{groupName}' exists under several Orgs ({orgNamesForGroup}). Specify which org you want in an Org metadata.", + ) + else: + m.die(f"Unknown Group '{groupName}'. See docs for recognized Group values.") + else: + groups = manager.getGroups(None, groupName) + if len(groups) > 0: + orgNamesForGroup = config.englishFromList((x.org.name for x in groups), "and") + m.die( + f"Your Group '{groupName}' doesn't exist under the {orgFromStatus} '{orgName}', but does exist under {orgNamesForGroup}. Specify the correct Org (or the correct Group).", + ) + else: + m.die(f"Unknown Group '{rawGroup}'. See docs for recognized Group values.") else: group = None - if group and status: - # If using a standards-body status, group must match. + # If Org wasn't specified anywhere, default it from Group if possible + if org is None and group is not None: + org = group.org + + if statusName is not None: + if orgFromStatus is not None: + # If status explicitly specified an org, use that + status = manager.getStatus(orgFromStatus, statusName) + elif org: + # Otherwise, if we found an org, look for it there, + # but fall back to looking for it in the generic statuses + status = manager.getStatus(org.name, statusName, allowGeneric=True) + else: + # Otherwise, just look in the generic statuses; + # the error stuff later will catch it if that doesn't work. + status = manager.getStatus(None, statusName) + else: + # Just quick exit on this case, nothing we can do. + return org, None, group + + # See if your org-specific Status matches your Org + if org and status and status.org is not None and status.org != org: + m.die(f"Your {orgInferredFrom} is '{org.name}', but your Status is only usable in the '{status.org.name}' Org.") + + if group and status and status.org is not None and status.org != group.org: + # If using an org-specific Status, Group must match. # (Any group can use a generic status.) - if status.sb is not None and status.sb != group.sb: - possibleStatusNames = config.englishFromList(group.sb.statuses.keys()) - m.die(f"Your Group metadata is in the standards-body '{group.sb.name}', but your Status metadata is in the standards-body '{status.sb.name}'. Allowed Status values for '{group.sb.name}' are: {possibleStatusNames}") - if group.sb.name == "w3c": - # Apply the special w3c rules - validateW3CStatus(group, status) + possibleStatusNames = config.englishFromList(f"'{x}'" for x in group.org.statuses) + m.die( + f"Your Group is in the '{group.org.name}' Org, but your Status is only usable in the '{status.org.name}' Org. Allowed Status values for '{group.org.name}' are: {possibleStatusNames}", + ) + if group and status and group.org.name == "w3c": + # Apply the special w3c rules + validateW3CStatus(group, status) + + # Reconciliation done, return everything if Status exists. if status: - return status + return org, status, group - # Try and figure out why we failed to find the status + # Otherwise, try and figure out why we failed to find the status - # Does that status just not exist at all? possibleStatuses = manager.getStatuses(statusName) - if not possibleStatuses: + if len(possibleStatuses) == 0: m.die(f"Unknown Status metadata '{rawStatus}'. Check the docs for valid Status values.") - return None - - possibleSbNames = config.englishFromList(x.sb.name if x.sb else "(None)" for x in possibleStatuses) - - # Okay, it exists, but didn't come up. So you gave the wrong standards-body. Does that standards-body exist? - if sbName is not None and manager.getStandardsBody(sbName) is None: - m.die(f"Unknown standards-body prefix '{sbName}' on your Status metadata '{rawStatus}'. Recognized standards-body prefixes for that Status are: {possibleSbNames}") - return None - - # Standards-body exists, but your status isn't in it. - - - # If they specified a standards-org prefix and it wasn't found, - # that's an error. - if megaGroup: - # Was the error because the megagroup doesn't exist? - if possibleMgs: - msg = f"Status '{status}' can't be used with the org '{megaGroup}'." - if "" in possibleMgs: - if len(possibleMgs) == 1: - msg += f" That status must be used without an org at all, like `Status: {status}`" - else: - msg += " That status can only be used with the org{} {}, or without an org at all.".format( - "s" if len(possibleMgs) > 1 else "", - main.englishFromList(f"'{x}'" for x in possibleMgs if x != ""), - ) - else: - if len(possibleMgs) == 1: - msg += f" That status can only be used with the org '{possibleMgs[0]}', like `Status: {possibleMgs[0]}/{status}`" - else: - msg += " That status can only be used with the orgs {}.".format( - main.englishFromList(f"'{x}'" for x in possibleMgs), - ) - - else: - if megaGroup not in megaGroups: - msg = f"Unknown Status metadata '{canonStatus}'. Check the docs for valid Status values." - else: - msg = f"Status '{status}' can't be used with the org '{megaGroup}'. Check the docs for valid Status values." - m.die(msg) - return canonStatus - - # Otherwise, they provided a bare status. - # See if their group is compatible with any of the prefixed statuses matching the bare status. - assert "" not in possibleMgs # if it was here, the literal "in" test would have caught this bare status - for mg in possibleMgs: - if group in megaGroups[mg]: - canonStatus = mg + "/" + status - - if mg == "w3c": - validateW3Cstatus(group, canonStatus, rawStatus) - - return canonStatus - - # Group isn't in any compatible org, so suggest prefixing. - if possibleMgs: - msg = "You used Status: {}, but that's limited to the {} org{}".format( - rawStatus, - main.englishFromList(f"'{mg}'" for mg in possibleMgs), - "s" if len(possibleMgs) > 1 else "", - ) - if group: - msg += ", and your group '{}' isn't recognized as being in {}.".format( - group, - "any of those orgs" if len(possibleMgs) > 1 else "that org", + return org, status, group + elif len(possibleStatuses) == 1: + possibleStatus = possibleStatuses[0] + if possibleStatus.org is None: + m.die( + f"Your Status '{statusName}' is a generic status, but you explicitly specified '{rawStatus}'. Remove the org prefix from your Status.", ) - msg += " If this is wrong, please file a Bikeshed issue to categorize your group properly, and/or try:\n" - msg += "\n".join(f"Status: {mg}/{status}" for mg in possibleMgs) else: - msg += ", and you don't have a Group metadata. Please declare your Group, or check the docs for statuses that can be used by anyone." + m.die( + f"Your Status '{statusName}' only exists in the '{possibleStatus.org.name}' Org, but you specified the {orgInferredFrom} '{orgName}'.", + ) else: - msg = f"Unknown Status metadata '{canonStatus}'. Check the docs for valid Status values." - m.die(msg) - return canonStatus + statusNames = config.englishFromList((x.org.name for x in possibleStatuses if x.org), "and") + includesDefaultStatus = any(x.org is None for x in possibleStatuses) + if includesDefaultStatus: + msg = f"Your Status '{statusName}' only exists in Org(s) {statusNames}, or is a generic status." + else: + msg = f"Your Status '{statusName}' only exists in the Orgs {statusNames}." + if orgName: + if org: + msg += f" Your specified {orgInferredFrom} is '{orgName}'." + else: + msg += f" Your specified {orgInferredFrom} is an unknown value '{orgName}'." + else: + msg += " Declare one of those Orgs in your Org metadata." + m.die(msg) + return (org, status, group) -def validateW3Cstatus(group: str, status: str, rawStatus: str) -> None: - if status == "DREAM": - m.warn("You used Status: DREAM for a W3C document. Consider UD instead.") - return - if "w3c/" + status in shortToLongStatus: - status = "w3c/" + status +def reconcileOrgs(fromRaw: str | None, fromStatus: str | None, fromGroup: str | None) -> str | None: + # Since there are three potential sources of "org" name, + # figure out what the name actually is, + # and complain if they disagree. + fromRaw = fromRaw.lower() if fromRaw else None + fromStatus = fromStatus.lower() if fromStatus else None + fromGroup = fromGroup.lower() if fromGroup else None - def formatStatusSet(statuses: frozenset[str]) -> str: - return ", ".join(sorted({status.split("/")[-1] for status in statuses})) + orgName: str | None = fromRaw - if group in w3cIgs and status not in w3cIGStatuses: - m.warn( - f"You used Status: {rawStatus}, but W3C Interest Groups are limited to these statuses: {formatStatusSet(w3cIGStatuses)}.", - ) + if fromStatus is not None: + if orgName is None: + orgName = fromStatus + elif orgName == fromStatus: + pass + else: + m.die( + f"Your Org metadata specifies '{fromRaw}', but your Status metadata states an org of '{fromStatus}'. These must agree - either fix them or remove one of them.", + ) + + if fromGroup is not None: + if orgName is None: + orgName = fromGroup + elif orgName == fromGroup: + pass + else: + m.die( + f"Your Org metadata specifies '{fromRaw}', but your Group metadata states an org of '{fromGroup}'. These must agree - either fix them or remove one of them.", + ) + + return orgName - if group == "tag" and status not in w3cTAGStatuses: - m.warn( - f"You used Status: {rawStatus}, but the TAG is are limited to these statuses: {formatStatusSet(w3cTAGStatuses)}", - ) - if group in w3cCgs and status not in w3cCommunityStatuses: +def validateW3CStatus(group: Group, status: Status) -> None: + assert isinstance(group, GroupW3C) + assert isinstance(status, StatusW3C) + if status.org is None and status.shortName == "DREAM": + m.warn("You used Status:DREAM for a W3C document. Consider Status:UD instead.") + return + + if group.type is not None and group.type not in status.groupTypes: + if group.type == "ig": + longTypeName = "W3C Interest Groups" + elif group.type == "tag": + longTypeName = "the W3C TAG" + elif group.type == "cg": + longTypeName = "W3C Community/Business Groups" + else: + longTypeName = "W3C Working Groups" + allowedStatuses = config.englishFromList( + x.shortName for x in t.cast("list[StatusW3C]", group.org.statuses.values()) if group.type in x.groupTypes + ) m.warn( - f"You used Status: {rawStatus}, but W3C Community and Business Groups are limited to these statuses: {formatStatusSet(w3cCommunityStatuses)}.", + f"You used Status:{status.shortName}, but {longTypeName} are limited to these statuses: {allowedStatuses}.", ) - -def megaGroupsForStatus(status: str) -> list[str]: - # Returns a list of megagroups that recognize the given status - mgs = [] - for key in shortToLongStatus: - mg, _, s = key.partition("/") - if s == status: - mgs.append(mg) - return mgs \ No newline at end of file + return