resforge 0.2.0__tar.gz → 0.3.1__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.2.0 → resforge-0.3.1}/PKG-INFO +41 -80
  2. {resforge-0.2.0 → resforge-0.3.1}/README.md +38 -78
  3. {resforge-0.2.0 → resforge-0.3.1}/pyproject.toml +53 -3
  4. resforge-0.3.1/resforge/_utils.py +62 -0
  5. {resforge-0.2.0 → resforge-0.3.1}/resforge/android/__init__.py +5 -4
  6. resforge-0.3.1/resforge/android/compose.py +140 -0
  7. resforge-0.3.1/resforge/android/types.py +75 -0
  8. {resforge-0.2.0 → resforge-0.3.1}/resforge/android/values.py +38 -26
  9. resforge-0.2.0/resforge/apple/base.py → resforge-0.3.1/resforge/apple/_base.py +10 -9
  10. resforge-0.2.0/resforge/apple/colorset.py → resforge-0.3.1/resforge/apple/_colorset.py +15 -14
  11. {resforge-0.2.0 → resforge-0.3.1}/resforge/apple/catalog.py +13 -9
  12. {resforge-0.2.0 → resforge-0.3.1}/resforge/apple/types.py +21 -5
  13. resforge-0.3.1/resforge/codegen/kotlin.py +100 -0
  14. resforge-0.3.1/resforge/types.py +74 -0
  15. resforge-0.3.1/tests/__init__.py +0 -0
  16. resforge-0.3.1/tests/test_android_compose.py +130 -0
  17. resforge-0.2.0/tests/test_android.py → resforge-0.3.1/tests/test_android_values.py +12 -23
  18. resforge-0.2.0/tests/test_ios_catalog.py → resforge-0.3.1/tests/test_apple_catalog.py +1 -1
  19. resforge-0.3.1/tests/test_atomic_write.py +35 -0
  20. resforge-0.3.1/tests/test_codegen_kotlin.py +117 -0
  21. resforge-0.3.1/tests/test_types.py +53 -0
  22. resforge-0.2.0/resforge/android/dimension.py +0 -61
  23. resforge-0.2.0/resforge/android/plural.py +0 -22
  24. resforge-0.2.0/resforge/types.py +0 -56
  25. resforge-0.2.0/resforge/utils.py +0 -19
  26. resforge-0.2.0/tests/test_types.py +0 -58
  27. {resforge-0.2.0 → resforge-0.3.1}/.github/workflows/ci.yml +0 -0
  28. {resforge-0.2.0 → resforge-0.3.1}/.github/workflows/publish.yml +0 -0
  29. {resforge-0.2.0 → resforge-0.3.1}/.gitignore +0 -0
  30. {resforge-0.2.0 → resforge-0.3.1}/LICENSE +0 -0
  31. {resforge-0.2.0 → resforge-0.3.1}/resforge/__init__.py +0 -0
  32. {resforge-0.2.0 → resforge-0.3.1}/resforge/apple/__init__.py +1 -1
  33. {resforge-0.2.0/tests → resforge-0.3.1/resforge/codegen}/__init__.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: resforge
3
- Version: 0.2.0
3
+ Version: 0.3.1
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/ok100/resforge
6
+ Project-URL: Repository, https://github.com/kipila-dev/resforge
7
7
  Author-email: Ondrej Kipila <ondrej@kipila.dev>
8
8
  License: MIT
9
9
  License-File: LICENSE
@@ -17,13 +17,21 @@ Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Topic :: Software Development :: Build Tools
18
18
  Requires-Python: >=3.12
19
19
  Provides-Extra: dev
20
+ Requires-Dist: defusedxml; extra == 'dev'
20
21
  Requires-Dist: pytest; extra == 'dev'
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
  # resforge
24
25
 
25
- A type-safe Python DSL for generating Android XML resources and Xcode Asset
26
- Catalogs (`.xcassets`).
26
+ A type-safe Python DSL for generating native Android and iOS resources from design tokens.
27
+
28
+ ## Features
29
+
30
+ - Fluent, Pythonic API
31
+ - Jetpack Compose theme generation with type-safe color and dimension properties
32
+ - Supports all Android `res/values/` types
33
+ - Native Apple Asset Catalog (`.xcassets`) with dark mode support
34
+ - Built-in validation for resource names and color formats
27
35
 
