resforge 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {resforge-0.4.0 → resforge-0.4.2}/PKG-INFO +1 -1
- {resforge-0.4.0 → resforge-0.4.2}/pyproject.toml +1 -1
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/__init__.py +1 -1
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/_codegen/kotlin.py +2 -9
- resforge-0.4.2/src/resforge/_utils.py +31 -0
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/android/compose.py +11 -8
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/android/values.py +16 -12
- resforge-0.4.2/src/resforge/apple/_base.py +33 -0
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/apple/_colorset.py +4 -2
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/apple/catalog.py +33 -16
- resforge-0.4.0/src/resforge/_utils.py → resforge-0.4.2/src/resforge/io.py +33 -31
- resforge-0.4.2/tests/__init__.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/tests/test_atomic_write.py +1 -1
- {resforge-0.4.0 → resforge-0.4.2}/tests/test_codegen_kotlin.py +0 -18
- resforge-0.4.0/src/resforge/apple/_base.py +0 -33
- {resforge-0.4.0 → resforge-0.4.2}/.github/workflows/ci.yml +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/.github/workflows/publish.yml +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/.gitignore +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/LICENSE +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/README.md +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/_codegen/__init__.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/android/__init__.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/android/types.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/apple/__init__.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/apple/types.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/src/resforge/color.py +0 -0
- /resforge-0.4.0/tests/__init__.py → /resforge-0.4.2/src/resforge/py.typed +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/tests/test_android_compose.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/tests/test_android_values.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/tests/test_apple_catalog.py +0 -0
- {resforge-0.4.0 → resforge-0.4.2}/tests/test_color.py +0 -0
|
@@ -47,7 +47,7 @@ target-version = "py312"
|
|
|
47
47
|
|
|
48
48
|
[tool.ruff.lint]
|
|
49
49
|
select = ["ALL"]
|
|
50
|
-
ignore = ["D100", "D104", "D105", "ANN401", "PLR2004", "E741"]
|
|
50
|
+
ignore = ["D100", "D104", "D105", "D107", "ANN401", "PLR2004", "E741"]
|
|
51
51
|
|
|
52
52
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
53
53
|
"tests/**/*.py" = [
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
from typing import IO, Literal, Self, final
|
|
4
3
|
|
|
5
4
|
PropertyMutability = Literal["val", "var"]
|
|
@@ -96,11 +95,5 @@ class KotlinFile:
|
|
|
96
95
|
|
|
97
96
|
return "\n\n".join(sections) + "\n"
|
|
98
97
|
|
|
99
|
-
def write(self, dest:
|
|
100
|
-
|
|
101
|
-
path = Path(dest)
|
|
102
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
-
with path.open("wb") as f:
|
|
104
|
-
f.write(self.render().encode())
|
|
105
|
-
else:
|
|
106
|
-
dest.write(self.render().encode())
|
|
98
|
+
def write(self, dest: IO[bytes]) -> None:
|
|
99
|
+
dest.write(self.render().encode())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Concatenate, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
__all__ = ["require_context"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class _HasActiveContext(Protocol):
|
|
10
|
+
_active: bool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def require_context[T: _HasActiveContext, **P, R](
|
|
14
|
+
func: Callable[Concatenate[T, P], R],
|
|
15
|
+
) -> Callable[Concatenate[T, P], R]:
|
|
16
|
+
"""Ensures a method is only called within an active context.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
RuntimeError: If the method is called while the instance's
|
|
20
|
+
`_active` attribute is False.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@wraps(func)
|
|
25
|
+
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
26
|
+
if not self._active: # pyright: ignore[reportPrivateUsage]
|
|
27
|
+
msg = f"'{func.__name__}' requires an active 'with' context."
|
|
28
|
+
raise RuntimeError(msg)
|
|
29
|
+
return func(self, *args, **kwargs)
|
|
30
|
+
|
|
31
|
+
return wrapper
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from io import BytesIO
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import TYPE_CHECKING, Self, final, override
|
|
5
6
|
|
|
6
7
|
from resforge import Color
|
|
7
8
|
from resforge._codegen.kotlin import KotlinFile, KotlinObject
|
|
8
|
-
from resforge._utils import
|
|
9
|
+
from resforge._utils import require_context
|
|
10
|
+
from resforge.io import FileSystemSink, WriteSink
|
|
9
11
|
|
|
10
12
|
if TYPE_CHECKING:
|
|
11
13
|
from resforge.android.types import Dimension
|
|
@@ -73,7 +75,6 @@ class _BaseComposeScope:
|
|
|
73
75
|
|
|
74
76
|
Raises:
|
|
75
77
|
ValueError: If an unsupported unit (not dp, sp, or em) is provided.
|
|
76
|
-
|
|
77
78
|
"""
|
|
78
79
|
mapping = {"dp": "Dp", "sp": "Sp", "em": "TextUnit"}
|
|
79
80
|
for name, dimen in values.items():
|
|
@@ -110,24 +111,26 @@ class ComposeWriter(_BaseComposeScope):
|
|
|
110
111
|
object_name="AppColors"
|
|
111
112
|
) as compose:
|
|
112
113
|
... compose.color(primary="#6200EE", background="#FFFFFF")
|
|
113
|
-
|
|
114
114
|
"""
|
|
115
115
|
|
|
116
116
|
def __init__( # pyright: ignore[reportMissingSuperCall]
|
|
117
117
|
self,
|
|
118
118
|
path: str | Path,
|
|
119
119
|
package: str,
|
|
120
|
+
sink: WriteSink | None = None,
|
|
120
121
|
) -> None:
|
|
121
122
|
"""Initializes the ComposeWriter.
|
|
122
123
|
|
|
123
124
|
Args:
|
|
124
125
|
path: The filesystem path where the Kotlin file will be saved.
|
|
125
126
|
package: The Kotlin package declaration.
|
|
126
|
-
|
|
127
|
+
sink: The custom output to write data to. If None,
|
|
128
|
+
defaults to a standard file write.
|
|
127
129
|
"""
|
|
130
|
+
self._active = False
|
|
128
131
|
self._path = Path(path)
|
|
129
132
|
self._package = package
|
|
130
|
-
self.
|
|
133
|
+
self._sink = sink or FileSystemSink()
|
|
131
134
|
self._kotlin_file = KotlinFile(package=self._package)
|
|
132
135
|
|
|
133
136
|
@override
|
|
@@ -149,8 +152,9 @@ class ComposeWriter(_BaseComposeScope):
|
|
|
149
152
|
) -> None:
|
|
150
153
|
try:
|
|
151
154
|
if exc_type is None:
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
buf = BytesIO()
|
|
156
|
+
self._kotlin_file.write(buf)
|
|
157
|
+
self._sink.write(self._path, buf.getvalue())
|
|
154
158
|
finally:
|
|
155
159
|
self._active = False
|
|
156
160
|
|
|
@@ -163,7 +167,6 @@ class ComposeWriter(_BaseComposeScope):
|
|
|
163
167
|
|
|
164
168
|
Returns:
|
|
165
169
|
A new context tied to the generated object.
|
|
166
|
-
|
|
167
170
|
"""
|
|
168
171
|
obj = KotlinObject(name=name)
|
|
169
172
|
self._kotlin_file.member(obj)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import xml.etree.ElementTree as ET
|
|
3
|
+
from io import BytesIO
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from re import Pattern
|
|
5
6
|
from typing import Self, final
|
|
6
7
|
|
|
7
8
|
from resforge import Color
|
|
8
|
-
from resforge._utils import
|
|
9
|
+
from resforge._utils import require_context
|
|
10
|
+
from resforge.io import FileSystemSink, WriteSink
|
|
9
11
|
|
|
10
12
|
from .types import Dimension, PluralValues
|
|
11
13
|
|
|
@@ -28,15 +30,17 @@ class ValuesWriter:
|
|
|
28
30
|
|
|
29
31
|
"""
|
|
30
32
|
|
|
31
|
-
def __init__(self, path: str | Path) -> None:
|
|
33
|
+
def __init__(self, path: str | Path, sink: WriteSink | None = None) -> None:
|
|
32
34
|
"""Initializes the ValuesWriter.
|
|
33
35
|
|
|
34
36
|
Args:
|
|
35
37
|
path: The filesystem path where the XML will be saved.
|
|
36
|
-
|
|
38
|
+
sink: The custom output to write data to. If None,
|
|
39
|
+
defaults to a standard file write.
|
|
37
40
|
"""
|
|
38
|
-
self._path = Path(path)
|
|
39
41
|
self._active = False
|
|
42
|
+
self._path = Path(path)
|
|
43
|
+
self._sink = sink or FileSystemSink()
|
|
40
44
|
|
|
41
45
|
# Structure: {"string": ["app_name"], ...}
|
|
42
46
|
self._names: dict[str, set[str]] = {}
|
|
@@ -52,14 +56,14 @@ class ValuesWriter:
|
|
|
52
56
|
try:
|
|
53
57
|
if exc_type is None:
|
|
54
58
|
ET.indent(self._root, space=" " * 4, level=0)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
buf = BytesIO()
|
|
60
|
+
ET.ElementTree(self._root).write(
|
|
61
|
+
buf,
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
xml_declaration=True,
|
|
64
|
+
short_empty_elements=True,
|
|
65
|
+
)
|
|
66
|
+
self._sink.write(self._path, buf.getvalue())
|
|
63
67
|
finally:
|
|
64
68
|
self._active = False
|
|
65
69
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Self
|
|
6
|
+
|
|
7
|
+
from resforge.io import WriteSink
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AssetNode(ABC):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
path: str | Path,
|
|
14
|
+
name: str,
|
|
15
|
+
extension: str,
|
|
16
|
+
sink: WriteSink,
|
|
17
|
+
) -> None:
|
|
18
|
+
self._path: Path = Path(path) / f"{name}.{extension}"
|
|
19
|
+
self._sink: WriteSink = sink
|
|
20
|
+
|
|
21
|
+
def __enter__(self) -> Self:
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def __exit__(self, exc_type: type[BaseException] | None, *_: object) -> None:
|
|
25
|
+
if exc_type is None:
|
|
26
|
+
contents = self._create_contents()
|
|
27
|
+
buf = BytesIO()
|
|
28
|
+
buf.write(json.dumps(contents, indent=2).encode())
|
|
29
|
+
path = Path(self._path) / "Contents.json"
|
|
30
|
+
self._sink.write(path, buf.getvalue())
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def _create_contents(self) -> dict[str, object]: ...
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Self, override
|
|
3
3
|
|
|
4
|
+
from resforge.io import WriteSink
|
|
5
|
+
|
|
4
6
|
from ._base import AssetNode
|
|
5
7
|
from .types import AppleColor
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class ColorSet(AssetNode):
|
|
9
|
-
def __init__(self, path: str | Path, name: str) -> None:
|
|
10
|
-
super().__init__(path, name, "colorset")
|
|
11
|
+
def __init__(self, path: str | Path, name: str, sink: WriteSink) -> None:
|
|
12
|
+
super().__init__(path, name, "colorset", sink)
|
|
11
13
|
self._colors: list[AppleColor] = []
|
|
12
14
|
|
|
13
15
|
def color(self, *colors: AppleColor) -> Self:
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import shutil
|
|
3
|
+
from io import BytesIO
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
from typing import Self, final
|
|
4
6
|
|
|
5
7
|
from resforge._utils import require_context
|
|
8
|
+
from resforge.io import FileSystemSink, WriteSink
|
|
6
9
|
|
|
7
|
-
from ._base import write_contents
|
|
8
10
|
from ._colorset import ColorSet
|
|
9
11
|
from .types import AppleColor
|
|
10
12
|
|
|
@@ -21,39 +23,54 @@ class AssetCatalog:
|
|
|
21
23
|
Example:
|
|
22
24
|
>>> with AssetCatalog("App", "Assets") as assets:
|
|
23
25
|
... assets.colorset("primary", "#FF0000")
|
|
24
|
-
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
|
-
def __init__(
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
path: str | Path,
|
|
31
|
+
name: str,
|
|
32
|
+
sink: WriteSink | None = None,
|
|
33
|
+
) -> None:
|
|
28
34
|
"""Initializes the AssetCatalog.
|
|
29
35
|
|
|
30
36
|
Args:
|
|
31
37
|
path: The filesystem path where the asset catalog will be saved.
|
|
32
38
|
name: The name of the catalog (without .xcassets extension).
|
|
33
|
-
|
|
39
|
+
sink: The custom output to write data to. If None,
|
|
40
|
+
defaults to a standard file write.
|
|
34
41
|
"""
|
|
35
|
-
output_dir = Path(path).resolve()
|
|
36
|
-
self._temp_path = output_dir / f".tmp_{name}.xcassets"
|
|
37
|
-
self._final_path = output_dir / f"{name}.xcassets"
|
|
38
42
|
self._active = False
|
|
43
|
+
output_dir = Path(path)
|
|
44
|
+
self._final_path = output_dir / f"{name}.xcassets"
|
|
45
|
+
self._sink = sink or FileSystemSink()
|
|
46
|
+
self._temp_path = (
|
|
47
|
+
(output_dir / f".tmp_{name}.xcassets")
|
|
48
|
+
if isinstance(self._sink, FileSystemSink)
|
|
49
|
+
else self._final_path
|
|
50
|
+
)
|
|
39
51
|
|
|
40
52
|
def __enter__(self) -> Self:
|
|
41
53
|
self._active = True
|
|
42
|
-
if self.
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
if isinstance(self._sink, FileSystemSink):
|
|
55
|
+
if self._temp_path.exists():
|
|
56
|
+
shutil.rmtree(self._temp_path)
|
|
57
|
+
self._temp_path.mkdir(parents=True)
|
|
45
58
|
return self
|
|
46
59
|
|
|
47
60
|
def __exit__(self, exc_type: type[BaseException] | None, *_: object) -> None:
|
|
48
61
|
try:
|
|
49
62
|
if exc_type is None:
|
|
63
|
+
buf = BytesIO()
|
|
50
64
|
contents = {"info": {"author": "xcode", "version": 1}}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
buf.write(json.dumps(contents, indent=2).encode())
|
|
66
|
+
path = Path(self._temp_path) / "Contents.json"
|
|
67
|
+
self._sink.write(path, buf.getvalue())
|
|
68
|
+
if isinstance(self._sink, FileSystemSink):
|
|
69
|
+
if self._final_path.exists():
|
|
70
|
+
shutil.rmtree(self._final_path)
|
|
71
|
+
self._temp_path.rename(self._final_path)
|
|
55
72
|
finally:
|
|
56
|
-
if self._temp_path.exists():
|
|
73
|
+
if isinstance(self._sink, FileSystemSink) and self._temp_path.exists():
|
|
57
74
|
shutil.rmtree(self._temp_path)
|
|
58
75
|
self._active = False
|
|
59
76
|
|
|
@@ -66,6 +83,6 @@ class AssetCatalog:
|
|
|
66
83
|
*colors: One or more AppleColor definitions.
|
|
67
84
|
|
|
68
85
|
"""
|
|
69
|
-
with ColorSet(self._temp_path, name) as colorset:
|
|
86
|
+
with ColorSet(self._temp_path, name, self._sink) as colorset:
|
|
70
87
|
colorset.color(*colors)
|
|
71
88
|
return self
|
|
@@ -1,43 +1,16 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shutil
|
|
3
|
-
from collections.abc import
|
|
3
|
+
from collections.abc import Generator
|
|
4
4
|
from contextlib import contextmanager, suppress
|
|
5
|
-
from functools import wraps
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from tempfile import NamedTemporaryFile
|
|
8
|
-
from typing import IO,
|
|
7
|
+
from typing import IO, Protocol, runtime_checkable
|
|
9
8
|
|
|
10
|
-
__all__ = ["
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@runtime_checkable
|
|
14
|
-
class _HasActiveContext(Protocol):
|
|
15
|
-
_active: bool
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def require_context[T: _HasActiveContext, **P, R](
|
|
19
|
-
func: Callable[Concatenate[T, P], R],
|
|
20
|
-
) -> Callable[Concatenate[T, P], R]:
|
|
21
|
-
"""Ensures a method is only called within an active context.
|
|
22
|
-
|
|
23
|
-
Raises:
|
|
24
|
-
RuntimeError: If the method is called while the instance's
|
|
25
|
-
`_active` attribute is False.
|
|
26
|
-
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
@wraps(func)
|
|
30
|
-
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
31
|
-
if not self._active: # pyright: ignore[reportPrivateUsage]
|
|
32
|
-
msg = f"'{func.__name__}' requires an active 'with' context."
|
|
33
|
-
raise RuntimeError(msg)
|
|
34
|
-
return func(self, *args, **kwargs)
|
|
35
|
-
|
|
36
|
-
return wrapper
|
|
9
|
+
__all__ = ["FileSystemSink", "MemorySink", "WriteSink", "atomic_write"]
|
|
37
10
|
|
|
38
11
|
|
|
39
12
|
@contextmanager
|
|
40
|
-
def atomic_write(target_path: Path) -> Generator[IO[bytes], None, None]:
|
|
13
|
+
def atomic_write(target_path: str | Path) -> Generator[IO[bytes], None, None]:
|
|
41
14
|
"""Yields a temporary file, then atomically replaces target_path on success.
|
|
42
15
|
|
|
43
16
|
Preserves file permissions of target_path, if target_path already exists.
|
|
@@ -80,3 +53,32 @@ def atomic_write(target_path: Path) -> Generator[IO[bytes], None, None]:
|
|
|
80
53
|
if temp_path and temp_path.exists():
|
|
81
54
|
with suppress(OSError):
|
|
82
55
|
temp_path.unlink()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@runtime_checkable
|
|
59
|
+
class WriteSink(Protocol):
|
|
60
|
+
"""An interface for writing binary content to a file path."""
|
|
61
|
+
|
|
62
|
+
def write(self, path: Path, content: bytes) -> None:
|
|
63
|
+
"""Write the given binary content to the specified path."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FileSystemSink:
|
|
68
|
+
"""A write sink that writes directly to the file system."""
|
|
69
|
+
|
|
70
|
+
def write(self, path: Path, content: bytes) -> None:
|
|
71
|
+
"""Atomically write binary content to a physical file path."""
|
|
72
|
+
with atomic_write(path) as f:
|
|
73
|
+
f.write(content)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MemorySink:
|
|
77
|
+
"""A write sink that stores files in a dictionary."""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self.files: dict[str, bytes] = {}
|
|
81
|
+
|
|
82
|
+
def write(self, path: Path, content: bytes) -> None:
|
|
83
|
+
"""Store the binary content in memory using the string path as a key."""
|
|
84
|
+
self.files[str(path)] = content
|
|
File without changes
|
|
@@ -102,21 +102,3 @@ def test_top_level_property():
|
|
|
102
102
|
assert f.render() == (
|
|
103
103
|
"package com.example\n\nval primary: Color = Color(0xFF6200EE)\n"
|
|
104
104
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def test_file_write(tmp_path):
|
|
108
|
-
f = KotlinFile(package="com.example")
|
|
109
|
-
f.import_("androidx.compose.ui.graphics.Color")
|
|
110
|
-
f.member(
|
|
111
|
-
KotlinObject("AppColors").property(
|
|
112
|
-
"primary",
|
|
113
|
-
type_="Color",
|
|
114
|
-
value="Color(0xFF6200EE)",
|
|
115
|
-
),
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
output = tmp_path / "ui" / "theme" / "Color.kt"
|
|
119
|
-
f.write(output)
|
|
120
|
-
|
|
121
|
-
assert output.exists()
|
|
122
|
-
assert output.read_text() == f.render()
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import shutil
|
|
3
|
-
import typing
|
|
4
|
-
from abc import ABC, abstractmethod
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Self
|
|
7
|
-
|
|
8
|
-
from resforge._utils import atomic_write
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def write_contents(path: str | Path, contents: dict[str, typing.Any]) -> None:
|
|
12
|
-
path = Path(path) / "Contents.json"
|
|
13
|
-
with atomic_write(path) as tf:
|
|
14
|
-
tf.write(json.dumps(contents, indent=2).encode())
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class AssetNode(ABC):
|
|
18
|
-
def __init__(self, path: str | Path, name: str, extension: str) -> None:
|
|
19
|
-
self._path: Path = Path(path) / f"{name}.{extension}"
|
|
20
|
-
|
|
21
|
-
def __enter__(self) -> Self:
|
|
22
|
-
self._path.mkdir(parents=True, exist_ok=True)
|
|
23
|
-
return self
|
|
24
|
-
|
|
25
|
-
def __exit__(self, exc_type: type[BaseException] | None, *_: object) -> None:
|
|
26
|
-
if exc_type is None:
|
|
27
|
-
contents = self._create_contents()
|
|
28
|
-
write_contents(self._path, contents)
|
|
29
|
-
elif self._path.exists():
|
|
30
|
-
shutil.rmtree(self._path)
|
|
31
|
-
|
|
32
|
-
@abstractmethod
|
|
33
|
-
def _create_contents(self) -> dict[str, object]: ...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|