resforge 0.4.2__tar.gz → 0.4.4__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.
Files changed (33) hide show
  1. {resforge-0.4.2 → resforge-0.4.4}/.gitignore +43 -29
  2. {resforge-0.4.2 → resforge-0.4.4}/PKG-INFO +6 -13
  3. {resforge-0.4.2 → resforge-0.4.4}/README.md +2 -8
  4. {resforge-0.4.2 → resforge-0.4.4}/pyproject.toml +9 -34
  5. resforge-0.4.4/src/resforge/__init__.py +7 -0
  6. resforge-0.4.4/src/resforge/_codegen/__init__.py +2 -0
  7. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/_codegen/kotlin.py +8 -0
  8. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/_utils.py +3 -0
  9. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/android/__init__.py +3 -0
  10. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/android/compose.py +15 -6
  11. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/android/types.py +3 -0
  12. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/android/values.py +7 -4
  13. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/apple/__init__.py +3 -0
  14. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/apple/_base.py +3 -0
  15. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/apple/_colorset.py +3 -0
  16. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/apple/catalog.py +5 -1
  17. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/apple/types.py +3 -0
  18. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/color.py +17 -0
  19. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/io.py +4 -1
  20. resforge-0.4.4/tests/__init__.py +2 -0
  21. {resforge-0.4.2 → resforge-0.4.4}/tests/test_android_compose.py +19 -10
  22. {resforge-0.4.2 → resforge-0.4.4}/tests/test_android_values.py +4 -2
  23. {resforge-0.4.2 → resforge-0.4.4}/tests/test_apple_catalog.py +12 -7
  24. {resforge-0.4.2 → resforge-0.4.4}/tests/test_atomic_write.py +8 -4
  25. {resforge-0.4.2 → resforge-0.4.4}/tests/test_codegen_kotlin.py +9 -0
  26. {resforge-0.4.2 → resforge-0.4.4}/tests/test_color.py +3 -1
  27. resforge-0.4.2/.github/workflows/ci.yml +0 -48
  28. resforge-0.4.2/.github/workflows/publish.yml +0 -29
  29. resforge-0.4.2/LICENSE +0 -21
  30. resforge-0.4.2/src/resforge/__init__.py +0 -4
  31. resforge-0.4.2/src/resforge/_codegen/__init__.py +0 -0
  32. resforge-0.4.2/tests/__init__.py +0 -0
  33. {resforge-0.4.2 → resforge-0.4.4}/src/resforge/py.typed +0 -0
@@ -27,8 +27,8 @@ share/python-wheels/
27
27
  MANIFEST
28
28
 
29
29
  # PyInstaller
30
- # Usually these files are written by a python script from a template
31
- # before PyInstaller builds the exe, so as to inject date/other infos into it.
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
32
  *.manifest
33
33
  *.spec
34
34
 
@@ -92,34 +92,34 @@ ipython_config.py
92
92
  # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
93
  # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
94
  # install all needed dependencies.
95
- #Pipfile.lock
95
+ # Pipfile.lock
96
96
 
97
97
  # UV
98
98
  # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
99
  # This is especially recommended for binary packages to ensure reproducibility, and is more
100
100
  # commonly ignored for libraries.
101
- #uv.lock
101
+ # uv.lock
102
102
 
103
103
  # poetry
104
104
  # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
105
  # This is especially recommended for binary packages to ensure reproducibility, and is more
106
106
  # commonly ignored for libraries.
107
107
  # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
- #poetry.lock
109
- #poetry.toml
108
+ # poetry.lock
109
+ # poetry.toml
110
110
 
111
111
  # pdm
112
112
  # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
113
  # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
114
  # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
- #pdm.lock
116
- #pdm.toml
115
+ # pdm.lock
116
+ # pdm.toml
117
117
  .pdm-python
118
118
  .pdm-build/
119
119
 
120
120
  # pixi
121
121
  # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
- #pixi.lock
122
+ # pixi.lock
123
123
  # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
124
  # in the .venv directory. It is recommended not to include this directory in version control.
125
125
  .pixi
@@ -131,6 +131,19 @@ __pypackages__/
131
131
  celerybeat-schedule
132
132
  celerybeat.pid
133
133
 
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
134
147
  # SageMath parsed files
135
148
  *.sage.py