28
36
  ## Installation
29
37
 
@@ -33,7 +41,33 @@ pip install resforge
33
41
 
34
42
  ## Quick Example
35
43
 
36
- ### Android
44
+ ### Android (Jetpack Compose)
45
+
46
+ ```python
47
+ from resforge.android import ComposeWriter, dp
48
+
49
+ with ComposeWriter("Theme.kt", "dev.kipila.example") as compose:
50
+ compose.dimension(border=dp(8))
51
+ with compose.object_("AppColors") as colors:
52
+ colors.color(primary="#FF0000", background="#FFFFFF")
53
+ ```
54
+
55
+ ```kotlin
56
+ package dev.kipila.example
57
+
58
+ 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
+ object AppColors {
65
+ val primary: Color = Color(0xFFFF0000)
66
+ val background: Color = Color(0xFFFFFFFF)
67
+ }
68
+ ```
69
+
70
+ ### Android (XML Resources)
37
71
 
38
72
  ```python
39
73
  from resforge.android import ValuesWriter, dp, sp
@@ -123,83 +157,10 @@ with AssetCatalog("App", "Assets") as ac:
123
157
  }
124
158
  ```
125
159
 
126
- ## Features
127
-
128
- - Fluent, Pythonic API
129
- - Supports all Android `res/values/` types
130
- - Built-in validation for Asset Catalog logic
131
-
132
- ## Full Android Example
133
-
134
- ```python
135
- from resforge.android import PluralValues, ValuesWriter, dp, sp
136
-
137
- with ValuesWriter("res/values/resources.xml") as res:
138
- res.comment("Strings")
139
- res.string(
140
- app_name="My App",
141
- welcome_message="Welcome to My App!",
142
- )
143
-
144
- res.comment("Booleans")
145
- res.boolean(
146
- feature_enabled=True,
147
- dark_mode=False,
148
- )
149
-
150
- res.comment("Colors")
151
- res.color(
152
- primary="#FF6200EE",
153
- secondary="#FF03DAC5",
154
- accent="#6200EE",
155
- )
156
-
157
- res.comment("Dimensions")
158
- res.dimension(
159
- padding_small=dp(8),
160
- padding_large=dp(24),
161
- text_body=sp(16),
162
- text_heading=sp(24),
163
- )
164
-
165
- res.comment("Integers")
166
- res.integer(
167
- max_retries=3,
168
- timeout_seconds=30,
169
- )
170
-
171
- res.comment("Resource IDs")
172
- res.res_id("btn_submit", "tv_title", "iv_logo")
173
-
174
- res.comment("String arrays")
175
- res.string_array("planets", ["Mercury", "Venus", "Earth", "Mars"])
176
-
177
- res.comment("Integer arrays")
178
- res.integer_array("fibonacci", [1, 1, 2, 3, 5, 8, 13])
179
-
180
- res.comment("Typed arrays")
181
- res.typed_array(
182
- "icons", ["@drawable/home", "@drawable/settings", "@drawable/logout"]
183
- )
184
-
185
- res.comment("Plurals")
186
- res.plurals(
187
- item_count=PluralValues(one="%d item", other="%d items"),
188
- file_count=PluralValues(one="%d file", other="%d files"),
189
- )
190
-
191
- res.comment("Styles")
192
- res.style(
193
- "AppTheme",
194
- parent="Theme.MaterialComponents.DayNight",
195
- colorPrimary="@color/primary",
196
- colorSecondary="@color/secondary",
197
- )
198
- ```
199
-
200
160
  ## Roadmap
201
161
 
202
- - Support for more Asset Catalog types (ImageSet, IconSet)
162
+ - Asset Catalog image support (ImageSet, IconSet)
163
+ - Android `res/drawable` vector asset support
203
164
 
204
165
  ## License
205
166
 
@@ -1,7 +1,14 @@
1
1
  # resforge
2
2
 
