yoga-python 0.1.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.
Files changed (75) hide show
  1. yoga_python-0.1.0/.github/workflows/create-release.yml +91 -0
  2. yoga_python-0.1.0/.github/workflows/quality.yml +54 -0
  3. yoga_python-0.1.0/.github/workflows/release.yml +159 -0
  4. yoga_python-0.1.0/.gitignore +30 -0
  5. yoga_python-0.1.0/.python-version +1 -0
  6. yoga_python-0.1.0/CMakeLists.txt +47 -0
  7. yoga_python-0.1.0/PKG-INFO +199 -0
  8. yoga_python-0.1.0/README.md +188 -0
  9. yoga_python-0.1.0/pyproject.toml +89 -0
  10. yoga_python-0.1.0/pyrightconfig.json +7 -0
  11. yoga_python-0.1.0/src/yoga/__init__.py +8 -0
  12. yoga_python-0.1.0/src/yoga/__init__.pyi +582 -0
  13. yoga_python-0.1.0/src/yoga/enums.py +132 -0
  14. yoga_python-0.1.0/src/yoga/value.py +54 -0
  15. yoga_python-0.1.0/src/yoga/yoga.cpp +1085 -0
  16. yoga_python-0.1.0/tests/conftest.py +11 -0
  17. yoga_python-0.1.0/tests/test_absolute_position.py +1550 -0
  18. yoga_python-0.1.0/tests/test_align_content.py +5075 -0
  19. yoga_python-0.1.0/tests/test_align_items.py +2175 -0
  20. yoga_python-0.1.0/tests/test_align_self.py +234 -0
  21. yoga_python-0.1.0/tests/test_android_news_feed.py +310 -0
  22. yoga_python-0.1.0/tests/test_aspect_ratio.py +978 -0
  23. yoga_python-0.1.0/tests/test_auto.py +283 -0
  24. yoga_python-0.1.0/tests/test_baseline.py +673 -0
  25. yoga_python-0.1.0/tests/test_baseline_func.py +55 -0
  26. yoga_python-0.1.0/tests/test_border.py +198 -0
  27. yoga_python-0.1.0/tests/test_box_sizing.py +2174 -0
  28. yoga_python-0.1.0/tests/test_clone_node.py +92 -0
  29. yoga_python-0.1.0/tests/test_computed_margin.py +105 -0
  30. yoga_python-0.1.0/tests/test_computed_padding.py +105 -0
  31. yoga_python-0.1.0/tests/test_config.py +33 -0
  32. yoga_python-0.1.0/tests/test_default_values.py +178 -0
  33. yoga_python-0.1.0/tests/test_dimension.py +87 -0
  34. yoga_python-0.1.0/tests/test_dirtied.py +109 -0
  35. yoga_python-0.1.0/tests/test_dirty_marking.py +309 -0
  36. yoga_python-0.1.0/tests/test_display.py +969 -0
  37. yoga_python-0.1.0/tests/test_display_contents.py +65 -0
  38. yoga_python-0.1.0/tests/test_edge.py +152 -0
  39. yoga_python-0.1.0/tests/test_events.py +266 -0
  40. yoga_python-0.1.0/tests/test_flex.py +574 -0
  41. yoga_python-0.1.0/tests/test_flex_direction.py +4060 -0
  42. yoga_python-0.1.0/tests/test_flex_gap.py +91 -0
  43. yoga_python-0.1.0/tests/test_flex_wrap.py +1845 -0
  44. yoga_python-0.1.0/tests/test_float_optional.py +299 -0
  45. yoga_python-0.1.0/tests/test_gap.py +2902 -0
  46. yoga_python-0.1.0/tests/test_had_overflow.py +139 -0
  47. yoga_python-0.1.0/tests/test_intrinsic_size.py +2776 -0
  48. yoga_python-0.1.0/tests/test_justify_content.py +1876 -0
  49. yoga_python-0.1.0/tests/test_layoutable_children.py +182 -0
  50. yoga_python-0.1.0/tests/test_margin.py +1647 -0
  51. yoga_python-0.1.0/tests/test_measure.py +859 -0
  52. yoga_python-0.1.0/tests/test_measure_cache.py +166 -0
  53. yoga_python-0.1.0/tests/test_measure_mode.py +258 -0
  54. yoga_python-0.1.0/tests/test_min_max_dimension.py +1202 -0
  55. yoga_python-0.1.0/tests/test_node_callback.py +77 -0
  56. yoga_python-0.1.0/tests/test_node_child.py +54 -0
  57. yoga_python-0.1.0/tests/test_ordinals.py +20 -0
  58. yoga_python-0.1.0/tests/test_padding.py +279 -0
  59. yoga_python-0.1.0/tests/test_percentage.py +1494 -0
  60. yoga_python-0.1.0/tests/test_persistence.py +249 -0
  61. yoga_python-0.1.0/tests/test_persistent_node_cloning.py +117 -0
  62. yoga_python-0.1.0/tests/test_relayout.py +257 -0
  63. yoga_python-0.1.0/tests/test_rounding.py +1031 -0
  64. yoga_python-0.1.0/tests/test_rounding_function.py +154 -0
  65. yoga_python-0.1.0/tests/test_rounding_measure.py +112 -0
  66. yoga_python-0.1.0/tests/test_scale_change.py +173 -0
  67. yoga_python-0.1.0/tests/test_size_overflow.py +164 -0
  68. yoga_python-0.1.0/tests/test_small_value_buffer.py +254 -0
  69. yoga_python-0.1.0/tests/test_static_position.py +5437 -0
  70. yoga_python-0.1.0/tests/test_style.py +43 -0
  71. yoga_python-0.1.0/tests/test_style_value_pool.py +92 -0
  72. yoga_python-0.1.0/tests/test_tree_mutation.py +93 -0
  73. yoga_python-0.1.0/tests/test_value.py +15 -0
  74. yoga_python-0.1.0/tests/test_zero_out_layout.py +36 -0
  75. yoga_python-0.1.0/uv.lock +246 -0