136
149
 
@@ -169,24 +182,26 @@ dmypy.json
169
182
  cython_debug/
170
183
 
171
184
  # PyCharm
172
- # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
- # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
- # and can be added to the global gitignore or merged into this file. For a more nuclear
175
- # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
- #.idea/
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
177
190
 
178
191
  # Abstra
179
- # Abstra is an AI-powered process automation framework.
180
- # Ignore directories containing user credentials, local state, and settings.
181
- # Learn more at https://abstra.io/docs
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
182
195
  .abstra/
183
196
 
184
197
  # Visual Studio Code
185
- # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
- # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
- # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
- # you could uncomment the following to ignore the entire vscode folder
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
189
202
  # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
190
205
 
191
206
  # Ruff stuff:
192
207
  .ruff_cache/
@@ -194,17 +209,16 @@ cython_debug/
194
209
  # PyPI configuration file
195
210
  .pypirc
196
211
 
197
- # Cursor
198
- # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
- # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
- # refer to https://docs.cursor.com/context/ignore-files
201
- .cursorignore
202
- .cursorindexingignore
203
-
204
212
  # Marimo
205
213
  marimo/_static/
206
214
  marimo/_lsp/
207
215
  __marimo__/
208
216
 
209
- pyrightconfig.json
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+
220
+ # Forje build files
221
+ *.forje
222
+
223
+ # macOS
210
224
  .DS_Store
@@ -1,15 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: resforge
3
- Version: 0.4.2
3
+ Version: 0.4.4
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/kipila-dev/resforge
6
+ Project-URL: Repository, https://github.com/kipila-dev/forje
7
7
  Author-email: Kipila Ltd <hello@kipila.dev>
8
- License: MIT
9
- License-File: LICENSE
8
+ License-Expression: Apache-2.0
10
9
  Keywords: android-xml,automation,code-generation,design-tokens,dsl,mobile-infra,type-safety,white-label,xcassets
11
10
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: License :: OSI Approved :: Apache Software License
13
12
  Classifier: Programming Language :: Python :: 3
14
13
  Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Programming Language :: Python :: 3.13
@@ -79,12 +78,10 @@ with ValuesWriter("res/values/resources.xml") as res:
79
78
  res.string(
80
79
  app_name="My App",
81
80
  welcome_message="Welcome!",
82
- )
83
- res.color(
81
+ ).color(
84
82
  primary="#6200EE",
85
83
  secondary="#03DAC5",
86
- )
87
- res.dimension(
84
+ ).dimension(
88
85
  padding_small=dp(8),
89
86
  text_body=sp(16),
90
87
  )
@@ -175,7 +172,3 @@ with AssetCatalog("App", "Assets") as catalog:
175
172
  - SwiftUI dimensions
176
173
  - Typography
177
174
  - Images (ImageSet, IconSet, `res/drawable` vectors)
178
-
179
- ## License
180
-
181
- MIT
@@ -58,12 +58,10 @@ with ValuesWriter("res/values/resources.xml") as res:
58
58
  res.string(
59
59
  app_name="My App",
60
60
  welcome_message="Welcome!",
61
- )
62
- res.color(
61
+ ).color(
63
62
  primary="#6200EE",
64
63
  secondary="#03DAC5",
65
- )
66
- res.dimension(
64
+ ).dimension(
67
65
  padding_small=dp(8),
68
66
  text_body=sp(16),
69
67
  )
@@ -154,7 +152,3 @@ with AssetCatalog("App", "Assets") as catalog:
154
152
  - SwiftUI dimensions
155
153
  - Typography
156
154
  - Images (ImageSet, IconSet, `res/drawable` vectors)
157
-
158
- ## License
159
-
160
- MIT
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  [build-system]
2
5
  requires = ["hatchling"]
3
6
  build-backend = "hatchling.build"
@@ -8,7 +11,7 @@ dynamic = ["version"]
8
11
  description = "A type-safe Python DSL for cross-platform mobile resource generation"
9
12
  readme = "README.md"
10
13
  requires-python = ">=3.12"
11
- license = { text = "MIT" }
14
+ license = "Apache-2.0"
12
15
  authors = [{ name = "Kipila Ltd", email = "hello@kipila.dev" }]
