77#
88# Copyright © 2021 Dominic Davis-Foster <[email protected] > 99#
10+ # PyProjectTomlEncoder.dumps based on https://github.com/hukkin/tomli-w
11+ # MIT Licensed
12+ # Copyright (c) 2021 Taneli Hukkinen
13+ #
1014# Permission is hereby granted, free of charge, to any person obtaining a copy
1115# of this software and associated documentation files (the "Software"), to deal
1216# in the Software without restriction, including without limitation the rights
2731#
2832
2933# stdlib
30- from typing import Any , ClassVar , Dict , Mapping , MutableMapping , Optional , Type , TypeVar , Union
34+ from typing import (
35+ Any ,
36+ ClassVar ,
37+ Dict ,
38+ Iterator ,
39+ List ,
40+ Mapping ,
41+ MutableMapping ,
42+ Optional ,
43+ Tuple ,
44+ Type ,
45+ TypeVar ,
46+ Union
47+ )
3148
3249# 3rd party
3350import attr
3451import dom_toml
35- import toml
36- from dom_toml .encoder import _dump_str
52+ from dom_toml . decoder import InlineTableDict
53+ from dom_toml .encoder import TomlEncoder
3754from dom_toml .parser import AbstractConfigParser , BadConfigError
3855from domdf_python_tools .paths import PathPlus , in_directory
3956from domdf_python_tools .typing import PathLike
6683
6784_PP = TypeVar ("_PP" , bound = "PyProject" )
6885
86+ _translation_table = {
87+ 8 : "\\ b" ,
88+ 9 : "\\ t" ,
89+ 10 : "\\ n" ,
90+ 12 : "\\ f" ,
91+ 13 : "\\ r" ,
92+ 92 : "\\ \\ " ,
93+ }
94+
95+
96+ def _dump_str (v : str ) -> str :
97+ v = str (v ).translate (_translation_table )
98+
99+ if "'" in v and '"' not in v :
100+ quote_char = '"'
101+ elif '"' in v and "'" not in v :
102+ quote_char = "'"
103+ else :
104+ quote_char = '"'
105+ v = v .replace ('"' , '\\ "' )
106+
107+ return f"{ quote_char } { v } { quote_char } "
108+
69109
70110class PyProjectTomlEncoder (dom_toml .TomlEncoder ):
71111 """
@@ -76,14 +116,108 @@ class PyProjectTomlEncoder(dom_toml.TomlEncoder):
76116 .. autosummary-widths:: 23/64
77117 """
78118
79- def __init__ (self , _dict = dict , preserve : bool = False ) -> None : # noqa: MAN001
80- super ().__init__ (_dict = _dict , preserve = preserve )
81- self .dump_funcs [str ] = _dump_str
82- self .dump_funcs [_NormalisedName ] = _dump_str
83- self .dump_funcs [Version ] = self .dump_packaging_types
84- self .dump_funcs [Requirement ] = self .dump_packaging_types
85- self .dump_funcs [Marker ] = self .dump_packaging_types
86- self .dump_funcs [SpecifierSet ] = self .dump_packaging_types
119+ def __init__ (self , preserve : bool = False ) -> None :
120+ super ().__init__ (preserve = preserve )
121+
122+ def dumps (
123+ self ,
124+ table : Mapping [str , Any ],
125+ * ,
126+ name : str ,
127+ inside_aot : bool = False ,
128+ ) -> Iterator [str ]:
129+ """
130+ Serialise the given table.
131+
132+ :param name: The table name.
133+ :param inside_aot:
134+
135+ :rtype:
136+
137+ .. versionadded:: 0.11.0
138+ """
139+
140+ yielded = False
141+ literals = []
142+ tables : List [Tuple [str , Any , bool ]] = []
143+ for k , v in table .items ():
144+ if v is None :
145+ continue
146+ if self .preserve and isinstance (v , InlineTableDict ):
147+ literals .append ((k , v ))
148+ elif isinstance (v , dict ):
149+ tables .append ((k , v , False ))
150+ elif self ._is_aot (v ):
151+ tables .extend ((k , t , True ) for t in v )
152+ else :
153+ literals .append ((k , v ))
154+
155+ if inside_aot or name and (literals or not tables ):
156+ yielded = True
157+ yield f"[[{ name } ]]\n " if inside_aot else f"[{ name } ]\n "
158+
159+ if literals :
160+ yielded = True
161+ for k , v in literals :
162+ yield f"{ self .format_key_part (k )} = { self .format_literal (v )} \n "
163+
164+ for k , v , in_aot in tables :
165+ if yielded :
166+ yield '\n '
167+ else :
168+ yielded = True
169+ key_part = self .format_key_part (k )
170+ display_name = f"{ name } .{ key_part } " if name else key_part
171+
172+ yield from self .dumps (v , name = display_name , inside_aot = in_aot )
173+
174+ def format_literal (self , obj : object , * , nest_level : int = 0 ) -> str :
175+ """
176+ Format a literal value.
177+
178+ :param obj:
179+ :param nest_level:
180+
181+ :rtype:
182+
183+ .. versionadded:: 0.11.0
184+ """
185+
186+ if isinstance (obj , (str , _NormalisedName )):
187+ return _dump_str (obj )
188+ elif isinstance (obj , (Version , Requirement , Marker , SpecifierSet )):
189+ return self .dump_packaging_types (obj )
190+ else :
191+ return super ().format_literal (obj , nest_level = nest_level )
192+
193+ def format_inline_array (self , obj : Union [Tuple , List ], nest_level : int ) -> str :
194+ """
195+ Format an inline array.
196+
197+ :param obj:
198+ :param nest_level:
199+
200+ :rtype:
201+
202+ .. versionadded:: 0.11.0
203+ """
204+
205+ if not len (obj ):
206+ return "[]"
207+
208+ item_indent = " " * (1 + nest_level )
209+ closing_bracket_indent = " " * nest_level
210+ single_line = "[ " + ", " .join (
211+ self .format_literal (item , nest_level = nest_level + 1 ) for item in obj
212+ ) + f",]"
213+
214+ if len (single_line ) <= self .max_width :
215+ return single_line
216+ else :
217+ start = "[\n "
218+ body = ",\n " .join (item_indent + self .format_literal (item , nest_level = nest_level + 1 ) for item in obj )
219+ end = f",\n { closing_bracket_indent } ]"
220+ return start + body + end
87221
88222 @staticmethod
89223 def dump_packaging_types (obj : Union [Version , Requirement , Marker , SpecifierSet ]) -> str :
@@ -227,12 +361,12 @@ def load(
227361
228362 def dumps (
229363 self ,
230- encoder : Union [Type [toml . TomlEncoder ], toml . TomlEncoder ] = PyProjectTomlEncoder ,
364+ encoder : Union [Type [TomlEncoder ], TomlEncoder ] = PyProjectTomlEncoder ,
231365 ) -> str :
232366 """
233367 Serialise to TOML.
234368
235- :param encoder: The :class:`toml .TomlEncoder` to use for constructing the output string.
369+ :param encoder: The :class:`~dom_toml.encoder .TomlEncoder` to use for constructing the output string.
236370 """
237371
238372 # TODO: filter out default values (lists and dicts)
@@ -250,7 +384,6 @@ def dumps(
250384 "license" : toml_dict ["project" ]["license" ].to_pep621_dict ()
251385 }
252386
253- if toml_dict ["project" ] is not None :
254387 if "readme" in toml_dict ["project" ] and toml_dict ["project" ]["readme" ] is not None :
255388 readme_dict = toml_dict ["project" ]["readme" ].to_pep621_dict ()
256389
@@ -268,13 +401,13 @@ def dumps(
268401 def dump (
269402 self ,
270403 filename : PathLike ,
271- encoder : Union [Type [toml . TomlEncoder ], toml . TomlEncoder ] = PyProjectTomlEncoder ,
404+ encoder : Union [Type [TomlEncoder ], TomlEncoder ] = PyProjectTomlEncoder ,
272405 ) -> str :
273406 """
274407 Write as TOML to the given file.
275408
276409 :param filename: The filename to write to.
277- :param encoder: The :class:`toml .TomlEncoder` to use for constructing the output string.
410+ :param encoder: The :class:`~dom_toml.encoder .TomlEncoder` to use for constructing the output string.
278411
279412 :returns: A string containing the TOML representation.
280413 """
@@ -288,13 +421,13 @@ def dump(
288421 def reformat (
289422 cls : Type [_PP ],
290423 filename : PathLike ,
291- encoder : Union [Type [toml . TomlEncoder ], toml . TomlEncoder ] = PyProjectTomlEncoder ,
424+ encoder : Union [Type [TomlEncoder ], TomlEncoder ] = PyProjectTomlEncoder ,
292425 ) -> str :
293426 """
294427 Reformat the given ``pyproject.toml`` file.
295428
296429 :param filename: The file to reformat.
297- :param encoder: The :class:`toml .TomlEncoder` to use for constructing the output string.
430+ :param encoder: The :class:`~dom_toml.encoder .TomlEncoder` to use for constructing the output string.
298431
299432 :returns: A string containing the reformatted TOML.
300433
0 commit comments