3
- A type-safe Python DSL for generating Android XML resources and Xcode Asset
4
- Catalogs (`.xcassets`).
3
+ A type-safe Python DSL for generating native Android and iOS resources from design tokens.
4
+
5
+ ## Features
6
+
7
+ - Fluent, Pythonic API
8
+ - Jetpack Compose theme generation with type-safe color and dimension properties
9
+ - Supports all Android `res/values/` types
10
+ - Native Apple Asset Catalog (`.xcassets`) with dark mode support
11
+ - Built-in validation for resource names and color formats
5
12
 
6
13
  ## Installation
7
14
 
@@ -11,7 +18,33 @@ pip install resforge
11
18
 
12
19
  ## Quick Example
13
20
 
14
- ### Android
21
+ ### Android (Jetpack Compose)
22
+
23
+ ```python
24
+ from resforge.android import ComposeWriter, dp
25
+
26
+ with ComposeWriter("Theme.kt", "dev.kipila.example") as compose:
27
+ compose.dimension(border=dp(8))
28
+ with compose.object_("AppColors") as colors:
29
+ colors.color(primary="#FF0000", background="#FFFFFF")
30
+ ```
31
+
32
+ ```kotlin
33
+ package dev.kipila.example
34
+
35
+ 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
+
41
+ object AppColors {
42
+ val primary: Color = Color(0xFFFF0000)
43
+ val background: Color = Color(0xFFFFFFFF)
44
+ }
45
+ ```
46
+
47
+ ### Android (XML Resources)
15
48
 
16
49
  ```python
17
50
  from resforge.android import ValuesWriter, dp, sp
@@ -101,83 +134,10 @@ with AssetCatalog("App", "Assets") as ac:
101
134
  }
102
135
  ```
103
136
 
104
- ## Features
105
-
106
- - Fluent, Pythonic API
107
- - Supports all Android `res/values/` types
108
- - Built-in validation for Asset Catalog logic
109
-
110
- ## Full Android Example
111
-
112
- ```python
113
- from resforge.android import PluralValues, ValuesWriter, dp, sp
114
-
115
- with ValuesWriter("res/values/resources.xml") as res:
116
- res.comment("Strings")
117
- res.string(
118
- app_name="My App",
119
- welcome_message="Welcome to My App!",
120
- )
121
-
122
- res.comment("Booleans")
123
- res.boolean(
124
- feature_enabled=True,
125
- dark_mode=False,
126
- )
127
-
128
- res.comment("Colors")
129
- res.color(
130
- primary="#FF6200EE",
131
- secondary="#FF03DAC5",
132
- accent="#6200EE",
133
- )
134
-
135
- res.comment("Dimensions")
136
- res.dimension(
137
- padding_small=dp(8),
138
- padding_large=dp(24),
139
- text_body=sp(16),
140
- text_heading=sp(24),
141
- )
142
-
143
- res.comment("Integers")
144
- res.integer(
145
- max_retries=3,
146
- timeout_seconds=30,
147
- )
148
-
149
- res.comment("Resource IDs")
150
- res.res_id("btn_submit", "tv_title", "iv_logo")
151
-
152
- res.comment("String arrays")
153
- res.string_array("planets", ["Mercury", "Venus", "Earth", "Mars"])
154
-
155
- res.comment("Integer arrays")
156
- res.integer_array("fibonacci", [1, 1, 2, 3, 5, 8, 13])
157
-
158
- res.comment("Typed arrays")
159
- res.typed_array(
160
- "icons", ["@drawable/home", "@drawable/settings", "@drawable/logout"]
161
- )
162
-
163
- res.comment("Plurals")
164
- res.plurals(
165
- item_count=PluralValues(one="%d item", other="%d items"),
166
- file_count=PluralValues(one="%d file", other="%d files"),
167
- )
168
-
169
- res.comment("Styles")
170
- res.style(
171
- "AppTheme",
172
- parent="Theme.MaterialComponents.DayNight",
173
- colorPrimary="@color/primary",
174
- colorSecondary="@color/secondary",
175
- )
176
- ```
177
-
178
137
  ## Roadmap
179
138
 
180
- - Support for more Asset Catalog types (ImageSet, IconSet)
139
+ - Asset Catalog image support (ImageSet, IconSet)
140
+ - Android `res/drawable` vector asset support
181
141
 