13
16
  keywords = [
14
17
  "dsl",
@@ -26,7 +29,7 @@ classifiers = [
26
29
  "Programming Language :: Python :: 3.12",
27
30
  "Programming Language :: Python :: 3.13",
28
31
  "Programming Language :: Python :: 3.14",
29
- "License :: OSI Approved :: MIT License",
32
+ "License :: OSI Approved :: Apache Software License",
30
33
  "Intended Audience :: Developers",
31
34
  "Topic :: Software Development :: Build Tools",
32
35
  ]
@@ -34,38 +37,10 @@ dependencies = ["coloraide"]
34
37
 
35
38
  [project.urls]
36
39
  Homepage = "https://kipila.dev"
37
- Repository = "https://github.com/kipila-dev/resforge"
40
+ Repository = "https://github.com/kipila-dev/forje"
41
+
42
+ [dependency-groups]
43
+ dev = ["defusedxml"]
38
44
 
39
45
  [tool.hatch.version]
40
46
  path = "src/resforge/__init__.py"
41
-
42
- [tool.hatch.envs.default]
43
- dependencies = ["basedpyright", "defusedxml", "pytest", "ruff"]
44
-
45
- [tool.ruff]
46
- target-version = "py312"
47
-
48
- [tool.ruff.lint]
49
- select = ["ALL"]
50
- ignore = ["D100", "D104", "D105", "D107", "ANN401", "PLR2004", "E741"]
51
-
52
- [tool.ruff.lint.extend-per-file-ignores]
53
- "tests/**/*.py" = [
54
- "ANN001",
55
- "ANN201",
56
- "D101",
57
- "D102",
58
- "D103",
59
- "EM101",
60
- "S101",
61
- "TRY301",
62
- ]
63
-
64
- [tool.ruff.lint.pydocstyle]
65
- convention = "google"
66
-
67
- [tool.basedpyright]
68
- include = ["src"]
69
- typeCheckingMode = "recommended"
70
- pythonVersion = "3.12"
71
- reportUnusedCallResult = "none"
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .color import Color
5
+
6
+ __all__ = ["Color"]
7
+ __version__ = "0.4.4"
@@ -0,0 +1,2 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from dataclasses import dataclass
2
5
  from typing import IO, Literal, Self, final
3
6
 
@@ -52,6 +55,11 @@ class KotlinFile:
52
55
  """A Kotlin file declaration."""
53
56
 
54
57
  def __init__(self, package: str) -> None:
58
+ package = package.strip()
59
+ if not package:
60
+ msg = "Package must not be empty"
61
+ raise ValueError(msg)
62
+
55
63
  self._package = package
56
64
  self._imports: set[str] = set()
57
65
  self._members: list[KotlinProperty | KotlinObject] = []
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from collections.abc import Callable
2
5
  from functools import wraps
3
6
  from typing import Concatenate, Protocol, runtime_checkable
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from .compose import ComposeWriter
2
5
  from .types import Dimension, PluralValues, dp, em, inch, mm, pt, px, sp
3
6
  from .values import ValuesWriter
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from __future__ import annotations
2
5
 
3
6
  from io import BytesIO
@@ -40,7 +43,7 @@ class _BaseComposeScope:
40
43
 
41
44
  def _to_compose_color_literal(self, color: Color) -> str:
42
45
  if color.in_srgb_gamut():
43
- return f"Color({color.to_srgb_argb_hex(prefix="0x")})"
46
+ return f"Color({color.to_srgb_argb_hex(prefix='0x')})"
44
47
 
45
48
  self._ctx.add_imports("androidx.compose.ui.graphics.colorspace.ColorSpaces")
46
49
  r, g, b, a = color.to_p3_components()
@@ -100,16 +103,16 @@ class _ObjectScope(_BaseComposeScope):
100
103
 
101
104
  @final
102
105
  class ComposeWriter(_BaseComposeScope):
103
- """A fluent context manager for generating Jetpack Compose color definitions.
106
+ """A context manager for generating Jetpack Compose color definitions.
104
107
 
105
108
  Writes a Kotlin file containing typed Color or Dimension properties.
106
109
 
107
110
  Example:
108
111
  >>> with ComposeWriter(
109
- "ui/theme/Color.kt",
110
- package="com.example.theme",
111
- object_name="AppColors"
112
- ) as compose:
112
+ ... "ui/theme/Color.kt",
113
+ ... package="com.example.theme",
114
+ ... object_name="AppColors",
115
+ ... ) as compose:
113
116
  ... compose.color(primary="#6200EE", background="#FFFFFF")
