Skip to content

Commit 75cf485

Browse files
MarkDaoustcopybara-github
authored andcommitted
Add a toc entry classes to simplify toc generation.
This will give a 400 line cleanup in generate_lib.py a few changes down the road. PiperOrigin-RevId: 438275925
1 parent a5dd8de commit 75cf485

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright 2022 The TensorFlow Authors. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# ==============================================================================
15+
"""Classes for generating the TOC."""
16+
17+
import contextlib
18+
import dataclasses
19+
import enum
20+
import os
21+
22+
from typing import Any, IO, Iterator, List, Optional, Tuple, Union
23+
24+
import yaml
25+
26+
27+
class _TocDumper(yaml.SafeDumper):
28+
29+
def ignore_aliases(self, data):
30+
"""Don't output references for duplicated objects (usually strings)."""
31+
return True
32+
33+
34+
def _dict_representer(dumper: yaml.SafeDumper, data: Any):
35+
"""Represent the object as a dict created of (key, value) pairs."""
36+
return dumper.represent_dict(iter(data))
37+
38+
39+
def _use_yaml_dict_representer(cls):
40+
"""Register the class's as using a `_dict_representer`."""
41+
yaml.add_representer(cls, representer=_dict_representer, Dumper=_TocDumper)
42+
return cls
43+
44+
45+
def _str_enum_representer(dumper: yaml.SafeDumper, data: Any):
46+
"""Represent a str-Enum as a string."""
47+
return dumper.represent_str(data.value)
48+
49+
50+
def _use_yaml_str_enum_representer(cls):
51+
"""Register the class as using `_str_enum_representer`."""
52+
yaml.add_representer(
53+
cls, representer=_str_enum_representer, Dumper=_TocDumper)
54+
return cls
55+
56+
57+
@_use_yaml_str_enum_representer
58+
class Status(enum.Enum):
59+
"""Represents a page status."""
60+
ALPHA = 'alpha'
61+
BETA = 'beta'
62+
DEPRECATED = 'deprecated'
63+
EXPERIMENTAL = 'experimental'
64+
EXTERNAL = 'external'
65+
LIMITED = 'limited'
66+
NEW = 'new'
67+
NIGHTLY = 'nightly'
68+
PREVIEW = 'preview'
69+
UNSUPPORTED = 'unsupported'
70+
71+
72+
@_use_yaml_str_enum_representer
73+
class HeadingStyle(enum.Enum):
74+
"""Represents a Heading Style."""
75+
ACCORDION = 'accordion'
76+
DIVIDER = 'divider'
77+
78+
79+
class Entry:
80+
"""Base class for toc entries."""
81+
82+
def replace(self, **kwargs):
83+
new_kwargs = dict(self)
84+
new_kwargs.update(kwargs)
85+
return type(self)(**new_kwargs)
86+
87+
def __iter__(self) -> Iterator[Tuple[str, Any]]:
88+
"""Support `dict(entry)` for yaml output."""
89+
for key, value in self.__dict__.items():
90+
if value is not None:
91+
yield (key, value)
92+
93+
94+
@_use_yaml_dict_representer
95+
@dataclasses.dataclass
96+
class Heading(Entry):
97+
"""A toc heading."""
98+
heading: str
99+
style: Optional[HeadingStyle] = None
100+
101+
102+
@_use_yaml_dict_representer
103+
@dataclasses.dataclass
104+
class Section(Entry):
105+
"""A toc section."""
106+
title: str
107+
section: List[Entry]
108+
status: Optional[Status] = None
109+
110+
def __iter__(self) -> Iterator[Tuple[str, Any]]:
111+
"""Support `dict(entry)` for yaml output."""
112+
yield 'title', self.title
113+
if self.status is not None:
114+
yield 'status', self.status
115+
yield 'section', self.section
116+
117+
118+
@_use_yaml_dict_representer
119+
@dataclasses.dataclass
120+
class Link(Entry):
121+
"""Represents toc page-link."""
122+
title: str
123+
path: str
124+
status: Optional[Status] = None
125+
126+
def __iter__(self) -> Iterator[Tuple[str, Any]]:
127+
"""Support `dict(entry)` for yaml output."""
128+
yield 'title', self.title
129+
if self.status is not None:
130+
yield 'status', self.status
131+
yield 'path', self.path
132+
133+
134+
@_use_yaml_dict_representer
135+
class Break(Entry):
136+
"""Represents a toc whitesoace break."""
137+
138+
def __init__(self):
139+
self.__dict__['break'] = True
140+
141+
142+
@_use_yaml_dict_representer
143+
@dataclasses.dataclass
144+
class Toc(Entry):
145+
"""Represents the top-level `toc` element in included files."""
146+
toc: List[Entry]
147+
148+
@contextlib.contextmanager
149+
def _maybe_open(self, file: Union[os.PathLike, IO]) -> Iterator[IO]:
150+
if isinstance(file, os.PathLike):
151+
with open(file, 'w') as stream:
152+
yield stream
153+
else:
154+
stream = file
155+
yield stream
156+
157+
def write(self, file: Union[os.PathLike, IO]) -> None:
158+
with self._maybe_open(file) as stream:
159+
yaml.dump(
160+
self, stream=stream, default_flow_style=False, Dumper=_TocDumper)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2022 The TensorFlow Authors. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# ==============================================================================
15+
16+
import io
17+
import textwrap
18+
import types
19+
20+
from tensorflow_docs.api_generator import toc as toc_lib
21+
from tensorflow_docs.api_generator import doc_generator_visitor
22+
from absl.testing import absltest
23+
24+
25+
class TestToc(absltest.TestCase):
26+
27+
def test_toc_write(self):
28+
link = toc_lib.Link(title='A link', path='/path/to/link')
29+
link2 = toc_lib.Link(
30+
title='Another link',
31+
path='/path/to/link2',
32+
status=toc_lib.Status.EXTERNAL)
33+
34+
subsection = toc_lib.Section(
35+
title='A subsection',
36+
section=[link],
37+
status=toc_lib.Status.EXPERIMENTAL)
38+
39+
toc = toc_lib.Toc([
40+
# pyformat: disable
41+
toc_lib.Heading('Hello'),
42+
link,
43+
link2,
44+
toc_lib.Break(),
45+
subsection
46+
])
47+
48+
stream = io.StringIO()
49+
toc.write(stream)
50+
51+
expected = textwrap.dedent("""\
52+
toc:
53+
- heading: Hello
54+
- title: A link
55+
path: /path/to/link
56+
- title: Another link
57+
status: external
58+
path: /path/to/link2
59+
- break: true
60+
- title: A subsection
61+
status: experimental
62+
section:
63+
- title: A link
64+
path: /path/to/link
65+
""")
66+
67+
self.assertEqual(expected, stream.getvalue())
68+
69+
def test_replace(self):
70+
link = toc_lib.Link(title='A link', path='/path/to/link')
71+
new_link = link.replace(status=toc_lib.Status.NEW, title='New title.')
72+
73+
expected = toc_lib.Link(
74+
title='New title.', path='/path/to/link', status=toc_lib.Status.NEW)
75+
self.assertEqual(expected, new_link)
76+
77+
78+
if __name__ == '__main__':
79+
absltest.main()

0 commit comments

Comments
 (0)