Skip to content

Commit e8e9670

Browse files
fixes: Add smoke test / open-cv dependency / types-module (#98)
* Add smoke tests, and expose the 'types' module to the user * Bump version and update changelog * Fix smoke test venv * Narrow down pytest to tests folder * Potential fix for code scanning alert no. 3: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * smoke-test does not depend on ci * Fix types import * Fix types import, move py.typed and add a trove classifier --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 2b47722 commit e8e9670

10 files changed

Lines changed: 233 additions & 4 deletions

File tree

.github/workflows/ci_tests.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
permissions:
2+
contents: read
13
name: Run automated tests
24

35
on:
@@ -35,3 +37,26 @@ jobs:
3537
run: uv run pyright
3638
- name: Run the automated tests (for example)
3739
run: uv run pytest -v
40+
41+
smoke-test:
42+
runs-on: ubuntu-latest
43+
steps:
44+
- uses: actions/checkout@v4
45+
- name: Install Python
46+
uses: actions/setup-python@v5
47+
with:
48+
python-version: "3.10"
49+
- name: Install uv
50+
uses: astral-sh/setup-uv@v6
51+
with:
52+
enable-cache: true
53+
- name: Create fresh virtual environment for smoke test
54+
run: uv venv .smoke-test-venv
55+
- name: Install package in fresh environment
56+
run: |
57+
source .smoke-test-venv/bin/activate
58+
uv sync --frozen --active
59+
- name: Run smoke test
60+
run: |
61+
source .smoke-test-venv/bin/activate
62+
python smoke_test.py

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
# Released
1010

11+
## 5.1.0 - 22/08/2025
12+
13+
### Fixed
14+
- Added OpenCV as required dependency
15+
16+
### Added
17+
- Expose the `types`-module to the user, so they can import Pylette types from `Pylette.types`.
18+
1119
## 5.0.1 - 22/08/2025
1220

1321
### Changed

Pylette/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
from Pylette import types
12
from Pylette.src.color import Color
23
from Pylette.src.color_extraction import batch_extract_colors, extract_colors
34
from Pylette.src.palette import Palette
4-
from Pylette.src.types import ImageInput
55

6-
__all__ = ["extract_colors", "batch_extract_colors", "Palette", "Color", "ImageInput"]
6+
__all__ = ["extract_colors", "batch_extract_colors", "Palette", "Color", "types"]

Pylette/src/extractors/k_means.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import numpy as np
22
from numpy.typing import NDArray
3+
from typing_extensions import override
34

45
from Pylette.src.color import Color
56
from Pylette.src.extractors.protocol import NP_T, ColorExtractorBase
67

78

89
class KMeansExtractor(ColorExtractorBase):
10+
@override
911
def extract(self, arr: NDArray[NP_T], height: int, width: int, palette_size: int) -> list[Color]:
1012
"""
1113
Extracts a color palette using KMeans.

Pylette/src/extractors/median_cut.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
22
from numpy.typing import ArrayLike, NDArray
3+
from typing_extensions import override
34

45
from Pylette.src.color import Color
56
from Pylette.src.extractors.protocol import NP_T, ColorExtractorBase
@@ -121,6 +122,7 @@ def pixel_count(self) -> int:
121122

122123

123124
class MedianCutExtractor(ColorExtractorBase):
125+
@override
124126
def extract(self, arr: NDArray[NP_T], height: int, width: int, palette_size: int) -> list[Color]:
125127
"""
126128
Extracts a color palette using the median cut algorithm.

Pylette/src/py.typed

Whitespace-only changes.

Pylette/types.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
This module allows users to import types in multiple ways:
3+
- from pylette.types import ImageInput
4+
- import pylette.types as types
5+
"""
6+
7+
from Pylette.src.types import (
8+
ArrayImage,
9+
ArrayLike,
10+
BatchResult,
11+
BytesImage,
12+
ColorArray,
13+
ColorSpace,
14+
ColorTuple,
15+
CV2Image,
16+
ExtractionMethod,
17+
ExtractionParams,
18+
FloatArray,
19+
ImageInfo,
20+
ImageInput,
21+
ImageLike,
22+
IntArray,
23+
PaletteMetaData,
24+
PathLikeImage,
25+
PILImage,
26+
ProcessingStats,
27+
RGBATuple,
28+
RGBTuple,
29+
SourceType,
30+
URLImage,
31+
)
32+
33+
# Define what gets exported with "from pylette.types import *"
34+
__all__ = [
35+
"ImageInput",
36+
"ImageLike",
37+
"ArrayLike",
38+
"PathLikeImage",
39+
"URLImage",
40+
"BytesImage",
41+
"ArrayImage",
42+
"CV2Image",
43+
"PILImage",
44+
"ColorArray",
45+
"FloatArray",
46+
"IntArray",
47+
"RGBTuple",
48+
"RGBATuple",
49+
"ColorTuple",
50+
"ExtractionMethod",
51+
"ColorSpace",
52+
"SourceType",
53+
"ExtractionParams",
54+
"ImageInfo",
55+
"ProcessingStats",
56+
"PaletteMetaData",
57+
"BatchResult",
58+
]

pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pylette"
3-
version = "5.0.1"
3+
version = "5.1.0"
44
description = "A Python library for extracting color palettes from images."
55
authors = [
66
{name = "Ivar Stangeby"}
@@ -16,9 +16,12 @@ classifiers = [
1616
"Programming Language :: Python :: 3.10",
1717
"Programming Language :: Python :: 3.11",
1818
"Programming Language :: Python :: 3.12",
19+
"Programming Language :: Python :: 3.13",
20+
"Typing :: Typed"
1921
]
2022
dependencies = [
2123
"numpy>=1.26.4",
24+
"opencv-python>=4.11.0.86",
2225
"pillow>=9.3,<11.0",
2326
"requests>=2.32.3",
2427
"scikit-learn>=1.2",
@@ -55,6 +58,11 @@ dev = [
5558
[project.scripts]
5659
pylette = "Pylette.cmd:main_typer"
5760

61+
[tool.pytest.ini_options]
62+
testpaths = [
63+
"tests"
64+
]
65+
5866
[tool.pyright]
5967
include = ["Pylette"]
6068
exclude = ["tests"]

smoke_test.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""
2+
Smoke test for Pylette library.
3+
Tests basic functionality to ensure the library works correctly after installation.
4+
5+
This is here because I've pushed changes twice now that broke the library... :(
6+
"""
7+
8+
import json
9+
import subprocess
10+
import sys
11+
import tempfile
12+
from pathlib import Path
13+
14+
from PIL import Image
15+
16+
import Pylette
17+
18+
19+
def test_library_import():
20+
"""Test that the library can be imported successfully."""
21+
print("✓ Testing library import...")
22+
# Import should work if we get here
23+
assert hasattr(Pylette, "extract_colors"), "extract_colors function should be available"
24+
print(" Import successful")
25+
26+
27+
def test_basic_color_extraction():
28+
"""Test basic color extraction functionality."""
29+
print("✓ Testing basic color extraction...")
30+
31+
# Create a simple test image
32+
test_img = Image.new("RGB", (100, 100), color="red")
33+
palette = Pylette.extract_colors(test_img, palette_size=5)
34+
35+
assert len(palette) <= 5, f"Palette size should not exceed 5, got {len(palette)}"
36+
assert len(palette) > 0, "Should extract at least one color"
37+
38+
print(f" Extracted {len(palette)} colors successfully")
39+
40+
41+
def test_cli_functionality():
42+
"""Test CLI functionality."""
43+
print("✓ Testing CLI functionality...")
44+
45+
# Create a temporary test image
46+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
47+
test_img = Image.new("RGB", (50, 50), color="blue")
48+
test_img.save(tmp.name)
49+
50+
# Test CLI command
51+
result = subprocess.run(
52+
[sys.executable, "-m", "Pylette.cmd", tmp.name, "--palette_size", "3"], capture_output=True, text=True
53+
)
54+
55+
if result.returncode != 0:
56+
raise RuntimeError(f"CLI test failed: {result.stderr}")
57+
58+
# Clean up
59+
Path(tmp.name).unlink()
60+
61+
print(" CLI test passed")
62+
63+
64+
def test_extraction_methods():
65+
"""Test different extraction methods."""
66+
print("✓ Testing different extraction methods...")
67+
68+
test_img = Image.new("RGB", (50, 50), color="green")
69+
70+
# Test K-means extractor
71+
kmeans_palette = Pylette.extract_colors(test_img, palette_size=3, mode=Pylette.types.ExtractionMethod.KM)
72+
assert len(kmeans_palette) <= 3, "K-means should respect palette size"
73+
print(f" K-means extracted {len(kmeans_palette)} colors")
74+
75+
# Test Median cut extractor
76+
mediancut_palette = Pylette.extract_colors(test_img, palette_size=3, mode=Pylette.types.ExtractionMethod.MC)
77+
assert len(mediancut_palette) <= 3, "Median cut should respect palette size"
78+
print(f" Median cut extracted {len(mediancut_palette)} colors")
79+
80+
81+
def test_json_export():
82+
"""Test JSON export functionality."""
83+
print("✓ Testing JSON export...")
84+
85+
test_img = Image.new("RGB", (50, 50), color="purple")
86+
palette = Pylette.extract_colors(test_img, palette_size=2)
87+
88+
# Test JSON export
89+
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
90+
palette.to_json(tmp.name)
91+
92+
# Verify JSON file was created and is valid
93+
with open(tmp.name, "r") as f:
94+
data = json.load(f)
95+
assert "colors" in data, "JSON should contain 'colors' key"
96+
assert len(data["colors"]) <= 2, f"Should have at most 2 colors, got {len(data['colors'])}"
97+
98+
# Clean up
99+
Path(tmp.name).unlink()
100+
101+
print(" JSON export test passed")
102+
103+
104+
def main():
105+
"""Run all smoke tests."""
106+
print("🧪 Starting Pylette smoke test...")
107+
108+
try:
109+
test_library_import()
110+
test_basic_color_extraction()
111+
test_cli_functionality()
112+
test_extraction_methods()
113+
test_json_export()
114+
115+
print("✅ All smoke tests passed!")
116+
print("🎉 Pylette is working correctly")
117+
118+
except Exception as e:
119+
print(f"❌ Smoke test failed: {e}")
120+
sys.exit(1)
121+
122+
123+
if __name__ == "__main__":
124+
main()

uv.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)