2Python functions for manipulating JavaScript "data files" -- that is,
3JavaScript files that define a single variable with a JSON value.
5This is an example of a JavaScript data file:
7 const shape = { "sides": 5, "colour": "red" };
9Think of this like the JSON module, but for JavaScript files.
20from .decoder import decode_from_js
21from .encoder import encode_as_js, encode_as_json
30 "append_to_js_object",
34T = typing.TypeVar("T")
37def read_js(p: pathlib.Path | str, *, varname: str) -> typing.Any:
39 Read a JavaScript "data file".
41 For example, if you have a file `shape.js` with the following contents:
43 const redPentagon = { "sides": 5, "colour": "red" };
45 Then you can read it using this function:
47 >>> read_js('shape.js', varname='redPentagon')
48 {'sides': 5, 'colour': 'red'}
53 return decode_from_js(js_string=p.read_text(), varname=varname)
56def read_typed_js[T](p: pathlib.Path | str, *, varname: str, model: type[T]) -> T:
58 Read a JavaScript "data file".
60 This will validate the contents of the data file against the type
61 you provide, and will throw a ``pydantic.ValidationError`` if the
62 contents does not match the specified type.
64 from .validate_type import validate_type
66 data = read_js(p, varname=varname)
68 return validate_type(data, model=model)
72 p: pathlib.Path | str | io.TextIOBase | io.BufferedIOBase,
76 ensure_ascii: bool = False,
77 sort_keys: bool = False,
80 Write a JavaScript "data file".
82 You can pass a path-like or file-like object as the first parameter ``p``.
85 >>> red_pentagon = {'sides': 5, 'colour': 'red'}
86 >>> write_js('shape.js', value=red_pentagon, varname='redPentagon')
87 >>> open('shape.js').read()
88 'const redPentagon = {\n "sides": 5,\n "colour": "red"\n};\n'
91 js_string = encode_as_js(
92 value, varname, ensure_ascii=ensure_ascii, sort_keys=sort_keys
95 if isinstance(p, io.TextIOBase):
97 elif isinstance(p, io.BufferedIOBase):
98 p.write(js_string.encode("utf8"))
99 elif isinstance(p, pathlib.Path) or isinstance(p, str):
103 raise IsADirectoryError(p)
105 p.parent.mkdir(exist_ok=True, parents=True)
107 # Write to a temporary file first, then rename this into place.
109 # This gives us pseudo-atomic writes -- it's probably not perfect, but
110 # it avoids situations where:
112 # * Somebody tries to read the file, and it contains a partial JS string
113 # * The write is interrupted, and the file is left empty
115 # Both of which have happened! Because I often use this running on
116 # files on a semi-slow external hard drive, and sometimes things break.
118 # The UUID is probably overkill because it would be very unusual for
119 # me to have multiple, concurrent writes going on, but it doesn't hurt.
120 tmp_p = p.with_suffix(f".{uuid.uuid4()}.js.tmp")
122 with tmp_p.open("x") as out_file:
123 out_file.write(js_string)
127 raise TypeError(f"Cannot write JavaScript to {type(p)}!")
130def append_to_js_array(p: pathlib.Path | str, *, value: typing.Any) -> None:
132 Append a single value to an array in a JavaScript "data file".
135 >>> write_js('food.js', value=['apple', 'banana', 'coconut'], varname='fruit')
136 >>> append_to_js_array('food.js', value='damson')
137 >>> read_js('food.js', varname='fruit')
138 ['apple', 'banana', 'coconut', 'damson']
140 If you have a large file, this is usually faster than reading,
141 appending, and re-writing the entire file.
145 file_size = p.stat().st_size
149 + textwrap.indent(encode_as_json(value), prefix=" ").encode("utf8")
153 with open(p, "rb+") as out_file:
154 out_file.seek(file_size - 4)
156 if out_file.read(4) == b"\n];\n":
157 out_file.seek(file_size - 4)
158 out_file.write(json_to_append)
161 out_file.seek(file_size - 3)
162 if out_file.read(3) in {b"\n];", b"];\n"}:
163 out_file.seek(file_size - 3)
164 out_file.write(json_to_append)
167 out_file.seek(file_size - 2)
168 if out_file.read(2) in {b"];", b"]\n"}:
169 out_file.seek(file_size - 2)
170 out_file.write(json_to_append)
173 out_file.seek(file_size - 1)
174 if out_file.read(1) == b"]":
175 out_file.seek(file_size - 1)
176 out_file.write(json_to_append)
179 raise ValueError(f"End of file {p!r} does not look like an array")
182def append_to_js_object(p: pathlib.Path | str, *, key: str, value: typing.Any) -> None:
184 Append a single key/value pair to a JSON object in a JavaScript "data file".
187 >>> write_js('shape.js',
188 ... value={'colour': 'red', 'sides': 5},
189 ... varname='redPentagon')
190 >>> append_to_js_object('shape.js', key='sideLengths', value=[5, 5, 6, 6, 6])
191 >>> read_js('shape.js', varname='redPentagon')
192 {'colour': 'red', 'sides': 5, 'sideLengths': [5, 5, 6, 6, 6]}
194 If you have a large file, this is usually faster than reading,
195 appending, and re-writing the entire file.
199 file_size = p.stat().st_size
201 enc_key = json.dumps(key)
202 enc_value = textwrap.indent(encode_as_json(value), prefix=" ").lstrip()
204 json_to_append = f",\n {enc_key}: {enc_value}\n}};\n".encode("utf8")
206 with open(p, "rb+") as out_file:
207 out_file.seek(file_size - 4)
209 if out_file.read(4) == b"\n};\n":
210 out_file.seek(file_size - 4)
211 out_file.write(json_to_append)
214 out_file.seek(file_size - 3)
215 if out_file.read(3) in {b"\n};", b"};\n"}:
216 out_file.seek(file_size - 3)
217 out_file.write(json_to_append)
220 out_file.seek(file_size - 2)
221 if out_file.read(2) in {b"};", b"}\n"}:
222 out_file.seek(file_size - 2)
223 out_file.write(json_to_append)
226 out_file.seek(file_size - 1)
227 if out_file.read(1) == b"}":
228 out_file.seek(file_size - 1)
229 out_file.write(json_to_append)
232 raise ValueError(f"End of file {p!r} does not look like an object")