182
142
  ## License
183
143
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "resforge"
7
- version = "0.2.0"
7
+ version = "0.3.1"
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"
@@ -32,8 +32,58 @@ classifiers = [
32
32
  ]
33
33
 
34
34
  [project.optional-dependencies]
35
- dev = ["pytest"]
35
+ dev = ["pytest", "defusedxml"]
36
36
 
37
37
  [project.urls]
38
38
  Homepage = "https://kipila.dev"
39
- Repository = "https://github.com/ok100/resforge"
39
+ Repository = "https://github.com/kipila-dev/resforge"
40
+
41
+ [tool.ruff]
42
+ target-version = "py312"
43
+ line-length = 120
44
+
45
+ [tool.ruff.lint]
46
+ select = ["ALL"]
47
+ ignore = [
48
+ "ANN001",
49
+ "ANN002",
50
+ "ANN003",
51
+ "COM812",
52
+ "COM819",
53
+ "D100",
54
+ "D102",
55
+ "D104",
56
+ "D105",
57
+ "D107",
58
+ "D206",
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",
73
+ ]
74
+ fixable = ["ALL"]
75
+
76
+ [tool.ruff.lint.per-file-ignores]
77
+ "tests/**/*.py" = [
78
+ "ANN201",
79
+ "D101",
80
+ "D103",
81
+ "EM101",
82
+ "S101",
83
+ "TRY301",
84
+ ]
85
+
86
+ [tool.basedpyright]
87
+ venvPath = "."
88
+ venv = ".venv"
89
+ typeCheckingMode = "standard"
@@ -0,0 +1,62 @@
1
+ import os
2
+ from collections.abc import Callable, Generator
3
+ from contextlib import contextmanager, suppress
4
+ from functools import wraps
5
+ from pathlib import Path
6
+ from tempfile import NamedTemporaryFile
7
+ from typing import IO, Concatenate
8
+
9
+
10
+ def require_context[T, **P, R](
11
+ func: Callable[Concatenate[T, P], R],
12
+ ) -> Callable[Concatenate[T, P], R]:
13
+ """Ensures a method is only called within an active context.
14
+
15
+ Raises:
16
+ RuntimeError: If the method is called while the instance's
17
+ `_active` attribute is False or missing.
18
+
19
+ """
20
+
21
+ @wraps(func)
22
+ def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
23
+ if not getattr(self, "_active", False):
24
+ msg = f"'{func.__name__}' requires an active 'with' context."
25
+ raise RuntimeError(msg)
26
+ return func(self, *args, **kwargs)
27
+
28
+ return wrapper
29
+
30
+
31
+ @contextmanager
32
+ def atomic_write(target_path: Path) -> Generator[IO[bytes], None, None]:
33
+ """Yields a temporary file, then atomically replaces target_path on success."""
34
+ target_path = Path(target_path)
35
+ target_path.parent.mkdir(parents=True, exist_ok=True)
36
+
37
+ temp_path = None
38
+ try:
39
+ with NamedTemporaryFile(
40
+ dir=target_path.parent, delete=False, suffix=".tmp"
41
+ ) as tf:
42
+ temp_path = Path(tf.name)
43
+ yield tf
44
+
45
+ tf.flush()
46
+ os.fsync(tf.fileno())
47
+
48
+ temp_path.replace(target_path)
49
+
50
+ if os.name == "posix":
51
+ flags = os.O_RDONLY
52
+ if hasattr(os, "O_DIRECTORY"):
53
+ flags |= os.O_DIRECTORY
54
+ dir_fd = os.open(target_path.parent, flags)
55
+ try:
56
+ os.fsync(dir_fd)
57
+ finally:
58
+ os.close(dir_fd)
59
+ finally:
60
+ if temp_path and temp_path.exists():
61
+ with suppress(OSError):
62
+ temp_path.unlink()
@@ -1,15 +1,16 @@
1
- from .dimension import Dimension, dp, inch, mm, pt, px, sp
2
- from .plural import PluralValues
1
+ from .compose import ComposeWriter
2
+ from .types import Dimension, PluralValues, dp, inch, mm, pt, px, sp
3
3
  from .values import ValuesWriter
