resforge 0.3.0__tar.gz → 0.4.0__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.3.0 → resforge-0.4.0}/.github/workflows/ci.yml +14 -16
- {resforge-0.3.0 → resforge-0.4.0}/LICENSE +1 -1
- {resforge-0.3.0 → resforge-0.4.0}/PKG-INFO +54 -41
- {resforge-0.3.0 → resforge-0.4.0}/README.md +50 -35
- {resforge-0.3.0 → resforge-0.4.0}/pyproject.toml +26 -44
- resforge-0.4.0/src/resforge/__init__.py +4 -0
- {resforge-0.3.0/resforge/codegen → resforge-0.4.0/src/resforge/_codegen}/kotlin.py +18 -9
- resforge-0.4.0/src/resforge/_utils.py +82 -0
- {resforge-0.3.0 → resforge-0.4.0/src}/resforge/android/__init__.py +2 -1
- {resforge-0.3.0 → resforge-0.4.0/src}/resforge/android/compose.py +55 -24
- {resforge-0.3.0 → resforge-0.4.0/src}/resforge/android/types.py +8 -3
- {resforge-0.3.0 → resforge-0.4.0/src}/resforge/android/values.py +29 -18
- {resforge-0.3.0 → resforge-0.4.0/src}/resforge/apple/__init__.py +1 -2
- {resforge-0.3.0 → resforge-0.4.0/src}/resforge/apple/_base.py +10 -7
- {resforge-0.3.0 → resforge-0.4.0/src}/resforge/apple/_colorset.py +12 -17
- {resforge-0.3.0 → resforge-0.4.0/src}/resforge/apple/catalog.py +7 -8
- resforge-0.4.0/src/resforge/apple/types.py +98 -0
- resforge-0.4.0/src/resforge/color.py +128 -0
- {resforge-0.3.0 → resforge-0.4.0}/tests/test_android_values.py +17 -9
- {resforge-0.3.0 → resforge-0.4.0}/tests/test_apple_catalog.py +6 -5
- resforge-0.4.0/tests/test_atomic_write.py +36 -0
- {resforge-0.3.0 → resforge-0.4.0}/tests/test_codegen_kotlin.py +9 -4
- resforge-0.4.0/tests/test_color.py +75 -0
- resforge-0.3.0/resforge/__init__.py +0 -3
- resforge-0.3.0/resforge/_utils.py +0 -24
- resforge-0.3.0/resforge/apple/types.py +0 -77
- resforge-0.3.0/resforge/types.py +0 -74
- resforge-0.3.0/tests/test_types.py +0 -53
- {resforge-0.3.0 → resforge-0.4.0}/.github/workflows/publish.yml +0 -0
- {resforge-0.3.0 → resforge-0.4.0}/.gitignore +0 -0
- {resforge-0.3.0/resforge/codegen → resforge-0.4.0/src/resforge/_codegen}/__init__.py +0 -0
- {resforge-0.3.0 → resforge-0.4.0}/tests/__init__.py +0 -0
- {resforge-0.3.0 → resforge-0.4.0}/tests/test_android_compose.py +0 -0
|
@@ -16,35 +16,33 @@ jobs:
|
|
|
16
16
|
steps:
|
|
17
17
|
- uses: actions/checkout@v5
|
|
18
18
|
|
|
19
|
-
- name: Set up Python
|
|
19
|
+
- name: Set up Python
|
|
20
20
|
uses: actions/setup-python@v5
|
|
21
21
|
with:
|
|
22
22
|
python-version: ${{ matrix.python-version }}
|
|
23
|
-
cache: "pip"
|
|
24
23
|
|
|
25
|
-
- name: Install
|
|
26
|
-
run:
|
|
27
|
-
pip install -e ".[dev]"
|
|
24
|
+
- name: Install Hatch
|
|
25
|
+
run: pip install hatch
|
|
28
26
|
|
|
29
27
|
- name: Run tests
|
|
30
|
-
run:
|
|
28
|
+
run: hatch run pytest
|
|
31
29
|
|
|
32
|
-
lint-
|
|
30
|
+
lint-and-check:
|
|
33
31
|
runs-on: ubuntu-latest
|
|
34
32
|
steps:
|
|
35
33
|
- uses: actions/checkout@v5
|
|
36
34
|
|
|
37
35
|
- name: Set up Python
|
|
38
36
|
uses: actions/setup-python@v5
|
|
39
|
-
with:
|
|
40
|
-
python-version: "3.12"
|
|
41
|
-
cache: "pip"
|
|
42
37
|
|
|
43
|
-
- name: Install
|
|
44
|
-
run: pip install
|
|
38
|
+
- name: Install Hatch
|
|
39
|
+
run: pip install hatch
|
|
40
|
+
|
|
41
|
+
- name: Lint
|
|
42
|
+
run: hatch run ruff check .
|
|
45
43
|
|
|
46
|
-
- name:
|
|
47
|
-
run:
|
|
44
|
+
- name: Type check
|
|
45
|
+
run: hatch run basedpyright
|
|
48
46
|
|
|
49
|
-
- name: Verify
|
|
50
|
-
run:
|
|
47
|
+
- name: Verify Build Metadata
|
|
48
|
+
run: hatch build
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: resforge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A type-safe Python DSL for cross-platform mobile resource generation
|
|
5
5
|
Project-URL: Homepage, https://kipila.dev
|
|
6
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
-
Author-email:
|
|
6
|
+
Project-URL: Repository, https://github.com/kipila-dev/resforge
|
|
7
|
+
Author-email: Kipila Ltd <hello@kipila.dev>
|
|
8
8
|
License: MIT
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Keywords: android-xml,automation,code-generation,design-tokens,dsl,mobile-infra,type-safety,white-label,xcassets
|
|
@@ -16,21 +16,21 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Classifier: Topic :: Software Development :: Build Tools
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
|
-
|
|
20
|
-
Requires-Dist: defusedxml; extra == 'dev'
|
|
21
|
-
Requires-Dist: pytest; extra == 'dev'
|
|
19
|
+
Requires-Dist: coloraide
|
|
22
20
|
Description-Content-Type: text/markdown
|
|
23
21
|
|
|
24
22
|
# resforge
|
|
25
23
|
|
|
26
|
-
A type-safe Python DSL for generating native Android and iOS resources from
|
|
24
|
+
A type-safe Python DSL for generating native Android and iOS resources from
|
|
25
|
+
design tokens.
|
|
27
26
|
|
|
28
27
|
## Features
|
|
29
28
|
|
|
30
29
|
- Fluent, Pythonic API
|
|
31
|
-
- Jetpack Compose theme generation
|
|
30
|
+
- Jetpack Compose theme generation for colors and dimensions
|
|
32
31
|
- Supports all Android `res/values/` types
|
|
33
|
-
- Native Apple Asset Catalog (`.xcassets`)
|
|
32
|
+
- Native Apple Asset Catalog (`.xcassets`) support
|
|
33
|
+
- Wide Gamut support with automatic sRGB fallbacks
|
|
34
34
|
- Built-in validation for resource names and color formats
|
|
35
35
|
|
|
36
36
|
## Installation
|
|
@@ -44,27 +44,30 @@ pip install resforge
|
|
|
44
44
|
### Android (Jetpack Compose)
|
|
45
45
|
|
|
46
46
|
```python
|
|
47
|
-
from resforge.android import ComposeWriter
|
|
48
|
-
|
|
49
|
-
with ComposeWriter("
|
|
50
|
-
compose.
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
from resforge.android import ComposeWriter
|
|
48
|
+
|
|
49
|
+
with ComposeWriter("Color.kt", "com.example.myapplication.ui.theme") as compose:
|
|
50
|
+
compose.color(
|
|
51
|
+
Purple80="#D0BCFF",
|
|
52
|
+
PurpleGrey80="#CCC2DC",
|
|
53
|
+
Pink80="#EFB8C8",
|
|
54
|
+
Purple40="#6650a4",
|
|
55
|
+
PurpleGrey40="#625b71",
|
|
56
|
+
Pink40="#7D5260",
|
|
57
|
+
)
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
```kotlin
|
|
56
|
-
package
|
|
61
|
+
package com.example.myapplication.ui.theme
|
|
57
62
|
|
|
58
63
|
import androidx.compose.ui.graphics.Color
|
|
59
|
-
import androidx.compose.ui.unit.Dp
|
|
60
|
-
import androidx.compose.ui.unit.dp
|
|
61
|
-
|
|
62
|
-
val border: Dp = 8.dp
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
val Purple80: Color = Color(0xFFD0BCFF)
|
|
66
|
+
val PurpleGrey80: Color = Color(0xFFCCC2DC)
|
|
67
|
+
val Pink80: Color = Color(0xFFEFB8C8)
|
|
68
|
+
val Purple40: Color = Color(0xFF6650A4)
|
|
69
|
+
val PurpleGrey40: Color = Color(0xFF625B71)
|
|
70
|
+
val Pink40: Color = Color(0xFF7D5260)
|
|
68
71
|
```
|
|
69
72
|
|
|
70
73
|
### Android (XML Resources)
|
|
@@ -77,12 +80,10 @@ with ValuesWriter("res/values/resources.xml") as res:
|
|
|
77
80
|
app_name="My App",
|
|
78
81
|
welcome_message="Welcome!",
|
|
79
82
|
)
|
|
80
|
-
|
|
81
83
|
res.color(
|
|
82
|
-
primary="#
|
|
83
|
-
secondary="#
|
|
84
|
+
primary="#6200EE",
|
|
85
|
+
secondary="#03DAC5",
|
|
84
86
|
)
|
|
85
|
-
|
|
86
87
|
res.dimension(
|
|
87
88
|
padding_small=dp(8),
|
|
88
89
|
text_body=sp(16),
|
|
@@ -90,13 +91,12 @@ with ValuesWriter("res/values/resources.xml") as res:
|
|
|
90
91
|
```
|
|
91
92
|
|
|
92
93
|
```xml
|
|
94
|
+
<?xml version='1.0' encoding='utf-8'?>
|
|
93
95
|
<resources>
|
|
94
96
|
<string name="app_name">My App</string>
|
|
95
97
|
<string name="welcome_message">Welcome!</string>
|
|
96
|
-
|
|
97
98
|
<color name="primary">#FF6200EE</color>
|
|
98
|
-
<color name="secondary">#
|
|
99
|
-
|
|
99
|
+
<color name="secondary">#FF00FF00</color>
|
|
100
100
|
<dimen name="padding_small">8dp</dimen>
|
|
101
101
|
<dimen name="text_body">16sp</dimen>
|
|
102
102
|
</resources>
|
|
@@ -105,15 +105,15 @@ with ValuesWriter("res/values/resources.xml") as res:
|
|
|
105
105
|
### Asset Catalog (iOS)
|
|
106
106
|
|
|
107
107
|
```python
|
|
108
|
+
from resforge import Color
|
|
108
109
|
from resforge.apple import Appearance, AppleColor, AssetCatalog
|
|
109
110
|
|
|
110
|
-
with AssetCatalog("App", "Assets") as
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
|
|
111
|
+
with AssetCatalog("App", "Assets") as catalog:
|
|
112
|
+
catalog.colorset(
|
|
113
|
+
"MyColor",
|
|
114
|
+
AppleColor(Color.p3(0.0, 1.0, 0.0)),
|
|
114
115
|
AppleColor("#000000", appearances=[Appearance.Dark]),
|
|
115
116
|
)
|
|
116
|
-
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
```json
|
|
@@ -127,14 +127,27 @@ with AssetCatalog("App", "Assets") as ac:
|
|
|
127
127
|
"idiom": "universal",
|
|
128
128
|
"color": {
|
|
129
129
|
"components": {
|
|
130
|
-
"red": "
|
|
130
|
+
"red": "0.000",
|
|
131
131
|
"green": "1.000",
|
|
132
|
-
"blue": "
|
|
132
|
+
"blue": "0.000",
|
|
133
133
|
"alpha": "1.000"
|
|
134
134
|
},
|
|
135
135
|
"color-space": "srgb"
|
|
136
136
|
}
|
|
137
137
|
},
|
|
138
|
+
{
|
|
139
|
+
"idiom": "universal",
|
|
140
|
+
"color": {
|
|
141
|
+
"components": {
|
|
142
|
+
"red": "0.000",
|
|
143
|
+
"green": "1.000",
|
|
144
|
+
"blue": "0.000",
|
|
145
|
+
"alpha": "1.000"
|
|
146
|
+
},
|
|
147
|
+
"color-space": "display-p3"
|
|
148
|
+
},
|
|
149
|
+
"display-gamut": "display-P3"
|
|
150
|
+
},
|
|
138
151
|
{
|
|
139
152
|
"idiom": "universal",
|
|
140
153
|
"color": {
|
|
@@ -159,9 +172,9 @@ with AssetCatalog("App", "Assets") as ac:
|
|
|
159
172
|
|
|
160
173
|
## Roadmap
|
|
161
174
|
|
|
162
|
-
- SwiftUI
|
|
163
|
-
-
|
|
164
|
-
-
|
|
175
|
+
- SwiftUI dimensions
|
|
176
|
+
- Typography
|
|
177
|
+
- Images (ImageSet, IconSet, `res/drawable` vectors)
|
|
165
178
|
|
|
166
179
|
## License
|
|
167
180
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# resforge
|
|
2
2
|
|
|
3
|
-
A type-safe Python DSL for generating native Android and iOS resources from
|
|
3
|
+
A type-safe Python DSL for generating native Android and iOS resources from
|
|
4
|
+
design tokens.
|
|
4
5
|
|
|
5
6
|
## Features
|
|
6
7
|
|
|
7
8
|
- Fluent, Pythonic API
|
|
8
|
-
- Jetpack Compose theme generation
|
|
9
|
+
- Jetpack Compose theme generation for colors and dimensions
|
|
9
10
|
- Supports all Android `res/values/` types
|
|
10
|
-
- Native Apple Asset Catalog (`.xcassets`)
|
|
11
|
+
- Native Apple Asset Catalog (`.xcassets`) support
|
|
12
|
+
- Wide Gamut support with automatic sRGB fallbacks
|
|
11
13
|
- Built-in validation for resource names and color formats
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
@@ -21,27 +23,30 @@ pip install resforge
|
|
|
21
23
|
### Android (Jetpack Compose)
|
|
22
24
|
|
|
23
25
|
```python
|
|
24
|
-
from resforge.android import ComposeWriter
|
|
25
|
-
|
|
26
|
-
with ComposeWriter("
|
|
27
|
-
compose.
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
from resforge.android import ComposeWriter
|
|
27
|
+
|
|
28
|
+
with ComposeWriter("Color.kt", "com.example.myapplication.ui.theme") as compose:
|
|
29
|
+
compose.color(
|
|
30
|
+
Purple80="#D0BCFF",
|
|
31
|
+
PurpleGrey80="#CCC2DC",
|
|
32
|
+
Pink80="#EFB8C8",
|
|
33
|
+
Purple40="#6650a4",
|
|
34
|
+
PurpleGrey40="#625b71",
|
|
35
|
+
Pink40="#7D5260",
|
|
36
|
+
)
|
|
30
37
|
```
|
|
31
38
|
|
|
32
39
|
```kotlin
|
|
33
|
-
package
|
|
40
|
+
package com.example.myapplication.ui.theme
|
|
34
41
|
|
|
35
42
|
import androidx.compose.ui.graphics.Color
|
|
36
|
-
import androidx.compose.ui.unit.Dp
|
|
37
|
-
import androidx.compose.ui.unit.dp
|
|
38
|
-
|
|
39
|
-
val border: Dp = 8.dp
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
val Purple80: Color = Color(0xFFD0BCFF)
|
|
45
|
+
val PurpleGrey80: Color = Color(0xFFCCC2DC)
|
|
46
|
+
val Pink80: Color = Color(0xFFEFB8C8)
|
|
47
|
+
val Purple40: Color = Color(0xFF6650A4)
|
|
48
|
+
val PurpleGrey40: Color = Color(0xFF625B71)
|
|
49
|
+
val Pink40: Color = Color(0xFF7D5260)
|
|
45
50
|
```
|
|
46
51
|
|
|
47
52
|
### Android (XML Resources)
|
|
@@ -54,12 +59,10 @@ with ValuesWriter("res/values/resources.xml") as res:
|
|
|
54
59
|
app_name="My App",
|
|
55
60
|
welcome_message="Welcome!",
|
|
56
61
|
)
|
|
57
|
-
|
|
58
62
|
res.color(
|
|
59
|
-
primary="#
|
|
60
|
-
secondary="#
|
|
63
|
+
primary="#6200EE",
|
|
64
|
+
secondary="#03DAC5",
|
|
61
65
|
)
|
|
62
|
-
|
|
63
66
|
res.dimension(
|
|
64
67
|
padding_small=dp(8),
|
|
65
68
|
text_body=sp(16),
|
|
@@ -67,13 +70,12 @@ with ValuesWriter("res/values/resources.xml") as res:
|
|
|
67
70
|
```
|
|
68
71
|
|
|
69
72
|
```xml
|
|
73
|
+
<?xml version='1.0' encoding='utf-8'?>
|
|
70
74
|
<resources>
|
|
71
75
|
<string name="app_name">My App</string>
|
|
72
76
|
<string name="welcome_message">Welcome!</string>
|
|
73
|
-
|
|
74
77
|
<color name="primary">#FF6200EE</color>
|
|
75
|
-
<color name="secondary">#
|
|
76
|
-
|
|
78
|
+
<color name="secondary">#FF00FF00</color>
|
|
77
79
|
<dimen name="padding_small">8dp</dimen>
|
|
78
80
|
<dimen name="text_body">16sp</dimen>
|
|
79
81
|
</resources>
|
|
@@ -82,15 +84,15 @@ with ValuesWriter("res/values/resources.xml") as res:
|
|
|
82
84
|
### Asset Catalog (iOS)
|
|
83
85
|
|
|
84
86
|
```python
|
|
87
|
+
from resforge import Color
|
|
85
88
|
from resforge.apple import Appearance, AppleColor, AssetCatalog
|
|
86
89
|
|
|
87
|
-
with AssetCatalog("App", "Assets") as
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
90
|
+
with AssetCatalog("App", "Assets") as catalog:
|
|
91
|
+
catalog.colorset(
|
|
92
|
+
"MyColor",
|
|
93
|
+
AppleColor(Color.p3(0.0, 1.0, 0.0)),
|
|
91
94
|
AppleColor("#000000", appearances=[Appearance.Dark]),
|
|
92
95
|
)
|
|
93
|
-
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
```json
|
|
@@ -104,14 +106,27 @@ with AssetCatalog("App", "Assets") as ac:
|
|
|
104
106
|
"idiom": "universal",
|
|
105
107
|
"color": {
|
|
106
108
|
"components": {
|
|
107
|
-
"red": "
|
|
109
|
+
"red": "0.000",
|
|
108
110
|
"green": "1.000",
|
|
109
|
-
"blue": "
|
|
111
|
+
"blue": "0.000",
|
|
110
112
|
"alpha": "1.000"
|
|
111
113
|
},
|
|
112
114
|
"color-space": "srgb"
|
|
113
115
|
}
|
|
114
116
|
},
|
|
117
|
+
{
|
|
118
|
+
"idiom": "universal",
|
|
119
|
+
"color": {
|
|
120
|
+
"components": {
|
|
121
|
+
"red": "0.000",
|
|
122
|
+
"green": "1.000",
|
|
123
|
+
"blue": "0.000",
|
|
124
|
+
"alpha": "1.000"
|
|
125
|
+
},
|
|
126
|
+
"color-space": "display-p3"
|
|
127
|
+
},
|
|
128
|
+
"display-gamut": "display-P3"
|
|
129
|
+
},
|
|
115
130
|
{
|
|
116
131
|
"idiom": "universal",
|
|
117
132
|
"color": {
|
|
@@ -136,9 +151,9 @@ with AssetCatalog("App", "Assets") as ac:
|
|
|
136
151
|
|
|
137
152
|
## Roadmap
|
|
138
153
|
|
|
139
|
-
- SwiftUI
|
|
140
|
-
-
|
|
141
|
-
-
|
|
154
|
+
- SwiftUI dimensions
|
|
155
|
+
- Typography
|
|
156
|
+
- Images (ImageSet, IconSet, `res/drawable` vectors)
|
|
142
157
|
|
|
143
158
|
## License
|
|
144
159
|
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "resforge"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "A type-safe Python DSL for cross-platform mobile resource generation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
|
-
authors = [{ name = "
|
|
12
|
+
authors = [{ name = "Kipila Ltd", email = "hello@kipila.dev" }]
|
|
13
13
|
keywords = [
|
|
14
14
|
"dsl",
|
|
15
15
|
"design-tokens",
|
|
@@ -30,60 +30,42 @@ classifiers = [
|
|
|
30
30
|
"Intended Audience :: Developers",
|
|
31
31
|
"Topic :: Software Development :: Build Tools",
|
|
32
32
|
]
|
|
33
|
-
|
|
34
|
-
[project.optional-dependencies]
|
|
35
|
-
dev = ["pytest", "defusedxml"]
|
|
33
|
+
dependencies = ["coloraide"]
|
|
36
34
|
|
|
37
35
|
[project.urls]
|
|
38
36
|
Homepage = "https://kipila.dev"
|
|
39
|
-
Repository = "https://github.com/
|
|
37
|
+
Repository = "https://github.com/kipila-dev/resforge"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.version]
|
|
40
|
+
path = "src/resforge/__init__.py"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.envs.default]
|
|
43
|
+
dependencies = ["basedpyright", "defusedxml", "pytest", "ruff"]
|
|
40
44
|
|
|
41
45
|
[tool.ruff]
|
|
42
46
|
target-version = "py312"
|
|
43
|
-
line-length = 120
|
|
44
47
|
|
|
45
48
|
[tool.ruff.lint]
|
|
46
49
|
select = ["ALL"]
|
|
47
|
-
ignore = [
|
|
50
|
+
ignore = ["D100", "D104", "D105", "ANN401", "PLR2004", "E741"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
53
|
+
"tests/**/*.py" = [
|
|
48
54
|
"ANN001",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"COM812",
|
|
52
|
-
"COM819",
|
|
53
|
-
"D100",
|
|
55
|
+
"ANN201",
|
|
56
|
+
"D101",
|
|
54
57
|
"D102",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"D300",
|
|
60
|
-
"D401",
|
|
61
|
-
"E111",
|
|
62
|
-
"E114",
|
|
63
|
-
"E117",
|
|
64
|
-
"FIX",
|
|
65
|
-
"I001",
|
|
66
|
-
"PLR2004",
|
|
67
|
-
"Q000",
|
|
68
|
-
"Q001",
|
|
69
|
-
"Q002",
|
|
70
|
-
"Q003",
|
|
71
|
-
"TD",
|
|
72
|
-
"W191",
|
|
58
|
+
"D103",
|
|
59
|
+
"EM101",
|
|
60
|
+
"S101",
|
|
61
|
+
"TRY301",
|
|
73
62
|
]
|
|
74
|
-
fixable = ["ALL"]
|
|
75
63
|
|
|
76
|
-
[tool.ruff.lint.
|
|
77
|
-
|
|
78
|
-
"ANN201",
|
|
79
|
-
"D101",
|
|
80
|
-
"D103",
|
|
81
|
-
"EM101",
|
|
82
|
-
"S101",
|
|
83
|
-
"TRY301",
|
|
84
|
-
]
|
|
64
|
+
[tool.ruff.lint.pydocstyle]
|
|
65
|
+
convention = "google"
|
|
85
66
|
|
|
86
67
|
[tool.basedpyright]
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
68
|
+
include = ["src"]
|
|
69
|
+
typeCheckingMode = "recommended"
|
|
70
|
+
pythonVersion = "3.12"
|
|
71
|
+
reportUnusedCallResult = "none"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Literal, Self
|
|
3
|
+
from typing import IO, Literal, Self, final
|
|
4
4
|
|
|
5
5
|
PropertyMutability = Literal["val", "var"]
|
|
6
6
|
|
|
@@ -18,6 +18,7 @@ class KotlinProperty:
|
|
|
18
18
|
return f"{self.mutability} {self.name}: {self.type_} = {self.value}"
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
@final
|
|
21
22
|
class KotlinObject:
|
|
22
23
|
"""A Kotlin object declaration."""
|
|
23
24
|
|
|
@@ -33,7 +34,7 @@ class KotlinObject:
|
|
|
33
34
|
mutability: PropertyMutability = "val",
|
|
34
35
|
) -> Self:
|
|
35
36
|
self._properties.append(
|
|
36
|
-
KotlinProperty(name=name, type_=type_, value=value, mutability=mutability)
|
|
37
|
+
KotlinProperty(name=name, type_=type_, value=value, mutability=mutability),
|
|
37
38
|
)
|
|
38
39
|
return self
|
|
39
40
|
|
|
@@ -47,6 +48,7 @@ class KotlinObject:
|
|
|
47
48
|
return "\n".join(lines)
|
|
48
49
|
|
|
49
50
|
|
|
51
|
+
@final
|
|
50
52
|
class KotlinFile:
|
|
51
53
|
"""A Kotlin file declaration."""
|
|
52
54
|
|
|
@@ -60,7 +62,11 @@ class KotlinFile:
|
|
|
60
62
|
return self
|
|
61
63
|
|
|
62
64
|
def property(
|
|
63
|
-
self,
|
|
65
|
+
self,
|
|
66
|
+
name: str,
|
|
67
|
+
type_: str,
|
|
68
|
+
value: str,
|
|
69
|
+
mutability: PropertyMutability = "val",
|
|
64
70
|
) -> Self:
|
|
65
71
|
self._members.append(KotlinProperty(name, type_, value, mutability))
|
|
66
72
|
return self
|
|
@@ -75,7 +81,7 @@ class KotlinFile:
|
|
|
75
81
|
if self._imports:
|
|
76
82
|
sections.append("\n".join(f"import {fqn}" for fqn in sorted(self._imports)))
|
|
77
83
|
|
|
78
|
-
current_props = []
|
|
84
|
+
current_props: list[str] = []
|
|
79
85
|
for member in self._members:
|
|
80
86
|
if isinstance(member, KotlinProperty):
|
|
81
87
|
current_props.append(member.render())
|
|
@@ -90,8 +96,11 @@ class KotlinFile:
|
|
|
90
96
|
|
|
91
97
|
return "\n\n".join(sections) + "\n"
|
|
92
98
|
|
|
93
|
-
def write(self,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
def write(self, dest: str | Path | IO[bytes]) -> None:
|
|
100
|
+
if isinstance(dest, (str, Path)):
|
|
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())
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from collections.abc import Callable, Generator
|
|
4
|
+
from contextlib import contextmanager, suppress
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import NamedTemporaryFile
|
|
8
|
+
from typing import IO, Concatenate, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
__all__ = ["atomic_write", "require_context"]
|
|
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
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@contextmanager
|
|
40
|
+
def atomic_write(target_path: Path) -> Generator[IO[bytes], None, None]:
|
|
41
|
+
"""Yields a temporary file, then atomically replaces target_path on success.
|
|
42
|
+
|
|
43
|
+
Preserves file permissions of target_path, if target_path already exists.
|
|
44
|
+
"""
|
|
45
|
+
target_path = Path(target_path)
|
|
46
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
temp_path = None
|
|
49
|
+
try:
|
|
50
|
+
with NamedTemporaryFile(
|
|
51
|
+
dir=target_path.parent,
|
|
52
|
+
delete=False,
|
|
53
|
+
suffix=".tmp",
|
|
54
|
+
) as tf:
|
|
55
|
+
temp_path = Path(tf.name)
|
|
56
|
+
yield tf
|
|
57
|
+
|
|
58
|
+
tf.flush()
|
|
59
|
+
os.fsync(tf.fileno())
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
shutil.copymode(target_path, temp_path)
|
|
63
|
+
except OSError:
|
|
64
|
+
mask = os.umask(0)
|
|
65
|
+
os.umask(mask)
|
|
66
|
+
temp_path.chmod(0o666 & ~mask)
|
|
67
|
+
|
|
68
|
+
temp_path.replace(target_path)
|
|
69
|
+
|
|
70
|
+
if os.name == "posix":
|
|
71
|
+
flags = os.O_RDONLY
|
|
72
|
+
if hasattr(os, "O_DIRECTORY"):
|
|
73
|
+
flags |= os.O_DIRECTORY
|
|
74
|
+
dir_fd = os.open(target_path.parent, flags)
|
|
75
|
+
try:
|
|
76
|
+
os.fsync(dir_fd)
|
|
77
|
+
finally:
|
|
78
|
+
os.close(dir_fd)
|
|
79
|
+
finally:
|
|
80
|
+
if temp_path and temp_path.exists():
|
|
81
|
+
with suppress(OSError):
|
|
82
|
+
temp_path.unlink()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .compose import ComposeWriter
|
|
2
|
-
from .types import Dimension, PluralValues, dp, inch, mm, pt, px, sp
|
|
2
|
+
from .types import Dimension, PluralValues, dp, em, inch, mm, pt, px, sp
|
|
3
3
|
from .values import ValuesWriter
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
@@ -8,6 +8,7 @@ __all__ = [
|
|
|
8
8
|
"PluralValues",
|
|
9
9
|
"ValuesWriter",
|
|
10
10
|
"dp",
|
|
11
|
+
"em",
|
|
11
12
|
"inch",
|
|
12
13
|
"mm",
|
|
13
14
|
"pt",
|