11"""Defines the `SiteMap` object, for storing the parsed ToC."""
22from collections .abc import MutableMapping
3+ from dataclasses import asdict , dataclass
34from typing import Any , Dict , Iterator , List , Optional , Set , Union
45
5- import attr
6- from attr .validators import deep_iterable , instance_of , matches_re , optional
6+ from ._compat import (
7+ DC_SLOTS ,
8+ deep_iterable ,
9+ field ,
10+ instance_of ,
11+ matches_re ,
12+ optional ,
13+ validate_fields ,
14+ )
715
816#: Pattern used to match URL items.
917URL_PATTERN : str = r".+://.*"
@@ -21,35 +29,41 @@ class GlobItem(str):
2129 """A document glob in a toctree list."""
2230
2331
24- @attr . s ( slots = True )
32+ @dataclass ( ** DC_SLOTS )
2533class UrlItem :
2634 """A URL in a toctree."""
2735
2836 # regex should match sphinx.util.url_re
29- url : str = attr . ib (validator = [instance_of (str ), matches_re (URL_PATTERN )])
30- title : Optional [str ] = attr . ib ( None , validator = optional (instance_of (str )))
37+ url : str = field (validator = [instance_of (str ), matches_re (URL_PATTERN )])
38+ title : Optional [str ] = field ( default = None , validator = optional (instance_of (str )))
3139
40+ def __post_init__ (self ):
41+ validate_fields (self )
3242
33- @attr .s (slots = True )
43+
44+ @dataclass (** DC_SLOTS )
3445class TocTree :
3546 """An individual toctree within a document."""
3647
3748 # TODO validate uniqueness of docnames (at least one item)
38- items : List [Union [GlobItem , FileItem , UrlItem ]] = attr . ib (
49+ items : List [Union [GlobItem , FileItem , UrlItem ]] = field (
3950 validator = deep_iterable (
4051 instance_of ((GlobItem , FileItem , UrlItem )), instance_of (list )
4152 )
4253 )
43- caption : Optional [str ] = attr . ib (
54+ caption : Optional [str ] = field (
4455 default = None , kw_only = True , validator = optional (instance_of (str ))
4556 )
46- hidden : bool = attr . ib (default = True , kw_only = True , validator = instance_of (bool ))
47- maxdepth : int = attr . ib (default = - 1 , kw_only = True , validator = instance_of (int ))
48- numbered : Union [bool , int ] = attr . ib (
57+ hidden : bool = field (default = True , kw_only = True , validator = instance_of (bool ))
58+ maxdepth : int = field (default = - 1 , kw_only = True , validator = instance_of (int ))
59+ numbered : Union [bool , int ] = field (
4960 default = False , kw_only = True , validator = instance_of ((bool , int ))
5061 )
51- reversed : bool = attr .ib (default = False , kw_only = True , validator = instance_of (bool ))
52- titlesonly : bool = attr .ib (default = False , kw_only = True , validator = instance_of (bool ))
62+ reversed : bool = field (default = False , kw_only = True , validator = instance_of (bool ))
63+ titlesonly : bool = field (default = False , kw_only = True , validator = instance_of (bool ))
64+
65+ def __post_init__ (self ):
66+ validate_fields (self )
5367
5468 def files (self ) -> List [str ]:
5569 """Returns a list of file items included in this ToC tree.
@@ -66,17 +80,20 @@ def globs(self) -> List[str]:
6680 return [str (item ) for item in self .items if isinstance (item , GlobItem )]
6781
6882
69- @attr . s ( slots = True )
83+ @dataclass ( ** DC_SLOTS )
7084class Document :
7185 """A document in the site map."""
7286
7387 # TODO validate uniqueness of docnames across all parts (and none should be the docname)
74- docname : str = attr . ib (validator = instance_of (str ))
75- subtrees : List [TocTree ] = attr . ib (
76- factory = list ,
88+ docname : str = field (validator = instance_of (str ))
89+ subtrees : List [TocTree ] = field (
90+ default_factory = list ,
7791 validator = deep_iterable (instance_of (TocTree ), instance_of (list )),
7892 )
79- title : Optional [str ] = attr .ib (default = None , validator = optional (instance_of (str )))
93+ title : Optional [str ] = field (default = None , validator = optional (instance_of (str )))
94+
95+ def __post_init__ (self ):
96+ validate_fields (self )
8097
8198 def child_files (self ) -> List [str ]:
8299 """Return all children files.
@@ -183,24 +200,29 @@ def __len__(self) -> int:
183200 """
184201 return len (self ._docs )
185202
186- @staticmethod
187- def _serializer (inst : Any , field : attr .Attribute , value : Any ) -> Any :
188- """Serialize to JSON compatible value.
189-
190- (parsed to ``attr.asdict``)
191- """
192- if isinstance (value , (GlobItem , FileItem )):
193- return str (value )
194- return value
195-
196203 def as_json (self ) -> Dict [str , Any ]:
197204 """Return JSON serialized site-map representation."""
198205 doc_dict = {
199- k : attr .asdict (self ._docs [k ], value_serializer = self ._serializer )
200- if self ._docs [k ]
201- else self ._docs [k ]
206+ k : asdict (self ._docs [k ]) if self ._docs [k ] else self ._docs [k ]
202207 for k in sorted (self ._docs )
203208 }
209+
210+ def _replace_items (d : Dict [str , Any ]) -> Dict [str , Any ]:
211+ for k , v in d .items ():
212+ if isinstance (v , dict ):
213+ d [k ] = _replace_items (v )
214+ elif isinstance (v , (list , tuple )):
215+ d [k ] = [
216+ _replace_items (i )
217+ if isinstance (i , dict )
218+ else (str (i ) if isinstance (i , str ) else i )
219+ for i in v
220+ ]
221+ elif isinstance (v , str ):
222+ d [k ] = str (v )
223+ return d
224+
225+ doc_dict = _replace_items (doc_dict )
204226 data = {
205227 "root" : self .root .docname ,
206228 "documents" : doc_dict ,
0 commit comments