4
4
 
5
5
  __all__ = [
6
+ "ComposeWriter",
6
7
  "Dimension",
8
+ "PluralValues",
9
+ "ValuesWriter",
7
10
  "dp",
8
11
  "inch",
9
12
  "mm",
10
13
  "pt",
11
14
  "px",
12
15
  "sp",
13
- "PluralValues",
14
- "ValuesWriter",
15
16
  ]
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Self
5
+
6
+ from resforge._utils import atomic_write, require_context
7
+ from resforge.codegen.kotlin import KotlinFile, KotlinObject
8
+ from resforge.types import Color
9
+
10
+ if TYPE_CHECKING:
11
+ from resforge.android.types import Dimension
12
+
13
+ __all__ = ["ComposeWriter"]
14
+
15
+
16
+ class _ComposeContext:
17
+ def __init__(self, file: KotlinFile) -> None:
18
+ self._file = file
19
+
20
+ def add_imports(self, *fqns: str) -> None:
21
+ for fqn in fqns:
22
+ self._file.import_(fqn)
23
+
24
+
25
+ class _BaseComposeScope:
26
+ def __init__(self, ctx: _ComposeContext, target: KotlinFile | KotlinObject) -> None:
27
+ self._ctx = ctx
28
+ self._target = target
29
+
30
+ @staticmethod
31
+ def _to_compose_color_literal(color: Color) -> str:
32
+ return f"Color({color.hex.replace('#', '0x')})"
33
+
34
+ @staticmethod
35
+ def _to_compose_dimen_literal(dimen: Dimension) -> str:
36
+ return f"{dimen.value}.{dimen.unit}"
37
+
38
+ @require_context
39
+ def color(self, **values: str | Color) -> Self:
40
+ """Appends one or more Color properties to the Kotlin object."""
41
+ if values:
42
+ self._ctx.add_imports("androidx.compose.ui.graphics.Color")
43
+ for name, color in values.items():
44
+ resolved = Color(color)
45
+ self._target.property(
46
+ name=name,
47
+ type_="Color",
48
+ value=self._to_compose_color_literal(resolved),
49
+ )
50
+ return self
51
+
52
+ @require_context
53
+ def dimension(self, **values: Dimension) -> Self:
54
+ """Appends one or more Dimension properties to the Kotlin object. Supports dp, sp and em.
55
+
56
+ Raises:
57
+ ValueError: If an unsupported unit (not dp, sp, or em) is provided.
58
+
59
+ """
60
+ mapping = {"dp": "Dp", "sp": "Sp", "em": "TextUnit"}
61
+ for name, dimen in values.items():
62
+ if dimen.unit not in mapping:
63
+ msg = f"Unsupported dimension type. Only dp, sp and em are supported (got {dimen.unit})."
64
+ raise ValueError(msg)
65
+ self._ctx.add_imports(
66
+ f"androidx.compose.ui.unit.{dimen.unit}",
67
+ f"androidx.compose.ui.unit.{mapping[dimen.unit]}",
68
+ )
69
+ self._target.property(
70
+ name=name,
71
+ type_=mapping[dimen.unit],
72
+ value=self._to_compose_dimen_literal(dimen),
73
+ )
74
+
75
+ return self
76
+
77
+
78
+ class _ObjectScope(_BaseComposeScope):
79
+ def __enter__(self) -> Self:
80
+ self._active = True
81
+ return self
82
+
83
+ def __exit__(self, *_) -> None:
84
+ self._active = False
85
+
86
+
87
+ class ComposeWriter(_BaseComposeScope):
88
+ """A fluent context manager for generating Jetpack Compose color definitions.
89
+
90
+ Writes a Kotlin file containing typed Color or Dimension properties.
91
+
92
+ Example:
93
+ >>> with ComposeWriter("ui/theme/Color.kt", package="com.example.theme", object_name="AppColors") as compose:
94
+ ... compose.color(primary="#6200EE", background="#FFFFFF")
95
+
96
+ """
97
+
98
+ def __init__(self, path: str | Path, package: str) -> None:
99
+ """Initializes the ComposeWriter.
100
+
101
+ Args:
102
+ path: The filesystem path where the Kotlin file will be saved.
103
+ package: The Kotlin package declaration.
104
+
105
+ """
106
+ self._path = Path(path)
107
+ self._package = package
108
+ self._active = False
109
+
110
+ def __enter__(self) -> Self:
111
+ self._active = True
112
+ self._kotlin_file = KotlinFile(package=self._package)
113
+
114
+ ctx = _ComposeContext(self._kotlin_file)
115
+ super().__init__(ctx, self._kotlin_file)
116
+
117
+ return self
118
+
119
+ def __exit__(self, exc_type, *_) -> None:
120
+ try:
121
+ if exc_type is None:
122
+ with atomic_write(self._path) as tf:
123
+ self._kotlin_file.write(tf)
124
+ finally:
125
+ self._active = False
126
+
127
+ @require_context
128
+ def object_(self, name: str) -> _ObjectScope:
129
+ """Creates a new Kotlin object within the file.
130
+
131
+ Args:
132
+ name: The name of the Kotlin object to generate.
133
+
134
+ Returns:
135
+ A new context tied to the generated object.
136
+
137
+ """
138
+ obj = KotlinObject(name=name)
139
+ self._kotlin_file.member(obj)
140
+ return _ObjectScope(self._ctx, obj)
@@ -0,0 +1,75 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import Literal, NotRequired, TypedDict
4
+
5
+ DimensionUnit = Literal["dp", "sp", "px", "pt", "mm", "in", "em"]
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Dimension:
10
+ """Represents an Android dimension value (e.g., '16dp', '12sp').
11
+
12
+ Args:
13
+ value: The numeric dimension value. Negative values are permitted
14
+ to support negative margins/offsets, though they are
15
+ typically ignored by padding and size attributes.
16
+ unit: The unit of measure (dp, sp, px, pt, mm, in, em).
17
+
18
+ """
19
+
20
+ value: int | float
21
+ unit: DimensionUnit
22
+
23
+ def __str__(self) -> str:
24
+ return f"{self.value:g}{self.unit}"
25
+
26
+ def __repr__(self) -> str:
27
+ return f"Dimension(value={self.value!r}, unit={self.unit!r})"
28
+
29
+ def __mul__(self, other: float) -> "Dimension":
30
+ return Dimension(self.value * other, self.unit)
31
+
32
+ __rmul__ = __mul__
33
+
34
+
35
+ def _make_unit_func(
36
+ unit: DimensionUnit, doc: str
37
+ ) -> Callable[[int | float], Dimension]:
38
+ def f(value: float) -> Dimension:
39
+ return Dimension(value, unit)
40
+
41
+ f.__name__ = unit
42
+ f.__doc__ = doc
43
+ return f
44
+
45
+
46
+ dp = _make_unit_func("dp", "Create a dimension in density-independent pixels (dp).")
47
+ sp = _make_unit_func(
48
+ "sp", "Create a dimension in scale-independent pixels (sp). Use for text sizes."
49
+ )
50
+ px = _make_unit_func("px", "Create a dimension in pixels (px).")
51
+ pt = _make_unit_func("pt", "Create a dimension in points (pt).")
52
+ mm = _make_unit_func("mm", "Create a dimension in millimeters (mm).")
53
+ inch = _make_unit_func("in", "Create a dimension in inches (in).")
54
+ em = _make_unit_func("em", "Create a dimension using a relative font size (em).")
55
+
56
+
57
+ class PluralValues(TypedDict):
58
+ """Represents the quantity-based strings for an Android plurals resource.
59
+
60
+ Attributes:
61
+ zero: String for quantity 0 (optional).
62
+ one: String for quantity 1 (optional).
63
+ two: String for quantity 2 (optional).
64
+ few: String for quantity 'few' (optional).
65
+ many: String for quantity 'many' (optional).
66
+ other: The default fallback string (required).
67
+
68
+ """
69
+
70
+ zero: NotRequired[str]
71
+ one: NotRequired[str]
72
+ two: NotRequired[str]
73
+ few: NotRequired[str]
74
+ many: NotRequired[str]
75
+ other: str