mkdocstrings-github 0.4.2__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocstrings-github
3
- Version: 0.4.2
3
+ Version: 0.6.1
4
4
  Summary: A GitHub Action handler for mkdocstrings
5
5
  Author-email: Mark Hu <watermarkhu@gmail.com>
6
6
  License: MIT
@@ -21,9 +21,9 @@ Classifier: Topic :: Software Development :: Documentation
21
21
  Classifier: Topic :: Utilities
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: <3.15,>=3.10
24
- Requires-Dist: gitpython~=3.1.45
25
- Requires-Dist: mkdocstrings~=0.29
26
- Requires-Dist: packaging~=25.0
24
+ Requires-Dist: gitpython<4,>=3.1.45
25
+ Requires-Dist: mkdocstrings<1,>=0.29
26
+ Requires-Dist: ruamel-yaml<1,>=0.18.16
27
27
  Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
28
28
  Description-Content-Type: text/markdown
29
29
 
@@ -33,14 +33,26 @@ Description-Content-Type: text/markdown
33
33
 
34
34
  <p align="center">A GitHub Actions handler for <a href="https://github.com/mkdocstrings/mkdocstrings"><i>mkdocstrings</i></a>.</p>
35
35
 
36
- <p align="center"><img width=300px src="logo.png"></p>
36
+ <!-- --8<-- [end:header] -->
37
+
38
+ <p align="center"><img width=300px src="docs/img/logo.png"></p>
37
39
 
