Skip to content

Commit 8ed68d9

Browse files
authored
Merge pull request #315 from thisisthepy/feat/binder-colorscheme-docs
Docs/Test: colour schemes from Python via the ColorScheme constructor (#309)
2 parents 2068aa1 + 24f6a1e commit 8ed68d9

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

‎docs/SPEC.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,12 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
463463
- **B-3** Default arguments can be omitted from Python (a call per subset of defaulted parameters,
464464
ambiguous omissions refused). `Status: implemented`, `GP/artifact/DefaultOmissionTest.kt`,
465465
`python-multiplatform-gradle-plugin/fixtures/artifact/.../WalkedArtifactDefaultOmissionTest.kt`, `PM/commonTest/.../PythonxDefaultsTest.kt`.
466+
A declaration with more than 6 defaulted parameters (`lightColorScheme`, `darkColorScheme`,
467+
`ColorScheme.copy`, 48 each) binds with every parameter required, because the omission plan is
468+
`2^n` (`ArtifactScanner.MAX_OMITTABLE_PARAMETERS`). A colour scheme is built from Python with the
469+
`ColorScheme` constructor, all 48 arguments (`ComposableRenderTest.aColorSchemeBuiltByItsConstructorPaintsThePrimaryItWasGiven`,
470+
`colorSchemeCopyIsBoundWithEveryParameterRequired`); a keyword factory that fills the rest is
471+
pythonx-compose's job (#309).
466472
- **B-4** Value classes round-trip; only allow-listed ones (e.g. `Dp`) may be written as their raw
467473
primitive. `Status: implemented`, `GP/artifact/ComposableValueClassSlotTest.kt`,
468474
`WalkedArtifactPythonImportTest.kt`. A value class is opened as its raw primitive only when both its

‎python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposableRenderTest.kt‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,97 @@ class ComposableRenderTest {
797797
assertTrue(0xFFFF0000.toInt() !in green, "a green scheme painted red, so the argument was not read: $green")
798798
}
799799

800+
/**
801+
* Issue #309: a colour scheme from Python without `lightColorScheme`'s 48 defaults.
802+
*
803+
* `lightColorScheme` / `darkColorScheme` / `ColorScheme.copy` each declare 48 defaulted
804+
* parameters, past `ArtifactScanner.MAX_OMITTABLE_PARAMETERS` (6), so they bind with every
805+
* parameter required (a keyword factory that fills the rest is pythonx-compose's job; defaults
806+
* depend on each other, e.g. `surfaceTint` defaults to `primary`, so "call defaults then copy"
807+
* was rejected). What Python can do today is call the `ColorScheme` constructor, the public
808+
* 48-argument one (no defaults; the 36- and 29-argument ones are `@Deprecated(HIDDEN)` and
809+
* declined), and pass the handle to `MaterialTheme(colorScheme=...)`.
810+
*
811+
* Rendered, not just constructed: every slot black except `primary`, so a shifted argument would
812+
* paint a different slot's black.
813+
*/
814+
@Test
815+
fun aColorSchemeBuiltByItsConstructorPaintsThePrimaryItWasGiven() {
816+
Python3.exec(
817+
"""
818+
import python_multiplatform.binding as _pm_binding
819+
_ctors = [d for d in _pm_binding._BY_PACKAGE['androidx.compose.material3']['ColorScheme']
820+
if d.declared_arity() == 48]
821+
assert len(_ctors) == 1, repr(_ctors)
822+
assert all(_ctors[0].param_has_default[i] is False for i in range(48)), 'ColorScheme(...) has no defaults'
823+
""".trimIndent(),
824+
)
825+
val red = distinctColorsOf(schemeButton(constructed(255, 0, 0)))
826+
val green = distinctColorsOf(schemeButton(constructed(0, 255, 0)))
827+
println("compose render: MaterialTheme(ColorScheme(48 args, primary=red)) -> ${red.size} colors, green -> ${green.size}")
828+
assertTrue(0xFFFF0000.toInt() in red, "the constructor's primary never reached the Button's container: $red")
829+
assertTrue(0xFF00FF00.toInt() in green, "the green primary never reached the Button's container: $green")
830+
assertTrue(0xFFFF0000.toInt() !in green, "a green scheme painted red: $green")
831+
}
832+
833+
/**
834+
* `ColorScheme.copy` is a member with 48 defaulted parameters: past the cap, so every parameter is
835+
* required. Expected today: `copy(primary=...)` alone raises `TypeError: ... no value for ...`,
836+
* and `copy` with all 48 works and paints the new primary. If either half flips (the cap grew, or
837+
* `copy` is not bound at all and this raises `AttributeError`), this test says which.
838+
*/
839+
@Test
840+
fun colorSchemeCopyIsBoundWithEveryParameterRequired() {
841+
// The prelude is multi-line, so it is joined rather than interpolated into an indented block,
842+
// where trimIndent would see its unindented lines and keep the rest indented.
843+
Python3.exec(
844+
schemePrelude() + "\n" + """
845+
_base = ColorScheme(**_written(255, 0, 0))
846+
try:
847+
_base.copy(primary=_Color(0, 0, 255))
848+
raise AssertionError('copy(primary=) alone must not be callable: 48 defaults are past the omission cap')
849+
except TypeError as _e:
850+
assert 'no value for' in str(_e), str(_e)
851+
""".trimIndent(),
852+
)
853+
val blue = distinctColorsOf(
854+
schemeButton(
855+
"""
856+
_base = ColorScheme(**_written(255, 0, 0))
857+
_scheme = _base.copy(**_written(0, 0, 255))
858+
""".trimIndent(),
859+
),
860+
)
861+
assertTrue(0xFF0000FF.toInt() in blue, "copy(all 48) did not paint the copied primary: $blue")
862+
assertTrue(0xFFFF0000.toInt() !in blue, "copy(all 48) kept the original primary: $blue")
863+
}
864+
865+
private fun schemePrelude(): String =
866+
"""
867+
import python_multiplatform.binding as _pm_binding
868+
from androidx.compose.ui.graphics import Color__Int_Int_Int_Int as _Color
869+
from androidx.compose.material3 import ColorScheme, MaterialTheme, Button
870+
_slots = list([d for d in _pm_binding._BY_PACKAGE['androidx.compose.material3']['ColorScheme']
871+
if d.declared_arity() == 48][0].param_names)
872+
def _written(r, g, b):
873+
_w = dict((_s, _Color(0, 0, 0)) for _s in _slots)
874+
_w['primary'] = _Color(r, g, b)
875+
return _w
876+
""".trimIndent()
877+
878+
private fun constructed(red: Int, green: Int, blue: Int): String =
879+
"_scheme = ColorScheme(**_written($red, $green, $blue))"
880+
881+
/** [schemeBuild] must leave the scheme in `_scheme`. */
882+
private fun schemeButton(schemeBuild: String): String =
883+
schemePrelude() + "\n" + schemeBuild + """
884+
885+
MaterialTheme(
886+
colorScheme=_scheme,
887+
content=lambda: Button(onClick=lambda: None, content=lambda: None),
888+
)
889+
""".trimIndent()
890+
800891
/**
801892
* **`Icon`, which the same commit pinned as unreachable -- two of its three overloads are not.**
802893
*

0 commit comments

Comments
 (0)