114
117
  """
115
118
 
@@ -117,6 +120,7 @@ class ComposeWriter(_BaseComposeScope):
117
120
  self,
118
121
  path: str | Path,
119
122
  package: str,
123
+ *,
120
124
  sink: WriteSink | None = None,
121
125
  ) -> None:
122
126
  """Initializes the ComposeWriter.
@@ -127,6 +131,11 @@ class ComposeWriter(_BaseComposeScope):
127
131
  sink: The custom output to write data to. If None,
128
132
  defaults to a standard file write.
129
133
  """
134
+ package = package.strip()
135
+ if not package:
136
+ msg = "Package must not be empty"
137
+ raise ValueError(msg)
138
+
130
139
  self._active = False
131
140
  self._path = Path(path)
132
141
  self._package = package
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from collections.abc import Callable
2
5
  from dataclasses import dataclass
3
6
  from typing import Literal, NotRequired, TypedDict, final, override
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  import re
2
5
  import xml.etree.ElementTree as ET
3
6
  from io import BytesIO
@@ -19,7 +22,7 @@ _STYLE_NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\.]*$")
19
22
 
20
23
  @final
21
24
  class ValuesWriter:
22
- """A fluent context manager for generating Android XML resource files.
25
+ """A context manager for generating Android XML resource files.
23
26
 
24
27
  Provides a type-safe interface for creating strings, dimensions, colors,
25
28
  and arrays. Validates resource names and color formats at runtime.
@@ -30,7 +33,7 @@ class ValuesWriter:
30
33
 
31
34
  """
32
35
 
33
- def __init__(self, path: str | Path, sink: WriteSink | None = None) -> None:
36
+ def __init__(self, path: str | Path, *, sink: WriteSink | None = None) -> None:
34
37
  """Initializes the ValuesWriter.
35
38
 
36
39
  Args:
@@ -153,8 +156,8 @@ class ValuesWriter:
153
156
 
154
157
  """
155
158
  for name, color in values.items():
156
- resolved = Color.parse(color)
157
- self._append("color", name, resolved.to_srgb_argb_hex())
159
+ parsed_color = Color.parse(color)
160
+ self._append("color", name, parsed_color.to_srgb_argb_hex())
158
161
  return self
159
162
 
160
163
  @require_context
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from .catalog import AssetCatalog
2
5
  from .types import Appearance, AppleColor, ColorSpace, DisplayGamut, Idiom, Subtype
3
6
 
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  import json
2
5
  from abc import ABC, abstractmethod
3
6
  from io import BytesIO
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from pathlib import Path
2
5
  from typing import Self, override
3
6
 
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  import json
2
5
  import shutil
3
6
  from io import BytesIO
@@ -15,7 +18,7 @@ __all__ = ["AssetCatalog"]
15
18
 
16
19
  @final
17
20
  class AssetCatalog:
18
- """A fluent context manager for generating Apple Asset Catalogs (.xcassets).
21
+ """A context manager for generating Apple Asset Catalogs (.xcassets).
19
22
 
20
23
  To ensure atomic writes, it works in a temporary directory and only swaps
21
24
  to the final destination upon successful completion of the context.
@@ -29,6 +32,7 @@ class AssetCatalog:
29
32
  self,
30
33
  path: str | Path,
31
34
  name: str,
35
+ *,
32
36
  sink: WriteSink | None = None,
33
37
  ) -> None:
34
38
  """Initializes the AssetCatalog.
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from __future__ import annotations
2
5
 
3
6
  import typing
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from __future__ import annotations
2
5
 
3
6
  from dataclasses import dataclass
@@ -110,6 +113,20 @@ class Color:
110
113
  round(self.alpha, 3) + 0.0,
111
114
  )
112
115
 
116
+ def to_oklch_components(self) -> tuple[float, float, float, float]:
117
+ """Convert the color to Oklch components (L, C, H, A).
118
+
119
+ Colors outside Oklch gamut are automatically clipped to the nearest
120
+ valid boundary.
121
+ """
122
+ oklch = self._to_coloraide().convert("oklch").clip()
123
+ return (
124
+ round(oklch["lightness"], 3) + 0.0,
125
+ round(oklch["chroma"], 3) + 0.0,
126
+ round(oklch["hue"], 3) + 0.0,
127
+ round(self.alpha, 3) + 0.0,
128
+ )
129
+
113
130
  def to_srgb_argb_hex(self, prefix: str = "#") -> str:
114
131
  """Convert the color to an ARGB hexadecimal string.
