Skip to content

Commit 4085ebe

Browse files
authored
Fill in example title, title_abbrev, and synopsis from Category templates. (#140)
* Fill in example title, title_abbrev, and synopsis from Category templates. * Rename a couple methods.
1 parent 5fc1ab4 commit 4085ebe

File tree

7 files changed

+165
-23
lines changed

7 files changed

+165
-23
lines changed

README.md

+10-9
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ python -m pip install -r requirements.txt
6262
python -m pip install -e .
6363
python -m mypy aws_doc_sdk_examples_tools
6464
python -m pytest -vv
65-
python -m black --check
65+
python -m black --check aws_doc_sdk_examples_tools
6666
```
6767

6868
## Validation Extensions
@@ -87,18 +87,19 @@ There are two stages, testing and deployment.
8787
5. **Open a Draft PR to main branch**: Do not publish for review. Wait for checks/tests to pass on the PR.
8888

8989
### 2. Deployment
90+
9091
1. **Run `stamp.sh --release` from the `main` branch to automatically perform the following actions**:
91-
- Update the `setup.py` version.
92-
- Create a tag in the -tools repository at the same SHA you identified earlier.
93-
- stamp.sh will create the next [stamp](https://blog.aspect.build/versioning-releases-from-a-monorepo) (which is valid [semver](https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers)) number as appropriate for the changes in this release. e.g. `2024.40.2`.
94-
- Push the new tag to `main`
92+
- Update the `setup.py` version.
93+
- Create a tag in the -tools repository at the same SHA you identified earlier.
94+
- stamp.sh will create the next [stamp](https://blog.aspect.build/versioning-releases-from-a-monorepo) (which is valid [semver](https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers)) number as appropriate for the changes in this release. e.g. `2024.40.2`.
95+
- Push the new tag to `main`
9596
1. **Update your testing PR branch**
96-
- Remove SHA and add tag to [validate-doc-metadata.yml](https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/.github/workflows/validate-doc-metadata.yml)
97-
- Remove the SHA from [.doc_gen/validation.yaml](https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/.doc_gen/validation.yaml)
98-
- This is easily accomplished in the Github UI.
97+
- Remove SHA and add tag to [validate-doc-metadata.yml](https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/.github/workflows/validate-doc-metadata.yml)
98+
- Remove the SHA from [.doc_gen/validation.yaml](https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/.doc_gen/validation.yaml)
99+
- This is easily accomplished in the Github UI.
99100
1. **Create a release**: Use the automated ["Create release from tag" button](https://github.com/awsdocs/aws-doc-sdk-examples-tools/releases/new) to create a new release with the new tag.
100101
1. **Perform internal update process**.
101-
- See `update.sh` script in internal package.
102+
- See `update.sh` script in internal package.
102103

103104
## Security
104105

aws_doc_sdk_examples_tools/categories.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
from __future__ import annotations
55

6+
import re
7+
68
from pathlib import Path
7-
from typing import Any, Dict, List, Optional
9+
from typing import Any, Callable, Dict, List, Optional
810
from dataclasses import dataclass, field
911

1012
from aws_doc_sdk_examples_tools import metadata_errors
@@ -13,6 +15,22 @@
1315
)
1416

1517

18+
def fake_gotmpl(tmpl: Optional[str], service: str, action: str):
19+
if not tmpl:
20+
return
21+
values = {
22+
".ServiceEntity.Short": service,
23+
".Action": action,
24+
}
25+
return re.sub(
26+
r"{{(?P<name>[.\w]+)}}",
27+
lambda x: values[
28+
x.groupdict()["name"]
29+
], # This will be a KeyError if the replacement isn't in the values dict
30+
tmpl,
31+
)
32+
33+
1634
@dataclass
1735
class TitleInfo:
1836
title: Optional[str] = field(default=None)
@@ -44,6 +62,9 @@ def message(self):
4462
return "Category has no display value"
4563

4664

65+
empty_title_info = TitleInfo()
66+
67+
4768
@dataclass
4869
class Category:
4970
key: str
@@ -52,6 +73,23 @@ class Category:
5273
overrides: Optional[TitleInfo] = field(default=None)
5374
description: Optional[str] = field(default=None)
5475

76+
def evaluate(
77+
self,
78+
value: Optional[str],
79+
field: Callable[[TitleInfo], Optional[str]],
80+
service: str,
81+
action: str,
82+
):
83+
overrides = field(self.overrides or empty_title_info)
84+
if overrides:
85+
return fake_gotmpl(overrides, service, action)
86+
if value:
87+
return value
88+
defaults = field(self.defaults or empty_title_info)
89+
if defaults:
90+
return fake_gotmpl(defaults, service, action)
91+
return ""
92+
5593
def validate(self, errors: MetadataErrors):
5694
if not self.display:
5795
errors.append(CategoryWithNoDisplayError(id=self.key))

aws_doc_sdk_examples_tools/doc_gen.py

+16
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ def merge(self, other: "DocGen") -> MetadataErrors:
150150
self.snippet_files.update(other.snippet_files)
151151
self.cross_blocks.update(other.cross_blocks)
152152
self.extend_examples(other.examples.values(), warnings)
153+
for name, category in other.categories.items():
154+
if name not in self.categories:
155+
self.categories[name] = category
153156

154157
return warnings
155158

@@ -318,6 +321,19 @@ def validate(self):
318321
self.root,
319322
)
320323

324+
def fill_missing_fields(self):
325+
for example in self.examples.values():
326+
service_id = example.service_main or next(
327+
k for (k, _) in example.services.items()
328+
)
329+
action = (
330+
next((k for k in example.services[service_id]), None)
331+
or example.id.split("_", 1)[1]
332+
)
333+
example.fill_display_fields(
334+
self.categories, self.services[service_id].short, action
335+
)
336+
321337
def stats(self):
322338
values = self.examples.values()
323339
initial = defaultdict(int)

aws_doc_sdk_examples_tools/doc_gen_cli.py

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def main():
5353
unmerged_doc_gen = DocGen.from_root(Path(root))
5454
merged_doc_gen.merge(unmerged_doc_gen)
5555

56+
merged_doc_gen.validate()
57+
merged_doc_gen.fill_missing_fields()
58+
5659
if not args.skip_entity_expansion:
5760
# Replace entities
5861
merged_doc_gen.expand_entity_fields(merged_doc_gen)

aws_doc_sdk_examples_tools/doc_gen_cli_test.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import pytest
2-
from unittest.mock import patch, mock_open
2+
from unittest.mock import patch, mock_open, MagicMock
33
from argparse import Namespace
44
from pathlib import Path
55

6+
from .categories import Category
67
from .doc_gen import DocGen, MetadataError, Example
78
from .doc_gen_cli import main
8-
from .metadata import DocFilenames, Sdk, Language, SDKPageVersion, Version
9-
from .sdks import SdkVersion
9+
from .metadata import DocFilenames, Language, SDKPageVersion, Version
10+
from .sdks import Sdk, SdkVersion
11+
from .services import Service
1012

1113

1214
@pytest.fixture
@@ -46,6 +48,15 @@ def mock_doc_gen(mock_example):
4648
MetadataError(file="a.yaml", id="Error 1"),
4749
MetadataError(file="b.yaml", id="Error 2"),
4850
]
51+
doc_gen.categories = {"Actions": Category(key="Actions", display="Action")}
52+
doc_gen.services = {
53+
"medical-imaging": Service(
54+
long="&AHIlong;",
55+
short="&AHI;",
56+
sort="HealthImaging",
57+
version="medical-imaging-2023-07-19",
58+
)
59+
}
4960
doc_gen.sdks = {
5061
"JavaScript": Sdk(
5162
name="JavaScript",
@@ -60,6 +71,7 @@ def mock_doc_gen(mock_example):
6071

6172
@pytest.fixture
6273
def patched_environment(mock_doc_gen):
74+
mock_doc_gen.validate = MagicMock()
6375
with patch("argparse.ArgumentParser.parse_args") as mock_parse_args, patch(
6476
"aws_doc_sdk_examples_tools.doc_gen.DocGen.empty", return_value=mock_doc_gen
6577
), patch("aws_doc_sdk_examples_tools.doc_gen.DocGen.from_root"), patch(

aws_doc_sdk_examples_tools/doc_gen_test.py

+53-3
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from pathlib import Path
1010
import json
1111

12-
from .metadata_errors import MetadataErrors, MetadataError
12+
from .categories import Category, TitleInfo
1313
from .doc_gen import DocGen, DocGenEncoder
14+
from .metadata import Example
15+
from .metadata_errors import MetadataErrors, MetadataError
1416
from .sdks import Sdk, SdkVersion
1517
from .services import Service, ServiceExpanded
1618
from .snippets import Snippet
@@ -115,7 +117,25 @@ def sample_doc_gen() -> DocGen:
115117
)
116118
},
117119
snippet_files={"test.py"},
118-
examples={},
120+
examples={
121+
"s3_PutObject": Example(
122+
"s3_PutObject",
123+
file=Path("filea.txt"),
124+
languages={},
125+
services={"s3": set(["PutObject"])},
126+
)
127+
},
128+
categories={
129+
"Actions": Category(
130+
"Actions",
131+
"Actions",
132+
defaults=TitleInfo(
133+
title="<code>{{.Action}}</code>",
134+
synopsis="{{.ServiceEntity.Short}} {{.Action}}",
135+
),
136+
overrides=TitleInfo(title_abbrev="ExcerptPartsUsage"),
137+
)
138+
},
119139
cross_blocks={"test_block"},
120140
)
121141

@@ -183,7 +203,29 @@ def test_doc_gen_encoder(sample_doc_gen: DocGen):
183203

184204
# Verify examples (empty in this case)
185205
assert "examples" in decoded
186-
assert decoded["examples"] == {}
206+
assert decoded["examples"] == {
207+
"s3_PutObject": {
208+
"category": None,
209+
"doc_filenames": None,
210+
"file": "filea.txt",
211+
"guide_topic": None,
212+
"id": "s3_PutObject",
213+
"languages": {},
214+
"service_main": None,
215+
"services": {
216+
"s3": {
217+
"__set__": [
218+
"PutObject",
219+
],
220+
},
221+
},
222+
"source_key": None,
223+
"synopsis": "",
224+
"synopsis_list": [],
225+
"title": "",
226+
"title_abbrev": "",
227+
},
228+
}
187229

188230

189231
def test_doc_gen_load_snippets():
@@ -195,3 +237,11 @@ def test_doc_gen_load_snippets():
195237
doc_gen.collect_snippets()
196238
assert doc_gen.snippet_files == set(["snippet_file.txt"])
197239
assert doc_gen.snippets["snippet_file.txt"].code == "Line A\nLine C\n"
240+
241+
242+
def test_fill_fields(sample_doc_gen: DocGen):
243+
sample_doc_gen.fill_missing_fields()
244+
example = sample_doc_gen.examples["s3_PutObject"]
245+
assert example.title == "<code>PutObject</code>"
246+
assert example.title_abbrev == "ExcerptPartsUsage"
247+
assert example.synopsis == "&S3; PutObject"

aws_doc_sdk_examples_tools/metadata.py

+29-7
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,19 @@
66

77
from collections import defaultdict
88
from dataclasses import dataclass, field
9-
from typing import Any, Dict, Literal, List, Optional, Set, Union, Iterable
9+
from typing import Dict, Literal, List, Optional, Set, Iterable
1010
from os.path import splitext
1111
from pathlib import Path
1212

1313
from . import metadata_errors
14+
from .categories import Category
1415
from .metadata_errors import (
1516
MetadataErrors,
1617
MetadataParseError,
1718
ExampleMergeMismatchedId,
1819
ExampleMergeMismatchedLanguage,
1920
ExampleMergeConflict,
2021
)
21-
from .project_validator import ValidationConfig
22-
from .services import Service
23-
from .sdks import Sdk
2422

2523

2624
@dataclass
@@ -132,9 +130,9 @@ class Example:
132130
id: str
133131
file: Optional[Path]
134132
languages: Dict[str, Language]
135-
# Human readable title. TODO: Defaults to slug-to-title of the ID if not provided.
133+
# Human readable title.
136134
title: Optional[str] = field(default="")
137-
# Used in the TOC. TODO: Defaults to slug-to-title of the ID if not provided.
135+
# Used in the TOC.
138136
title_abbrev: Optional[str] = field(default="")
139137
synopsis: Optional[str] = field(default="")
140138
# String label categories. Categories inferred by cross-service with multiple services, and can be whatever else it wants. Controls where in the TOC it appears.
@@ -151,6 +149,30 @@ class Example:
151149
synopsis_list: List[str] = field(default_factory=list)
152150
source_key: Optional[str] = field(default=None)
153151

152+
def fill_display_fields(self, categories: Dict[str, Category], service, action):
153+
category = self.choose_category(categories)
154+
if category:
155+
self.title = category.evaluate(
156+
self.title, lambda x: x.title, service, action
157+
)
158+
self.title_abbrev = category.evaluate(
159+
self.title_abbrev, lambda x: x.title_abbrev, service, action
160+
)
161+
self.synopsis = category.evaluate(
162+
self.synopsis, lambda x: x.synopsis, service, action
163+
)
164+
165+
def choose_category(self, categories: Dict[str, Category]) -> Optional[Category]:
166+
"""Find a category for an example. This logic is taken from directories and zexii.
167+
168+
Original Zexii code at https://code.amazon.com/packages/GoAmzn-AWSDocsCodeExampleDocBuilder/blobs/1321fffadd8ff02e6acbae4a1f42b81006cdfa72/--/zexi/zonbook/category.go#L31-L50.
169+
"""
170+
if self.category in categories:
171+
return categories[self.category]
172+
if len(self.services) == 1:
173+
return categories["Actions"]
174+
return categories["Scenarios"]
175+
154176
def merge(self, other: Example, errors: MetadataErrors):
155177
"""Combine `other` Example into self example.
156178
@@ -184,7 +206,7 @@ def merge(self, other: Example, errors: MetadataErrors):
184206
err.id = self.id
185207
err.file = self.file
186208
if hasattr(err, "other_file"):
187-
err.other_file = other.file
209+
err.other_file = other.file # type: ignore
188210
errors.extend(merge_errs)
189211

190212
def validate(self, errors: MetadataErrors, root: Path):

0 commit comments

Comments
 (0)