Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b872fe3
replace monolithic setup.py with pyproject.toml + scripts
samizdatco Jul 9, 2026
19809d0
bump minimum python version to 3.9
samizdatco Jul 9, 2026
3256a87
publish wheels containing the native extension
samizdatco Jul 9, 2026
18f985c
reflect test results in exit code
samizdatco Jul 9, 2026
d486a29
test for presence of pyobjc+extension, not just paths
samizdatco Jul 9, 2026
cafc691
fix regex escape
samizdatco Jul 10, 2026
62e9ca2
fix objc.super use in ConsoleScript
samizdatco Jul 10, 2026
665bc57
fix `ordered()` crash on grobs and silent dotted-path failures
samizdatco Jul 10, 2026
3f69b4c
add python 3.14 support by bumping pyobjc to 11.1
samizdatco Jul 10, 2026
3639d49
ignore rsrc files resulting from in-place build
samizdatco Jul 10, 2026
dc2ac7f
generate MANIFEST.in only when needed for sdist
samizdatco Jul 10, 2026
cb696af
rename build.py to make.py (to prevent `-m build` shadowing)
samizdatco Jul 10, 2026
46052ee
opt into secure restorable state
samizdatco Jul 10, 2026
ddab30c
update embedded python to 3.14.6
samizdatco Jul 10, 2026
f8ca3c8
update python version compat
samizdatco Jul 10, 2026
7df6d3b
update 1.0.1 docs
samizdatco Jul 11, 2026
ef7093a
bump copyright year
samizdatco Jul 11, 2026
2f58879
fix typos
samizdatco Jul 11, 2026
e0e6d59
add missing 3.14 classifier
samizdatco Jul 11, 2026
00d7e22
use system fonts in typography tests
samizdatco Jul 11, 2026
6363a2a
fixed Text.flow() when invoked w/ callback
samizdatco Jul 11, 2026
30f574f
fix the test for blend-only effects
samizdatco Jul 11, 2026
e4b666a
fix MagicNumber's numeric-protocol implementation
samizdatco Jul 11, 2026
41556e2
fix rasterization of clipping masks based on vector images
samizdatco Jul 13, 2026
318b8d9
add clip() test cases that use canvas transforms
samizdatco Jul 13, 2026
e5b8d1b
replace deprecated WebView with WKWebView
samizdatco Jul 13, 2026
79b7051
update document contents after each edit
samizdatco Jul 13, 2026
bbff6e9
fix license format
samizdatco Jul 13, 2026
74fc923
rename `sync_edits` and `flash_menu` methods
samizdatco Jul 13, 2026
293f85a
add new default editor theme
samizdatco Jul 13, 2026
5280134
add `Assets.car` for macOS 26+ icon
samizdatco Jul 14, 2026
43e2599
Replace setup.py with direct call to tests
samizdatco Jul 14, 2026
cf15d6e
handle quadratic curves in `path_to_polygon`
samizdatco Jul 14, 2026
8315113
Add logo banner to readme
samizdatco Jul 14, 2026
0226c2b
Fix deprecation warning from pyobjc
samizdatco Jul 14, 2026
2e5635e
update module providing UTType*
samizdatco Jul 14, 2026
a119caf
set exit code on script errors when running `plotdevice` headless
samizdatco Jul 14, 2026
8338229
replace `xrange` with `range` in code samples
samizdatco Jul 14, 2026
1773ce0
update markdown docs
samizdatco Jul 14, 2026
ac940c6
fix crash when canvas is zoomed before script has rendered
samizdatco Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix ordered() crash on grobs and silent dotted-path failures
  • Loading branch information
samizdatco committed Jul 10, 2026
commit 665bc579bf7e1d868deb69b0a22e15660f06b8ed
22 changes: 10 additions & 12 deletions plotdevice/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import json
import csv
from contextlib import contextmanager
from functools import cmp_to_key
from functools import cmp_to_key, reduce
from collections import OrderedDict, defaultdict
from collections.abc import Mapping
from os.path import abspath, dirname, exists, join, splitext
from random import choice, shuffle

Expand Down Expand Up @@ -106,18 +107,15 @@ def _key(seq, names):
# treat objects without a given key and objects with the key set to None equivalently
MISSING = None

# look for dict items by default
getter = lambda obj: tuple(obj.get(n, MISSING) for n in names)
def getter(obj):
# use item lookup if obj is dict-like
if isinstance(obj, Mapping):
return tuple(obj.get(n, MISSING) for n in names)

# walk through the objects and see if any of them lacks the dict fields but has attrs
for obj in seq:
try:
if any(n in obj for n in names):
break # dict fields exist so use the itemgetter
except TypeError:
# for non-dict objects like namedtuples and dataclasses use an attrgetter
if any(hasattr(obj, n) for n in names):
getter = lambda obj: tuple(getattr(obj, n, MISSING) for n in names)
# otherwise check attrs
return tuple(
reduce(lambda o, part: getattr(o, part, MISSING), n.split('.'), obj) for n in names
)

def compare(a, b):
default = 0
Expand Down
4 changes: 2 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ def render(self, save_output=True):
pass

def suites():
from . import typography, primitives, drawing, compositing, geometry, module
from . import typography, primitives, drawing, compositing, geometry, module, canvas

suite = unittest.TestSuite()
for mod in (typography, primitives, drawing, compositing, geometry, module):
for mod in (typography, primitives, drawing, compositing, geometry, module, canvas):
suite.addTest(mod.suite())
return suite

Expand Down
32 changes: 32 additions & 0 deletions tests/canvas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# encoding: utf-8
import unittest
from types import SimpleNamespace
from collections import UserDict
from . import PlotDeviceTestCase
from plotdevice import *

class CanvasTests(PlotDeviceTestCase):
def test_ordered_by_dotted_attr(self):
size(100, 100)
positions = [80, 20, 50]
for y in positions:
rect(10, y, 10, 10)

sorted_grobs = ordered(canvas, 'bounds.y')
self.assertEqual([g.bounds.y for g in sorted_grobs], sorted(positions))

def test_ordered_by_dict_like_non_dict(self):
items = [UserDict({'y': 80}), UserDict({'y': 20}), UserDict({'y': 50})]
sorted_items = ordered(items, 'y')
self.assertEqual([d['y'] for d in sorted_items], [20, 50, 80])

def test_ordered_heterogeneous_items(self):
items = [{'y': 80}, SimpleNamespace(y=20), {'y': 50}]
sorted_items = ordered(items, 'y')
ys = [it['y'] if isinstance(it, dict) else it.y for it in sorted_items]
self.assertEqual(ys, [20, 50, 80])

def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(CanvasTests))
return suite