115
132
 
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  import os
2
5
  import shutil
3
6
  from collections.abc import Generator
@@ -13,7 +16,7 @@ __all__ = ["FileSystemSink", "MemorySink", "WriteSink", "atomic_write"]
13
16
  def atomic_write(target_path: str | Path) -> Generator[IO[bytes], None, None]:
14
17
  """Yields a temporary file, then atomically replaces target_path on success.
15
18
 
16
- Preserves file permissions of target_path, if target_path already exists.
19
+ Preserves file permissions if `target_path` already exists.
17
20
  """
18
21
  target_path = Path(target_path)
19
22
  target_path.parent.mkdir(parents=True, exist_ok=True)
@@ -0,0 +1,2 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -1,9 +1,13 @@
1
- import pytest
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from pathlib import Path
2
5
 
6
+ import pytest
3
7
  from resforge.android import ComposeWriter, Dimension, dp, sp
4
8
 
5
9
 
6
- def test_object_colors(tmp_path):
10
+ def test_object_colors(tmp_path: Path):
7
11
  output = tmp_path / "AppColors.kt"
8
12
  with (
9
13
  ComposeWriter(output, package="com.example.theme") as compose,
@@ -23,7 +27,7 @@ def test_object_colors(tmp_path):
23
27
  )
24
28
 
25
29
 
26
- def test_top_level_dimension(tmp_path):
30
+ def test_top_level_dimension(tmp_path: Path):
27
31
  output = tmp_path / "Dimens.kt"
28
32
  with ComposeWriter(output, package="com.example.theme") as compose:
29
33
  compose.dimension(paddingSmall=dp(8), textBody=sp(16))
@@ -41,7 +45,7 @@ def test_top_level_dimension(tmp_path):
41
45
  )
42
46
 
43
47
 
44
- def test_mixed_top_level_and_object(tmp_path):
48
+ def test_mixed_top_level_and_object(tmp_path: Path):
45
49
  output = tmp_path / "Theme.kt"
46
50
  with ComposeWriter(output, package="com.example.theme") as compose:
47
51
  compose.dimension(border=dp(8))
@@ -64,7 +68,7 @@ def test_mixed_top_level_and_object(tmp_path):
64
68
  )
65
69
 
66
70
 
67
- def test_multiple_objects(tmp_path):
71
+ def test_multiple_objects(tmp_path: Path):
68
72
  output = tmp_path / "Theme.kt"
69
73
  with ComposeWriter(output, package="com.example.theme") as compose:
70
74
  with compose.object_("AppColors") as colors:
@@ -78,7 +82,7 @@ def test_multiple_objects(tmp_path):
78
82
  assert rendered.index("AppColors") < rendered.index("AppDimens")
79
83
 
80
84
 
81
- def test_object_require_context_raises(tmp_path):
85
+ def test_object_require_context_raises(tmp_path: Path):
82
86
  output = tmp_path / "Color.kt"
83
87
  with ComposeWriter(output, package="com.example.theme") as compose:
84
88
  scope = compose.object_("AppColors")
@@ -87,7 +91,7 @@ def test_object_require_context_raises(tmp_path):
87
91
  scope.color(primary="#6200EE")
88
92
 
89
93
 
90
- def test_no_write_on_exception(tmp_path):
94
+ def test_no_write_on_exception(tmp_path: Path):
91
95
  output = tmp_path / "Color.kt"
