From 6d84d631474a4318443c56afffae583c38ada541 Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Mon, 15 Nov 2021 15:36:37 -0800 Subject: [PATCH 01/25] wip --- bikeshed/Spec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bikeshed/Spec.py b/bikeshed/Spec.py index d6a261f57a..818cbc54e2 100644 --- a/bikeshed/Spec.py +++ b/bikeshed/Spec.py @@ -3,6 +3,7 @@ import glob import json +import kdl import os import re import sys @@ -108,6 +109,7 @@ def initializeState(self) -> bool: self.widl: widlparser.Parser = idl.getParser() self.languages: dict[str, language.Language] = fetchLanguages(self.dataFile) + self.statuses = kdl.parse(self.dataFile.fetch("statuses.kdl", str=True)) self.extraJC = stylescript.JCManager() self.extraJC.addColors() From 9dc7a14aeba410fdb2118e56fabbe10b2800c499 Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Tue, 6 Aug 2024 15:56:07 -0700 Subject: [PATCH 02/25] WIP rewrite the kdl format, start creating the manager class --- bikeshed/Spec.py | 4 +- bikeshed/config/status.py | 318 ------------------ bikeshed/spec-data/readonly/statuses.kdl | 407 ++++++++++++++++------- bikeshed/status/GroupStatusManager.py | 71 ++++ bikeshed/status/__init__.py | 1 + 5 files changed, 354 insertions(+), 447 deletions(-) create mode 100644 bikeshed/status/GroupStatusManager.py create mode 100644 bikeshed/status/__init__.py diff --git a/bikeshed/Spec.py b/bikeshed/Spec.py index 818cbc54e2..895ffca5ec 100644 --- a/bikeshed/Spec.py +++ b/bikeshed/Spec.py @@ -3,7 +3,6 @@ import glob import json -import kdl import os import re import sys @@ -35,6 +34,7 @@ refs, retrieve, shorthands, + status, stylescript, t, wpt, @@ -109,7 +109,7 @@ def initializeState(self) -> bool: self.widl: widlparser.Parser = idl.getParser() self.languages: dict[str, language.Language] = fetchLanguages(self.dataFile) - self.statuses = kdl.parse(self.dataFile.fetch("statuses.kdl", str=True)) + self.statuses = status.GroupStatusManager.fromKDLStr(self.dataFile.fetch("statuses.kdl", str=True)) self.extraJC = stylescript.JCManager() self.extraJC.addColors() diff --git a/bikeshed/config/status.py b/bikeshed/config/status.py index f8a4512af4..5262861d4d 100644 --- a/bikeshed/config/status.py +++ b/bikeshed/config/status.py @@ -4,324 +4,6 @@ from .. import t from . import main -shortToLongStatus = { - "DREAM": "A Collection of Interesting Ideas", - "LS": "Living Standard", - "LS-COMMIT": "Commit Snapshot", - "LS-BRANCH": "Branch Snapshot", - "LS-PR": "PR Preview", - "LD": "Living Document", - "DRAFT-FINDING": "Draft Finding", - "FINDING": "Finding", - "whatwg/RD": "Review Draft", - "w3c/ED": "Editor’s Draft", - "w3c/WD": "W3C Working Draft", - "w3c/FPWD": "W3C First Public Working Draft", - "w3c/LCWD": "W3C Last Call Working Draft", - "w3c/CR": "W3C Candidate Recommendation Snapshot", - "w3c/CRD": "W3C Candidate Recommendation Draft", - "w3c/PR": "W3C Proposed Recommendation", - "w3c/REC": "W3C Recommendation", - "w3c/PER": "W3C Proposed Edited Recommendation", - "w3c/WG-NOTE": "W3C Group Note", - "w3c/IG-NOTE": "W3C Group Note", - "w3c/NOTE": "W3C Group Note", - "w3c/NOTE-ED": "Editor’s Draft", - "w3c/NOTE-WD": "W3C Group Draft Note", - "w3c/NOTE-FPWD": "W3C Group Draft Note", - "w3c/DRY": "W3C Draft Registry", - "w3c/CRYD": "W3C Candidate Registry Draft", - "w3c/CRY": "W3C Candidate Registry", - "w3c/RY": "W3C Registry", - "w3c/MO": "W3C Member-only Draft", - "w3c/UD": "Unofficial Proposal Draft", - "w3c/CG-DRAFT": "Draft Community Group Report", - "w3c/CG-FINAL": "Final Community Group Report", - "tc39/STAGE0": "Stage 0: Strawman", - "tc39/STAGE1": "Stage 1: Proposal", - "tc39/STAGE2": "Stage 2: Draft", - "tc39/STAGE3": "Stage 3: Candidate", - "tc39/STAGE4": "Stage 4: Finished", - "iso/I": "Issue", - "iso/DR": "Defect Report", - "iso/D": "Draft Proposal", - "iso/P": "Published Proposal", - "iso/MEET": "Meeting Announcements", - "iso/RESP": "Records of Response", - "iso/MIN": "Minutes", - "iso/ER": "Editor’s Report", - "iso/SD": "Standing Document", - "iso/PWI": "Preliminary Work Item", - "iso/NP": "New Proposal", - "iso/NWIP": "New Work Item Proposal", - "iso/WD": "Working Draft", - "iso/CD": "Committee Draft", - "iso/FCD": "Final Committee Draft", - "iso/DIS": "Draft International Standard", - "iso/FDIS": "Final Draft International Standard", - "iso/PRF": "Proof of a new International Standard", - "iso/IS": "International Standard", - "iso/TR": "Technical Report", - "iso/DTR": "Draft Technical Report", - "iso/TS": "Technical Specification", - "iso/DTS": "Draft Technical Specification", - "iso/PAS": "Publicly Available Specification", - "iso/TTA": "Technology Trends Assessment", - "iso/IWA": "International Workshop Agreement", - "iso/COR": "Technical Corrigendum", - "iso/GUIDE": "Guidance to Technical Committees", - "iso/NP-AMD": "New Proposal Amendment", - "iso/AWI-AMD": "Approved new Work Item Amendment", - "iso/WD-AMD": "Working Draft Amendment", - "iso/CD-AMD": "Committee Draft Amendment", - "iso/PD-AMD": "Proposed Draft Amendment", - "iso/FPD-AMD": "Final Proposed Draft Amendment", - "iso/D-AMD": "Draft Amendment", - "iso/FD-AMD": "Final Draft Amendment", - "iso/PRF-AMD": "Proof Amendment", - "iso/AMD": "Amendment", - "fido/ED": "Editor’s Draft", - "fido/WD": "Working Draft", - "fido/RD": "Review Draft", - "fido/ID": "Implementation Draft", - "fido/PS": "Proposed Standard", - "fido/FD": "Final Document", - "khronos/ED": "Editor’s Draft", - "aom/PD": "Pre-Draft", - "aom/WGD": "AOM Working Group Draft", - "aom/WGA": "AOM Working Group Approved Draft", - "aom/FD": "AOM Final Deliverable", -} -snapshotStatuses = [ - "w3c/WD", - "w3c/FPWD", - "w3c/LCWD", - "w3c/CR", - "w3c/CRD", - "w3c/PR", - "w3c/REC", - "w3c/PER", - "w3c/WG-NOTE", - "w3c/IG-NOTE", - "w3c/NOTE", - "w3c/NOTE-WD", - "w3c/NOTE-FPWD", - "w3c/DRY", - "w3c/CRYD", - "w3c/CRY", - "w3c/RY", - "w3c/MO", -] -datedStatuses = [ - "w3c/WD", - "w3c/FPWD", - "w3c/LCWD", - "w3c/CR", - "w3c/CRD", - "w3c/PR", - "w3c/REC", - "w3c/PER", - "w3c/WG-NOTE", - "w3c/IG-NOTE", - "w3c/NOTE", - "w3c/NOTE-WD", - "w3c/NOTE-FPWD", - "w3c/DRY", - "w3c/CRYD", - "w3c/CRY", - "w3c/RY", - "w3c/MO", - "whatwg/RD", -] -implementationStatuses = ["w3c/CR", "w3c/CRD", "w3c/PR", "w3c/REC"] -unlevelledStatuses = [ - "LS", - "LD", - "DREAM", - "w3c/UD", - "LS-COMMIT", - "LS-BRANCH", - "LS-PR", - "FINDING", - "DRAFT-FINDING", - "whatwg/RD", -] -deadlineStatuses = ["w3c/LCWD", "w3c/PR"] -noEDStatuses = [ - "LS", - "LS-COMMIT", - "LS-BRANCH", - "LS-PR", - "LD", - "FINDING", - "DRAFT-FINDING", - "DREAM", - "iso/NP", - "whatwg/RD", -] - -# W3C statuses are restricted in various confusing ways. - -# These statuses are usable by any group operating under the W3C Process -# Document. (So, not by Community and Business Groups.) -w3cProcessDocumentStatuses = frozenset( - [ - "w3c/ED", - "w3c/NOTE", - "w3c/NOTE-ED", - "w3c/NOTE-WD", - "w3c/NOTE-FPWD", - "w3c/UD", - ], -) - -# Interest Groups are limited to these statuses -w3cIGStatuses = frozenset(["w3c/IG-NOTE"]).union(w3cProcessDocumentStatuses) -# Working Groups are limited to these statuses -w3cWGStatuses = frozenset( - [ - "w3c/WD", - "w3c/FPWD", - "w3c/LCWD", - "w3c/CR", - "w3c/CRD", - "w3c/PR", - "w3c/REC", - "w3c/PER", - "w3c/WG-NOTE", - "w3c/DRY", - "w3c/CRYD", - "w3c/CRY", - "w3c/RY", - ], -).union(w3cProcessDocumentStatuses) -# The TAG is limited to these statuses -w3cTAGStatuses = frozenset( - [ - "DRAFT-FINDING", - "FINDING", - "w3c/WG-NOTE", # despite the TAG not being a WG. I know, it's weird. - ], -).union(w3cProcessDocumentStatuses) -# Community and Business Groups are limited to these statuses -w3cCommunityStatuses = frozenset(["w3c/CG-DRAFT", "w3c/CG-FINAL", "w3c/UD"]) - -megaGroups = { - "w3c": frozenset( - [ - "ab", - "act-framework", - "act-rules-format", - "audiowg", - "browser-testing-tools", - "csswg", - "dap", - "fedidcg", - "fxtf", - "geolocation", - "gpuwg", - "houdini", - "html", - "htmlwg", - "httpslocal", - "i18n", - "immersivewebcg", - "immersivewebwg", - "mediacapture", - "mediawg", - "patcg", - "patcg-id", - "ping", - "pngwg", - "privacycg", - "processcg", - "ricg", - "sacg", - "secondscreencg", - "secondscreenwg", - "serviceworkers", - "solidcg", - "svg", - "tag", - "texttracks", - "uievents", - "wasm", - "web-bluetooth-cg", - "web-payments", - "webapps", - "webappsec", - "webauthn", - "webediting", - "webfontswg", - "webml", - "webmlwg", - "webperf", - "webplatform", - "webrtc", - "webspecs", - "webtransport", - "webvr", - "wecg", - "wicg", - "wintercg", - "w3t", - ], - ), - "whatwg": frozenset(["whatwg"]), - "tc39": frozenset(["tc39"]), - "iso": frozenset(["wg14", "wg21"]), - "fido": frozenset(["fido"]), - "priv-sec": frozenset( - [ - "audiowg", - "csswg", - "dap", - "fxtf", - "fxtf-csswg", - "geolocation", - "houdini", - "html", - "mediacapture", - "mediawg", - "ricg", - "svg", - "texttracks", - "uievents", - "web-bluetooth-cg", - "webappsec", - "webfontswg", - "webplatform", - "webspecs", - "whatwg", - ], - ), - "khronos": frozenset(["webgl"]), - "aom": frozenset(["aom"]), -} -# Community and business groups within the W3C: -w3cCgs = frozenset( - [ - "fedidcg", - "immersivewebcg", - "patcg", - "patcg-id", - "privacycg", - "processcg", - "ricg", - "sacg", - "solidcg", - "web-bluetooth-cg", - "webml", - "wecg", - "wicg", - "wintercg", - ], -) -assert w3cCgs.issubset(megaGroups["w3c"]) -# Interest Groups within the W3C: -w3cIgs = frozenset(["ping"]) -assert w3cIgs.issubset(megaGroups["w3c"]) - @t.overload def canonicalizeStatus(rawStatus: None, group: str | None) -> None: ... diff --git a/bikeshed/spec-data/readonly/statuses.kdl b/bikeshed/spec-data/readonly/statuses.kdl index 5ae9d388ec..d292c7e45e 100644 --- a/bikeshed/spec-data/readonly/statuses.kdl +++ b/bikeshed/spec-data/readonly/statuses.kdl @@ -1,184 +1,337 @@ /* -All recognized Status values. +Data about recognized Group and Status metadata values. -Slashed values namespace the status under an org; -any Group tagged to that org can use it by default -(without the slash), -or anyone can use it by referring to the full name. +`status` nodes list a Status value and the long version of the name. +They can have a `requires` child, whose attributes list required metadata. -The `requires` child node lists what metadata keys are required for this Status. +Statuses can be restricted to a "megagroup". +You can use these statuses if you specify the megagroup explicitly, +like `w3c/WD`, +or if your group is listed as part of that megagroup. -* w3c-ig: Usable by groups tagged as W3C Interest Groups -* w3c-wg: Usable by groups tagged as W3C Working Groups -* w3c-cg: Usable by groups tagged as W3C Community Groups -* w3c-tag: Usable by the W3C TAG +Within a megagroup, `group` nodes list the Group values +that are part of that megagroup. +They can have a `priv-sec` child, +indicating they require Privacy and Security sections in their documents. +They can also have megagroup-specific children. */ -statuses { - DREAM "A Collection of Interesting Ideas" - LS "Living Standard" - LS-COMMIT "Commit Snapshot" - LS-BRANCH "Branch Snapshot" - LS-PR "PR Preview" - LD "Living Document" +status DREAM "A Collection of Interesting Ideas" +status LS "Living Standard" +status LS-COMMIT "Commit Snapshot" +status LS-BRANCH "Branch Snapshot" +status LS-PR "PR Preview" +status LD "Living Document" +status DRAFT-FINDING "Draft Finding" +status FINDING "Finding" - whatwg/RD "Review Draft" { +megagroup "whatwg" { + group "whatwg" { + priv-sec + } + status RD "Review Draft" { requires "Date" } +} + +megagroup "w3c" { + + /* + Any group in this megagroup has a secondary default + for its boilerplate - + if its personal boilerplate folder is missing a file, + it will look in the `w3c` folder first, + before falling back to the global files. + */ - w3c/DRAFT-FINDING "Draft Finding" { - w3c-tag + /* + Every group in this megagroup must have a `type` attribute, + containing "wg", "ig", "cg", or "tag"; + this matches with the `group-types` children of the statuses + (and might have some formatting effects, too). + If a group should be able to use anything + (or it's a weirdo one-off that's not worth codifying), + use `type=null`. + */ + + group "ab" type=null + group "act-framework" type="wg" + group "act-rules-format" type="wg" + group "audiowg" type="wg" { + priv-sec + } + group "browser-testing-tools" type="wg" + group "csswg" type="wg" { + priv-sec + } + group "dap" type="wg" { + priv-sec + } + group "fedidcg" type="cg" + group "fxtf" type="wg" { + priv-sec + } + group "geolocation" type="wg" { + priv-sec + } + group "gpuwg" + group "houdini" type="wg" { + priv-sec + } + group "html" type="wg" { + priv-sec + } + group "htmlwg" type="wg" + group "httpslocal" + group "i18n" type="wg" + group "immersivewebcg" type="cg" + group "immersivewebwg" type="wg" + group "mediacapture" type="wg" { + priv-sec + } + group "mediawg" type="wg" { + priv-sec + } + group "patcg" type="cg" + group "patcg-id" type="cg" + group "ping" type="ig" + group "pngwg" type="wg" + group "privacycg" type="cg" + group "processcg" type="cg" + group "ricg" type="cg" { + priv-sec + } + group "sacg" type="cg" + group "secondscreencg" type="cg" + group "secondscreenwg" type="wg" + group "serviceworkers" type="wg" + group "solidcg" type="cg" + group "svg" type="wg" { + priv-sec + } + group "tag" type="tag" + group "texttracks" type="cg" { + priv-sec + } + group "uievents" type="wg" { + priv-sec + } + group "wasm" type="wg" + group "web-bluetooth-cg" type="cg" { + priv-sec + } + group "web-payments" type="wg" + group "webapps" type="wg" + group "webappsec" type="wg" { + priv-sec + } + group "webauthn" type="wg" + group "webediting" type="wg" + group "webfontswg" type="wg" { + priv-sec } - w3c/FINDING "Finding" { - w3c-tag + group "webml" type="cg" + group "webmlwg" type="wg" + group "webperf" type="wg" + group "webplatform" type="cg" { + priv-sec } - w3c/ED "Editor's Draft" { + group "webrtc" type="wg" + group "webspecs" type="wg" { + priv-sec + } + group "webtransport" type="wg" + group "webvr" type="wg" + group "wecg" type="cg" + group "wicg" type="cg" + group "wintercg" type="cg" + group "w3t" type=null + + /* + Every status in this group requires a `group-types` child, + with attributes that are any of "ig" "wg" "cg" and/or "tag" + */ + status ED "Editor's Draft" { requires "Level" "ED" - w3c-ig - w3c-wg - w3c-tag + group-types "ig" "wg" "tag" } - w3c/WD "W3C Working Draft" { + status WD "W3C Working Draft" { requires "Level" "ED" "TR" "Issue Tracking" "Date" - w3c-wg + group-types "wg" } - w3c/FPWD "W3C First Public Working Draft" { + status FPWD "W3C First Public Working Draft" { requires "Level" "ED" "TR" "Issue Tracking" "Date" - w3c-wg + group-types "wg" } - w3c/LCWD "W3C Last Call Working Draft" { + status LCWD "W3C Last Call Working Draft" { requires "Level" "ED" "TR" "Issue Tracking" "Date" "Deadline" - w3c-wg + group-types "wg" } - w3c/CR "W3C Candidate Recommendation Snapshot" { + status CR "W3C Candidate Recommendation Snapshot" { requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implentation Report" - w3c-wg + group-types "wg" } - w3c/CRD "W3C Candidate Recommendation Draft" { + status CRD "W3C Candidate Recommendation Draft" { requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" - w3c-wg + group-types "wg" } - w3c/PR "W3C Proposed Recommendation" { + status PR "W3C Proposed Recommendation" { requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" - w3c-wg + group-types "wg" } - w3c/REC "W3C Recommendation" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" - w3c-wg + status REC "W3C Recommendation" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" + group-types "wg" } - w3c/PER "W3C Proposed Edited Recommendation" { + status PER "W3C Proposed Edited Recommendation" { requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" - w3c-wg + group-types "wg" + } + status WG-NOTE "W3C Group Note" { + requires "TR" "Issue Tracking" "Date" + group-types "wg" "tag" + } + status IG-NOTE "W3C Group Note" { + requires "TR" "Issue Tracking" "Date" + group-types "ig" } - w3c/NOTE "W3C Note" { + status NOTE "W3C Group Note" { requires "TR" "Issue Tracking" "Date" - w3c-ig - w3c-wg - w3c-tag + group-types "wg" "ig" "tag" } - w3c/NOTE-ED "Editor's Draft" { + status NOTE-ED "Editor's Draft" { requires "ED" - w3c-ig - w3c-wg - w3c-tag + group-types "wg" "ig" "tag" } - w3c/NOTE-WD "W3C Working Draft" { + status NOTE-WD "W3C Working Draft" { requires "ED" "TR" "Issue Tracking" "Date" - w3c-ig - w3c-wg - w3c-tag + group-types "wg" "ig" "tag" } - w3c/NOTE-FPWD "W3C First Public Working Draft" { + status NOTE-FPWD "W3C First Public Working Draft" { requires "ED" "TR" "Issue Tracking" "Date" - w3c-ig - w3c-wg - w3c-tag + group-types "wg" "ig" "tag" + } + status DRY "W3C Draft Registry" { + requires "TR" "Date" + group-types "wg" + } + status CRYD "W3C Candidate Registry Draft" { + requires "TR" "Date" + group-types "wg" + } + status CRY "W3C Candidate Registry" { + requires "TR" "Date" + group-types "wg" + } + status RY "W3C Registry" { + requires "TR" "Date" + group-types "wg" } - w3c/MO "W3C Member-only Draft" { + status MO "W3C Member-only Draft" { requires "TR" "Issue Tracking" "Date" + group-types "wg" } - w3c/UD "Unofficial Proposal Draft" { + status UD "Unofficial Proposal Draft" { requires "ED" - w3c-ig - w3c-wg - w3c-tag + group-types "wg" "ig" "tag" "cg" } - w3c/CG-DRAFT "Draft Community Group Report" { + status CG-DRAFT "Draft Community Group Report" { requires "Level" "ED" - w3c-cg + group-types "cg" } - w3c/CG-FINAL "Final Community Group Report" { + status CG-FINAL "Final Community Group Report" { requires "Level" "ED" "TR" "Issue Tracking" - w3c-cg - } - - tc39/STAGE0 "Stage 0: Strawman" - tc39/STAGE1 "Stage 1: Proposal" - tc39/STAGE2 "Stage 2: Draft" - tc39/STAGE3 "Stage 3: Candidate" - tc39/STAGE4 "Stage 4: Finished" - - iso/I "Issue" - iso/DR "Defect Report" - iso/D "Draft Proposal" - iso/P "Published Proposal" - iso/MEET "Meeting Announcements" - iso/RESP "Records of Response" - iso/MIN "Minutes" - iso/ER "Editor's Report" - iso/SD "Standing Document" - iso/PWI "Preliminary Work Item" - iso/NP "New Proposal" - iso/NWIP "New Work Item Proposal" - iso/WD "Working Draft" - iso/CD "Committee Draft" - iso/FCD "Final Committee Draft" - iso/DIS "Draft International Standard" - iso/FDIS "Final Draft International Standard" - iso/PRF "Proof of a new International Standard" - iso/IS "International Standard" - iso/TR "Technical Report" - iso/DTR "Draft Technical Report" - iso/TS "Technical Specification" - iso/DTS "Draft Technical Specification" - iso/PAS "Publicly Available Specification" - iso/TTA "Technology Trends Assessment" - iso/IWA "International Workshop Agreement" - iso/COR "Technical Corrigendum" - iso/GUIDE "Guidance to Technical Committees" - iso/NP-AMD "New Proposal Amendment" - iso/AWI-AMD "Approved new Work Item Amendment" - iso/WD-AMD "Working Draft Amendment" - iso/CD-AMD "Committee Draft Amendment" - iso/PD-AMD "Proposed Draft Amendment" - iso/FPD-AMD "Final Proposed Draft Amendment" - iso/D-AMD "Draft Amendment" - iso/FD-AMD "Final Draft Amendment" - iso/PRF-AMD "Proof Amendment" - iso/AMD "Amendment" - - fido/ED "Editor's Draft" - fido/WD "Working Draft" { + group-types "cg" + } +} + +megagroup "tc39" { + group "tc39" + + status STAGE0 "Stage 0: Strawman" + status STAGE1 "Stage 1: Proposal" + status STAGE2 "Stage 2: Draft" + status STAGE3 "Stage 3: Candidate" + status STAGE4 "Stage 4: Finished" +} + +megagroup "iso" { + group "wg14" + group "wg21" + + status I "Issue" + status DR "Defect Report" + status D "Draft Proposal" + status P "Published Proposal" + status MEET "Meeting Announcements" + status RESP "Records of Response" + status MIN "Minutes" + status ER "Editor's Report" + status SD "Standing Document" + status PWI "Preliminary Work Item" + status NP "New Proposal" + status NWIP "New Work Item Proposal" + status WD "Working Draft" + status CD "Committee Draft" + status FCD "Final Committee Draft" + status DIS "Draft International Standard" + status FDIS "Final Draft International Standard" + status PRF "Proof of a new International Standard" + status IS "International Standard" + status TR "Technical Report" + status DTR "Draft Technical Report" + status TS "Technical Specification" + status DTS "Draft Technical Specification" + status PAS "Publicly Available Specification" + status TTA "Technology Trends Assessment" + status IWA "International Workshop Agreement" + status COR "Technical Corrigendum" + status GUIDE "Guidance to Technical Committees" + status NP-AMD "New Proposal Amendment" + status AWI-AMD "Approved new Work Item Amendment" + status WD-AMD "Working Draft Amendment" + status CD-AMD "Committee Draft Amendment" + status PD-AMD "Proposed Draft Amendment" + status FPD-AMD "Final Proposed Draft Amendment" + status D-AMD "Draft Amendment" + status FD-AMD "Final Draft Amendment" + status PRF-AMD "Proof Amendment" + status AMD "Amendment" +} + +megagroup "fido" { + group "fido" + + status ED "Editor's Draft" + status WD "Working Draft" { requires "ED" } - fido/RD "Review Draft" { + status RD "Review Draft" { requires "ED" } - fido/ID "Implementation Draft" { + status ID "Implementation Draft" { requires "ED" } - fido/PS "Proposed Standard" { + status PS "Proposed Standard" { requires "ED" } - fido/FD "Final Document" { + status FD "Final Document" { requires "ED" } +} + +megagroup "khronos" { + group "webgl" + + status ED "Editor's Draft" +} - khronos/ED "Editor's Draft" +megagroup "aom" { + group "aom" - aom/PD "Pre-Draft" - aom/WGD "AOM Working Group Draft" - aom/WGA "AOM Working Group Approved Draft" - aom/FD "AOM Final Deliverable" -} \ No newline at end of file + status PD "Pre-Draft" + status WGD "AOM Working Group Draft" + status WGA "AOM Working Group Approved Draft" + status FD "AOM Final Deliverable" +} diff --git a/bikeshed/status/GroupStatusManager.py b/bikeshed/status/GroupStatusManager.py new file mode 100644 index 0000000000..782f4d4742 --- /dev/null +++ b/bikeshed/status/GroupStatusManager.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import dataclasses +import kdl + +from .. import t, messages as m + +@dataclasses.dataclass +class GroupStatusManager: + genericStatuses: dict[str, Status] = dataclasses.field(default_factory=dict) + megaGroups: dict[str, MegaGroup] = dataclasses.field(default_factory=dict) + + @classmethod + def fromKDLStr(cls, data: str) -> t.Self: + self = cls() + kdlDoc = kdl.parse(data) + + for node in kdlDoc.nodes: + if node.name.lower() == "status": + status = Status.fromKdlNode(node) + genericStatuses[status.name] = status + elif node.name.lower() == "megagroup": + mg = MegaGroup.fromKDLNode(node) + megaGroups[mg.name] = mg + else: + m.die(f"Unknown node type '{node.name}' in the group/status KDL file.") + return self + return self + + + +@dataclasses.dataclass +class MegaGroup: + name: str + groups: dict[str, Group] = dataclasses.field(default_factory=dict) + statuses: dict[str, Status] = dataclasses.field(default_factory=dict) + + @classmethod + def fromKDLNode(cls, node: kdl.Node) -> t.Self: + self = cls(node.args[0]) + for child in node.nodes: + if child.name.lower() == "group": + g = Group.fromKDLNode(child) + self.groups[g.name] = g + elif child.name.lower() == "status": + s = Status.fromKDLNode(child) + self.statuses[s.name] = s + else: + m.die(f"Unknown node type '{child.name}' in megagroup '{self.name}'.") + continue + return self + + +@dataclasses.dataclass +class Group: + name: str + privSec: bool + +@dataclasses.dataclass +class GroupW3C: + type: str + +@dataclasses.dataclass +class Status: + name: str + longName: str + requires: list[str] = dataclasses.field(default_factory=list) + +@dataclasses.dataclass +class StatusW3C: + groupTypes: list[str] = dataclasses.field(default_factory=list) \ No newline at end of file diff --git a/bikeshed/status/__init__.py b/bikeshed/status/__init__.py new file mode 100644 index 0000000000..f838430954 --- /dev/null +++ b/bikeshed/status/__init__.py @@ -0,0 +1 @@ +from .GroupStatusManager import GroupStatusManager From eca96de9aa7ee3945dc7818602b87ab52aec7348 Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Thu, 8 Aug 2024 14:44:15 -0700 Subject: [PATCH 03/25] Put statuses.kdl in the boilerplate folder, as it'll need to be modified alongside boilerplate changes/additions --- .../readonly/boilerplate/statuses.kdl | 353 ++++++++++++++++++ bikeshed/spec-data/readonly/statuses.kdl | 337 ----------------- 2 files changed, 353 insertions(+), 337 deletions(-) create mode 100644 bikeshed/spec-data/readonly/boilerplate/statuses.kdl delete mode 100644 bikeshed/spec-data/readonly/statuses.kdl diff --git a/bikeshed/spec-data/readonly/boilerplate/statuses.kdl b/bikeshed/spec-data/readonly/boilerplate/statuses.kdl new file mode 100644 index 0000000000..74fbaec7b6 --- /dev/null +++ b/bikeshed/spec-data/readonly/boilerplate/statuses.kdl @@ -0,0 +1,353 @@ +/* +This is a KDL document listing all the status "metadata" values, +and optionally listing Group metadata values along with some associated data for them. +(Not all Group values must be listed here; +bespoke groups are allowed, and simply must namespace their `Status`.) + +`status` nodes can appear at the top level, listing statuses usable by anyone. +A status "contains" two attributes, listing their short name (what you write in the status "metadata") +and their long name (what gets written into specs; the `[LONGSTATUS]` macro). +They can have a `requires` child, with attributes listing the names of additional metadata +required by documents with that status. + +`standards-body` nodes can also appear at the top level. +A standards-body has a single attribute, giving its abbreviation/name. +It has `group` and `status` children. + +`group` children define the groups that are part of the parent `standards-body`. +A group has an attribute giving the group name (what you write in the Group metadata). +It can optionally have a `priv-sec` child, +indicating that documents for that group require Privacy Considerations and Security Considerations sections. + +`status` children are only usable by groups in that standards-body. +`status` shortnames can clash between standards-body nodes; +they're namespaced by the standards-body name, if needed. + +The w3c `standards-body` has special rules: +* its `group` nodes must have a `type` property, + with the value being null, "wg", "ig", "cg", or "tag", + indicating the type of the group. + (null indicates a group that isn't otherwise categorized) +* its `status` nodes must have a `group-types` child, + with attributes listing the group type values they can be used by. + +*/ + +status "DREAM" "A Collection of Interesting Ideas" +status "LS" "Living Standard" +status "LS-COMMIT" "Commit Snapshot" +status "LS-BRANCH" "Branch Snapshot" +status "LS-PR" "PR Preview" +status "LD" "Living Document" +status "DRAFT-FINDING" "Draft Finding" +status "FINDING" "Finding" + +standards-body "whatwg" { + group "whatwg" { + priv-sec + } + status "RD" "Review Draft" { + requires "Date" + } +} + +standards-body "w3c" { + + /* + Any group in this standards-body has a secondary default + for its boilerplate - + if its personal boilerplate folder is missing a file, + it will look in the `w3c` folder first, + before falling back to the global files. + */ + + /* + Every group in this standards-body must have a `type` attribute, + containing "wg", "ig", "cg", or "tag"; + this matches with the `group-types` children of the statuses + (and might have some formatting effects, too). + If a group should be able to use anything + (or it's a weirdo one-off that's not worth codifying), + use `type=null`. + */ + + group "ab" type=null + group "act-framework" type="wg" + group "act-rules-format" type="wg" + group "audiowg" type="wg" { + priv-sec + } + group "browser-testing-tools" type="wg" + group "csswg" type="wg" { + priv-sec + } + group "dap" type="wg" { + priv-sec + } + group "fedidcg" type="cg" + group "fxtf" type="wg" { + priv-sec + } + group "geolocation" type="wg" { + priv-sec + } + group "gpuwg" + group "houdini" type="wg" { + priv-sec + } + group "html" type="wg" { + priv-sec + } + group "htmlwg" type="wg" + group "httpslocal" + group "i18n" type="wg" + group "immersivewebcg" type="cg" + group "immersivewebwg" type="wg" + group "mediacapture" type="wg" { + priv-sec + } + group "mediawg" type="wg" { + priv-sec + } + group "patcg" type="cg" + group "patcg-id" type="cg" + group "ping" type="ig" + group "pngwg" type="wg" + group "privacycg" type="cg" + group "processcg" type="cg" + group "ricg" type="cg" { + priv-sec + } + group "sacg" type="cg" + group "secondscreencg" type="cg" + group "secondscreenwg" type="wg" + group "serviceworkers" type="wg" + group "solidcg" type="cg" + group "svg" type="wg" { + priv-sec + } + group "tag" type="tag" + group "texttracks" type="cg" { + priv-sec + } + group "uievents" type="wg" { + priv-sec + } + group "wasm" type="wg" + group "web-bluetooth-cg" type="cg" { + priv-sec + } + group "web-payments" type="wg" + group "webapps" type="wg" + group "webappsec" type="wg" { + priv-sec + } + group "webauthn" type="wg" + group "webediting" type="wg" + group "webfontswg" type="wg" { + priv-sec + } + group "webml" type="cg" + group "webmlwg" type="wg" + group "webperf" type="wg" + group "webplatform" type="cg" { + priv-sec + } + group "webrtc" type="wg" + group "webspecs" type="wg" { + priv-sec + } + group "webtransport" type="wg" + group "webvr" type="wg" + group "wecg" type="cg" + group "wicg" type="cg" + group "wintercg" type="cg" + group "w3t" type=null + + /* + Every status "in" this group requires a `group-types` child, + with attributes that are any of "ig" "wg" "cg" and/or "tag" + */ + status "ED" "Editor's Draft" { + requires "Level" "ED" + group-types "ig" "wg" "tag" + } + status "WD" "W3C Working Draft" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" + group-types "wg" + } + status "FPWD" "W3C First Public Working Draft" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" + group-types "wg" + } + status "LCWD" "W3C Last Call Working Draft" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" "Deadline" + group-types "wg" + } + status "CR" "W3C Candidate Recommendation Snapshot" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implentation Report" + group-types "wg" + } + status "CRD" "W3C Candidate Recommendation Draft" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" + group-types "wg" + } + status "PR" "W3C Proposed Recommendation" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" + group-types "wg" + } + status "REC" "W3C Recommendation" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" + group-types "wg" + } + status "PER" "W3C Proposed Edited Recommendation" { + requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" + group-types "wg" + } + status "WG-NOTE" "W3C Group Note" { + requires "TR" "Issue Tracking" "Date" + group-types "wg" "tag" + } + status "IG-NOTE" "W3C Group Note" { + requires "TR" "Issue Tracking" "Date" + group-types "ig" + } + status "NOTE" "W3C Group Note" { + requires "TR" "Issue Tracking" "Date" + group-types "wg" "ig" "tag" + } + status "NOTE-ED" "Editor's Draft" { + requires "ED" + group-types "wg" "ig" "tag" + } + status "NOTE-WD" "W3C Working Draft" { + requires "ED" "TR" "Issue Tracking" "Date" + group-types "wg" "ig" "tag" + } + status "NOTE-FPWD" "W3C First Public Working Draft" { + requires "ED" "TR" "Issue Tracking" "Date" + group-types "wg" "ig" "tag" + } + status "DRY" "W3C Draft Registry" { + requires "TR" "Date" + group-types "wg" + } + status "CRYD" "W3C Candidate Registry Draft" { + requires "TR" "Date" + group-types "wg" + } + status "CRY" "W3C Candidate Registry" { + requires "TR" "Date" + group-types "wg" + } + status "RY" "W3C Registry" { + requires "TR" "Date" + group-types "wg" + } + status "MO" "W3C Member-only Draft" { + requires "TR" "Issue Tracking" "Date" + group-types "wg" + } + status "UD" "Unofficial Proposal Draft" { + requires "ED" + group-types "wg" "ig" "tag" "cg" + } + status "CG-DRAFT" "Draft Community Group Report" { + requires "Level" "ED" + group-types "cg" + } + status "CG-FINAL" "Final Community Group Report" { + requires "Level" "ED" "TR" "Issue Tracking" + group-types "cg" + } +} + +standards-body "tc39" { + group "tc39" + + status "STAGE0" "Stage 0: Strawman" + status "STAGE1" "Stage 1: Proposal" + status "STAGE2" "Stage 2: Draft" + status "STAGE3" "Stage 3: Candidate" + status "STAGE4" "Stage 4: Finished" +} + +standards-body "iso" { + group "wg14" + group "wg21" + + status "I" "Issue" + status "DR" "Defect Report" + status "D" "Draft Proposal" + status "P" "Published Proposal" + status "MEET" "Meeting Announcements" + status "RESP" "Records of Response" + status "MIN" "Minutes" + status "ER" "Editor's Report" + status "SD" "Standing Document" + status "PWI" "Preliminary Work Item" + status "NP" "New Proposal" + status "NWIP" "New Work Item Proposal" + status "WD" "Working Draft" + status "CD" "Committee Draft" + status "FCD" "Final Committee Draft" + status "DIS" "Draft International Standard" + status "FDIS" "Final Draft International Standard" + status "PRF" "Proof of a new International Standard" + status "IS" "International Standard" + status "TR" "Technical Report" + status "DTR" "Draft Technical Report" + status "TS" "Technical Specification" + status "DTS" "Draft Technical Specification" + status "PAS" "Publicly Available Specification" + status "TTA" "Technology Trends Assessment" + status "IWA" "International Workshop Agreement" + status "COR" "Technical Corrigendum" + status "GUIDE" "Guidance to Technical Committees" + status "NP-AMD" "New Proposal Amendment" + status "AWI-AMD" "Approved new Work Item Amendment" + status "WD-AMD" "Working Draft Amendment" + status "CD-AMD" "Committee Draft Amendment" + status "PD-AMD" "Proposed Draft Amendment" + status "FPD-AMD" "Final Proposed Draft Amendment" + status "D-AMD" "Draft Amendment" + status "FD-AMD" "Final Draft Amendment" + status "PRF-AMD" "Proof Amendment" + status "AMD" "Amendment" +} + +standards-body "fido" { + group "fido" + + status "ED" "Editor's Draft" + status "WD" "Working Draft" { + requires "ED" + } + status "RD" "Review Draft" { + requires "ED" + } + status "ID" "Implementation Draft" { + requires "ED" + } + status "PS" "Proposed Standard" { + requires "ED" + } + status "FD" "Final Document" { + requires "ED" + } +} + +standards-body "khronos" { + group "webgl" + + status "ED" "Editor's Draft" +} + +standards-body "aom" { + group "aom" + + status "PD" "Pre-Draft" + status "WGD" "AOM Working Group Draft" + status "WGA" "AOM Working Group Approved Draft" + status "FD" "AOM Final Deliverable" +} diff --git a/bikeshed/spec-data/readonly/statuses.kdl b/bikeshed/spec-data/readonly/statuses.kdl deleted file mode 100644 index d292c7e45e..0000000000 --- a/bikeshed/spec-data/readonly/statuses.kdl +++ /dev/null @@ -1,337 +0,0 @@ -/* -Data about recognized Group and Status metadata values. - -`status` nodes list a Status value and the long version of the name. -They can have a `requires` child, whose attributes list required metadata. - -Statuses can be restricted to a "megagroup". -You can use these statuses if you specify the megagroup explicitly, -like `w3c/WD`, -or if your group is listed as part of that megagroup. - -Within a megagroup, `group` nodes list the Group values -that are part of that megagroup. -They can have a `priv-sec` child, -indicating they require Privacy and Security sections in their documents. -They can also have megagroup-specific children. - -*/ - -status DREAM "A Collection of Interesting Ideas" -status LS "Living Standard" -status LS-COMMIT "Commit Snapshot" -status LS-BRANCH "Branch Snapshot" -status LS-PR "PR Preview" -status LD "Living Document" -status DRAFT-FINDING "Draft Finding" -status FINDING "Finding" - -megagroup "whatwg" { - group "whatwg" { - priv-sec - } - status RD "Review Draft" { - requires "Date" - } -} - -megagroup "w3c" { - - /* - Any group in this megagroup has a secondary default - for its boilerplate - - if its personal boilerplate folder is missing a file, - it will look in the `w3c` folder first, - before falling back to the global files. - */ - - /* - Every group in this megagroup must have a `type` attribute, - containing "wg", "ig", "cg", or "tag"; - this matches with the `group-types` children of the statuses - (and might have some formatting effects, too). - If a group should be able to use anything - (or it's a weirdo one-off that's not worth codifying), - use `type=null`. - */ - - group "ab" type=null - group "act-framework" type="wg" - group "act-rules-format" type="wg" - group "audiowg" type="wg" { - priv-sec - } - group "browser-testing-tools" type="wg" - group "csswg" type="wg" { - priv-sec - } - group "dap" type="wg" { - priv-sec - } - group "fedidcg" type="cg" - group "fxtf" type="wg" { - priv-sec - } - group "geolocation" type="wg" { - priv-sec - } - group "gpuwg" - group "houdini" type="wg" { - priv-sec - } - group "html" type="wg" { - priv-sec - } - group "htmlwg" type="wg" - group "httpslocal" - group "i18n" type="wg" - group "immersivewebcg" type="cg" - group "immersivewebwg" type="wg" - group "mediacapture" type="wg" { - priv-sec - } - group "mediawg" type="wg" { - priv-sec - } - group "patcg" type="cg" - group "patcg-id" type="cg" - group "ping" type="ig" - group "pngwg" type="wg" - group "privacycg" type="cg" - group "processcg" type="cg" - group "ricg" type="cg" { - priv-sec - } - group "sacg" type="cg" - group "secondscreencg" type="cg" - group "secondscreenwg" type="wg" - group "serviceworkers" type="wg" - group "solidcg" type="cg" - group "svg" type="wg" { - priv-sec - } - group "tag" type="tag" - group "texttracks" type="cg" { - priv-sec - } - group "uievents" type="wg" { - priv-sec - } - group "wasm" type="wg" - group "web-bluetooth-cg" type="cg" { - priv-sec - } - group "web-payments" type="wg" - group "webapps" type="wg" - group "webappsec" type="wg" { - priv-sec - } - group "webauthn" type="wg" - group "webediting" type="wg" - group "webfontswg" type="wg" { - priv-sec - } - group "webml" type="cg" - group "webmlwg" type="wg" - group "webperf" type="wg" - group "webplatform" type="cg" { - priv-sec - } - group "webrtc" type="wg" - group "webspecs" type="wg" { - priv-sec - } - group "webtransport" type="wg" - group "webvr" type="wg" - group "wecg" type="cg" - group "wicg" type="cg" - group "wintercg" type="cg" - group "w3t" type=null - - /* - Every status in this group requires a `group-types` child, - with attributes that are any of "ig" "wg" "cg" and/or "tag" - */ - status ED "Editor's Draft" { - requires "Level" "ED" - group-types "ig" "wg" "tag" - } - status WD "W3C Working Draft" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" - group-types "wg" - } - status FPWD "W3C First Public Working Draft" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" - group-types "wg" - } - status LCWD "W3C Last Call Working Draft" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" "Deadline" - group-types "wg" - } - status CR "W3C Candidate Recommendation Snapshot" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implentation Report" - group-types "wg" - } - status CRD "W3C Candidate Recommendation Draft" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" - group-types "wg" - } - status PR "W3C Proposed Recommendation" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" - group-types "wg" - } - status REC "W3C Recommendation" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" - group-types "wg" - } - status PER "W3C Proposed Edited Recommendation" { - requires "Level" "ED" "TR" "Issue Tracking" "Date" "Implementation Report" "Deadline" - group-types "wg" - } - status WG-NOTE "W3C Group Note" { - requires "TR" "Issue Tracking" "Date" - group-types "wg" "tag" - } - status IG-NOTE "W3C Group Note" { - requires "TR" "Issue Tracking" "Date" - group-types "ig" - } - status NOTE "W3C Group Note" { - requires "TR" "Issue Tracking" "Date" - group-types "wg" "ig" "tag" - } - status NOTE-ED "Editor's Draft" { - requires "ED" - group-types "wg" "ig" "tag" - } - status NOTE-WD "W3C Working Draft" { - requires "ED" "TR" "Issue Tracking" "Date" - group-types "wg" "ig" "tag" - } - status NOTE-FPWD "W3C First Public Working Draft" { - requires "ED" "TR" "Issue Tracking" "Date" - group-types "wg" "ig" "tag" - } - status DRY "W3C Draft Registry" { - requires "TR" "Date" - group-types "wg" - } - status CRYD "W3C Candidate Registry Draft" { - requires "TR" "Date" - group-types "wg" - } - status CRY "W3C Candidate Registry" { - requires "TR" "Date" - group-types "wg" - } - status RY "W3C Registry" { - requires "TR" "Date" - group-types "wg" - } - status MO "W3C Member-only Draft" { - requires "TR" "Issue Tracking" "Date" - group-types "wg" - } - status UD "Unofficial Proposal Draft" { - requires "ED" - group-types "wg" "ig" "tag" "cg" - } - status CG-DRAFT "Draft Community Group Report" { - requires "Level" "ED" - group-types "cg" - } - status CG-FINAL "Final Community Group Report" { - requires "Level" "ED" "TR" "Issue Tracking" - group-types "cg" - } -} - -megagroup "tc39" { - group "tc39" - - status STAGE0 "Stage 0: Strawman" - status STAGE1 "Stage 1: Proposal" - status STAGE2 "Stage 2: Draft" - status STAGE3 "Stage 3: Candidate" - status STAGE4 "Stage 4: Finished" -} - -megagroup "iso" { - group "wg14" - group "wg21" - - status I "Issue" - status DR "Defect Report" - status D "Draft Proposal" - status P "Published Proposal" - status MEET "Meeting Announcements" - status RESP "Records of Response" - status MIN "Minutes" - status ER "Editor's Report" - status SD "Standing Document" - status PWI "Preliminary Work Item" - status NP "New Proposal" - status NWIP "New Work Item Proposal" - status WD "Working Draft" - status CD "Committee Draft" - status FCD "Final Committee Draft" - status DIS "Draft International Standard" - status FDIS "Final Draft International Standard" - status PRF "Proof of a new International Standard" - status IS "International Standard" - status TR "Technical Report" - status DTR "Draft Technical Report" - status TS "Technical Specification" - status DTS "Draft Technical Specification" - status PAS "Publicly Available Specification" - status TTA "Technology Trends Assessment" - status IWA "International Workshop Agreement" - status COR "Technical Corrigendum" - status GUIDE "Guidance to Technical Committees" - status NP-AMD "New Proposal Amendment" - status AWI-AMD "Approved new Work Item Amendment" - status WD-AMD "Working Draft Amendment" - status CD-AMD "Committee Draft Amendment" - status PD-AMD "Proposed Draft Amendment" - status FPD-AMD "Final Proposed Draft Amendment" - status D-AMD "Draft Amendment" - status FD-AMD "Final Draft Amendment" - status PRF-AMD "Proof Amendment" - status AMD "Amendment" -} - -megagroup "fido" { - group "fido" - - status ED "Editor's Draft" - status WD "Working Draft" { - requires "ED" - } - status RD "Review Draft" { - requires "ED" - } - status ID "Implementation Draft" { - requires "ED" - } - status PS "Proposed Standard" { - requires "ED" - } - status FD "Final Document" { - requires "ED" - } -} - -megagroup "khronos" { - group "webgl" - - status ED "Editor's Draft" -} - -megagroup "aom" { - group "aom" - - status PD "Pre-Draft" - status WGD "AOM Working Group Draft" - status WGA "AOM Working Group Approved Draft" - status FD "AOM Final Deliverable" -} From dc9028581a41ee7acacffbe88bdd1fd5fb0cfd8d Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Fri, 9 Aug 2024 11:44:04 -0700 Subject: [PATCH 04/25] Finish the basic formating/importing of data. --- bikeshed/Spec.py | 6 +- bikeshed/config/__init__.py | 15 -- .../readonly/boilerplate/statuses.kdl | 2 +- bikeshed/status/GroupStatusManager.py | 140 ++++++++++++------ 4 files changed, 100 insertions(+), 63 deletions(-) diff --git a/bikeshed/Spec.py b/bikeshed/Spec.py index 895ffca5ec..94cf416ff9 100644 --- a/bikeshed/Spec.py +++ b/bikeshed/Spec.py @@ -109,7 +109,7 @@ def initializeState(self) -> bool: self.widl: widlparser.Parser = idl.getParser() self.languages: dict[str, language.Language] = fetchLanguages(self.dataFile) - self.statuses = status.GroupStatusManager.fromKDLStr(self.dataFile.fetch("statuses.kdl", str=True)) + self.statuses: status.GroupStatusManager = fetchGroupsStatuses(self.dataFile) self.extraJC = stylescript.JCManager() self.extraJC.addColors() @@ -556,6 +556,10 @@ def fetchLanguages(dataFile: retrieve.DataFileRequester) -> dict[str, language.L } +def fetchGroupsStatuses(dataFile: retrieve.DataFileRequester) -> status.GroupStatusManager: + return status.GroupStatusManager.fromKdlStr(self.dataFile.fetch("statuses.kdl", str=True)) + + def addDomintroStyles(doc: Spec) -> None: # Adds common WHATWG styles for domintro blocks. diff --git a/bikeshed/config/__init__.py b/bikeshed/config/__init__.py index 104068b50d..f2edffd81a 100644 --- a/bikeshed/config/__init__.py +++ b/bikeshed/config/__init__.py @@ -37,21 +37,6 @@ ) from .status import ( canonicalizeStatus, - datedStatuses, - deadlineStatuses, - implementationStatuses, looselyMatch, - megaGroups, - noEDStatuses, - shortToLongStatus, - snapshotStatuses, splitStatus, - unlevelledStatuses, - w3cCgs, - w3cCommunityStatuses, - w3cIgs, - w3cIGStatuses, - w3cProcessDocumentStatuses, - w3cTAGStatuses, - w3cWGStatuses, ) diff --git a/bikeshed/spec-data/readonly/boilerplate/statuses.kdl b/bikeshed/spec-data/readonly/boilerplate/statuses.kdl index 74fbaec7b6..8497b96a9b 100644 --- a/bikeshed/spec-data/readonly/boilerplate/statuses.kdl +++ b/bikeshed/spec-data/readonly/boilerplate/statuses.kdl @@ -165,7 +165,7 @@ standards-body "w3c" { group "w3t" type=null /* - Every status "in" this group requires a `group-types` child, + Every status in w3c needs a `group-types` child, with attributes that are any of "ig" "wg" "cg" and/or "tag" */ status "ED" "Editor's Draft" { diff --git a/bikeshed/status/GroupStatusManager.py b/bikeshed/status/GroupStatusManager.py index 782f4d4742..050bf73ec5 100644 --- a/bikeshed/status/GroupStatusManager.py +++ b/bikeshed/status/GroupStatusManager.py @@ -1,71 +1,119 @@ from __future__ import annotations import dataclasses + import kdl -from .. import t, messages as m +from .. import messages as m +from .. import t + @dataclasses.dataclass class GroupStatusManager: - genericStatuses: dict[str, Status] = dataclasses.field(default_factory=dict) - megaGroups: dict[str, MegaGroup] = dataclasses.field(default_factory=dict) + genericStatuses: dict[str, Status] = dataclasses.field(default_factory=dict) + standardsBodies: dict[str, StandardsBody] = dataclasses.field(default_factory=dict) + + @staticmethod + def fromKdlStr(data: str) -> GroupStatusManager: + self = GroupStatusManager() + kdlDoc = kdl.parse(data) - @classmethod - def fromKDLStr(cls, data: str) -> t.Self: - self = cls() - kdlDoc = kdl.parse(data) + for node in kdlDoc.getAll("status"): + status = Status.fromKdlNode(node) + self.genericStatuses[status.shortName] = status - for node in kdlDoc.nodes: - if node.name.lower() == "status": - status = Status.fromKdlNode(node) - genericStatuses[status.name] = status - elif node.name.lower() == "megagroup": - mg = MegaGroup.fromKDLNode(node) - megaGroups[mg.name] = mg - else: - m.die(f"Unknown node type '{node.name}' in the group/status KDL file.") - return self - return self + for node in kdlDoc.getAll("standards-body"): + sb = StandardsBody.fromKdlNode(node) + self.standardsBodies[sb.name] = sb + return self @dataclasses.dataclass -class MegaGroup: - name: str - groups: dict[str, Group] = dataclasses.field(default_factory=dict) - statuses: dict[str, Status] = dataclasses.field(default_factory=dict) - - @classmethod - def fromKDLNode(cls, node: kdl.Node) -> t.Self: - self = cls(node.args[0]) - for child in node.nodes: - if child.name.lower() == "group": - g = Group.fromKDLNode(child) - self.groups[g.name] = g - elif child.name.lower() == "status": - s = Status.fromKDLNode(child) - self.statuses[s.name] = s - else: - m.die(f"Unknown node type '{child.name}' in megagroup '{self.name}'.") - continue - return self +class StandardsBody: + 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: + name = t.cast(str, node.args[0]) + self = StandardsBody(name) + for child in node.getAll("group"): + g = Group.fromKdlNode(child, sbName=self.name) + self.groups[g.name] = g + for child in node.getAll("status"): + s = Status.fromKdlNode(child, sbName=self.name) + self.statuses[s.shortName] = s + return self @dataclasses.dataclass class Group: - name: str - privSec: bool + name: str + privSec: bool + sbName: str | None = None + + @staticmethod + def fromKdlNode(node: kdl.Node, sbName: str | None = None) -> Group: + if sbName == "w3c": + return GroupW3C.fromKdlNode(node, sbName) + name = t.cast(str, node.args[0]) + privSec = node.get("priv-sec") is not None + return Group(name, privSec, sbName) + @dataclasses.dataclass -class GroupW3C: - type: str +class GroupW3C(Group): + type: str | None = None + + @staticmethod + def fromKdlNode(node: kdl.Node, sbName: str | None = None) -> 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, sbName, groupType) + @dataclasses.dataclass class Status: - name: str - longName: str - requires: list[str] = dataclasses.field(default_factory=list) + shortName: str + longName: str + sbName: str | None = None + requires: list[str] = dataclasses.field(default_factory=list) + + def fullShortname(self) -> str: + if self.sbName is None: + return self.shortName + else: + return self.sbName + "/" + self.shortName + + @staticmethod + def fromKdlNode(node: kdl.Node, sbName: str | None = None) -> Status: + if sbName == "w3c": + return StatusW3C.fromKdlNode(node, sbName) + shortName = t.cast(str, node.args[0]) + longName = t.cast(str, node.args[1]) + self = Status(shortName, longName, sbName) + requiresNode = node.get("requires") + if requiresNode: + self.requires = t.cast("list[str]", list(node.getArgs((..., str)))) + return self + @dataclasses.dataclass -class StatusW3C: - groupTypes: list[str] = dataclasses.field(default_factory=list) \ No newline at end of file +class StatusW3C(Status): + groupTypes: list[str] = dataclasses.field(default_factory=list) + + @staticmethod + def fromKdlNode(node: kdl.Node, sbName: str | None = None) -> StatusW3C: + shortName = t.cast(str, node.args[0]) + longName = t.cast(str, node.args[1]) + self = StatusW3C(shortName, longName, sbName) + requiresNode = node.get("requires") + if requiresNode: + self.requires = t.cast("list[str]", list(node.getArgs((..., str)))) + groupTypesNode = node.get("requires") + if groupTypesNode: + self.requires = t.cast("list[str]", list(node.getArgs((..., str)))) + return self From 75fe9154e255e64cb1f44fa009c14af6d03d9591 Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Tue, 13 Aug 2024 09:51:36 -0700 Subject: [PATCH 05/25] wip --- bikeshed/status/GroupStatusManager.py | 63 +++++++--- bikeshed/status/utils.py | 169 ++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 bikeshed/status/utils.py diff --git a/bikeshed/status/GroupStatusManager.py b/bikeshed/status/GroupStatusManager.py index 050bf73ec5..029d98bf0a 100644 --- a/bikeshed/status/GroupStatusManager.py +++ b/bikeshed/status/GroupStatusManager.py @@ -6,6 +6,7 @@ from .. import messages as m from .. import t +from . import utils @dataclasses.dataclass @@ -28,6 +29,36 @@ def fromKdlStr(data: str) -> GroupStatusManager: return self + def getStatuses(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]) + 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: + return self.genericStatuses.get(statusName) + elif sbName in self.standardsBodies: + return self.standardsBodies[sbName].statuses.get(statusName) + 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) + + + @dataclasses.dataclass class StandardsBody: @@ -40,10 +71,10 @@ def fromKdlNode(node: kdl.Node) -> StandardsBody: name = t.cast(str, node.args[0]) self = StandardsBody(name) for child in node.getAll("group"): - g = Group.fromKdlNode(child, sbName=self.name) + g = Group.fromKdlNode(child, sb=self) self.groups[g.name] = g for child in node.getAll("status"): - s = Status.fromKdlNode(child, sbName=self.name) + s = Status.fromKdlNode(child, sb=self) self.statuses[s.shortName] = s return self @@ -52,15 +83,15 @@ def fromKdlNode(node: kdl.Node) -> StandardsBody: class Group: name: str privSec: bool - sbName: str | None = None + sb: StandardsBody | None = None @staticmethod - def fromKdlNode(node: kdl.Node, sbName: str | None = None) -> Group: - if sbName == "w3c": - return GroupW3C.fromKdlNode(node, sbName) + def fromKdlNode(node: kdl.Node, sb: StandardsBody | None = None) -> Group: + if sb.name == "w3c": + return GroupW3C.fromKdlNode(node, sb) name = t.cast(str, node.args[0]) privSec = node.get("priv-sec") is not None - return Group(name, privSec, sbName) + return Group(name, privSec, sb) @dataclasses.dataclass @@ -68,33 +99,33 @@ class GroupW3C(Group): type: str | None = None @staticmethod - def fromKdlNode(node: kdl.Node, sbName: str | None = None) -> GroupW3C: + def fromKdlNode(node: kdl.Node, sb: StandardsBody | None = None) -> 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, sbName, groupType) + return GroupW3C(name, privSec, sb, groupType) @dataclasses.dataclass class Status: shortName: str longName: str - sbName: str | None = None + sb: StandardsBody | None = None requires: list[str] = dataclasses.field(default_factory=list) def fullShortname(self) -> str: - if self.sbName is None: + if self.sb.name is None: return self.shortName else: - return self.sbName + "/" + self.shortName + return self.sb.name + "/" + self.shortName @staticmethod - def fromKdlNode(node: kdl.Node, sbName: str | None = None) -> Status: - if sbName == "w3c": - return StatusW3C.fromKdlNode(node, sbName) + def fromKdlNode(node: kdl.Node, sb: StandardsBody | None = None) -> Status: + if sb.name == "w3c": + return StatusW3C.fromKdlNode(node, sb) shortName = t.cast(str, node.args[0]) longName = t.cast(str, node.args[1]) - self = Status(shortName, longName, sbName) + self = Status(shortName, longName, sb) 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 new file mode 100644 index 0000000000..bc472db973 --- /dev/null +++ b/bikeshed/status/utils.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +from .. import t, 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: ... + + +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 + + sbName: str|None + statusName: str + if "/" in rawStatus: + sbName, _, statusName = rawStatus.partition("/") + sbName = sbName.lower() + else: + sbName = None + statusName = rawStatus + statusName = statusName.upper() + + status = manager.getStatus(sbName, statusName) + + if groupName is not None: + group = manager.getGroup(groupName.lower()) + else: + group = None + + if group and status: + # If using a standards-body 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) + + if status: + return status + + # 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: + 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", + ) + 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." + else: + msg = f"Unknown Status metadata '{canonStatus}'. Check the docs for valid Status values." + m.die(msg) + return canonStatus + + +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 formatStatusSet(statuses: frozenset[str]) -> str: + return ", ".join(sorted({status.split("/")[-1] for status in statuses})) + + 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 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: + m.warn( + f"You used Status: {rawStatus}, but W3C Community and Business Groups are limited to these statuses: {formatStatusSet(w3cCommunityStatuses)}.", + ) + +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 From 92e5a1fc1a6ffe8a6c44583f202f2e399db273c5 Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Thu, 15 Aug 2024 18:44:43 -0700 Subject: [PATCH 06/25] 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 From 68332b821f32e22fb93020ffbdb165f817701651 Mon Sep 17 00:00:00 2001 From: Tab Atkins-Bittner Date: Fri, 16 Aug 2024 16:16:35 -0700 Subject: [PATCH 07/25] Finish updating all usage sites of the old status code. --- bikeshed/Spec.py | 12 +- bikeshed/boilerplate.py | 4 +- bikeshed/conditional.py | 7 +- bikeshed/config/__init__.py | 5 - bikeshed/config/status.py | 178 ------------------ bikeshed/doctypes/__init__.py | 2 + bikeshed/{status => doctypes}/manager.py | 57 ++++-- bikeshed/{status => doctypes}/utils.py | 45 ++--- bikeshed/headings.py | 2 +- bikeshed/metadata.py | 107 +++++------ bikeshed/refs/manager.py | 9 +- bikeshed/retrieve.py | 57 +++--- .../{statuses.kdl => doctypes.kdl} | 103 ++++------ bikeshed/status/__init__.py | 2 - 14 files changed, 198 insertions(+), 392 deletions(-) delete mode 100644 bikeshed/config/status.py create mode 100644 bikeshed/doctypes/__init__.py rename bikeshed/{status => doctypes}/manager.py (75%) rename bikeshed/{status => doctypes}/utils.py (89%) rename bikeshed/spec-data/readonly/boilerplate/{statuses.kdl => doctypes.kdl} (84%) delete mode 100644 bikeshed/status/__init__.py diff --git a/bikeshed/Spec.py b/bikeshed/Spec.py index 94cf416ff9..7f1b8ab853 100644 --- a/bikeshed/Spec.py +++ b/bikeshed/Spec.py @@ -18,6 +18,7 @@ constants, datablocks, dfns, + doctypes, extensions, fingerprinting, h, @@ -34,7 +35,6 @@ refs, retrieve, shorthands, - status, stylescript, t, wpt, @@ -109,7 +109,7 @@ def initializeState(self) -> bool: self.widl: widlparser.Parser = idl.getParser() self.languages: dict[str, language.Language] = fetchLanguages(self.dataFile) - self.statuses: status.GroupStatusManager = fetchGroupsStatuses(self.dataFile) + self.doctypes: doctypes.DoctypeManager = fetchDoctypes(self.dataFile) self.extraJC = stylescript.JCManager() self.extraJC.addColors() @@ -148,7 +148,7 @@ def initMetadata(self, inputContent: InputSource.InputContent) -> None: # Using that to determine the Group and Status, load the correct defaults.include boilerplate self.mdDefaults = metadata.fromJson( - data=retrieve.retrieveBoilerplateFile(self, "defaults", error=True), + data=retrieve.retrieveBoilerplateFile(self, "defaults"), source="defaults", ) self.md = metadata.join(self.mdBaseline, self.mdDefaults, self.mdDocument, self.mdCommandLine) @@ -157,7 +157,7 @@ def initMetadata(self, inputContent: InputSource.InputContent) -> None: self.md.fillTextMacros(self.macros, doc=self) jsonEscapedMacros = {k: json.dumps(v)[1:-1] for k, v in self.macros.items()} computedMdText = h.replaceMacrosTextly( - retrieve.retrieveBoilerplateFile(self, "computed-metadata", error=True), + retrieve.retrieveBoilerplateFile(self, "computed-metadata"), macros=jsonEscapedMacros, context="? of computed-metadata.include", ) @@ -556,8 +556,8 @@ def fetchLanguages(dataFile: retrieve.DataFileRequester) -> dict[str, language.L } -def fetchGroupsStatuses(dataFile: retrieve.DataFileRequester) -> status.GroupStatusManager: - return status.GroupStatusManager.fromKdlStr(self.dataFile.fetch("statuses.kdl", str=True)) +def fetchDoctypes(dataFile: retrieve.DataFileRequester) -> doctypes.DoctypeManager: + return doctypes.DoctypeManager.fromKdlStr(dataFile.fetch("doctypes.kdl", str=True)) def addDomintroStyles(doc: Spec) -> None: diff --git a/bikeshed/boilerplate.py b/bikeshed/boilerplate.py index 8f4cb7ad4a..d22157bb31 100644 --- a/bikeshed/boilerplate.py +++ b/bikeshed/boilerplate.py @@ -240,7 +240,7 @@ def removeUnwantedBoilerplate(doc: t.SpecT) -> None: def w3cStylesheetInUse(doc: t.SpecT) -> bool: - return doc.md.prepTR or doc.md.status in config.snapshotStatuses + return doc.md.prepTR or (doc.md.group is not None and doc.md.group.name == "w3c") def addBikeshedBoilerplate(doc: t.SpecT) -> None: @@ -933,7 +933,7 @@ def printPreviousVersion(v: dict[str, str]) -> t.ElementT | None: md.setdefault("This version", []).append(h.E.a({"href": mac["version"], "class": "u-url"}, mac["version"])) if doc.md.TR: md.setdefault("Latest published version", []).append(h.E.a({"href": doc.md.TR}, doc.md.TR)) - if doc.md.ED and doc.md.status in config.snapshotStatuses: + if doc.md.ED and (doc.md.status and "TR" in doc.md.status.requires): md.setdefault("Editor's Draft", []).append(h.E.a({"href": doc.md.ED}, doc.md.ED)) if doc.md.previousVersions: md["Previous Versions"] = [printPreviousVersion(ver) for ver in doc.md.previousVersions] diff --git a/bikeshed/conditional.py b/bikeshed/conditional.py index de150f01d0..9ae9e827d1 100644 --- a/bikeshed/conditional.py +++ b/bikeshed/conditional.py @@ -3,7 +3,7 @@ import dataclasses import re -from . import config, h, t +from . import h, t from . import messages as m # Any element can have an include-if or exclude-if attribute, @@ -61,7 +61,10 @@ def processConditionals(doc: t.SpecT, container: t.ElementT | None = None) -> No def evalConditions(doc: t.SpecT, el: t.ElementT, conditionString: str) -> t.Generator[bool, None, None]: for cond in parseConditions(conditionString, el): if cond.type == "status": - yield config.looselyMatch(cond.value, doc.md.status) + if doc.md.status: + yield doc.md.status.looselyMatch(cond.value) + else: + yield False elif cond.type == "text macro": for k in doc.macros: if k.upper() == cond.value: diff --git a/bikeshed/config/__init__.py b/bikeshed/config/__init__.py index f2edffd81a..78231cf841 100644 --- a/bikeshed/config/__init__.py +++ b/bikeshed/config/__init__.py @@ -35,8 +35,3 @@ simplifyText, splitForValues, ) -from .status import ( - canonicalizeStatus, - looselyMatch, - splitStatus, -) diff --git a/bikeshed/config/status.py b/bikeshed/config/status.py deleted file mode 100644 index 5262861d4d..0000000000 --- a/bikeshed/config/status.py +++ /dev/null @@ -1,178 +0,0 @@ -from __future__ import annotations - -from .. import messages as m -from .. import t -from . import main - - -@t.overload -def canonicalizeStatus(rawStatus: None, group: str | None) -> None: ... - - -@t.overload -def canonicalizeStatus(rawStatus: str, group: str | None) -> str: ... - - -def canonicalizeStatus(rawStatus: str | None, group: str | None) -> str | None: - if rawStatus is None: - return None - - 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 formatStatusSet(statuses: frozenset[str]) -> str: - return ", ".join(sorted({status.split("/")[-1] for status in statuses})) - - 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 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: - m.warn( - f"You used Status: {rawStatus}, but W3C Community and Business Groups are limited to these statuses: {formatStatusSet(w3cCommunityStatuses)}.", - ) - - 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 - - # Canonicalize the rawStatus that was passed in, into a known form. - # Might be foo/BAR, or just BAR. - megaGroup, _, status = rawStatus.partition("/") - if status == "": - status = megaGroup - megaGroup = "" - megaGroup = megaGroup.lower() - status = status.upper() - if megaGroup: - canonStatus = megaGroup + "/" + status - else: - canonStatus = status - - if group is not None: - group = group.lower() - - if group in megaGroups["w3c"]: - validateW3Cstatus(group, canonStatus, rawStatus) - - # Using a directly-recognized status is A-OK. - # (Either one of the unrestricted statuses, - # or one of the restricted statuses with the correct standards-org prefix.) - if canonStatus in shortToLongStatus: - return canonStatus - - possibleMgs = megaGroupsForStatus(status) - - # 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: - if megaGroup not in megaGroups: - msg = f"Status metadata specified an unrecognized '{megaGroup}' organization." - else: - 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", - ) - 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." - else: - msg = f"Unknown Status metadata '{canonStatus}'. Check the docs for valid Status values." - m.die(msg) - return canonStatus - - -@t.overload -def splitStatus(st: None) -> tuple[None, None]: ... - - -@t.overload -def splitStatus(st: str) -> tuple[str | None, str]: ... - - -def splitStatus(st: str | None) -> tuple[str | None, str | None]: - if st is None: - return None, None - - parts = st.partition("/") - if parts[2] == "": - return None, parts[0] - - return parts[0], parts[2] - - -def looselyMatch(s1: str | None, s2: str | None) -> bool: - # Loosely matches two statuses: - # they must have the same status name, - # and either the same or missing group name - group1, status1 = splitStatus(s1) - group2, status2 = splitStatus(s2) - if status1 != status2: - return False - if group1 == group2 or group1 is None or group2 is None: - return True - return False diff --git a/bikeshed/doctypes/__init__.py b/bikeshed/doctypes/__init__.py new file mode 100644 index 0000000000..f3e73d2795 --- /dev/null +++ b/bikeshed/doctypes/__init__.py @@ -0,0 +1,2 @@ +from .manager import DoctypeManager, Group, GroupW3C, Org, Status, StatusW3C +from .utils import canonicalizeOrgStatusGroup diff --git a/bikeshed/status/manager.py b/bikeshed/doctypes/manager.py similarity index 75% rename from bikeshed/status/manager.py rename to bikeshed/doctypes/manager.py index 401a669d91..6082df01fe 100644 --- a/bikeshed/status/manager.py +++ b/bikeshed/doctypes/manager.py @@ -4,24 +4,23 @@ import kdl -from .. import messages as m from .. import t from . import utils @dataclasses.dataclass -class GroupStatusManager: +class DoctypeManager: genericStatuses: dict[str, Status] = dataclasses.field(default_factory=dict) orgs: dict[str, Org] = dataclasses.field(default_factory=dict) @staticmethod - def fromKdlStr(data: str) -> GroupStatusManager: - self = GroupStatusManager() + def fromKdlStr(data: str) -> DoctypeManager: + self = DoctypeManager() kdlDoc = kdl.parse(data) for node in kdlDoc.getAll("status"): status = Status.fromKdlNode(node) - self.genericStatuses[status.shortName] = status + self.genericStatuses[status.name] = status for node in kdlDoc.getAll("org"): org = Org.fromKdlNode(node) @@ -91,7 +90,7 @@ def fromKdlNode(node: kdl.Node) -> Org: self.groups[g.name] = g for child in node.getAll("status"): s = Status.fromKdlNode(child, org=self) - self.statuses[s.shortName] = s + self.statuses[s.name] = s return self @@ -100,14 +99,22 @@ class Group: name: str privSec: bool org: Org + requires: list[str] = dataclasses.field(default_factory=list) + + def fullName(self) -> str: + return self.org.name + "/" + self.name @staticmethod 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, org) + privSec = t.cast(bool, node.props.get("priv-sec", False)) + requiresNode = node.get("requires") + self = Group(name, privSec, org) + if requiresNode: + self.requires = t.cast("list[str]", list(node.getArgs((..., str)))) + return self @dataclasses.dataclass @@ -117,31 +124,45 @@ class GroupW3C(Group): @staticmethod def fromKdlNode(node: kdl.Node, org: Org) -> GroupW3C: name = t.cast(str, node.args[0]) - privSec = node.get("priv-sec") is not None + privSec = t.cast(bool, node.props.get("priv-sec", False)) groupType = t.cast("str|None", node.props["type"]) - return GroupW3C(name, privSec, org, groupType) + return GroupW3C(name, privSec, org, [], groupType) @dataclasses.dataclass class Status: - shortName: str + name: str longName: str org: Org | None = None requires: list[str] = dataclasses.field(default_factory=list) - def fullShortname(self) -> str: + def fullName(self) -> str: if self.org is None: - return self.shortName + return self.name else: - return self.org.name + "/" + self.shortName + return self.org.name + "/" + self.name + + def orgName(self) -> str | None: + if self.org: + return self.org.name + else: + return None + + def looselyMatch(self, rawStatus: str) -> bool: + orgName, statusName = utils.splitOrg(rawStatus) + if statusName and self.name.upper() != statusName.upper(): + return False + if orgName and self.org and self.orgName() != orgName.lower(): + return False + return True @staticmethod 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]) + name = t.cast(str, node.args[0]) longName = t.cast(str, node.args[1]) - self = Status(shortName, longName, org) + self = Status(name, longName, org) requiresNode = node.get("requires") if requiresNode: self.requires = t.cast("list[str]", list(node.getArgs((..., str)))) @@ -154,9 +175,9 @@ class StatusW3C(Status): @staticmethod def fromKdlNode(node: kdl.Node, org: Org | None = None) -> StatusW3C: - shortName = t.cast(str, node.args[0]) + name = t.cast(str, node.args[0]) longName = t.cast(str, node.args[1]) - self = StatusW3C(shortName, longName, org) + self = StatusW3C(name, 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/doctypes/utils.py similarity index 89% rename from bikeshed/status/utils.py rename to bikeshed/doctypes/utils.py index 7d85ec1fc7..ab59088154 100644 --- a/bikeshed/status/utils.py +++ b/bikeshed/doctypes/utils.py @@ -4,11 +4,11 @@ from .. import messages as m if t.TYPE_CHECKING: - from . import Group, GroupStatusManager, GroupW3C, Org, Status, StatusW3C + from . import DoctypeManager, Group, GroupW3C, Org, Status, StatusW3C def canonicalizeOrgStatusGroup( - manager: GroupStatusManager, + manager: DoctypeManager, rawOrg: str | None, rawStatus: str | None, rawGroup: str | None, @@ -19,24 +19,10 @@ def canonicalizeOrgStatusGroup( # 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 + orgFromStatus, statusName = splitOrg(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 + orgFromGroup, groupName = splitOrg(rawGroup) groupName = groupName.lower() if groupName is not None else None orgName = reconcileOrgs(rawOrg, orgFromStatus, orgFromGroup) @@ -157,6 +143,17 @@ def canonicalizeOrgStatusGroup( return (org, status, group) +def splitOrg(st: str | None) -> tuple[str | None, str | None]: + if st is None: + return None, None + + if "/" in st: + parts = st.partition("/") + return parts[0].strip().lower(), parts[2].strip() + else: + return None, st.strip() + + 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, @@ -193,9 +190,13 @@ def reconcileOrgs(fromRaw: str | None, fromStatus: str | None, fromGroup: str | def validateW3CStatus(group: Group, status: Status) -> None: assert isinstance(group, GroupW3C) assert isinstance(status, StatusW3C) - if status.org is None and status.shortName == "DREAM": + if status.org is None and status.name == "DREAM": m.warn("You used Status:DREAM for a W3C document. Consider Status:UD instead.") - return + + if status.name in ("IG-NOTE", "WG-NOTE"): + m.die( + f"Under Process2021, {status.name} is no longer a valid status. Use NOTE (or one of its variants NOTE-ED, NOTE-FPWD, NOTE-WD) instead.", + ) if group.type is not None and group.type not in status.groupTypes: if group.type == "ig": @@ -207,9 +208,9 @@ def validateW3CStatus(group: Group, status: Status) -> None: 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 + x.name for x in t.cast("list[StatusW3C]", group.org.statuses.values()) if group.type in x.groupTypes ) m.warn( - f"You used Status:{status.shortName}, but {longTypeName} are limited to these statuses: {allowedStatuses}.", + f"You used Status:{status.name}, but {longTypeName} are limited to these statuses: {allowedStatuses}.", ) return diff --git a/bikeshed/headings.py b/bikeshed/headings.py index e9375c98c2..4424c23dcb 100644 --- a/bikeshed/headings.py +++ b/bikeshed/headings.py @@ -22,7 +22,7 @@ def processHeadings(doc: t.SpecT, scope: str = "doc") -> None: addHeadingBonuses(headings) for el in headings: h.addClass(doc, el, "settled") - if scope == "all" and doc.md.group in config.megaGroups["priv-sec"]: + if scope == "all" and doc.md.group and doc.md.group.privSec: checkPrivacySecurityHeadings(h.findAll(".heading", doc)) diff --git a/bikeshed/metadata.py b/bikeshed/metadata.py index 2e5dc49d17..49612a9ae0 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, status, t +from . import config, constants, datablocks, doctypes, h, markdown, repository, 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: self.Status | None = None + self.status: doctypes.Status | None = None self.rawStatus: str | None = None # optional metadata @@ -80,7 +80,7 @@ def __init__(self) -> None: self.externalInfotrees: config.BoolSet = config.BoolSet(default=False) self.favicon: str | None = None self.forceCrossorigin: bool = False - self.group: status.Group | None = None + self.group: doctypes.Group | None = None self.rawGroup: str | None = None self.h1: str | None = None self.ignoreCanIUseUrlFailure: list[str] = [] @@ -115,7 +115,7 @@ 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.org: doctypes.Org | None = None self.rawOrg: str | None = None self.prepTR: bool = False self.previousEditors: list[dict[str, str | None]] = [] @@ -163,11 +163,11 @@ def addData(self, key: str, val: str, lineNum: str | int | None = None) -> Metad if key not in ("ED", "TR", "URL"): key = key.title() - if key not in knownKeys: + if key not in KNOWN_KEYS: m.die(f'Unknown metadata key "{key}". Prefix custom keys with "!".', lineNum=lineNum) return self - md = knownKeys[key] + md = KNOWN_KEYS[key] try: parsedVal = md.parse(key, val, lineNum=lineNum) except Exception as e: @@ -179,7 +179,7 @@ def addData(self, key: str, val: str, lineNum: str | int | None = None) -> Metad return self def addParsedData(self, key: str, val: t.Any) -> MetadataManager: - md = knownKeys[key] + md = KNOWN_KEYS[key] result = md.join(getattr(self, md.attrName), val) setattr(self, md.attrName, result) self.manuallySetKeys.add(key) @@ -199,8 +199,8 @@ def computeImplicitMetadata(self, doc: t.SpecT) -> None: ): self.issues.append(("GitHub", self.repository.formatIssueUrl())) - self.org, self.status, self.group = status.canonicalizeOrgStatusGroup( - doc.statuses, + self.org, self.status, self.group = doctypes.canonicalizeOrgStatusGroup( + doc.doctypes, self.rawOrg, self.rawStatus, self.rawGroup, @@ -229,57 +229,38 @@ def validate(self) -> bool: m.die("The document requires at least one