@@ -0,0 +1,91 @@
1
+ name: Create Release Tag
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ release_type:
7
+ description: 'Release type'
8
+ required: true
9
+ type: choice
10
+ options:
11
+ - patch
12
+ - minor
13
+ - major
14
+ default: patch
15
+
16
+ jobs:
17
+ create-tag:
18
+ runs-on: ubuntu-latest
19
+ permissions:
20
+ contents: write
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ with:
25
+ fetch-depth: 0
26
+ token: ${{ secrets.RELEASE_TOKEN }}
27
+
28
+ - name: Configure Git
29
+ run: |
30
+ git config user.name "${{ github.actor }}"
31
+ git config user.email "${{ github.actor }}@users.noreply.github.com"
32
+
33
+ - name: Get current version and calculate next version
34
+ id: version
35
+ run: |
36
+ CURRENT_VERSION=$(grep "^version = " pyproject.toml | cut -d'"' -f2)
37
+ echo "Current version: $CURRENT_VERSION"
38
+
39
+ if [ -z "$CURRENT_VERSION" ]; then
40
+ echo "Error: Could not extract version"
41
+ exit 1
42
+ fi
43
+
44
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
45
+
46
+ RELEASE_TYPE="${{ inputs.release_type }}"
47
+ case $RELEASE_TYPE in
48
+ major)
49
+ NEW_MAJOR=$((MAJOR + 1)); NEW_MINOR=0; NEW_PATCH=0
50
+ ;;
51
+ minor)
52
+ NEW_MAJOR=$MAJOR; NEW_MINOR=$((MINOR + 1)); NEW_PATCH=0
53
+ ;;
54
+ patch)
55
+ NEW_MAJOR=$MAJOR; NEW_MINOR=$MINOR; NEW_PATCH=$((PATCH + 1))
56
+ ;;
57
+ esac
58
+
59
+ NEW_VERSION="$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH"
60
+
61
+ echo "Current: $CURRENT_VERSION -> New: $NEW_VERSION ($RELEASE_TYPE)"
62
+ echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
63
+ echo "new=$NEW_VERSION" >> $GITHUB_OUTPUT
64
+
65
+ - name: Check if version already exists
66
+ run: |
67
+ VERSION="v${{ steps.version.outputs.new }}"
68
+ if git rev-parse "$VERSION" >/dev/null 2>&1; then
69
+ echo "Tag $VERSION already exists!"
70
+ exit 1
71
+ fi
72
+ echo "Tag $VERSION does not exist"
73
+
74
+ - name: Update version in pyproject.toml
75
+ run: |
76
+ VERSION="${{ steps.version.outputs.new }}"
77
+ sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
78
+ grep "^version = " pyproject.toml
79
+
80
+ - name: Commit and push
81
+ run: |
82
+ git add pyproject.toml
83
+ git commit -m "chore: bump version to ${{ steps.version.outputs.new }}"
84
+ git push origin main
85
+
86
+ - name: Create and push tag
87
+ run: |
88
+ VERSION="v${{ steps.version.outputs.new }}"
89
+ git tag -a "$VERSION" -m "Release $VERSION"
90
+ git push origin "$VERSION"
91
+ echo "Created tag $VERSION - this will trigger release workflow"
@@ -0,0 +1,54 @@
1
+ name: Code Quality
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ quality:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v4
21
+
22
+ - name: Set up Python ${{ matrix.python-version }}
23
+ run: uv python install ${{ matrix.python-version }}
24
+
25
+ - name: Install dependencies and build extension
26
+ run: uv sync --all-extras
27
+
28
+ - name: Lint with ruff
29
+ run: |
30
+ uv run ruff check
31
+ uv run ruff format --check
32
+
33
+ - name: Type check with pyright
34
+ run: uv run pyright
35
+
36
+ - name: Run tests
37
+ run: |
38
+ uv run pytest tests/ -v --tb=short --cov=src/yoga --cov-report=term-missing --cov-report=xml
39
+
40
+ - name: Upload coverage to Codecov
41
+ uses: codecov/codecov-action@v4
42
+ if: matrix.python-version == '3.12'
43
+ with:
44
+ file: ./coverage.xml
45
+ flags: unittests
46
+ name: codecov-umbrella
47
+ fail_ci_if_error: false
48
+
49
+ quality-check:
50
+ runs-on: ubuntu-latest
51
+ needs: quality
52
+ steps:
53
+ - name: All quality checks passed
54
+ run: echo "✅ All quality checks passed!"
@@ -0,0 +1,159 @@
1
+ name: 🚀 Release to PyPI and GitHub
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+ inputs:
9
+ version:
10
+ description: 'Version to release (e.g., 0.1.0)'
11
+ required: true
12
+ type: string
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ build-and-validate:
19
+ name: 🏗️ Build and Validate Package
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - name: Checkout code
24
+ uses: actions/checkout@v4
25
+ with:
26
+ fetch-depth: 0
27
+
28
+ - name: Set up Python
29
+ uses: actions/setup-python@v5
30
+ with:
31
+ python-version: '3.12'
32
+
33
+ - name: Install uv
34
+ uses: astral-sh/setup-uv@v4
35
+
36
+ - name: Set up Python
37
+ run: uv python install 3.12
38
+
39
+ - name: Install Python build dependencies
40
+ run: |
41
+ python -m pip install --upgrade pip
42
+ pip install twine check-wheel-contents validate-pyproject auditwheel
43
+
44
+ - name: Validate pyproject.toml
45
+ run: validate-pyproject pyproject.toml
46
+
47
+ - name: Build Python package
48
+ run: uv build
49
+
50
+ - name: Repair Linux wheel for PyPI (manylinux tag)
51
+ run: |
52
+ for whl in dist/*linux_x86_64.whl dist/*linux_aarch64.whl; do
53
+ [ -f "$whl" ] || continue
54
+ auditwheel repair "$whl" -w dist
55
+ rm -f "$whl"
56
+ done
57
+
58
+ - name: Validate package contents
59
+ run: |
60
+ echo "📦 Validating package structure..."
61
+ check-wheel-contents dist/*.whl --ignore W004,W005,W009,W010
62
+
63
+ echo "📋 Package contents summary:"
64
+ python -m zipfile -l dist/*.whl | grep -E "\.py$|\.so$" | head -25
65
+ echo "..."
66
+
67
+ - name: Upload build artifacts
68
+ uses: actions/upload-artifact@v4
69
+ with:
70
+ name: python-package-distributions
71
+ path: dist/
72
+ retention-days: 7
73
+
74
+ publish-to-testpypi:
75
+ name: 🧪 Publish to TestPyPI
76
+ needs: [build-and-validate]
77
+ runs-on: ubuntu-latest
78
+ if: github.event_name == 'workflow_dispatch'
79
+ environment:
80
+ name: test-pypi
81
+ url: https://test.pypi.org/p/yoga-python
82
+ permissions:
83
+ id-token: write # OIDC trusted publishing
84
+
85
+ steps:
86
+ - name: Download package distributions
87
+ uses: actions/download-artifact@v4
88
+ with:
89
+ name: python-package-distributions
90
+ path: dist/
91
+
92
+ - name: Publish to TestPyPI
93
+ uses: pypa/gh-action-pypi-publish@release/v1
94
+ with:
95
+ repository-url: https://test.pypi.org/legacy/
96
+ print-hash: true
97
+
98
+ publish-to-pypi:
99
+ name: 🚀 Publish to PyPI
100
+ needs: [build-and-validate]
101
+ runs-on: ubuntu-latest
102
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
103
+ environment:
104
+ name: pypi
105
+ url: https://pypi.org/p/yoga-python
106
+ permissions:
107
+ id-token: write # OIDC trusted publishing
108
+ contents: write # For GitHub release creation
109
+
110
+ steps:
111
+ - name: Download package distributions
112
+ uses: actions/download-artifact@v4
113
+ with:
114
+ name: python-package-distributions
115
+ path: dist/
116
+
117
+ - name: Publish to PyPI
118
+ uses: pypa/gh-action-pypi-publish@release/v1
119
+ with:
120
+ print-hash: true
121
+
122
+ create-github-release:
123
+ name: 📋 Create GitHub Release
124
+ needs: [publish-to-pypi]
125
+ runs-on: ubuntu-latest
126
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
127
+ permissions:
128
+ contents: write
129
+
130
+ steps:
131
+ - name: Checkout code
132
+ uses: actions/checkout@v4
133
+ with:
134
+ fetch-depth: 0
135
+
136
+ - name: Download package distributions
137
+ uses: actions/download-artifact@v4
138
+ with:
139
+ name: python-package-distributions
140
+ path: dist/
141
+
142
+ - name: Determine release type
143
+ id: release-type
144
+ run: |
145
+ if [[ ${{ github.ref_name }} =~ -alpha|-beta|-rc ]]; then
146
+ echo "prerelease=true" >> $GITHUB_OUTPUT
147
+ else
148
+ echo "prerelease=false" >> $GITHUB_OUTPUT
149
+ fi
150
+
151
+ - name: Create GitHub Release
152
+ run: |
153
+ gh release create ${{ github.ref_name }} \
154
+ --title "yoga-python ${{ github.ref_name }}" \
155
+ --generate-notes \
156
+ ${{ steps.release-type.outputs.prerelease == 'true' && '--prerelease' || '--latest' }} \
157
+ dist/*
158
+ env:
159
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,30 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv/
11
+
12
+ # Cache directories
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .mypy_cache/
16
+
17
+ # Build artifacts
18
+ *.so
19
+ *.cmake/
20
+ CMakeFiles/
21
+ CMakeCache.txt
22
+ build_test/
23
+ src/yoga/*.so
24
+ src/yoga/CMakeInit.txt
25
+ src/yoga/install_manifest.txt
26
+ src/yoga/__pycache__/
27
+
28
+ # Reference C++ code (used for porting, not part of this repo)
29
+ reference/
30
+
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,47 @@
1
+ cmake_minimum_required(VERSION 3.15)
2
+ project(yoga_python)
3
+
4
+ # Allow FetchContent_Populate (we use it to add only inner yoga, not root install)
5
+ if(POLICY CMP0169)
6
+ cmake_policy(SET CMP0169 OLD)
7
+ endif()
8
+
9
+ set(CMAKE_CXX_STANDARD 20)
10
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
11
+ set(PYBIND11_FINDPYTHON ON)
12
+
13
+ set(yoga_BUILD_TESTS OFF CACHE BOOL "Build Yoga tests" FORCE)
14
+
15
+ # Use local reference yoga if present, otherwise fetch from GitHub
16
+ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/reference/yoga/CMakeLists.txt)
17
+ message(STATUS "Using local reference yoga")
18
+ find_package(pybind11 REQUIRED)
19
+ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/reference/yoga ${CMAKE_BINARY_DIR}/yoga-build EXCLUDE_FROM_ALL)
20
+ else()
21
+ message(STATUS "Fetching yoga from GitHub...")
22
+ include(FetchContent)
23
+ FetchContent_Declare(
24
+ yoga
25
+ GIT_REPOSITORY https://github.com/facebook/yoga.git
26
+ GIT_TAG cfdacac0e3c2e91ab15939027688756271a66025
27
+ GIT_PROGRESS TRUE
28
+ )
29
+ # Populate but do not add root project (avoids root install of include/lib).
30
+ # We only add the inner yoga library so we get yogacore without extra installs.
31
+ FetchContent_GetProperties(yoga)
32
+ if(NOT yoga_POPULATED)
33
+ FetchContent_Populate(yoga)
34
+ endif()
35
+ set(_yoga_root ${yoga_SOURCE_DIR})
36
+ set(yoga_SOURCE_DIR ${_yoga_root}/yoga)
37
+ set(yoga_BUILD_TESTS OFF CACHE BOOL "Build Yoga tests" FORCE)
38
+ add_subdirectory(${yoga_SOURCE_DIR} ${CMAKE_BINARY_DIR}/yoga-build EXCLUDE_FROM_ALL)
39
+ endif()
40
+
41
+ find_package(pybind11 REQUIRED)
42
+ pybind11_add_module(yoga src/yoga/yoga.cpp)
43
+ target_include_directories(yoga PRIVATE ${yoga_SOURCE_DIR})
44
+ target_link_libraries(yoga PRIVATE yogacore)
45
+
46
+ # Install the compiled extension to the yoga package directory
47
+ install(TARGETS yoga DESTINATION "yoga")
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.2
2
+ Name: yoga-python
3
+ Version: 0.1.0
4
+ Summary: Python binding for Facebook Yoga layout engine
5
+ Requires-Python: >=3.12
6
+ Provides-Extra: dev
7
+ Requires-Dist: pytest>=8.0; extra == "dev"
8
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
9
+ Requires-Dist: ruff>=0.3; extra == "dev"
10
+ Description-Content-Type: text/markdown
11
+
12
+ # yoga-python
13
+
14
+ Python bindings for [Facebook's Yoga](https://github.com/facebook/yoga) layout engine — a cross-platform implementation of [CSS Flexbox](https://www.w3.org/TR/css-flexbox-1/).
15
+
16
+ Yoga computes a layout tree of nodes with flexbox styles and outputs pixel-perfect positions. This package wraps the full C++ engine via [pybind11](https://github.com/pybind/pybind11), giving you the same layout behavior you get in React Native, Litho, and other Yoga-backed frameworks — directly from Python.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install yoga-python
22
+ ```
23
+
24
+ Building from source requires a C++20 compiler, CMake 3.15+, and pybind11:
25
+
26
+ ```bash
27
+ pip install scikit-build-core pybind11
28
+ pip install .
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```python
34
+ from yoga import (
35
+ Node, Config, Direction, FlexDirection, Justify,
36
+ Align, Edge, YGValuePoint, YGValuePercent,
37
+ )
38
+
39
+ root = Node()
40
+ root.flex_direction = FlexDirection.Row
41
+ root.width = YGValuePoint(300)
42
+ root.height = YGValuePoint(200)
43
+ root.set_padding(Edge.All, 10)
44
+
45
+ child_a = Node()
46
+ child_a.flex_grow = 1
47
+ child_a.set_margin(Edge.Right, 10)
48
+ root.insert_child(child_a, 0)
49
+
50
+ child_b = Node()
51
+ child_b.flex_grow = 2
52
+ root.insert_child(child_b, 1)
53
+
54
+ root.calculate_layout(300, 200, Direction.LTR)
55
+
56
+ print(f"child_a: {child_a.layout_left}, {child_a.layout_top}, "
57
+ f"{child_a.layout_width}x{child_a.layout_height}")
58
+ # child_a: 10.0, 10.0, 90.0x180.0
59
+
60
+ print(f"child_b: {child_b.layout_left}, {child_b.layout_top}, "
61
+ f"{child_b.layout_width}x{child_b.layout_height}")
62
+ # child_b: 110.0, 10.0, 180.0x180.0
63
+ ```
64
+
65
+ ## API overview
66
+
67
+ ### Node
68
+
69
+ The primary object. Create nodes, configure their styles, build a tree, then call `calculate_layout`.
70
+
71
+ ```python
72
+ node = Node() # default config
73
+ node = Node(config) # with explicit config
74
+ ```
75
+
76
+ **Style properties** (set directly as attributes):
77
+
78
+ | Property | Type | Example |
79
+ |---|---|---|
80
+ | `flex_direction` | `FlexDirection` | `node.flex_direction = FlexDirection.Row` |
81
+ | `justify_content` | `Justify` | `node.justify_content = Justify.Center` |
82
+ | `align_items` | `Align` | `node.align_items = Align.Stretch` |
83
+ | `align_self` | `Align` | `node.align_self = Align.FlexEnd` |
84
+ | `align_content` | `Align` | `node.align_content = Align.SpaceBetween` |
85
+ | `flex_wrap` | `Wrap` | `node.flex_wrap = Wrap.Wrap` |
86
+ | `overflow` | `Overflow` | `node.overflow = Overflow.Hidden` |
87
+ | `display` | `Display` | `node.display = Display.Flex` |
88
+ | `position_type` | `PositionType` | `node.position_type = PositionType.Absolute` |
89
+ | `flex_grow` | `float` | `node.flex_grow = 1.0` |
90
+ | `flex_shrink` | `float` | `node.flex_shrink = 0.0` |
91
+ | `flex_basis` | `YGValue` | `node.flex_basis = YGValuePoint(100)` |
92
+ | `width` / `height` | `YGValue` | `node.width = YGValuePercent(50)` |
93
+ | `min_width` / `max_width` | `YGValue` | `node.min_width = YGValuePoint(80)` |
94
+ | `min_height` / `max_height` | `YGValue` | `node.max_height = YGValuePoint(400)` |
95
+ | `aspect_ratio` | `float` | `node.aspect_ratio = 16 / 9` |
96
+ | `box_sizing` | `BoxSizing` | `node.box_sizing = BoxSizing.ContentBox` |
97
+ | `direction` | `Direction` | `node.direction = Direction.LTR` |
98
+
99
+ **Edge-based properties** (margin, padding, border, position):
100
+
101
+ ```python
102
+ node.set_margin(Edge.Left, 10) # points
103
+ node.set_margin(Edge.Top, YGValuePercent(5)) # percent
104
+ node.set_padding(Edge.All, 20)
105
+ node.set_border(Edge.Bottom, 1)
106
+ node.set_position(Edge.Left, YGValuePoint(50))
107
+ ```
108
+
109
+ **Tree manipulation:**
110
+
111
+ ```python
112
+ node.insert_child(child, index)
113
+ node.remove_child(child)
114
+ node.remove_all_children()
115
+ node.set_children([child_a, child_b])
116
+ node.child_count # int
117
+ node[index] # Node (supports indexing)
118
+ ```
119
+
120
+ **Layout:**
121
+
122
+ ```python
123
+ node.calculate_layout(width, height, Direction.LTR)
124
+
125
+ node.layout_left # float
126
+ node.layout_top # float
127
+ node.layout_width # float
128
+ node.layout_height # float
129
+ node.layout_direction # Direction
130
+ node.layout_margin(Edge.Left) # float
131
+ node.layout_padding(Edge.Top) # float
132
+ node.layout_border(Edge.Right) # float
133
+ ```
134
+
135
+ **Measure functions** for leaf nodes with custom content sizing:
136
+
137
+ ```python
138
+ def measure(node, width, width_mode, height, height_mode):
139
+ return {"width": 100, "height": 50}
140
+
141
+ node.set_measure_func(measure)
142
+ ```
143
+
144
+ **Baseline functions** for custom baseline alignment:
145
+
146
+ ```python
147
+ def baseline(node, width, height):
148
+ return height * 0.8
149
+
150
+ node.set_baseline_func(baseline)
151
+ ```
152
+
153
+ ### Config
154
+
155
+ ```python
156
+ config = Config()
157
+ config.use_web_defaults = True
158
+ config.point_scale_factor = 2.0
159
+ config.errata = Errata.StretchFlexBasis
160
+ ```
161
+
162
+ ### Values
163
+
164
+ ```python
165
+ YGValuePoint(100) # 100px
166
+ YGValuePercent(50) # 50%
167
+ YGValueAuto # auto
168
+ YGValueUndefined # undefined
169
+ ```
170
+
171
+ ## Enums
172
+
173
+ All Yoga enums are available as Python enum types (pybind11 enums, int-convertible):
174
+
175
+ `Direction`, `FlexDirection`, `Justify`, `Align`, `PositionType`, `Wrap`, `Overflow`, `Display`, `Edge`, `Unit`, `MeasureMode`, `Dimension`, `BoxSizing`, `Gutter`, `Errata`, `NodeType`, `LogLevel`, `ExperimentalFeature`
176
+
177
+ ## Development
178
+
179
+ ```bash
180
+ git clone https://github.com/banditburai/yoga-python.git
181
+ cd yoga-python
182
+ uv sync --dev
183
+
184
+ # Run tests
185
+ uv run pytest tests/ -v
186
+
187
+ # Lint & type checking
188
+ uv run ruff check
189
+ uv run ruff format --check
190
+ uv run pyright
191
+ ```
192
+
193
+ ## Test coverage
194
+
195
+ The test suite is a 1:1 port of the [Yoga C++ test suite](https://github.com/facebook/yoga/tree/main/tests) — 822 tests covering all layout modes, edge cases, and behaviors. Tests that the C++ suite itself skips (`GTEST_SKIP`) are also skipped in Python.
196
+
197
+ ## License
198
+
199
+ MIT — same as [Yoga](https://github.com/facebook/yoga/blob/main/LICENSE).