92
96
  with (
93
97
  pytest.raises(RuntimeError),
@@ -104,7 +108,7 @@ def test_require_context_raises():
104
108
  compose.color(primary="#6200EE")
105
109
 
106
110
 
107
- def test_creates_parent_directories(tmp_path):
111
+ def test_creates_parent_directories(tmp_path: Path):
108
112
  output = tmp_path / "ui" / "theme" / "Color.kt"
109
113
  with ComposeWriter(output, package="com.example.theme") as compose:
110
114
  compose.color(primary="#6200EE")
@@ -112,7 +116,7 @@ def test_creates_parent_directories(tmp_path):
112
116
  assert output.exists()
113
117
 
114
118
 
115
- def test_package_is_configurable(tmp_path):
119
+ def test_package_is_configurable(tmp_path: Path):
116
120
  output = tmp_path / "Color.kt"
117
121
  with ComposeWriter(output, package="dev.kipila.example") as compose:
118
122
  compose.color(primary="#6200EE")
@@ -120,7 +124,7 @@ def test_package_is_configurable(tmp_path):
120
124
  assert "package dev.kipila.example" in output.read_text()
121
125
 
122
126
 
123
- def test_unsupported_dimension_unit_raises(tmp_path):
127
+ def test_unsupported_dimension_unit_raises(tmp_path: Path):
124
128
  output = tmp_path / "Color.kt"
125
129
 
126
130
  with (
@@ -128,3 +132,8 @@ def test_unsupported_dimension_unit_raises(tmp_path):
128
132
  ComposeWriter(output, package="com.example.theme") as compose,
129
133
  ):
130
134
  compose.dimension(padding=Dimension(8, "px"))
135
+
136
+
137
+ def test_empty_package_raises():
138
+ with pytest.raises(ValueError, match="Package"):
139
+ ComposeWriter("Color.kt", package="")
@@ -1,8 +1,10 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  from pathlib import Path
2
5
 
3
6
  import pytest
4
7
  from defusedxml.ElementTree import parse
5
-
6
8
  from resforge import Color
7
9
  from resforge.android import PluralValues, ValuesWriter, dp, em, inch, mm, pt, px, sp
8
10
 
@@ -46,7 +48,7 @@ class TestDimensionType:
46
48
 
47
49
 
48
50
  @pytest.fixture
49
- def xml_path(tmp_path) -> Path:
51
+ def xml_path(tmp_path: Path) -> Path:
50
52
  return tmp_path / "res" / "values" / "test.xml"
51
53
 
52
54
 
@@ -1,12 +1,15 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  import json
5
+ from pathlib import Path
2
6
 
3
7
  import pytest
4
-
5
8
  from resforge.apple.catalog import AssetCatalog
6
9
  from resforge.apple.types import AppleColor
7
10
 
8
11
 
9
- def test_asset_catalog_lifecycle(tmp_path):
12
+ def test_asset_catalog_lifecycle(tmp_path: Path):
10
13
  catalog_name = "Assets"
11
14
  output_dir = tmp_path / "App"
12
15
  final_path = output_dir / f"{catalog_name}.xcassets"
@@ -21,12 +24,14 @@ def test_asset_catalog_lifecycle(tmp_path):
21
24
  assert (final_path / "Contents.json").exists()
22
25
  contents_file = final_path / "Primary.colorset" / "Contents.json"
23
26
  assert (contents_file).exists()
24
- data = json.loads(contents_file.read_text())
25
- assert data["colors"][0]["color"]["components"]["red"] == "1.000"
27
+ assert (
28
+ json.loads(contents_file.read_text())["colors"][0]["color"]["components"]["red"]
29
+ == "1.000"
30
+ )
26
31
  assert not temp_path.exists()
27
32
 
28
33
 
29
- def test_asset_catalog_atomic_failure(tmp_path):
34
+ def test_asset_catalog_atomic_failure(tmp_path: Path):
30
35
  catalog_name = "Assets"
31
36
  output_dir = tmp_path / "App"
32
37
  final_path = output_dir / f"{catalog_name}.xcassets"
@@ -44,7 +49,7 @@ def test_asset_catalog_atomic_failure(tmp_path):
44
49
  assert not temp_path.exists()
45
50
 
46
51
 
47
- def test_asset_catalog_overwrites_existing(tmp_path):
52
+ def test_asset_catalog_overwrites_existing(tmp_path: Path):
48
53
  catalog_name = "Assets"
49
54
  output_dir = tmp_path / "App"
50
55
  final_path = output_dir / f"{catalog_name}.xcassets"
@@ -60,7 +65,7 @@ def test_asset_catalog_overwrites_existing(tmp_path):
60
65
  assert (final_path / "Contents.json").exists()
61
66
 
62
67
 
63
- def test_require_context_enforcement(tmp_path):
68
+ def test_require_context_enforcement(tmp_path: Path):
64
69
  catalog = AssetCatalog(tmp_path / "App", "Assets")
65
70
 
66
71
  with pytest.raises(RuntimeError):
@@ -1,9 +1,13 @@
1
- import pytest
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from pathlib import Path
2
5
 
6
+ import pytest
3
7
  from resforge.io import atomic_write
4
8
 
5
9
 
6
- def test_atomic_replace_existing(tmp_path):
10
+ def test_atomic_replace_existing(tmp_path: Path):
7
11
  target = tmp_path / "output.bin"
8
12
  target.write_bytes(b"old")
9
13
  with atomic_write(target) as f:
@@ -12,14 +16,14 @@ def test_atomic_replace_existing(tmp_path):
12
16
  assert not any(tmp_path.glob("*.tmp"))
13
17
 
14
18
 
15
- def test_creates_parent_dirs(tmp_path):
19
+ def test_creates_parent_dirs(tmp_path: Path):
16
20
  target = tmp_path / "a" / "b" / "c" / "output.bin"
17
21
  with atomic_write(target) as f:
18
22
  f.write(b"hello")
19
23
  assert target.exists()
20
24
 
21
25
 
22
- def test_no_corruption_on_exception(tmp_path):
26
+ def test_no_corruption_on_exception(tmp_path: Path):
23
27
  target = tmp_path / "output.bin"
24
28
  target.write_bytes(b"original")
25
29
 
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import pytest
1
5
  from resforge._codegen.kotlin import KotlinFile, KotlinObject, KotlinProperty
2
6
 
3
7
 
@@ -102,3 +106,8 @@ def test_top_level_property():
102
106
  assert f.render() == (
103
107
  "package com.example\n\nval primary: Color = Color(0xFF6200EE)\n"
104
108
  )
109
+
110
+
111
+ def test_empty_package_raises():
112
+ with pytest.raises(ValueError, match="Package"):
113
+ KotlinFile(package="")
@@ -1,5 +1,7 @@
1
- import pytest
1
+ # SPDX-FileCopyrightText: 2026 Kipila Ltd
2
+ # SPDX-License-Identifier: Apache-2.0
2
3
 
4
+ import pytest
3
5
  from resforge.color import Color
4
6
 
5
7
 
@@ -1,48 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
- strategy:
13
- matrix:
14
- python-version: ["3.12", "3.13", "3.14"]
15
-
16
- steps:
17
- - uses: actions/checkout@v5
18
-
19
- - name: Set up Python
20
- uses: actions/setup-python@v5
21
- with:
22
- python-version: ${{ matrix.python-version }}
23
-
24
- - name: Install Hatch
25
- run: pip install hatch
26
-
27
- - name: Run tests
28
- run: hatch run pytest
29
-
30
- lint-and-check:
31
- runs-on: ubuntu-latest
32
- steps:
33
- - uses: actions/checkout@v5
34
-
35
- - name: Set up Python
36
- uses: actions/setup-python@v5
37
-
38
- - name: Install Hatch
39
- run: pip install hatch
40
-
41
- - name: Lint
42
- run: hatch run ruff check .
43
-
44
- - name: Type check
45
- run: hatch run basedpyright
46
-
47
- - name: Verify Build Metadata
48
- run: hatch build
@@ -1,29 +0,0 @@
1
- name: Publish
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- jobs:
8
- publish:
9
- runs-on: ubuntu-latest
10
-
11
- environment: pypi
12
- permissions:
13
- id-token: write
14
-
15
- steps:
16
- - uses: actions/checkout@v5
17
-
18
- - name: Set up Python
19
- uses: actions/setup-python@v5
20
- with:
21
- python-version: "3.12"
22
-
23
- - name: Build
24
- run: |
25
- pip install build
26
- python -m build
27
-
28
- - name: Publish to PyPI
29
- uses: pypa/gh-action-pypi-publish@release/v1
resforge-0.4.2/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Kipila Ltd
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,4 +0,0 @@
1
- from .color import Color
2
-
3
- __all__ = ["Color"]
4
- __version__ = "0.4.2"
File without changes
File without changes
File without changes