38
40
  [![Qualify](https://github.com/watermarkhu/mkdocstrings-github/actions/workflows/qualify.yaml/badge.svg?branch=main)](https://github.com/watermarkhu/mkdocstrings-github/actions/workflows/qualify.yaml)
39
41
  [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://watermarkhu.nl/mkdocstrings-github)
40
42
  [![pypi version](https://img.shields.io/pypi/v/mkdocstrings-github.svg)](https://pypi.org/project/mkdocstrings-github/)
41
43
  [![codecov](https://codecov.io/github/watermarkhu/mkdocstrings-github/graph/badge.svg?token=M6XW8UeURE)](https://codecov.io/github/watermarkhu/mkdocstrings-github)
42
44
 
43
- <!-- --8<-- [end:header] -->
45
+
46
+
47
+ For example, the following page is generated from [actions/checkout](https://github.com/actions/checkout):
48
+
49
+ <picture>
50
+ <source media="(prefers-color-scheme: dark)" srcset="docs/img/example_dark.png">
51
+ <source media="(prefers-color-scheme: light)" srcset="docs/img/example_light.png">
52
+ <img alt="Fallback image description" src="docs/img/example_light.png">
53
+ </picture>
54
+
55
+
44
56
  <!-- --8<-- [start:install] -->
45
57
  You can install the GitHub handler by specifying it as a dependency:
46
58
 
@@ -49,9 +61,17 @@ You can install the GitHub handler by specifying it as a dependency:
49
61
  # adapt to your dependencies manager
50
62
  [project]
51
63
  dependencies = [
52
- "mkdocstrings-github>=0.X.Y",
64
+ "mkdocstrings-github",
53
65
  ]
54
66
  ```
67
+
68
+ after which the generated documentation can be inserted in the markdown page with:
69
+
70
+ ```md
71
+ ::: <path-to-action-or-workflow>
72
+ handler: github
73
+ ```
74
+
55
75
  <!-- --8<-- [end:install] -->
56
76
 
57
77
  <!-- --8<-- [start:footer] -->
@@ -63,5 +83,6 @@ dependencies = [
63
83
  - 🧩 **Individual Parameter Hyperlinks**: Each action or workflow parameter—including inputs, outputs, and secrets—receives a unique HTML id, facilitating direct linking to specific parameter documentation.
64
84
  - 🔒 **Automated Permission Aggregation**: For reusable workflows, if permissions are specified at the job level rather than the workflow level, the required final permissions are automatically determined and displayed in the signature.
65
85
  - 🔗 **Parameter cross-linking**: Link to other parameters of the action or workflow via a simple Markdown syntax.
86
+ - 🧑‍🤝‍🧑 **Parameter grouping**: Organize related inputs, outputs, and secrets into visual groups using inline YAML comments for clearer documentation structure.
66
87
 
67
88
  <!-- --8<-- [end:footer] -->
@@ -0,0 +1,18 @@
1
+ mkdocstrings_handlers/github/__init__.py,sha256=0WdFUIq4Xu2ZFtlZNIYCQSoqcx3Ot9Wv41_X_dwbFww,248
2
+ mkdocstrings_handlers/github/config.py,sha256=r7efiI-vKbVEeD6utrc55h4RP6VIlabqDebhiIIx_ZA,7120
3
+ mkdocstrings_handlers/github/handler.py,sha256=SQcd08VA3g4f3Fof2mam85ahPLiK99TteAJJpUg-iB4,8489
4
+ mkdocstrings_handlers/github/objects.py,sha256=v1GchB9fzqasnXbVEOXoDzOR2iVTwcfPQ9mFT4sdjgs,7625
5
+ mkdocstrings_handlers/github/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ mkdocstrings_handlers/github/rendering.py,sha256=pFk621Fqp_R6ZS2dJ0zwEEez0Urqpekial6kqKYDag8,3371
7
+ mkdocstrings_handlers/github/templates/material/_macros.html.jinja,sha256=1TNUgoOIxm-a1S7eiESrW1fYuzXOjrwFqXQSyA4tkkU,1576
8
+ mkdocstrings_handlers/github/templates/material/action.html.jinja,sha256=C7S8I9bjnrEUahyDB7CkFxtakFrr53KE3t3uyczBPzc,2864
9
+ mkdocstrings_handlers/github/templates/material/heading.html.jinja,sha256=wnvZpNED8Dhb935qnddeDExXN-MIUz8frRRfgq-A9VA,1396
10
+ mkdocstrings_handlers/github/templates/material/inputs.html.jinja,sha256=UE8RmfMlF6np7CP-920CwgkbXIcKNgl9KgK1BJ588GM,3271
11
+ mkdocstrings_handlers/github/templates/material/outputs.html.jinja,sha256=Z9QD1KSALLDS9VEyugWG1BHpLCHTKYEx4TEQkKIoC3M,2693
12
+ mkdocstrings_handlers/github/templates/material/secrets.html.jinja,sha256=jQ_HaG2ivbZTH74pyV4BAFGQu5Vn03kA_vaEbTSABds,2839
13
+ mkdocstrings_handlers/github/templates/material/style.css,sha256=Nfmds-xHtPJ_IzOv5svA7ih5talHDTiQryN_n0DGdZs,1553
14
+ mkdocstrings_handlers/github/templates/material/workflow.html.jinja,sha256=5dLdHRSQyulyFAVCVZAR_pkw-WXxCtM20cj6RS7IH1Q,4206
15
+ mkdocstrings_github-0.6.1.dist-info/METADATA,sha256=fdZ69Xer6TQ4ZpkPnzyv1P2H2eE5KLi27DltQAJWNqA,4055
16
+ mkdocstrings_github-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
+ mkdocstrings_github-0.6.1.dist-info/licenses/LICENSE,sha256=5enZtJ4zSp0Ps3jTqFQ4kadcx62BhgTaDNPrXWb3g3E,1069
18
+ mkdocstrings_github-0.6.1.dist-info/RECORD,,
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import re
6
5
  import sys
7
6
  from typing import Literal
8
7
 
@@ -92,6 +91,15 @@ class GitHubOptions(BaseModel):
92
91
  description="Whether to show the signature in the documentation.",
93
92
  )
94
93
 
94
+ signature_repository: str = Field(
95
+ default="",
96
+ description="""The GitHub repository in the format *owner/repo*.
97
+
98
+ By default, the repository is inferred from the current git repository using the default origin remote.
99
+ If it cannot be inferred, it must be set manually.
100
+ """,
101
+ )
102
+
95
103
  signature_show_secrets: bool = Field(
96
104
  default=False,
97
105
  description="Whether to show secrets in the signature.",
@@ -162,6 +170,32 @@ class GitHubOptions(BaseModel):
162
170
  """,
163
171
  )
164
172
 
173
+ parameters_groups: bool = Field(
174
+ default=True,
175
+ description="""Whether to group parameters by their group in the documentation.
176
+
177
+ This is done by adding a comment `# group: <group name>` directly after the parameter definition in the action/workflow file.
178
+ This can be done for inputs, outputs and secrets. E.g.:
179
+
180
+ ```yaml
181
+ inputs:
182
+ my_input: # group: Example Group
183
+ description: "An example input"
184
+ required: true
185
+ ```
186
+
187
+ This has no effect if there are no groups defined for any parameter.
188
+ """,
189
+ )
190
+
191
+ parameters_group_title_row: bool = Field(
192
+ default=True,
193
+ description="""Whether to add a title row for each parameter group in the documentation.
194
+ This only has an effect if [`parameters_groups`][mkdocstrings_handlers.github.config.GitHubOptions.parameters_groups] is set to `true`,
195
+ and if the [`parameters_section_style`][mkdocstrings_handlers.github.config.GitHubOptions.parameters_section_style] is set to `table`.
196
+ """,
197
+ )
198
+
165
199
  parameters_anchors: bool = Field(
166
200
  default=True,
167
201
  description="Whether to add anchors to parameters in the documentation.",
@@ -171,16 +205,6 @@ class GitHubOptions(BaseModel):
171
205
  class GitHubConfig(BaseModel):
172
206
  """Configuration options for the GitHub handler."""
173
207
 
174
- repo: str = Field(
175
- default="",
176
- description="""The GitHub repository in the format *owner/repo*.
177
-
178
- By default, the repository is inferred from the current git repository using the default origin remote.
179
- If it cannot be inferred, it must be set manually.
180
- """,
181
- pattern=re.compile(r"^[\w.-]+/[\w.-]+$"),
182
- )
183
-
184
208
  feather_icons_source: str = Field(
185
209
  default="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js",
186
210
  description="""The source URL for Feather icons.
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import re
7
- import sys
8
7
  from pathlib import Path
9
8
  from typing import TYPE_CHECKING, Any, ClassVar, Mapping
10
9
 
@@ -68,69 +67,14 @@ class GitHubHandler(BaseHandler):
68
67
  self.config = config
69
68
  self.repo = repo
70
69
  self.global_options = config.options.__dict__
71
- self.workflows: dict[Path, Workflow] = {}
72
- self.actions: dict[Path, Action] = {}
73
70
  self.major: str = ""
74
71
  self.semver: str = ""
75
72
 
76
- # Determine owner and repo name
77
- if not self.config.repo:
78
- # Try each remote to find a valid GitHub owner/repo
79
- owner = None
80
- repo_name = None
81
- for remote in self.repo.remotes:
82
- for url in remote.urls:
83
- match = re.search(
84
- r"(?P<host>[\w\.-]+)[/:](?P<owner>[^/]+)/(?P<repo>[^/.]+?)(?:\.git)?$",
85
- url,
86
- )
87
- if match:
88
- owner = match.group("owner")
89
- repo_name = match.group("repo")
90
- break
91
- if owner and repo_name:
92
- break
93
- if not (owner and repo_name):
94
- raise PluginError(
95
- f"Could not determine GitHub repository owner/name from config.repo='{self.config.repo}' or any git remote URL."
96
- )
97
- self.config.repo = f"{owner}/{repo_name}"
98
-
99
- # Only run GitHub releases code if required and not in testing
100
- not_testing = "pytest" not in sys.modules
101
- no_custom_tags = (
102
- rendering.ENV_MAJOR_TAG not in os.environ or rendering.ENV_SEMVER_TAG not in os.environ
103
- )
104
- if not_testing and no_custom_tags:
73
+ if rendering.ENV_MAJOR_TAG not in os.environ or rendering.ENV_SEMVER_TAG not in os.environ:
105
74
  self.get_releases()
106
75
 
107
- # Glob all workflow YAML files using pathlib
108
- working_tree_dir = Path(repo.working_tree_dir)
109
- workflows_dir = working_tree_dir / ".github" / "workflows"
110
- for workflow_file in list(workflows_dir.glob("*.yml")) + list(workflows_dir.glob("*.yaml")):
111
- id = str(workflow_file.relative_to(working_tree_dir))
112
- workflow = Workflow.from_file(workflow_file, id)
113
- if workflow is not None:
114
- self.workflows[workflow_file] = workflow
115
-
116
- # Glob all action.yaml and action.yml files, skipping .git folder entirely using pathlib
117
- def find_action_files(base: Path):
118
- for entry in base.iterdir():
119
- if entry.is_dir():
120
- if entry.name == ".git":
121
- continue
122
- yield from find_action_files(entry)
123
- elif entry.is_file() and entry.name in ("action.yaml", "action.yml"):
124
- yield entry
125
-
126
- for action_file in find_action_files(working_tree_dir):
127
- id = str(action_file.relative_to(working_tree_dir).parent)
128
- action = Action.from_file(action_file, id)
129
- if action is not None:
130
- self.actions[action_file] = action
131
-
132
76
  def get_releases(self) -> None:
133
- # Get all tags from the local git repository
77
+ # Get all tags from the local git repository.
134
78
  try:
135
79
  tags = [tag.name for tag in self.repo.tags]
136
80
  except Exception as e:
@@ -191,6 +135,34 @@ class GitHubHandler(BaseHandler):
191
135
  except Exception as error:
192
136
  raise PluginError(f"Invalid options: {error}") from error
193
137
 
138
+ def get_repository_name(self) -> str:
139
+ # Get repo from environment variable or git remotes.
140
+ if os.environ.get("GITHUB_ACTIONS") == "true" and (
141
+ repo := os.environ.get("GITHUB_REPOSITORY")
142
+ ):
143
+ return repo
144
+ else:
145
+ # Try each remote to find a valid GitHub owner/repo
146
+ owner = None
147
+ repo_name = None
148
+ for remote in self.repo.remotes:
149
+ for url in remote.urls:
150
+ match = re.search(
151
+ r"(?P<host>[\w\.-]+)[/:](?P<owner>[^/]+)/(?P<repo>[^/.]+?)(?:\.git)?$",
152
+ url,
153
+ )
154
+ if match:
155
+ owner = match.group("owner")
156
+ repo_name = match.group("repo")
157
+ break
158
+ if owner and repo_name:
159
+ break
160
+ if not (owner and repo_name):
161
+ raise PluginError(
162
+ "Could not determine GitHub repository owner/name from any git remote URL."
163
+ )
164
+ return f"{owner}/{repo_name}"
165
+
194
166
  def update_env(self, config: Any) -> None:
195
167
  self.env.trim_blocks = True
196
168
  self.env.lstrip_blocks = True
@@ -198,22 +170,34 @@ class GitHubHandler(BaseHandler):
198
170
  self.env.filters["format_action_signature"] = rendering.format_action_signature
199
171
  self.env.filters["order_parameters"] = rendering.order_parameters
200
172
  self.env.filters["filter_parameters"] = rendering.filter_parameters
173
+ self.env.filters["group_parameters"] = rendering.group_parameters
201
174
  self.env.filters["anchor_id"] = rendering.anchor_id
202
175
  self.env.filters["as_string"] = rendering.as_string
203
176
  self.env.globals["semver_tag"] = self.semver
204
177
  self.env.globals["major_tag"] = self.major
205
178
  self.env.globals["git_repo"] = self.repo
179
+ self.env.globals["repository_name"] = self.get_repository_name()
206
180
 
207
- def collect(self, identifier: str, options: GitHubOptions) -> Workflow | Action:
181
+ def collect(self, identifier: str, options: GitHubOptions) -> Workflow | Action | None:
208
182
  path = Path(self.repo.working_tree_dir) / identifier
209
- if path in self.workflows:
210
- return self.workflows[path]
211
- elif (action_path := path / "action.yml") in self.actions:
212
- return self.actions[action_path]
213
- elif (action_path := path / "action.yaml") in self.actions:
214
- return self.actions[action_path]
183
+
184
+ if path.suffix in (".yml", ".yaml"):
185
+ if not path.is_file():
186
+ raise CollectionError(f"Identifier '{identifier}' is not a valid workflow file.")
187
+ data = Workflow.from_file(path, id=identifier)
188
+ elif not path.is_dir():
189
+ raise CollectionError(
190
+ f"Identifier '{identifier}' is not a valid workflow file or action directory."
191
+ )
192
+ elif (action_path := path / "action.yml").is_file():
193
+ data = Action.from_file(action_path, id=identifier)
194
+ elif (action_path := path / "action.yaml").is_file():
195
+ data = Action.from_file(action_path, id=identifier)
215
196
  else:
216
- raise CollectionError(f"Identifier '{identifier}' not found as a workflow or action.")
197
+ raise CollectionError(
198
+ f"Identifier '{identifier}' is not a valid workflow file or action directory."
199
+ )
200
+ return data
217
201
 
218
202
  def render(self, data: Workflow | Action, options: GitHubOptions) -> str:
219
203
  """Render a template using provided data and configuration options.
@@ -1,10 +1,28 @@
1
- from collections import OrderedDict
1
+ import re
2
2
  from dataclasses import dataclass, field
3
3
  from enum import Enum
4
4
  from os import PathLike
5
5
  from typing import Any, Literal, Optional
6
6
 
7
- import yaml
7
+ from ruamel.yaml import YAML
8
+ from ruamel.yaml.comments import CommentedMap
9
+
10
+ yaml = YAML()
11
+
12
+
13
+ GROUP_PATTERN = r"#\s*group:\s*(.+)$"
14
+
15
+
16
+ def group_from_map(map: CommentedMap) -> str:
17
+ """Extract group string from a comment line if it matches the group pattern."""
18
+ if map.ca.comment:
19
+ for comment in map.ca.comment:
20
+ if comment is not None:
21
+ group_matches = re.finditer(GROUP_PATTERN, comment.value)
22
+ group_string = next((m.group(1).strip() for m in group_matches), "")
23
+ if group_string:
24
+ return group_string
25
+ return ""
8
26
 
9
27
 
10
28
  @dataclass
@@ -15,28 +33,15 @@ class Input:
15
33
  type: Literal["boolean", "number", "string"] = "string"
16
34
  default: bool | float | int | str | None = None
17
35
  deprecationMessage: Optional[str] = None
18
-
19
- @staticmethod
20
- def from_data(
21
- name: str,
22
- description: str = "",
23
- required: bool = False,
24
- type: Literal["boolean", "number", "string"] = "string",
25
- default: bool | float | int | str | None = None,
26
- deprecationMessage: Optional[str] = None,
27
- **kwargs,
28
- ) -> "Input":
29
- return Input(name, description, required, type, default, deprecationMessage)
36
+ group: str = ""
30
37
 
31
38
 
32
39
  @dataclass
33
40
  class Output:
34
41
  name: str
35
42
  description: str = ""
36
-
37
- @staticmethod
38
- def from_data(name: str, description: str = "", **kwargs) -> "Output":
39
- return Output(name, description)
43
+ value: str = ""
44
+ group: str = ""
40
45
 
41
46
 
42
47
  @dataclass
@@ -44,10 +49,7 @@ class Secret:
44
49
  name: str
45
50
  description: str = ""
46
51
  required: bool = False
47
-
48
- @staticmethod
49
- def from_data(name: str, description: str = "", required: bool = False, **kwargs) -> "Secret":
50
- return Secret(name, description, required)
52
+ group: str = ""
51
53
 
52
54
 
53
55
  def _get_member(d: dict, key: str, error_message: str = "", default: Any = None) -> Any:
@@ -58,36 +60,11 @@ def _get_member(d: dict, key: str, error_message: str = "", default: Any = None)
58
60
  return d[key]
59
61
 
60
62
 
61
- class _OrderedLoader(yaml.Loader):
62
- pass
63
-
64
-
65
- # Remove boolean resolver for "on", "off", "yes", "no" (and case variants)
66
- for ch in "yYnNoO":
67
- if ch in _OrderedLoader.yaml_implicit_resolvers:
68
- _OrderedLoader.yaml_implicit_resolvers[ch] = [
69
- res
70
- for res in _OrderedLoader.yaml_implicit_resolvers[ch]
71
- if res[0] != "tag:yaml.org,2002:bool"
72
- ]
73
-
74
-
75
- def _construct_mapping(loader, node):
76
- loader.flatten_mapping(node)
77
- return OrderedDict(loader.construct_pairs(node))
78
-
79
-
80
- _OrderedLoader.add_constructor(
81
- yaml.SafeLoader.DEFAULT_MAPPING_TAG,
82
- _construct_mapping,
83
- )
84
-
85
-
86
63
  def _read_file(file: PathLike) -> tuple[str, dict]:
87
64
  with open(file, "r", encoding="utf-8") as f:
88
65
  source = f.read()
89
66
  f.seek(0)
90
- data = yaml.load(f, Loader=_OrderedLoader)
67
+ data = yaml.load(f)
91
68
  return source, data
92
69
 
93
70
 
@@ -106,10 +83,6 @@ class Action:
106
83
  branding: dict = field(default_factory=dict)
107
84
  template: Literal["action.html.jinja"] = "action.html.jinja"
108
85
 
109
- @property
110
- def members(self) -> list[Input | Output]:
111
- return self.inputs + self.outputs
112
-
113
86
  @staticmethod
114
87
  def from_file(file: PathLike, id: str) -> "Action":
115
88
  source, data = _read_file(file)
@@ -125,12 +98,31 @@ class Action:
125
98
  branding=_get_member(data, "branding", default={}),
126
99
  )
127
100
  for key, value in data.get("inputs", {}).items():
128
- action.inputs.append(Input.from_data(key, **value))
101
+ action.inputs.append(Input(name=key, **value, group=group_from_map(value)))
129
102
  for key, value in data.get("outputs", {}).items():
130
- action.outputs.append(Output.from_data(key, **value))
103
+ action.outputs.append(Output(name=key, **value, group=group_from_map(value)))
131
104
  return action
132
105
 
133
106
 
107
+ # https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idpermissions
108
+ PERMISSION_SCOPES: list[str] = [
109
+ "actions",
110
+ "attestations",
111
+ "checks",
112
+ "contents",
113
+ "deployments",
114
+ "discussions",
115
+ "id-token",
116
+ "issues",
117
+ "models",
118
+ "packages",
119
+ "pages",
120
+ "pull-requests",
121
+ "security-events",
122
+ "statuses",
123
+ ]
124
+
125
+
134
126
  class PermissionLevel(Enum):
135
127
  none = ("none", 0)
136
128
  read = ("read", 1)
@@ -151,21 +143,6 @@ class PermissionLevel(Enum):
151
143
  return perm
152
144
  raise ValueError(f"No Permission with label '{label}'")
153
145
 
154
- def __le__(self, other):
155
- if isinstance(other, PermissionLevel):
156
- return self.number <= other.number
157
- return NotImplemented
158
-
159
- def __lt__(self, other):
160
- if isinstance(other, PermissionLevel):
161
- return self.number < other.number
162
- return NotImplemented
163
-
164
- def __ge__(self, other):
165
- if isinstance(other, PermissionLevel):
166
- return self.number >= other.number
167
- return NotImplemented
168
-
169
146
  def __gt__(self, other):
170
147
  if isinstance(other, PermissionLevel):
171
148
  return self.number > other.number
@@ -187,8 +164,18 @@ class Workflow:
187
164
  template: Literal["workflow.html.jinja"] = "workflow.html.jinja"
188
165
 
189
166
  @property
190
- def members(self) -> list[Input | Output | Secret]:
191
- return self.inputs + self.outputs + self.secrets
167
+ def permission_read_all(self) -> bool:
168
+ return all(
169
+ scope in self.permissions and self.permissions[scope] == PermissionLevel.read
170
+ for scope in PERMISSION_SCOPES
171
+ )
172
+
173
+ @property
174
+ def permission_write_all(self) -> bool:
175
+ return all(
176
+ scope in self.permissions and self.permissions[scope] == PermissionLevel.write
177
+ for scope in PERMISSION_SCOPES
178
+ )
192
179
 
193
180
  @staticmethod
194
181
  def from_file(file: PathLike, id: str) -> "Workflow | None":
@@ -208,18 +195,41 @@ class Workflow:
208
195
  call = data["on"]["workflow_call"]
209
196
  if call:
210
197
  for key, value in call.get("inputs", {}).items():
211
- workflow.inputs.append(Input.from_data(key, **value))
198
+ workflow.inputs.append(Input(name=key, **value, group=group_from_map(value)))
212
199
  for key, value in call.get("outputs", {}).items():
213
- workflow.outputs.append(Output.from_data(key, **value))
200
+ workflow.outputs.append(Output(name=key, **value, group=group_from_map(value)))
214
201
  for key, value in call.get("secrets", {}).items():
215
- workflow.secrets.append(Secret.from_data(key, **value))
216
- for key, label in data.get("permissions", {}).items():
217
- workflow.permissions[key] = PermissionLevel.from_label(label)
202
+ workflow.secrets.append(Secret(name=key, **value, group=group_from_map(value)))
203
+
204
+ def set_all_permissions(level: str):
205
+ if level == "read-all":
206
+ for key in PERMISSION_SCOPES:
207
+ workflow.permissions[key] = PermissionLevel.read
208
+ elif level == "write-all":
209
+ for key in PERMISSION_SCOPES:
210
+ workflow.permissions[key] = PermissionLevel.write
211
+ else:
212
+ raise ValueError(f"Unknown permission level '{level}'")
213
+
214
+ if isinstance(permissions := data.get("permissions", {}), str):
215
+ set_all_permissions(permissions)
216
+ elif isinstance(permissions, dict):
217
+ for key, label in permissions.items():
218
+ workflow.permissions[key] = PermissionLevel.from_label(label)
219
+ else:
220
+ raise ValueError("permissions must be a string or a dictionary")
218
221
  for job in data.get("jobs", {}).values():
219
- for key, label in job.get("permissions", {}).items():
220
- if key in workflow.permissions:
221
- if permission := PermissionLevel.from_label(label) > workflow.permissions[key]:
222
- workflow.permissions[key] = permission
223
- else:
224
- workflow.permissions[key] = PermissionLevel.from_label(label)
222
+ if isinstance(permissions := job.get("permissions", {}), str):
223
+ set_all_permissions(permissions)
224
+ elif isinstance(permissions, dict):
225
+ for key, label in job.get("permissions", {}).items():
226
+ if key in workflow.permissions:
227
+ permission = PermissionLevel.from_label(label)
228
+ if permission > workflow.permissions[key]:
229
+ workflow.permissions[key] = permission
230
+ else:
231
+ workflow.permissions[key] = PermissionLevel.from_label(label)
232
+ else:
233
+ raise ValueError("permissions must be a string or a dictionary")
234
+
225
235
  return workflow
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from collections import OrderedDict
4
5
  from typing import TYPE_CHECKING, Sequence
5
6
 
6
7
  from jinja2 import pass_context
@@ -23,8 +24,11 @@ def format_action_signature(context: Context, id: str, repo: str, options: GitHu
23
24
  match options.signature_version:
24
25
  case "ref":
25
26
  try:
26
- repo: Repo = context.environment.globals["git_repo"]
27
- version = repo.head.ref.name
27
+ git_repo = context.environment.globals["git_repo"]
28
+ if isinstance(git_repo, Repo):
29
+ version = git_repo.head.ref.name
30
+ else:
31
+ version = "unknown"
28
32
  except Exception:
29
33
  version = "unknown"
30
34
  case "major":
@@ -37,6 +41,22 @@ def format_action_signature(context: Context, id: str, repo: str, options: GitHu
37
41
  return f"{name}@{version}"
38
42
 
39
43
 
44
+ def group_parameters(
45
+ parameters: Sequence[Input | Output | Secret],
46
+ do_group: bool,
47
+ ) -> OrderedDict[str, list[Input | Output | Secret]]:
48
+ grouped: OrderedDict[str, list[Input | Output | Secret]] = OrderedDict()
49
+ if not do_group:
50
+ grouped[""] = list(parameters)
51
+ return grouped
52
+ for parameter in parameters:
53
+ group = getattr(parameter, "group", "")
54
+ if group not in grouped:
55
+ grouped[group] = []
56
+ grouped[group].append(parameter)
57
+ return grouped
58
+
59
+
40
60
  def order_parameters(
41
61
  parameters: Sequence[Input | Output | Secret], parameters_order: PARAMETERS_ORDER
42
62
  ):
@@ -0,0 +1,38 @@
1
+ {#- Shared macros for rendering parameters in different styles -#}
2
+
3
+ {#- Macro to render a single parameter item in list style -#}
4
+ {% macro render_parameter_item(item, item_type, options, data) %}
5
+ {% set anchor_id = item.name | anchor_id(item_type, data.id) %}
6
+ <li class="doc-section-item field-body">
7
+ <b><code>{{ item.name }}</code></b>
8
+ {%- if item.required is defined and item.required %} - <em>required</em>{% endif %}
9
+ {% if options.parameters_anchors %}
10
+ <a class="headerlink" href="#{{ anchor_id }}" title="Link to {{ item.name }}">¤</a>
11
+ {% endif %}
12
+ <div class="doc-md-description">
13
+ {{ item.description | convert_markdown(options.heading_level, data.id) }}
14
+ </div>
15
+ {% if item.default is defined and item.default is not none %}
16
+ <div class="doc-md-description">
17
+ <code>{{ item.default | as_string }}</code>
18
+ </div>
19
+ {% endif %}
20
+ </li>
21
+ {% endmacro %}
22
+
23
+ {#- Macro to render grouped parameters in list style -#}
24
+ {% macro render_grouped_list(items, item_type, options, data) %}
25
+ {% set grouped_items = items | group_parameters(options.parameters_groups) %}
26
+ {% for group, group_items in grouped_items.items() %}
27
+ {% if group %}
28
+ <li class="gh-group-item">
29
+ <em>{{ group }}:</em>
30
+ <ul>
31
+ {% for item in group_items %}{{ render_parameter_item(item, item_type, options, data) }}{% endfor %}
32
+ </ul>
33
+ </li>
34
+ {% else %}
35
+ {% for item in group_items %}{{ render_parameter_item(item, item_type, options, data) }}{% endfor %}
36
+ {% endif %}
37
+ {% endfor %}
38
+ {% endmacro %}
@@ -7,7 +7,6 @@ Context:
7
7
  config (mkdocstrings_handlers.github.config.GitHubConfig): The global configuration
8
8
  options (dict): The local options
9
9
  -#}
10
-
11
10
  {% block logs scoped %}
12
11
  {#- Logging block.
13
12
 
@@ -23,20 +22,30 @@ Context:
23
22
 
24
23
  {% include "heading.html.jinja" with context %}
25
24
 
26
- {% set signature = data.id | format_action_signature(config.repo, options) %}
25
+ {% set signature = data.id | format_action_signature(options.signature_repository if options.signature_repository else repository_name, options) %}
27
26
 
28
27
  {% block signature scoped %}
29
28
  {% if options.show_signature %}
30
29
  {% with inputs = data.inputs | order_parameters(options.parameters_order) | filter_parameters(required=True) %}
30
+ <div class="annotate">
31
31
  {% filter highlight(language="yaml", inline=False, linenums=False) %}
32
32
  - uses: {{ signature }}
33
+ {% if inputs|length > 0 %}
33
34
  with:
34
- {%+ for input in inputs %}{{ input.name }}: ''{% endfor +%}
35
+ {% for input in inputs %}
36
+ {{ input.name }}: ({{ loop.index }})
37
+ {% endfor %}
38
+ {% endif %}
35
39
  {% endfilter %}
40
+ </div>
41
+ <ol>
42
+ {% for input in inputs %}
43
+ <li>{{ input.description | convert_markdown(options.heading_level) if input.description else "(no description)" }}</li>
44
+ {% endfor %}
45
+ </ol>
36
46
  {% endwith %}
37
47
  {% endif %}
38
48
  {% endblock signature %}
39
-
40
49
  {% block description scoped %}
41
50
  {% if options.show_description %}
42
51
  {{ options.description if options.description else data.description | convert_markdown(options.heading_level, data.id) }}
@@ -7,6 +7,8 @@ Context:
7
7
  options (dict): The local options
8
8
  -#}
9
9
 
10
+ {% from "_macros.html.jinja" import render_grouped_list %}
11
+
10
12
  {% block logs scoped %}
11
13
  {{ log.debug("Rendering inputs of " + data.id) }}
12
14
  {% endblock logs %}
@@ -27,7 +29,7 @@ Context:
27
29
  <span class="doc-section-title">Inputs:</span>
28
30
  <a class="headerlink" href="#{{ header_id }}" title="Link to {{ data.name }} outputs">¤</a>
29
31
  </p>
30
- <table>
32
+ <table data-gh-parameters>
31
33
  <thead>
32
34
  <tr>
33
35
  <th>Name</th>
@@ -35,8 +37,15 @@ Context:
35
37
  {% if default_column %}<th>Default</th>{% endif %}
36
38
  </tr>
37
39
  </thead>
40
+ {% set grouped_inputs = inputs | group_parameters(options.parameters_groups) %}
41
+ {% for group, group_inputs in grouped_inputs.items() %}
38
42
  <tbody>
39
- {% for input in inputs %}
43
+ {% if options.parameters_group_title_row and grouped_inputs | length > 1 and group != "" %}
44
+ <tr class="doc-section-item">
45
+ <td class="gh-group-title" colspan="{% if default_column %}3{% else %}2{% endif %}">{{ group }}</td>
46
+ </tr>
47
+ {% endif %}
48
+ {% for input in group_inputs %}
40
49
  {% if options.parameters_anchors %}
41
50
  {% set anchor_id = input.name | anchor_id("inputs", data.id) %}
42
51
  {% else %}
@@ -66,6 +75,7 @@ Context:
66
75
  </tr>
67
76
  {% endfor %}
68
77
  </tbody>
78
+ {% endfor %}
69
79
  </table>
70
80
  {% endblock table_style %}
71
81
  {% elif options.parameters_section_style == "list" %}
@@ -75,24 +85,7 @@ Context:
75
85
  <a class="headerlink" href="#{{ header_id }}" title="Link to {{ data.name }} outputs">¤</a>
76
86
  </p>
77
87
  <ul>
78
- {% for input in inputs %}
79
- {% set anchor_id = input.name | anchor_id("inputs", data.id) %}
80
- <li class="doc-section-item field-body">
81
- <b><code>{{ input.name }}</code></b>
82
- {%- if input.required %} - <em>required</em>{% endif %}
83
- {% if options.parameters_anchors %}
84
- <a class="headerlink" href="#{{ anchor_id }}" title="Link to {{ input.name }}">¤</a>
85
- {% endif %}
86
- <div class="doc-md-description">
87
- {{ input.description | convert_markdown(options.heading_level, data.id) }}
88
- </div>
89
- {% if input.default is not none %}
90
- <div class="doc-md-description">
91
- Default: <code>{{ input.default | as_string }}</code>
92
- </div>
93
- {% endif %}
94
- </li>
95
- {% endfor %}
88
+ {{ render_grouped_list(inputs, "inputs", options, data) }}
96
89
  </ul>
97
90
  {% endblock list_style %}
98
91
  {% endif %}
@@ -7,6 +7,8 @@ Context:
7
7
  options (dict): The local options
8
8
  -#}
9
9
 
10
+ {% from "_macros.html.jinja" import render_grouped_list %}
11
+
10
12
  {% block logs scoped %}
11
13
  {{ log.debug("Rendering outputs of " + data.id) }}
12
14
  {% endblock logs %}
@@ -24,15 +26,22 @@ Context:
24
26
  <span class="doc-section-title">Outputs:</span>
25
27
  <a class="headerlink" href="#{{ header_id }}" title="Link to {{ data.name }} outputs">¤</a>
26
28
  </p>
27
- <table>
29
+ <table data-gh-parameters>
28
30
  <thead>
29
31
  <tr>
30
32
  <th>Name</th>
31
33
  <th>Description</th>
32
34
  </tr>
33
35
  </thead>
36
+ {% set grouped_outputs = outputs | group_parameters(options.parameters_groups) %}
37
+ {% for group, group_outputs in grouped_outputs.items() %}
34
38
  <tbody>
35
- {% for output in outputs %}
39
+ {% if options.parameters_group_title_row and grouped_outputs | length > 1 and group != "" %}
40
+ <tr class="doc-section-item">
41
+ <td class="gh-group-title" colspan="2">{{ group }}</td>
42
+ </tr>
43
+ {% endif %}
44
+ {% for output in group_outputs %}
36
45
  {% if options.parameters_anchors %}
37
46
  {% set anchor_id = output.name | anchor_id("outputs", data.id) %}
38
47
  {% else %}
@@ -48,8 +57,10 @@ Context:
48
57
  <td>
49
58
  {{ output.description | convert_markdown(options.heading_level, data.id) }}
50
59
  </td>
60
+ </tr>
51
61
  {% endfor %}
52
62
  </tbody>
63
+ {% endfor %}
53
64
  </table>
54
65
  {% endblock table_style %}
55
66
  {% elif options.parameters_section_style == "list" %}
@@ -59,18 +70,7 @@ Context:
59
70
  <a class="headerlink" href="#{{ header_id }}" title="Link to {{ data.name }} outputs">¤</a>
60
71
  </p>
61
72
  <ul>
62
- {% for output in outputs %}
63
- {% set anchor_id = output.name | anchor_id("outputs", data.id) %}
64
- <li class="doc-section-item field-body">
65
- <b><code>{{ output.name }}</code></b>
66
- {% if options.parameters_anchors %}
67
- <a class="headerlink" href="#{{ anchor_id }}" title="Link to {{ output.name }}">¤</a>
68
- {% endif %}
69
- <div class="doc-md-description">
70
- {{ output.description | convert_markdown(options.heading_level, data.id) }}
71
- </div>
72
- </li>
73
- {% endfor %}
73
+ {{ render_grouped_list(outputs, "outputs", options, data) }}
74
74
  </ul>
75
75
  {% endblock list_style %}
76
76
  {% endif %}
@@ -7,6 +7,8 @@ Context:
7
7
  options (dict): The local options
8
8
  -#}
9
9
 
10
+ {% from "_macros.html.jinja" import render_grouped_list %}
11
+
10
12
  {% block logs scoped %}
11
13
  {{ log.debug("Rendering secrets of " + data.id) }}
12
14
  {% endblock logs %}
@@ -24,15 +26,22 @@ Context:
24
26
  <span class="doc-section-title">Secrets:</span>
25
27
  <a class="headerlink" href="#{{ header_id }}" title="Link to {{ data.name }} outputs">¤</a>
26
28
  </p>
27
- <table>
29
+ <table data-gh-parameters>
28
30
  <thead>
29
31
  <tr>
30
32
  <th>Name</th>
31
33
  <th>Description</th>
32
34
  </tr>
33
35
  </thead>
36
+ {% set grouped_secrets = secrets | group_parameters(options.parameters_groups) %}
37
+ {% for group, group_secrets in grouped_secrets.items() %}
34
38
  <tbody>
35
- {% for secret in secrets %}
39
+ {% if options.parameters_group_title_row and grouped_secrets | length > 1 and group != "" %}
40
+ <tr class="doc-section-item">
41
+ <td class="gh-group-title" colspan="2">{{ group }}</td>
42
+ </tr>
43
+ {% endif %}
44
+ {% for secret in group_secrets %}
36
45
  {% if options.parameters_anchors %}
37
46
  {% set anchor_id = secret.name | anchor_id("secrets", data.id) %}
38
47
  {% else %}
@@ -52,8 +61,10 @@ Context:
52
61
  <td>
53
62
  {{ secret.description | convert_markdown(options.heading_level, data.id) }}
54
63
  </td>
64
+ </tr>
55
65
  {% endfor %}
56
66
  </tbody>
67
+ {% endfor %}
57
68
  </table>
58
69
  {% endblock table_style %}
59
70
  {% elif options.parameters_section_style == "list" %}
@@ -63,19 +74,7 @@ Context:
63
74
  <a class="headerlink" href="#{{ header_id }}" title="Link to {{ data.name }} outputs">¤</a>
64
75
  </p>
65
76
  <ul>
66
- {% for secret in secrets %}
67
- {% set anchor_id = secret.name | anchor_id("secrets", data.id) %}
68
- <li class="doc-section-item field-body">
69
- <b><code>{{ secret.name }}</code></b>
70
- {%- if secret.required %} - <em>required</em>{% endif %}
71
- {% if options.parameters_anchors %}
72
- <a class="headerlink" href="#{{ anchor_id }}" title="Link to {{ secret.name }}">¤</a>
73
- {% endif %}
74
- <div class="doc-md-description">
75
- {{ secret.description | convert_markdown(options.heading_level, data.id) }}
76
- </div>
77
- </li>
78
- {% endfor %}
77
+ {{ render_grouped_list(secrets, "secrets", options, data) }}
79
78
  </ul>
80
79
  {% endblock list_style %}
81
80
  {% endif %}
@@ -57,3 +57,22 @@
57
57
  background-color: #24292e;
58
58
  color: #ffffff;
59
59
  }
60
+
61
+
62
+ /* Add thicker divider between thead and tbody, and between tbody groups */
63
+ table[data-gh-parameters] thead + tbody tr:first-child td,
64
+ table[data-gh-parameters] tbody:not(:first-of-type) tr:first-child td {
65
+ border-top: 2px solid var(--md-default-fg-color--lighter) !important;
66
+ }
67
+
68
+ /* Disable word-wrap in first column (except group titles) */
69
+ table[data-gh-parameters] td:first-child:not(.gh-group-title) {
70
+ white-space: nowrap;
71
+ }
72
+
73
+ /* Style group title rows */
74
+ table[data-gh-parameters] td.gh-group-title {
75
+ text-align: center !important;
76
+ font-style: italic;
77
+ white-space: normal;
78
+ }
@@ -20,28 +20,50 @@ Context:
20
20
  <div class="doc doc-object doc-workflow">
21
21
  {% include "heading.html.jinja" with context %}
22
22
 
23
- {% set signature = data.id | format_action_signature(config.repo, options) %}
23
+ {% set signature = data.id | format_action_signature(options.signature_repository if options.signature_repository else repository_name, options) %}
24
24
 
25
25
  {% block signature scoped %}
26
26
  {% if options.show_signature %}
27
- {% with inputs = data.inputs | order_parameters(options.parameters_order) | filter_parameters(required=True) %}
27
+ {% with inputs = data.inputs | order_parameters(options.parameters_order) | filter_parameters(required=True), secrets = data.secrets | order_parameters(options.parameters_order) | filter_parameters(required=True) %}
28
+ <div class="annotate">
28
29
  {% filter highlight(language="yaml", inline=False, linenums=False) %}
29
30
  uses: {{ signature }}
30
- {% if options.signature_show_permissions and data.permissions %}
31
+ {% if options.signature_show_permissions and data.permissions|length > 0 %}
32
+ {% if data.permission_read_all %}
33
+ permissions: read-all
34
+ {% elif data.permission_write_all %}
35
+ permissions: write-all
36
+ {% else %}
31
37
  permissions:
32
38
  {% for scope, level in data.permissions | items %}
33
39
  {{ scope }}: {{ level.label }}
34
40
  {% endfor %}
35
41
  {% endif %}
42
+ {% endif %}
43
+ {% if inputs|length > 0 %}
36
44
  with:
37
- {%+ for input in inputs %}{{ input.name }}: ''{% endfor +%}
45
+ {% for input in inputs %}
46
+ {{ input.name }}: ({{ loop.index }})
47
+ {% endfor %}
48
+ {% endif %}
38
49
  {% if options.signature_show_secrets %}
39
- {% with secrets = data.secrets | order_parameters(options.parameters_order) | filter_parameters(required=True) %}
50
+ {% if secrets|length > 0 %}
40
51
  secrets:
41
- {%+ for secret in secrets %}{{ secret.name }}: ''{% endfor +%}
42
- {% endwith %}
52
+ {% for secret in secrets %}
53
+ {{ secret.name }}: ({{ inputs|length + loop.index }})
54
+ {% endfor %}
55
+ {% endif %}
43
56
  {% endif %}
44
57
  {% endfilter %}
58
+ </div>
59
+ <ol>
60
+ {% for input in inputs %}
61
+ <li>{{ input.description | convert_markdown(options.heading_level) if input.description else "(no description)" }}</li>
62
+ {% endfor %}
63
+ {% for secret in secrets %}
64
+ <li>{{ secret.description | convert_markdown(options.heading_level) if secret.description else "(no description)" }}</li>
65
+ {% endfor %}
66
+ </ol>
45
67
  {% endwith %}
46
68
  {% endif %}
47
69
  {% endblock signature %}
@@ -1,17 +0,0 @@
1
- mkdocstrings_handlers/github/__init__.py,sha256=0WdFUIq4Xu2ZFtlZNIYCQSoqcx3Ot9Wv41_X_dwbFww,248
2
- mkdocstrings_handlers/github/config.py,sha256=pSjA6gU-kC_mXQqNIIGOYEOhtX5VzZDT0H1hRFlGaj8,6089
3
- mkdocstrings_handlers/github/handler.py,sha256=d3bpciheO6BSH1EPAc8ijZ_xmcEzzhXTbfTrbcMmFIg,9310
4
- mkdocstrings_handlers/github/objects.py,sha256=JDkY7mg_LGlIEwZHP2wSd8ZkB6RVtRsu_JEpwV-PikQ,7069
5
- mkdocstrings_handlers/github/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- mkdocstrings_handlers/github/rendering.py,sha256=tJeAsSaijqSC2IkxYsIXq7d0lR2A3aB5PRUcFFTFpVU,2705
7
- mkdocstrings_handlers/github/templates/material/action.html.jinja,sha256=87NCgz-zY16rU7tEmJETJ0gLwdzxoBrqLtp9vsVCsiw,2435
8
- mkdocstrings_handlers/github/templates/material/heading.html.jinja,sha256=wnvZpNED8Dhb935qnddeDExXN-MIUz8frRRfgq-A9VA,1396
9
- mkdocstrings_handlers/github/templates/material/inputs.html.jinja,sha256=CIcw3OBQdCP4e5A4srLu1v3xoOjsedIw1Zh3qxtG0-A,3482
10
- mkdocstrings_handlers/github/templates/material/outputs.html.jinja,sha256=jlzVF93q5AyJfOiSl3_1VBVL3c6rjmEcS81s3sri5Gg,2670
11
- mkdocstrings_handlers/github/templates/material/secrets.html.jinja,sha256=1lMJoxjjeiqetVu0mdLKmZ1faYRReeyjiYvYTZESots,2881
12
- mkdocstrings_handlers/github/templates/material/style.css,sha256=R2hh1RfHwJ6XNT4YQl_txNkNXcPBZJMCBZn5kQEMQ6M,962
13
- mkdocstrings_handlers/github/templates/material/workflow.html.jinja,sha256=E1VUi2w7_NDkpS9VNrWRV52hhyhQvnKUrjPbV_j0Qcg,3312
14
- mkdocstrings_github-0.4.2.dist-info/METADATA,sha256=humw71gGT_wkykxy7fRgxh_t1ri6JOm3keuIkoiGIyU,3340
15
- mkdocstrings_github-0.4.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- mkdocstrings_github-0.4.2.dist-info/licenses/LICENSE,sha256=5enZtJ4zSp0Ps3jTqFQ4kadcx62BhgTaDNPrXWb3g3E,1069
17
- mkdocstrings_github-0.4.2.dist-info/RECORD,,