From 4567de815610538e7986b710017507920b80a0bb Mon Sep 17 00:00:00 2001 From: Jason Cooke Date: Tue, 30 Jul 2019 21:22:30 +1200 Subject: [PATCH 01/59] docs: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f04d2a4..8b480a41 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Using external libraries Since PlotDevice scripts are pure Python, the entirety of the [stdlib](http://docs.python.org/2/library/) and [PyPI](https://pypi.python.org/pypi) -are avaliable to you. In addition, a wide array of PlotDevice Libraries have been contributed +are available to you. In addition, a wide array of PlotDevice Libraries have been contributed by the community to solve more visualization-specific problems. #### Installing PlotDevice Libraries From e5a69e2a6be4c964c71de9b6c31e73da2c8e6f1e Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 10 Jul 2022 17:33:42 -0400 Subject: [PATCH 02/59] use script name as default export filename - when only a format is specified (e.g., `--export pdf`), use the basename of the script for the output file and write it to the current working directory - thanks to @dimitre for the suggestion - closes #32 --- plotdevice/__main__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plotdevice/__main__.py b/plotdevice/__main__.py index 94aa43e6..9013414c 100644 --- a/plotdevice/__main__.py +++ b/plotdevice/__main__.py @@ -29,7 +29,7 @@ import sys, os, re import argparse -from os.path import exists, islink, dirname, abspath, realpath, join +from os.path import exists, islink, dirname, basename, abspath, realpath, join, splitext def main(): @@ -105,7 +105,12 @@ def main(): opts.mode = 'headless' # screen out unsupported file extensions - basename, ext = opts.export.lower().rsplit('.',1) + try: + outname, ext = opts.export.lower().rsplit('.',1) + except ValueError: + ext = opts.export.lower() + outname = splitext(basename(opts.script))[0] + opts.export = '.'.join([outname, ext]) if ext not in ('pdf', 'eps', 'png', 'jpg', 'heic', 'tiff', 'gif', 'mov'): parser.exit(1, 'bad argument [--export]\nthe output filename must end with a supported format:\n pdf, eps, png, tiff, jpg, heic, gif, or mov\n') @@ -124,7 +129,7 @@ def main(): # it's a `single' doc or a sequence of numbered pdf files opts.single = bool(ext=='pdf' and not re.search('{\d+}', opts.export) and opts.last and opts.first < opts.last) - if m:= re.search(r'@(\d+)[xX]$', basename): + if m:= re.search(r'@(\d+)[xX]$', outname): opts.zoom = float(m.group(1)) else: opts.zoom = max(0.01, opts.zoom/100) From c431d5062f005f681ee6de4eefb06a22044cd22f Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 10 Jul 2022 18:55:41 -0400 Subject: [PATCH 03/59] add a horizontal rule to template --- plotdevice/util/ottobot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plotdevice/util/ottobot.py b/plotdevice/util/ottobot.py index d025dab0..295b1a10 100644 --- a/plotdevice/util/ottobot.py +++ b/plotdevice/util/ottobot.py @@ -227,7 +227,8 @@ def genTemplate(kind='sketch'): return """size(512, 512) background(1) -text("Welcome to PlotDevice", 40, 40) +rect(20, 15, WIDTH-40, 1) +text("Welcome to PlotDevice", 20, 40) """ elif kind=='anim': return """# to create an animation, call speed() with a From 918543f8a1a21a83ebfc8ae9c7f7e1a778a52acd Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Tue, 12 Jul 2022 11:18:33 -0400 Subject: [PATCH 04/59] documentation improvements --- README.md | 24 +++++++++++++++++------- setup.py | 4 +--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6f1b9908..dc011a73 100644 --- a/README.md +++ b/README.md @@ -24,32 +24,37 @@ and those [installed through Homebrew](https://docs.brew.sh/Homebrew-and-Python) Over the years since the last release, progress in both macOS and Python itself led to quite a bit of breakage. Some of the highlights of this maintenance release include: -New Features +###### New Features - Python 3 support (including a bundled 3.10 installation in the app) - images can now be exported in HEIC format and videos support H.265 (HEVC) -- image exports have a configurable 'zoom' to create 2x/3x/etc 'retina' images +- image exports have a configurable `zoom` to create 2x/3x/etc ‘retina’ images - revamped `var()` command for creating GUIs to modify values via sliders, buttons, toggles, etc. -- updated text editor with multiple tabs, new themes, and additional key-binding modes for sublime and vs code users +- updated text editor with multiple tabs, new themes, and additional key-binding modes emulating Sublime Text and VS Code - the module's command line interface is now accessible through `python3 -m plotdevice` -- user-configurable document autosaving +- document autosaving is now user-configurable -Bugfixes +###### Bugfixes - exported images generated on retina machines now have the proper dimensions - hex colors can now use lowercase letters -- automatic variables like WIDTH & HEIGHT correctly support the `/` operator +- automatic variables like `WIDTH` & `HEIGHT` correctly support the `/` operator - the Color object's `.blend()` method is working again - the `read()` command can now handle csv files with spaces in their header row names - the `translate()` command now incorporates non-pixel grid units set via the `size()` command - cmyk exports are working reliably for command line `--export` and via the `export(cmyk=True)` method - arguments defined using the command line tool's `--args` options are now passed to the script's `sys.argv` -Misc. Improvements +###### Misc. Improvements - the command line tool can be exited via ctrl-c in addtion to being Quit from the menu bar - simplified unicode handling (and improved support for normalization of user-provided strings) - building the module now only requires Xcode command line tools—not a full Xcode.app installation - the `text()` command will always treat its first argument as content (even if it's not a string) unless a `str`, `xml`, or `src` keyword argument is provided - the mouse pointer is now visible in full-screen mode (and will auto-hide when inactive) +###### Unfortunate Casualties +- The NodeBox Libraries (`coreimage`, `colors`, and friends) would require quite a bit of attention to get working properly again. + A first pass can be found in the [`plotdevice-libs` repository](https://github.com/plotdevice/plotdevice-libs) but they're not + ready for prime-time. If you're interested in contributing, this would be a terrific place to start! + Installation ------------ @@ -96,6 +101,11 @@ pip3 install plotdevice python3 -m plotdevice ``` +#### Building from source + +You can also clone the git repository and build PlotDevice as a module or application from scratch. +Consult the [build instructions](https://github.com/plotdevice/plotdevice/discussions/59) for details. + Documentation ------------- diff --git a/setup.py b/setup.py index c83a1f5a..af7130a4 100644 --- a/setup.py +++ b/setup.py @@ -213,9 +213,7 @@ def run(self): build_ext.inplace = 1 self.run_command('build_ext') - # build the sdist (primarily for access to its rsrc subdir) - self.run_command('build_py') - + print("\nA local development environment has been set up in %s" % venv_dir) from setuptools.command.sdist import sdist class BuildDistCommand(sdist): From 418484e9be678c70899b3325a699727f7a7d98da Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Tue, 12 Jul 2022 12:49:12 -0400 Subject: [PATCH 05/59] update the display after a var() button click --- plotdevice/gui/widgets.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plotdevice/gui/widgets.py b/plotdevice/gui/widgets.py index 2c01afc7..397e3d7f 100644 --- a/plotdevice/gui/widgets.py +++ b/plotdevice/gui/widgets.py @@ -271,6 +271,12 @@ def callHandler_(self, name): var = self.script.vm.params[name] result = self.script.vm.call(var.name) self.script.echo(result.output) + if result.ok: + try: + self.script.currentView.setCanvas(self.script.vm.canvas) + except DeviceError as e: + return self.script.crash() + @objc.python_method def updateInterface(self): From 6ba26ab5de9101891290c13d968d72d64f722312 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Tue, 12 Jul 2022 15:38:21 -0400 Subject: [PATCH 06/59] add `--install` subcommand to cli --- README.md | 1 + app/plotdevice | 24 +++++++++++++----------- plotdevice/__main__.py | 22 +++++++++++++++++++++- plotdevice/gui/app.py | 3 +++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index dc011a73..20c70be7 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ a bit of breakage. Some of the highlights of this maintenance release include: - revamped `var()` command for creating GUIs to modify values via sliders, buttons, toggles, etc. - updated text editor with multiple tabs, new themes, and additional key-binding modes emulating Sublime Text and VS Code - the module's command line interface is now accessible through `python3 -m plotdevice` +- the command line tool has a new `--install` option to download [PyPI](https://pypi.org) packages for use within the app - document autosaving is now user-configurable ###### Bugfixes diff --git a/app/plotdevice b/app/plotdevice index 28dc9599..9d7d3550 100755 --- a/app/plotdevice +++ b/app/plotdevice @@ -1,18 +1,20 @@ #!/bin/sh -export _p_l_o_t_d_e_v_i_c_e_=$(basename ${0}) -python=$( +env=$( self=${0} - while [ -L "${self}" ]; - do - cd "${self%/*}" - self=$(readlink "${self}") - done + if [ -L "${self}" ]; then + self=$(readlink -f "${self}") + fi cd "${self%/*}" - echo "$(pwd -P)/python3" + echo "$(pwd -P)" ) -if [ ! -x $python ]; then - python=python3 +PYTHON="$env/python3" +PIP="$env/pip3" +if [ ! -x $PYTHON ]; then + PYTHON=python3 + PIP=pip3 fi -$python -m plotdevice $@ \ No newline at end of file +export _p_l_o_t_d_e_v_i_c_e_=$(basename ${0}) +export _p_l_o_t_d_e_v_i_c_e___p_i_p_=$PIP +$PYTHON -m plotdevice $@ diff --git a/plotdevice/__main__.py b/plotdevice/__main__.py index 9013414c..e04b6b41 100644 --- a/plotdevice/__main__.py +++ b/plotdevice/__main__.py @@ -25,6 +25,9 @@ Create an animated gif that loops every 2 seconds: python3 -m plotdevice script.pv --export output.gif --frames 60 --fps 30 --loop + +Installing Libraries: + python3 -m plotdevice --install urllib3 jinja2 numpy """ import sys, os, re @@ -60,15 +63,28 @@ def main(): x = parser.add_argument_group("Export Options") x.add_argument('--export', '-o', metavar='FILE', help='a destination filename ending in pdf, eps, png, tiff, jpg, heic, gif, or mov') x.add_argument('--zoom', metavar='PERCENT', default=100, type=int, help='scale of the output image (100 = regular size) unless specified by a filename ending in @2x/@3x/etc') - o.add_argument('--cmyk', action='store_const', const=True, default=False, help='convert colors to c/m/y/k during exports') + x.add_argument('--cmyk', action='store_const', const=True, default=False, help='convert colors to c/m/y/k during exports') i = parser.add_argument_group("PlotDevice Script File", None) i.add_argument('script', help='the python script to be rendered') + p = parser.add_argument_group("Installing Packages", "Run `pip install` with ~/Library/Application Support/PlotDevice as the target") + p.add_argument('--install', nargs='*', default=[], metavar='package', help="Note: cannot be combined with any other plotdevice arguments") + + if len(sys.argv)==1: parser.print_usage() print('for more detail:\n %s --help' % parser.prog) return + elif sys.argv[1] == '--install': + # --install has to be the first argument (in which case we can handle it now and bail) + libDir = os.path.join(os.getenv("HOME"), "Library", "Application Support", "PlotDevice") + if not os.path.exists(libDir): + os.mkdir(libDir) + + from subprocess import call + PIP = os.environ.pop('_p_l_o_t_d_e_v_i_c_e___p_i_p_', 'pip3') + sys.exit(call([PIP, 'install', '--isolated', '--target', libDir, *sys.argv[2:]])) opts = parser.parse_args() @@ -134,6 +150,10 @@ def main(): else: opts.zoom = max(0.01, opts.zoom/100) + if opts.install: + print("The --install option must be used on its own, not in combination with other flags") + sys.exit(1) + # set it off plotdevice.__all__.clear() run(vars(opts)) diff --git a/plotdevice/gui/app.py b/plotdevice/gui/app.py index d7567aea..7d312844 100644 --- a/plotdevice/gui/app.py +++ b/plotdevice/gui/app.py @@ -9,6 +9,9 @@ from . import bundle_path, set_timeout LIB_DIR_README = """"You can put PlotDevice libraries In this directory to make them available to your scripts. + +You can also install anything you find on https://pypi.org here using the command line tool: + plotdevice --install """ class PlotDeviceAppDelegate(NSObject): From 7cf1ed5e5dc477730435a1775709ccdf22552a15 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Tue, 12 Jul 2022 15:47:40 -0400 Subject: [PATCH 07/59] keep the width of the editor fixed on window resize --- app/Resources/en.lproj/PlotDeviceDocument.xib | 3 +++ plotdevice/gui/document.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/app/Resources/en.lproj/PlotDeviceDocument.xib b/app/Resources/en.lproj/PlotDeviceDocument.xib index ff51e510..2e5791f6 100644 --- a/app/Resources/en.lproj/PlotDeviceDocument.xib +++ b/app/Resources/en.lproj/PlotDeviceDocument.xib @@ -119,6 +119,9 @@ + + + diff --git a/plotdevice/gui/document.py b/plotdevice/gui/document.py index 9d9b3a08..586dfa9d 100644 --- a/plotdevice/gui/document.py +++ b/plotdevice/gui/document.py @@ -331,6 +331,10 @@ def windowWillClose_(self, note): def shouldCloseDocument(self): return True + def splitView_shouldAdjustSizeOfSubview_(self, splitview, subview): + # keep the size of the editor fixed and adjust the graphics view on resize + return type(subview) is not NSSplitView + # # Toolbar state # From 091b136c12018e88378454d13a773df96cf5e389 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Tue, 12 Jul 2022 17:09:52 -0400 Subject: [PATCH 08/59] merge readme updates --- CHANGES.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5f567485..de120034 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,16 +4,17 @@ PlotDevice 1.0.0 ##### New Features * Python 3 support (including a bundled 3.10 installation in the app) * images can now be exported in HEIC format and videos support H.265 (HEVC) -* image exports have a configurable 'zoom' to create 2x/3x/etc 'retina' images +* image exports have a configurable `zoom` to create 2x/3x/etc ‘retina’ images * revamped `var()` command for creating GUIs to modify values via sliders, buttons, toggles, etc. -* updated text editor with multiple tabs, new themes, and additional key-binding modes for sublime and vs code users +* updated text editor with multiple tabs, new themes, and additional key-binding modes emulating Sublime Text and VS Code * the module's command line interface is now accessible through `python3 -m plotdevice` -* user-configurable document autosaving +* the command line tool has a new `--install` option to download [PyPI](https://pypi.org) packages for use within the app +* document autosaving is now user-configurable ##### Bugfixes * exported images generated on retina machines now have the proper dimensions * hex colors can now use lowercase letters -* automatic variables like WIDTH & HEIGHT correctly support the `/` operator +* automatic variables like `WIDTH` & `HEIGHT` correctly support the `/` operator * the Color object's `.blend()` method is working again * the `read()` command can now handle csv files with spaces in their header row names * the `translate()` command now incorporates non-pixel grid units set via the `size()` command @@ -27,6 +28,10 @@ PlotDevice 1.0.0 * the `text()` command will always treat its first argument as content (even if it's not a string) unless a `str`, `xml`, or `src` keyword argument is provided * the mouse pointer is now visible in full-screen mode (and will auto-hide when inactive) +###### Unfortunate Casualties +* The NodeBox Libraries (`coreimage`, `colors`, and friends) would require quite a bit of attention to get working properly again. + A first pass can be found in the [`plotdevice-libs` repository](https://github.com/plotdevice/plotdevice-libs) but they're not + ready for prime-time. If you're interested in contributing, this would be a terrific place to start! PlotDevice 0.10.0 ----------------- From cd44625dd6341c400f945e81a75121f37968e2f7 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Wed, 13 Jul 2022 11:43:31 -0400 Subject: [PATCH 09/59] add SVG support to `image()` command --- .gitignore | 3 +- README.md | 1 + deps/extensions/svg/Makefile | 24 ++++++++++++++ deps/extensions/svg/NSImage+PlotDevice.swift | 35 ++++++++++++++++++++ deps/extensions/svg/Package.swift | 16 +++++++++ plotdevice/gfx/image.py | 21 ++++++++---- setup.py | 12 +++++++ 7 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 deps/extensions/svg/Makefile create mode 100644 deps/extensions/svg/NSImage+PlotDevice.swift create mode 100644 deps/extensions/svg/Package.swift diff --git a/.gitignore b/.gitignore index e505b4f6..6da63f06 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,10 @@ Manifest.in deps/local /*.so -# app dependencies +# external dependencies deps/frameworks/*.framework deps/frameworks/relocatable-python +deps/extensions/svg/SwiftDraw* # cds garbage related/ diff --git a/README.md b/README.md index 20c70be7..16a3ab4c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ a bit of breakage. Some of the highlights of this maintenance release include: ###### New Features - Python 3 support (including a bundled 3.10 installation in the app) - images can now be exported in HEIC format and videos support H.265 (HEVC) +- SVG files can now be drawn to the canvas using the `image()` command (thanks to the magical [SwiftDraw](https://github.com/swhitty/SwiftDraw) library) - image exports have a configurable `zoom` to create 2x/3x/etc ‘retina’ images - revamped `var()` command for creating GUIs to modify values via sliders, buttons, toggles, etc. - updated text editor with multiple tabs, new themes, and additional key-binding modes emulating Sublime Text and VS Code diff --git a/deps/extensions/svg/Makefile b/deps/extensions/svg/Makefile new file mode 100644 index 00000000..46f37382 --- /dev/null +++ b/deps/extensions/svg/Makefile @@ -0,0 +1,24 @@ +REPO := https://github.com/swhitty/SwiftDraw.git +TAG := 0.9.3 +.PHONY: all + +all: SwiftDraw.o + @: + +SwiftDraw.o: SwiftDraw + cd SwiftDraw && swift build -c release --target SwiftDraw --arch arm64 --arch x86_64 + cp SwiftDraw/.build/apple/Products/Release/SwiftDraw.o . + +SwiftDraw: + git clone --depth 1 --branch $(TAG) $(REPO) + rm -r SwiftDraw/CommandLine + rm -r SwiftDraw/SwiftDrawTests + rm -r SwiftDraw/Examples + rm SwiftDraw/*.* + + cp NSImage+PlotDevice.swift SwiftDraw/SwiftDraw + cp Package.swift SwiftDraw/ + +clean: + rm -rf SwiftDraw + rm -f *.o \ No newline at end of file diff --git a/deps/extensions/svg/NSImage+PlotDevice.swift b/deps/extensions/svg/NSImage+PlotDevice.swift new file mode 100644 index 00000000..72daccab --- /dev/null +++ b/deps/extensions/svg/NSImage+PlotDevice.swift @@ -0,0 +1,35 @@ +import AppKit +import CoreGraphics + +public extension NSImage { + + convenience init?(svgData data: Data) { + guard let image = Image(data: data) else { return nil } + + self.init(size: image.size, flipped: true) { rect in + guard let ctx = NSGraphicsContext.current?.cgContext else { return false } + ctx.draw(image, in: CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height)) + return true + } + } + + convenience init?(svgFileURL url: URL) { + guard let image = Image(fileURL: url) else { return nil } + + self.init(size: image.size, flipped: true) { rect in + guard let ctx = NSGraphicsContext.current?.cgContext else { return false } + ctx.draw(image, in: CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height)) + return true + } + } + + @objc + static func svgFromData(_ data: Data) -> NSImage? { + NSImage(svgData: data) + } + + @objc + static func svgFromURL(_ url: URL) -> NSImage? { + NSImage(svgFileURL: url) + } +} \ No newline at end of file diff --git a/deps/extensions/svg/Package.swift b/deps/extensions/svg/Package.swift new file mode 100644 index 00000000..b4d14531 --- /dev/null +++ b/deps/extensions/svg/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version:5.4 + +import PackageDescription + +let package = Package( + name: "SwiftDraw", + platforms: [ + .macOS(.v10_12), + ], + targets: [ + .target( + name: "SwiftDraw", + dependencies: [], + path: "SwiftDraw" + )] +) diff --git a/plotdevice/gfx/image.py b/plotdevice/gfx/image.py index 88252208..770368e4 100644 --- a/plotdevice/gfx/image.py +++ b/plotdevice/gfx/image.py @@ -5,6 +5,7 @@ import warnings import math from contextlib import contextmanager +from urllib.parse import urlparse from ..lib.cocoa import * from plotdevice import DeviceError @@ -111,7 +112,10 @@ def _lazyload(self, path=None, data=None): if key in _cache: return _cache[key][0] # ...or load from the data - image = NSImage.alloc().initWithData_(data) + if b'= mtime: return _cache[path][0] # ...or load from the data - bytes = resp.content - data = NSData.dataWithBytes_length_(bytes, len(bytes)) - image = NSImage.alloc().initWithData_(data) + data = NSData.dataWithBytes_length_(resp.content, len(resp.content)) + if 'svg' in resp.headers['Content-Type'] or urlparse(path).path.lower().endswith('.svg'): + image = NSImage.svgFromData_(data) + else: + image = NSImage.alloc().initWithData_(data) else: # load from file path try: path = NSString.stringByExpandingTildeInPath(path) + print("PTH", path) mtime = os.path.getmtime(path) # return a cached image if possible... if path in _cache and _cache[path][1] >= mtime: @@ -138,7 +145,10 @@ def _lazyload(self, path=None, data=None): raise DeviceError(notfound) key = err_info = path # ...or load from the file - image = NSImage.alloc().initWithContentsOfFile_(path) + if path.lower().endswith('.svg'): + image = NSImage.svgFromURL_(NSURL.fileURLWithPath_(path)) + else: + image = NSImage.alloc().initWithContentsOfFile_(path) # if we wound up with a valid image, configure and cache the NSImage # before returning it @@ -249,7 +259,6 @@ def _draw(self): # NB: the nodebox source warns about quartz bugs triggered by drawing # EPSs to other origin points. no clue whether this still applies... - ### context manager for calls to `with export(...)` ### import time diff --git a/setup.py b/setup.py index af7130a4..04cf52da 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ from setuptools.extension import Extension from distutils.dir_util import remove_tree from distutils.command.build_py import build_py +from distutils.command.build_ext import build_ext from pkg_resources import DistributionNotFound from os.path import join, exists, dirname, basename, abspath, getmtime from subprocess import call, getoutput @@ -174,6 +175,7 @@ def run(self): os.system('rm -rf plotdevice.egg-info MANIFEST.in PKG') os.system('rm -rf ./tests/_out ./tests/_diff ./details.html') os.system('rm -f ./_plotdevice.*.so') + os.system('cd deps/extensions/svg && make clean') os.system('find plotdevice -name .DS_Store -exec rm {} \;') os.system('find plotdevice -name \*.pyc -exec rm {} \;') os.system('find plotdevice -name __pycache__ -type d -prune -exec rmdir {} \;') @@ -225,6 +227,7 @@ def finalize_options(self): prune app/Resources/ui include app/plotdevice include deps/extensions/*/*.h + recursive-include deps/extensions/svg *.swift Makefile include tests/*.py graft tests/_in graft examples @@ -238,6 +241,9 @@ def run(self): for dst, src in stale('app/Resources/viewer.nib', "app/Resources/en.lproj/PlotDeviceScript.xib"): self.spawn(['/usr/bin/ibtool','--compile', dst, src]) + # make sure we have the sources for SwiftDraw + call('cd deps/extensions/svg && make SwiftDraw', shell=True) + # build the sdist based on our MANIFEST additions sdist.run(self) @@ -264,6 +270,10 @@ def run(self): self.spawn(['/usr/bin/ibtool','--compile', nib, xib]) self.copy_file(nib, rsrc_dir) +class BuildExtCommand(build_ext): + def run(self): + call('cd deps/extensions/svg && make', shell=True) + build_ext.run(self) class TestCommand(Command): description = "Run unit tests" @@ -436,6 +446,7 @@ def codesign(root, name=None, exec=False, entitlement=False): ext_modules = [Extension( '_plotdevice', sources = ['deps/extensions/module.m', *glob('deps/extensions/*/*.[cm]')], + extra_objects=['deps/extensions/svg/SwiftDraw.o'], extra_link_args=sum((['-framework', fmwk] for fmwk in ['AppKit', 'Foundation', 'Quartz', 'Security', 'AVFoundation', 'CoreMedia', 'CoreVideo', 'CoreText'] ), []) @@ -454,6 +465,7 @@ def codesign(root, name=None, exec=False, entitlement=False): cmdclass={ 'app': BuildAppCommand, 'build_py': BuildCommand, + 'build_ext': BuildExtCommand, 'clean': CleanCommand, 'distclean': DistCleanCommand, 'dist': DistCommand, From ad3a955224ed5e0a068258cb537c1dacea732780 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Wed, 13 Jul 2022 11:46:11 -0400 Subject: [PATCH 10/59] recommend installing `wheel` --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 16a3ab4c..dcc4f0aa 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,9 @@ and launch scripts from the command line (or from a ‘shebang’ line at the to script invoking the `plotdevice` tool). To install the module and command line tool use `python3 setup.py install` -Easier still, you can install the module directly from PyPI with a simple `pip3 install plotdevice` +Easier still, you can install the module directly from PyPI with a simple `pip3 install plotdevice`. +It's a good idea to install the `wheel` module first since it greatly speeds up installation of the +PyObjC libraries PlotDevice depends on. #### Alternative Python Interpreters From 92cc2ae3c83fa30e9190ef3ca87b577334ba20a4 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Wed, 13 Jul 2022 15:52:24 -0400 Subject: [PATCH 11/59] remove debugging --- plotdevice/gfx/image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plotdevice/gfx/image.py b/plotdevice/gfx/image.py index 770368e4..9e3915c5 100644 --- a/plotdevice/gfx/image.py +++ b/plotdevice/gfx/image.py @@ -135,7 +135,6 @@ def _lazyload(self, path=None, data=None): # load from file path try: path = NSString.stringByExpandingTildeInPath(path) - print("PTH", path) mtime = os.path.getmtime(path) # return a cached image if possible... if path in _cache and _cache[path][1] >= mtime: From e000034d5d9a8f82accdbebe79d99b829430deca Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 14:43:05 -0400 Subject: [PATCH 12/59] merge readme updates --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index de120034..11e78e6d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ PlotDevice 1.0.0 ##### New Features * Python 3 support (including a bundled 3.10 installation in the app) * images can now be exported in HEIC format and videos support H.265 (HEVC) +* SVG files can be drawn to the canvas using the `image()` command (thanks to the magical [SwiftDraw](https://github.com/swhitty/SwiftDraw) library) * image exports have a configurable `zoom` to create 2x/3x/etc ‘retina’ images * revamped `var()` command for creating GUIs to modify values via sliders, buttons, toggles, etc. * updated text editor with multiple tabs, new themes, and additional key-binding modes emulating Sublime Text and VS Code From dc4c50301439b84a6ec6f6ab89786e128eed7bbb Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 14:51:37 -0400 Subject: [PATCH 13/59] add architecture support --- CHANGES.md | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 11e78e6d..c3a9ed5d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ PlotDevice 1.0.0 ----------------- ##### New Features +* Runs natively on Intel and Apple Silicon and supports retina displays * Python 3 support (including a bundled 3.10 installation in the app) * images can now be exported in HEIC format and videos support H.265 (HEVC) * SVG files can be drawn to the canvas using the `image()` command (thanks to the magical [SwiftDraw](https://github.com/swhitty/SwiftDraw) library) diff --git a/README.md b/README.md index dcc4f0aa..f0f07d23 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Over the years since the last release, progress in both macOS and Python itself a bit of breakage. Some of the highlights of this maintenance release include: ###### New Features +- Runs natively on Intel and Apple Silicon and supports retina displays - Python 3 support (including a bundled 3.10 installation in the app) - images can now be exported in HEIC format and videos support H.265 (HEVC) - SVG files can now be drawn to the canvas using the `image()` command (thanks to the magical [SwiftDraw](https://github.com/swhitty/SwiftDraw) library) From 7e9a47d6471c2ab2b8323606cc9075814da395b4 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 16:08:22 -0400 Subject: [PATCH 14/59] replace deprecated distutils bits with setuptools --- setup.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 04cf52da..05389bf8 100644 --- a/setup.py +++ b/setup.py @@ -20,11 +20,11 @@ # import os, sys, json, re, platform from glob import glob -from setuptools import setup, find_packages +from shutil import rmtree +from setuptools import setup, find_packages, Command from setuptools.extension import Extension -from distutils.dir_util import remove_tree -from distutils.command.build_py import build_py -from distutils.command.build_ext import build_ext +from setuptools.command.build_py import build_py +from setuptools.command.build_ext import build_ext from pkg_resources import DistributionNotFound from os.path import join, exists, dirname, basename, abspath, getmtime from subprocess import call, getoutput @@ -162,7 +162,6 @@ def stale(dst, src): ## Build Commands ## -from distutils.core import Command class CleanCommand(Command): description = "wipe out the ./build & ./dist dirs and other setup-generated files" user_options = [] @@ -248,7 +247,7 @@ def run(self): sdist.run(self) # clean up - remove_tree('plotdevice.egg-info') + rmtree('plotdevice.egg-info') os.unlink('MANIFEST.in') class BuildCommand(build_py): @@ -301,7 +300,7 @@ def finalize_options(self): def run(self): self.spawn(['xcodebuild', '-configuration', 'Release']) - remove_tree('dist/PlotDevice.app.dSYM') + rmtree('dist/PlotDevice.app.dSYM') print("done building PlotDevice.app in ./dist") try: From 1bf0b9c74f7e48e3a6b8b547718b3142801bfead Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 16:08:49 -0400 Subject: [PATCH 15/59] update description --- README.md | 2 +- setup.py | 30 +++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f0f07d23..1ad6ed19 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ a general purpose graphics library for use in external Python programs. PlotDevice scripts can create images from simple geometric primitives, text, and external vector or bitmap images. Drawing commands provide a thin abstraction -over Mac OS X's Quartz graphics engine, providing high-quality rendering +over macOS's Quartz graphics engine, providing high-quality rendering of 2D imagery and powerful compositing operations. #### Requirements diff --git a/setup.py b/setup.py index 05389bf8..838f552d 100644 --- a/setup.py +++ b/setup.py @@ -64,17 +64,36 @@ "Topic :: Text Editors :: Integrated Development Environments (IDE)", ] DESCRIPTION = "Create two-dimensional graphics and animations with code" -LONG_DESCRIPTION = """PlotDevice is a Macintosh application used for computational graphic design. It provides an -interactive Python environment where you can create two-dimensional graphics -and output them in a variety of vector, bitmap, and animation formats. It is -meant both as a sketch environment for exploring generative design and as a -general purpose graphics library for use in stand-alone Python programs. +LONG_DESCRIPTION = """PlotDevice is a Macintosh application used for computational graphic design. It +provides an interactive Python environment where you can create two-dimensional +graphics and output them in a variety of vector, bitmap, and animation formats. +It is meant both as a sketch environment for exploring generative design and as +a general purpose graphics library for use in external Python programs. + +PlotDevice scripts can create images from simple geometric primitives, text, and +external vector or bitmap images. Drawing commands provide a thin abstraction +over macOS's Quartz graphics engine, providing high-quality rendering +of 2D imagery and powerful compositing operations. PlotDevice is a fork of NodeBox 1.9.7rc1 with support for modern versions of Python and Mac OS. The new version features: +* Runs natively on Intel and Apple Silicon and supports retina displays +* Python 3 only (including a bundled 3.10 installation in the app) +* images can now be exported in HEIC format and videos support H.265 (HEVC) +* SVG files can now be drawn to the canvas using the `image()` command (thanks to the magical [SwiftDraw](https://github.com/swhitty/SwiftDraw) library) +* image exports have a configurable `zoom` to create 2x/3x/etc ‘retina’ images +* revamped `var()` command for creating GUIs to modify values via sliders, buttons, toggles, etc. +* updated text editor with multiple tabs, new themes, and additional key-binding modes emulating Sublime Text and VS Code +* the module's command line interface is now accessible through `python3 -m plotdevice` +* the command line tool has a new `--install` option to download [PyPI](https://pypi.org) packages for use within the app +* document autosaving is now user-configurable +* Bugfixes and misc. improvements detailed in the [changelog](https://github.com/plotdevice/plotdevice/blob/main/CHANGES.md) + +Version 0.9.5 added: + * Python 3 compatible * Can now be built with system Python or Homebrew versions of the interpreter * Much faster import times on Yosemite thanks to a bundled copy of PyObjC 3.0.4 @@ -436,6 +455,7 @@ def codesign(root, name=None, exec=False, entitlement=False): version = VERSION, description = DESCRIPTION, long_description = LONG_DESCRIPTION, + long_description_content_type = 'text/markdown', author = AUTHOR, author_email = AUTHOR_EMAIL, url = URL, From a07d6f8bc744b8b0a0937cc59c8f228e93a05e1e Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 16:08:56 -0400 Subject: [PATCH 16/59] include timestamp in release metadata --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 838f552d..d8c242df 100644 --- a/setup.py +++ b/setup.py @@ -172,7 +172,7 @@ def gosub(cmd, on_err=True): def timestamp(): from datetime import datetime - datetime.now().strftime("%a, %d %b %Y %H:%M:%S") + return datetime.now().strftime("%a, %d %b %Y %H:%M:%S") def stale(dst, src): if exists(src): From 4c94ce9f1ce6ae40420a655455e9b27339d78588 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 16:09:24 -0400 Subject: [PATCH 17/59] add twine to local env --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d8c242df..747c86ce 100644 --- a/setup.py +++ b/setup.py @@ -225,7 +225,7 @@ def run(self): import venv venv.create(venv_dir, symlinks=True, with_pip=True) PIP = '%s/bin/pip3' % venv_dir - call([PIP, 'install', '-q', '--upgrade', 'pip', 'wheel', 'py2app']) + call([PIP, 'install', '-q', '--upgrade', 'pip', 'wheel', 'py2app', 'twine']) call([PIP, '--isolated', 'install', *config['install_requires']]) # place the compiled c-extensions in the main repo dir From 2367a89f02b319593b5b3f1510a5f92673438d04 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 16:10:10 -0400 Subject: [PATCH 18/59] zap `.DS_Store` files at start of build --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 747c86ce..9038e56f 100644 --- a/setup.py +++ b/setup.py @@ -501,6 +501,9 @@ def codesign(root, name=None, exec=False, entitlement=False): # (this means the various commands don't have to play path games) os.chdir(dirname(abspath(__file__))) + # clear away any finder droppings that may have accumulated + call('find . -name .DS_Store -delete', shell=True) + # py2app-specific config if 'py2app' in sys.argv: config.update(dict( From 86efb7c0a45c492099ab0fe240bac64571d28d68 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 16:10:26 -0400 Subject: [PATCH 19/59] update sparkle feed url --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9038e56f..6cb0af89 100644 --- a/setup.py +++ b/setup.py @@ -383,7 +383,7 @@ def run(self): update_plist(info_pth, CFBundleVersion = last_commit(), CFBundleShortVersionString = VERSION, - SUFeedURL = 'http://plotdevice.io/app.xml', + SUFeedURL = 'https://plotdevice.io/app.xml', SUEnableSystemProfiling = 'YES' ) From 74b7c17a360aa76709ae4c21dd61c1136b96140d Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 16:10:47 -0400 Subject: [PATCH 20/59] include entitlements when signing app bundle --- setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 6cb0af89..2ca3c78b 100644 --- a/setup.py +++ b/setup.py @@ -423,14 +423,12 @@ def codesign(root, name=None, exec=False, entitlement=False): codesign('%s/Versions/Current/bin'%PYTHON, name="python3.*", entitlement=True) codesign('%s/Versions/Current/Resources/Python.app'%PYTHON, entitlement=True) codesign(PYTHON) - self.spawn(['codesign', '--verify', '-vv', PYTHON]) codesign('%s/Versions/Current/Updater.app'%SPARKLE) codesign(SPARKLE) - self.spawn(['codesign', '--verify', '-vv', SPARKLE]) - codesign(APP) - self.spawn(['codesign', '--verify', '-vv', APP]) + codesign(APP, entitlement=True) + self.spawn(['codesign', '--verify', '--deep', '-vv', APP]) # create versioned zipfile of the app & notarize it self.spawn(['ditto', '-ck', '--keepParent', APP, ZIP]) From 0464610934084b0b12717010b9f5c0194120cf46 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 19:03:53 -0400 Subject: [PATCH 21/59] var ranges should be inclusive - fixes #60 --- plotdevice/gfx/atoms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotdevice/gfx/atoms.py b/plotdevice/gfx/atoms.py index 4becd301..4141fc6d 100644 --- a/plotdevice/gfx/atoms.py +++ b/plotdevice/gfx/atoms.py @@ -427,7 +427,7 @@ def __init__(self, name, type, *args, **kwargs): small = min(self.min, self.max) big = max(self.min, self.max) - if not small < self.value < big: + if not small <= self.value <= big: raise DeviceError("The value %d doesn't fall with the range %d–%d" % (self.value, self.min, self.max)) elif self.type == TEXT: From 9be3d6abd07abd7d088faa2b634042d9f235c5db Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 19:13:02 -0400 Subject: [PATCH 22/59] add automation --- .github/workflows/publish.yml | 33 ++++++++++++++ .github/workflows/release.yml | 83 +++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 20 +++++++++ 3 files changed, 136 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..f17785da --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish to PyPI +on: + workflow_dispatch: + release: + types: [published] + +jobs: + publish: + name: Publish to PyPI + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Run Tests + run: | + python3 setup.py test + + - name: Build sdist + run: | + python3 setup.py sdist + + - name: Upload to PyPI + env: + TWINE_USERNAME: samizdat + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + TWINE_REPOSITORY: https://pypi.org/legacy/ + TWINE_NON_INTERACTIVE: 1 + run: | + pip3 install twine + twine check dist/* + twine upload dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..fc27eb77 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,83 @@ +name: New release +on: + push: + tags: + - "v*" + +jobs: + app: + name: Build App + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Keychain + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + AC_TEAM: ${{ secrets.AC_TEAM }} + AC_ID: ${{ secrets.AC_ID }} + run: | + # import code signing cert + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH + + # create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + # collect notarization credentials + xcrun notarytool store-credentials "AC_NOTARY" --apple-id "$AC_ID" --team-id "$AC_TEAM" --password "${{ secrets.AC_PASSWORD }}" + + - name: Run Tests + run: | + python3 setup.py test + + - name: Build & Notarize App + run: | + python3 setup.py dist + + - name: Create Draft Release + uses: softprops/action-gh-release@v1 + with: + draft: true + name: PlotDevice ${{ github.ref_name }} + body_path: CHANGES.md + files: | + dist/*.zip + + sdist: + name: Post to test.pypi.org + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Run Tests + run: | + python3 setup.py test + + - name: Build Source Distribution + run: | + python3 setup.py sdist + + - name: Upload to test.pypi.org + env: + TWINE_USERNAME: samizdat + TWINE_PASSWORD: ${{ secrets.PYPI_TEST_PASSWORD }} + TWINE_REPOSITORY: https://test.pypi.org/legacy/ + TWINE_NON_INTERACTIVE: 1 + run: | + pip3 install twine + twine check dist/* + twine upload -r testpypi dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..fde13dc9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: Run tests +on: + workflow_dispatch: + push: + pull_request: + branches: + - main + +jobs: + run-tests: + runs-on: macos-latest + + steps: + - name: Checkout repository + id: repo + uses: actions/checkout@v3 + + - name: Build PyPI Package + run: | + python3 setup.py test From 0c06c028233458ab7f81e927056e2988851e74c7 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Thu, 14 Jul 2022 23:24:20 -0400 Subject: [PATCH 23/59] remove python 2-isms --- plotdevice/gfx/typography.py | 4 ++-- plotdevice/lib/foundry.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plotdevice/gfx/typography.py b/plotdevice/gfx/typography.py index b7790a80..cd5c3832 100644 --- a/plotdevice/gfx/typography.py +++ b/plotdevice/gfx/typography.py @@ -75,7 +75,7 @@ def __repr__(self): spec.insert(2, self._face.variant) spec.insert(1, '/' if self._face.italic else '|') spec.insert(1, ("%.1fpt"%self._metrics['size']).replace('.0pt','pt')) - return ('Font(%s)'%" ".join(spec)).encode('utf-8') + return ('Font(%s)'%" ".join(spec)) def __enter__(self): if not hasattr(self, '_rollback'): @@ -256,7 +256,7 @@ def __repr__(self): n = len(getattr(self, group)) if n: contents.append('%i %s%s' % (n, group[:-1], '' if n==1 else 's')) - return ('Family(%s)'%", ".join(contents)).encode('utf-8') + return ('Family(%s)'%", ".join(contents)) @property def name(self): diff --git a/plotdevice/lib/foundry.py b/plotdevice/lib/foundry.py index 9af9ee21..7efffa30 100644 --- a/plotdevice/lib/foundry.py +++ b/plotdevice/lib/foundry.py @@ -636,7 +636,7 @@ def best_fam(self, word): matches = [self._fams[corpus.index(m)] for m in in_corpus] if matches: - nomatch += '.\nDid you mean: %s'%[m.encode('utf-8') for m in matches] + nomatch += '.\nDid you mean: %s'%matches return DeviceError(nomatch) From 95694a37cef4f7681ee58939516eddb7dfc06aad Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 15 Jul 2022 13:20:20 -0400 Subject: [PATCH 24/59] update email --- app/Resources/en.lproj/Credits.rtf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Resources/en.lproj/Credits.rtf b/app/Resources/en.lproj/Credits.rtf index b6212000..1b65a976 100755 --- a/app/Resources/en.lproj/Credits.rtf +++ b/app/Resources/en.lproj/Credits.rtf @@ -10,7 +10,7 @@ \f0\b\fs26 \cf2 Authors\ \pard\pardeftab720\ri4167\sl360\slmult1 -\b0 \cf0 Christian Swinehart \cf3 |\cf4 {\field{\*\fldinst{HYPERLINK "mailto:drafting@samizdat.cc"}}{\fldrslt \cf5 drafting@samizdat.cc}}\cf0 \ +\b0 \cf0 Christian Swinehart \cf3 |\cf4 {\field{\*\fldinst{HYPERLINK "mailto:drafting@samizdat.co"}}{\fldrslt \cf5 drafting@samizdat.co}}\cf0 \ \pard\pardeftab720\ri4167 \f1\i\fs30 \cf6 API redesign & docs, multithreaded export, enhanced code editor, command line tool, and syntax modernization From 8a660a7e30dbe759e4b20114061099498c240305 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 15 Jul 2022 13:20:30 -0400 Subject: [PATCH 25/59] fix headers --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c3a9ed5d..4cd9fdb0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,7 +30,7 @@ PlotDevice 1.0.0 * the `text()` command will always treat its first argument as content (even if it's not a string) unless a `str`, `xml`, or `src` keyword argument is provided * the mouse pointer is now visible in full-screen mode (and will auto-hide when inactive) -###### Unfortunate Casualties +##### Unfortunate Casualties * The NodeBox Libraries (`coreimage`, `colors`, and friends) would require quite a bit of attention to get working properly again. A first pass can be found in the [`plotdevice-libs` repository](https://github.com/plotdevice/plotdevice-libs) but they're not ready for prime-time. If you're interested in contributing, this would be a terrific place to start! From 1bd54f2cb2b457d5c19be36c551625e0842efc9c Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 15 Jul 2022 13:21:11 -0400 Subject: [PATCH 26/59] use https --- README.md | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ad6ed19..d43a3016 100644 --- a/README.md +++ b/README.md @@ -374,18 +374,18 @@ with export('{2}-img.png') as img: Lineage ------- -PlotDevice was derived from [NodeBox](http://nodebox.net/code)'s 1.9.7 release. Its current maintainer is +PlotDevice was derived from [NodeBox](https://nodebox.net/code/index.php/Home)'s 1.9.7 release. Its current maintainer is [Christian Swinehart](mailto:drafting@samizdat.co). NodeBox is a BSD-licensed graphics environment written by [Frederik De Bleser](mailto:frederik@burocrazy.com). The NodeBox manual and example code are by [Tom De Smedt](mailto:tomdesmedt@organisms.be). -NodeBox is a fork of [DrawBot](http://drawbot.com) by [Just van Rossum](mailto:just@letterror.com). +NodeBox is a fork of [DrawBot](https://drawbot.com) by [Just van Rossum](mailto:just@letterror.com). License ------- -PlotDevice is released under the [MIT license](http://opensource.org/licenses/MIT). Use it as you see fit. +PlotDevice is released under the [MIT license](https://opensource.org/licenses/MIT). Use it as you see fit. Contributing ------------ diff --git a/setup.py b/setup.py index 2ca3c78b..749ea83b 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ AUTHOR = plotdevice.__author__ AUTHOR_EMAIL = plotdevice.__email__ LICENSE = plotdevice.__license__ -URL = "http://plotdevice.io/" +URL = "https://plotdevice.io/" CLASSIFIERS = [ "Development Status :: 5 - Production/Stable", "Environment :: MacOS X :: Cocoa", From 6d95d96f0d41769f3356e21029dfda2d7d9c80e7 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 15 Jul 2022 13:21:53 -0400 Subject: [PATCH 27/59] add `--install` details --- README.md | 10 +++++++++- plotdevice/__main__.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d43a3016..d12a29af 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ directly via `python3 -m plotdevice` (which accepts all the same command line ar #### Command line usage ``` plotdevice [-h] [-f] [-b] [-q] [--live] [--cmyk] [--virtualenv PATH] [--args [a [b ...]]] - [--export FILE] [--frames N or M-N] [--fps N] [--rate N] [--loop [N]] + [--export FILE] [--frames N or M-N] [--fps N] [--rate N] [--loop [N]] [--install [PACKAGES ...]] file ``` @@ -159,6 +159,11 @@ plotdevice [-h] [-f] [-b] [-q] [--live] [--cmyk] [--virtualenv PATH] [--args [a > `--fps N` frames per second in exported video (default `30`) > `--rate N` video bitrate in megabits per second (default `1`) > `--loop [N]` number of times to loop an exported animated gif (omit `N` to loop forever) +> +> ##### Installing Packages from [PyPI](https://pypi.org): +> `--install [packages ...]` Use `pip install` to download libraries into the **~/Library/Application Support/PlotDevice** directory, making +> them `import`-able in the application and by scripts run from the command line + #### Usage examples @@ -181,6 +186,9 @@ plotdevice script.pv --export output.png --frames 10 # Create a 5 second long H.265 video at 2 megabits/sec plotdevice script.pv --export output.mov --frames 150 --rate 2.0 + +# Install some useful modules +plotdevice --install urllib3 jinja2 numpy ``` diff --git a/plotdevice/__main__.py b/plotdevice/__main__.py index e04b6b41..ed9e1eb7 100644 --- a/plotdevice/__main__.py +++ b/plotdevice/__main__.py @@ -68,8 +68,8 @@ def main(): i = parser.add_argument_group("PlotDevice Script File", None) i.add_argument('script', help='the python script to be rendered') - p = parser.add_argument_group("Installing Packages", "Run `pip install` with ~/Library/Application Support/PlotDevice as the target") - p.add_argument('--install', nargs='*', default=[], metavar='package', help="Note: cannot be combined with any other plotdevice arguments") + p = parser.add_argument_group("Installing Packages") + p.add_argument('--install', nargs='*', default=[], metavar='package', help="Use `pip` to download libraries into the ~/Library/Application Support/PlotDevice directory, making them `import`-able in the application and by scripts run from the command line") if len(sys.argv)==1: From 0aab5b84c5cacd78c08b1449d37bc7c880ecafbf Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 15 Jul 2022 15:27:30 -0400 Subject: [PATCH 28/59] use api token for pypi access --- .github/workflows/publish.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f17785da..0bd0c9a6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,11 +23,11 @@ jobs: - name: Upload to PyPI env: - TWINE_USERNAME: samizdat - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - TWINE_REPOSITORY: https://pypi.org/legacy/ + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + TWINE_REPOSITORY: https://upload.pypi.org/legacy/ TWINE_NON_INTERACTIVE: 1 run: | - pip3 install twine + pip3 install -U pip twine twine check dist/* twine upload dist/* From 24d214c521a52367f80d1c6ea44ba48e2bd3aa9c Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 15 Jul 2022 15:41:19 -0400 Subject: [PATCH 29/59] debugging publish workflow --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0bd0c9a6..d5450f43 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,7 @@ jobs: - name: Run Tests run: | + pip3 install -U pip wheel twine python3 setup.py test - name: Build sdist @@ -25,9 +26,8 @@ jobs: env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - TWINE_REPOSITORY: https://upload.pypi.org/legacy/ TWINE_NON_INTERACTIVE: 1 run: | - pip3 install -U pip twine + ls -la ~ twine check dist/* twine upload dist/* From 63992250982590a5290c0135fb9845581c5ccd89 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 15 Jul 2022 15:52:20 -0400 Subject: [PATCH 30/59] remove debugging --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d5450f43..77ac5b37 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,6 +28,5 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} TWINE_NON_INTERACTIVE: 1 run: | - ls -la ~ twine check dist/* twine upload dist/* From d056bed57ff806918ca337fc102fab31a7072234 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Wed, 12 Feb 2025 21:53:04 +0100 Subject: [PATCH 31/59] fix: prevent findpath from modifying input points list The findpath function was modifying its input points list when converting tuple coordinates to Point objects. This change makes a copy of the points list before processing to avoid this side effect. --- plotdevice/lib/pathmatics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plotdevice/lib/pathmatics.py b/plotdevice/lib/pathmatics.py index ecf91ba5..9e4ea7e8 100644 --- a/plotdevice/lib/pathmatics.py +++ b/plotdevice/lib/pathmatics.py @@ -479,6 +479,7 @@ def findpath(points, curvature=1.0): # The list of points consists of Point objects, # but it shouldn't crash on something straightforward # such as someone supplying a list of (x,y)-tuples. + points = points.copy() # Make a copy to avoid modifying the input for i, pt in enumerate(points): if isinstance(pt, (tuple, list)): points[i] = Point(pt[0], pt[1]) From ba5b4d0bc5499e8218a2983b14dae3d1e4682149 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Thu, 13 Feb 2025 10:32:26 +0100 Subject: [PATCH 32/59] Prefix build command with PYTHONNOUSERSITE=1 By prefixing the build command with PYTHONNOUSERSITE=1 we ignore user site packages, ensuring modules are properly installed into the relocatable framework. --- deps/frameworks/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/frameworks/Makefile b/deps/frameworks/Makefile index f085083d..613289cf 100644 --- a/deps/frameworks/Makefile +++ b/deps/frameworks/Makefile @@ -7,7 +7,7 @@ all: Python.framework $(BIN)/pip3 install --upgrade ../.. Python.framework: relocatable-python - python3 ./relocatable-python/make_relocatable_python_framework.py $(BUILD_OPTS) + PYTHONNOUSERSITE=1 python3 ./relocatable-python/make_relocatable_python_framework.py $(BUILD_OPTS) $(BIN)/python3 config.py ../../app/python.xcconfig relocatable-python: From 6b3c7d18f68c9576b9e5ff114f64d73c30466bf5 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Thu, 13 Feb 2025 10:55:05 +0100 Subject: [PATCH 33/59] fix: lazy load HTTP dependencies in readers.py The HTTP session was being initialized at module import time, which required all HTTP-related dependencies to be available before plotdevice could be imported. This caused build failures since the dependencies weren't yet installed during the build process. Changes: - Made HTTP session initialization lazy via get_http_session() - Only import HTTP dependencies when actually making requests - Added better error message if dependencies are missing - Updated read() function to use lazy initialization This allows plotdevice to be imported and built without having HTTP dependencies pre-installed, fixing the circular dependency issue during installation. --- plotdevice/util/readers.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/plotdevice/util/readers.py b/plotdevice/util/readers.py index 9a3550f4..2cdaa6e1 100644 --- a/plotdevice/util/readers.py +++ b/plotdevice/util/readers.py @@ -4,7 +4,7 @@ # files & io from io import open, StringIO, BytesIO -from os.path import abspath, dirname, exists, join, splitext +from os.path import abspath, dirname, exists, join, splitext, expanduser from plotdevice import DeviceError, INTERNAL # data formats @@ -190,13 +190,25 @@ def csv_dialect(fd): ### HTTP utils ### -import requests -from cachecontrol import CacheControl, CacheControlAdapter -from cachecontrol.caches import FileCache -from cachecontrol.heuristics import LastModified +HTTP = None -cache_dir = '%s/Library/Caches/PlotDevice'%os.environ['HOME'] -HTTP = CacheControl(requests.Session(), cache=FileCache(cache_dir), heuristic=LastModified()) +def get_http_session(): + """Returns a cached HTTP session (creating it if necessary)""" + global HTTP + if HTTP is None: + try: + from cachecontrol import CacheControl + from cachecontrol.caches import FileCache + from cachecontrol.heuristics import LastModified + import requests + + cache_dir = join(expanduser('~'), 'Library/Caches/PlotDevice') + HTTP = CacheControl(requests.Session(), + cache=FileCache(cache_dir), + heuristic=LastModified()) + except ImportError as e: + raise ImportError("HTTP dependencies not available. Install with: pip install requests cachecontrol[filecache]") from e + return HTTP def binaryish(content, format): bin_types = ('pdf','eps','png','jpg','jpeg','heic','gif','tiff','tif','zip','tar','gz') @@ -244,7 +256,8 @@ def read(pth, format=None, encoding=None, cols=None, **kwargs): """ if re.match(r'https?:', pth): - resp = HTTP.get(pth) + session = get_http_session() # Get the session only when needed + resp = session.get(pth) resp.raise_for_status() extension_type = splitext(urlparse(pth).path)[-1] From 5e2662bad6c4d70d59c6cc04842887623b519def Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Thu, 13 Feb 2025 11:42:29 +0100 Subject: [PATCH 34/59] fix: add missing dependency to requirements.txt Added missing dependency that was causing build failures: - filelock: Required by cachecontrol for file caching --- deps/frameworks/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/deps/frameworks/requirements.txt b/deps/frameworks/requirements.txt index 4823c92c..3d9ce21a 100644 --- a/deps/frameworks/requirements.txt +++ b/deps/frameworks/requirements.txt @@ -1,5 +1,6 @@ # --no-binary :all: xattr +filelock cachecontrol cffi lockfile From a5f6d754daa8393afc56236f752781e3cc38576f Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Thu, 13 Feb 2025 11:43:29 +0100 Subject: [PATCH 35/59] update: improve clean command to remove all build artifacts Updated the clean command to remove additional build artifacts: - Added .eggs directory - Added *.egg files - Added __pycache__ directories - Added .pyc files - Added local development virtualenv Also modified CleanCommand to: - Added --dist flag to CleanCommand for dist cleaning - Kept DistCleanCommand as separate command for backward compatibility --- setup.py | 72 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index 749ea83b..2b18441c 100644 --- a/setup.py +++ b/setup.py @@ -182,33 +182,63 @@ def stale(dst, src): ## Build Commands ## class CleanCommand(Command): - description = "wipe out the ./build & ./dist dirs and other setup-generated files" - user_options = [] + description = "wipe out generated files and build artifacts" + user_options = [ + ('dist', None, 'also remove Python.framework and local dependencies'), + ] + def initialize_options(self): - pass + self.dist = None + def finalize_options(self): pass + def run(self): - os.system('rm -rf ./build ./dist') - os.system('rm -rf plotdevice.egg-info MANIFEST.in PKG') - os.system('rm -rf ./tests/_out ./tests/_diff ./details.html') - os.system('rm -f ./_plotdevice.*.so') - os.system('cd deps/extensions/svg && make clean') - os.system('find plotdevice -name .DS_Store -exec rm {} \;') - os.system('find plotdevice -name \*.pyc -exec rm {} \;') - os.system('find plotdevice -name __pycache__ -type d -prune -exec rmdir {} \;') - -class DistCleanCommand(Command): + paths = [ + 'build', + 'dist', + '*.egg', + '*.egg-info', + '.eggs', + 'MANIFEST.in', + 'PKG', + 'tests/_out', + 'tests/_diff', + 'details.html', + '_plotdevice.*.so', + '**/*.pyc', + '**/__pycache__', + '**/.DS_Store', + ] + + # Add framework paths if --dist flag is used + if self.dist: + paths.extend([ + 'deps/local', + 'deps/frameworks/Python.framework', + 'deps/frameworks/relocatable-python', + ]) + + for path_pattern in paths: + for path in glob(path_pattern, recursive=True): + if exists(path): + print('removing %s'%path) + if os.path.isdir(path): + rmtree(path) + else: + os.unlink(path) + + # Run make clean in svg extensions dir + if exists('deps/extensions/svg'): + os.system('cd deps/extensions/svg && make clean') + +class DistCleanCommand(CleanCommand): + """Alias for `clean --dist` for backward compatibility""" description = "delete Python.framework, local pypi dependencies, and all generated files" - user_options = [] + def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - self.run_command('clean') - os.system('rm -rf ./deps/local') - os.system('rm -rf ./deps/frameworks/*.framework') + super().initialize_options() + self.dist = True # always do a deep clean class LocalDevCommand(Command): description = "set up environment to allow for running `python -m plotdevice` within the repo" From 6cd89214537384ac55d561b5506f153a120da65f Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Thu, 13 Feb 2025 12:40:15 +0100 Subject: [PATCH 36/59] enhancement: add --no-cache flag to app build command Added ability to bypass pip's cache when building the app bundle: - Added --no-cache flag to BuildAppCommand in setup.py - Modified Makefile to conditionally set PIP_NO_CACHE_DIR based on environment - Allows clean builds with fresh package downloads via `python setup.py app --no-cache` --- deps/frameworks/Makefile | 7 +++++-- setup.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/deps/frameworks/Makefile b/deps/frameworks/Makefile index 613289cf..151b4fb9 100644 --- a/deps/frameworks/Makefile +++ b/deps/frameworks/Makefile @@ -3,11 +3,14 @@ FRAMEWORK_REPO = https://github.com/gregneagle/relocatable-python.git BUILD_OPTS = --os-version=11 --python-version=$(PYTHON_VERSION) --upgrade-pip --pip-requirements=requirements.txt BIN = ./Python.framework/Versions/Current/bin +# Use PIP_NO_CACHE_DIR from environment if set, otherwise empty +PIP_ENV = $(if $(PIP_NO_CACHE_DIR),PIP_NO_CACHE_DIR=1,) + all: Python.framework - $(BIN)/pip3 install --upgrade ../.. + $(PIP_ENV) $(BIN)/pip3 install --upgrade ../.. Python.framework: relocatable-python - PYTHONNOUSERSITE=1 python3 ./relocatable-python/make_relocatable_python_framework.py $(BUILD_OPTS) + PYTHONNOUSERSITE=1 $(PIP_ENV) python3 ./relocatable-python/make_relocatable_python_framework.py $(BUILD_OPTS) $(BIN)/python3 config.py ../../app/python.xcconfig relocatable-python: diff --git a/setup.py b/setup.py index 2b18441c..f5110560 100644 --- a/setup.py +++ b/setup.py @@ -338,14 +338,20 @@ def run(self): class BuildAppCommand(Command): description = "Build PlotDevice.app with xcode" - user_options = [] + user_options = [ + ('no-cache', None, 'do not use pip cache when installing dependencies'), + ] + def initialize_options(self): - pass + self.no_cache = None def finalize_options(self): # make sure the embedded framework exists (and has updated app/python.xcconfig) print("Set up Python.framework for app build") - call('cd deps/frameworks && make', shell=True) + env = os.environ.copy() + if self.no_cache: + env['PIP_NO_CACHE_DIR'] = '1' + call('cd deps/frameworks && make', shell=True, env=env) def run(self): self.spawn(['xcodebuild', '-configuration', 'Release']) From 01b2fb58d69893d085aacb5337a17731a50eeed2 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Mon, 17 Feb 2025 09:18:30 +0100 Subject: [PATCH 37/59] Additional check for image() Throw an error if someone calls image() without any arguments, rather than crashing later when trying to access the non-existent _nsImage --- plotdevice/gfx/image.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plotdevice/gfx/image.py b/plotdevice/gfx/image.py index 9e3915c5..ec0d82d3 100644 --- a/plotdevice/gfx/image.py +++ b/plotdevice/gfx/image.py @@ -63,6 +63,10 @@ def __init__(self, *args, **kwargs): elif args and args[0] is None: args.pop(0) # make image(None, 10,20, image=...) work properly for compat + # Validate that we have either a source or data + if not (src or data): + raise DeviceError("Image requires either a source (path/url/Image) or image data") + # get an NSImage reference (once way or another) if data: self._nsImage = self._lazyload(data=data) From 217daf0c6b16ecbababb8c8f0aba981c293cd0d2 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 6 Apr 2025 16:45:55 -0400 Subject: [PATCH 38/59] use `cachecontrol`'s `filecache` support - now uses `filelock` rather than the deprecated `lockfile` --- deps/frameworks/requirements.txt | 4 +--- setup.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/deps/frameworks/requirements.txt b/deps/frameworks/requirements.txt index 3d9ce21a..7529ddc6 100644 --- a/deps/frameworks/requirements.txt +++ b/deps/frameworks/requirements.txt @@ -1,9 +1,7 @@ # --no-binary :all: xattr -filelock -cachecontrol +cachecontrol[filecache] cffi -lockfile pyobjc requests six \ No newline at end of file diff --git a/setup.py b/setup.py index f5110560..93956756 100644 --- a/setup.py +++ b/setup.py @@ -506,8 +506,7 @@ def codesign(root, name=None, exec=False, entitlement=False): )], install_requires = [ 'requests', - 'cachecontrol', - 'lockfile', + 'cachecontrol[filecache]', 'pyobjc-core==8.5', 'pyobjc-framework-Quartz==8.5', 'pyobjc-framework-LaunchServices==8.5', From 39871e3beadbf137a67c3418a5a7d332a2d047ad Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 6 Apr 2025 16:47:28 -0400 Subject: [PATCH 39/59] remove license from classifiers list - satisfies a deprecation warning from setuptools --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 93956756..cb11db47 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,6 @@ "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", From e8fc6d9c13a43933d4b9c08f64dee9b0bffdacdb Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 6 Apr 2025 16:48:05 -0400 Subject: [PATCH 40/59] pin same version of pyobjc in framework & module --- deps/frameworks/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/frameworks/requirements.txt b/deps/frameworks/requirements.txt index 7529ddc6..d04f012d 100644 --- a/deps/frameworks/requirements.txt +++ b/deps/frameworks/requirements.txt @@ -2,6 +2,6 @@ xattr cachecontrol[filecache] cffi -pyobjc +pyobjc==8.5 requests six \ No newline at end of file From 111d22550bc64ffe890dceaef85015283ea0ed32 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 6 Apr 2025 18:03:06 -0400 Subject: [PATCH 41/59] add missing `setuptools` dependency --- .github/workflows/test.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fde13dc9..e051121b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,8 @@ jobs: id: repo uses: actions/checkout@v3 - - name: Build PyPI Package - run: | - python3 setup.py test + - name: Add missing setuptools + run: brew install python-setuptools + + - name: Run tests + run: python3 setup.py test From f905b9b128ab82846c90934813995f046e4b556f Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 6 Apr 2025 18:36:40 -0400 Subject: [PATCH 42/59] drop trailing whitespace --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index cb11db47..ebd2f714 100644 --- a/setup.py +++ b/setup.py @@ -226,7 +226,7 @@ def run(self): rmtree(path) else: os.unlink(path) - + # Run make clean in svg extensions dir if exists('deps/extensions/svg'): os.system('cd deps/extensions/svg && make clean') @@ -234,7 +234,7 @@ def run(self): class DistCleanCommand(CleanCommand): """Alias for `clean --dist` for backward compatibility""" description = "delete Python.framework, local pypi dependencies, and all generated files" - + def initialize_options(self): super().initialize_options() self.dist = True # always do a deep clean @@ -340,7 +340,7 @@ class BuildAppCommand(Command): user_options = [ ('no-cache', None, 'do not use pip cache when installing dependencies'), ] - + def initialize_options(self): self.no_cache = None From 07ad3a9a4aae341eb3c2dbfdab1114ee7729ae8c Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 6 Apr 2025 18:40:04 -0400 Subject: [PATCH 43/59] move the src/data validation into the main conditional --- plotdevice/gfx/image.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plotdevice/gfx/image.py b/plotdevice/gfx/image.py index ec0d82d3..c413681a 100644 --- a/plotdevice/gfx/image.py +++ b/plotdevice/gfx/image.py @@ -63,10 +63,6 @@ def __init__(self, *args, **kwargs): elif args and args[0] is None: args.pop(0) # make image(None, 10,20, image=...) work properly for compat - # Validate that we have either a source or data - if not (src or data): - raise DeviceError("Image requires either a source (path/url/Image) or image data") - # get an NSImage reference (once way or another) if data: self._nsImage = self._lazyload(data=data) @@ -81,6 +77,9 @@ def __init__(self, *args, **kwargs): else: invalid = "Not a valid image source: %r" % type(src) raise DeviceError(invalid) + else: + undefined = "Image requires either a source (path/url/Image) or image data" + raise DeviceError(undefined) # set the bounds (in phases) if isinstance(src, Image): From b56d834df8587aace61a147a94ab2e1e3e43249d Mon Sep 17 00:00:00 2001 From: flimsyhat Date: Tue, 15 Apr 2025 09:48:17 +0200 Subject: [PATCH 44/59] use list(points) to copy input list --- plotdevice/lib/pathmatics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotdevice/lib/pathmatics.py b/plotdevice/lib/pathmatics.py index 9e4ea7e8..b0125b7d 100644 --- a/plotdevice/lib/pathmatics.py +++ b/plotdevice/lib/pathmatics.py @@ -479,7 +479,7 @@ def findpath(points, curvature=1.0): # The list of points consists of Point objects, # but it shouldn't crash on something straightforward # such as someone supplying a list of (x,y)-tuples. - points = points.copy() # Make a copy to avoid modifying the input + points = list(points) # copy to avoid modifying the input for i, pt in enumerate(points): if isinstance(pt, (tuple, list)): points[i] = Point(pt[0], pt[1]) From e0903620be472a5f2e761c85613283e6db931fbf Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Fri, 23 May 2025 20:29:54 +0200 Subject: [PATCH 45/59] fix scaling issue with image export (#76) * fix export zoom percentage bug corrects issue where zoom percentages were being stored as a multiplier, meaning the image size was shrinking with each export * remove print debugging * Revert "remove print debugging" This reverts commit cf58a78e84dd16032157e8d14454cbbcf17285f9. * Revert "fix export zoom percentage bug" This reverts commit 2dfb6476c4af26b0b3fa565136c8388aa08d2d6f. * fix zoom unit conversion - the `self.image["zoom"]` state field now uses 1.0 as 100% - the `self.imageZoom` outlet uses "100" as 100% - when exiting the modal, the zoom value is now persisted correctly (rather than accidentally being divided by 100) --------- Co-authored-by: Christian Swinehart --- plotdevice/gui/widgets.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/plotdevice/gui/widgets.py b/plotdevice/gui/widgets.py index 397e3d7f..3c93b59c 100644 --- a/plotdevice/gui/widgets.py +++ b/plotdevice/gui/widgets.py @@ -365,10 +365,9 @@ class ExportSheet(NSObject): def awakeFromNib(self): self.formats = dict(image=(0, 'pdf', 0,0, 'png', 'jpg', 'heic', 'tiff', 'gif', 0,0, 'pdf', 'eps'), movie=('mov', 'mov', 'gif')) self.movie = dict(format='mov', first=1, last=150, fps=30, bitrate=1, loop=0, codec=0) - self.image = dict(format='pdf', zoom=100, first=1, last=1, cmyk=False, single=True) + self.image = dict(format='pdf', zoom=1.0, first=1, last=1, cmyk=False, single=True) self.last = None - @objc.python_method def beginExport(self, kind): # configure the accessory controls @@ -464,7 +463,7 @@ def imageState(self, key=None): fmts = self.formats['image'] fmt_idx = self.imageFormat.indexOfSelectedItem() state = dict(format=fmts[fmt_idx], - zoom=self.image['zoom'] / 100, + zoom=self.image['zoom'], first=1, cmyk=self.imageCMYK.state()==NSOnState, single=fmt_idx==1, @@ -497,7 +496,7 @@ def imageZoomStepped_(self, sender): sender.setIntValue_(0) self.imageZoomChanged_(None) # reflect any editing in text field - pct = self.image['zoom'] + pct = self.image['zoom'] * 100 if step > 0: pct = 100 * ceil((pct + 1) / 100) @@ -505,16 +504,16 @@ def imageZoomStepped_(self, sender): pct = 100 * floor((pct - 1) / 100) if 0 < pct < 10000: - self.image['zoom'] = pct + self.image['zoom'] = pct / 100 self.imageZoom.setStringValue_("%i%%" % pct) @IBAction def imageZoomChanged_(self, sender): pct = self.imageZoom.intValue() if pct > 0: - self.image['zoom'] = pct + self.image['zoom'] = pct / 100 else: - pct = self.image['zoom'] + pct = self.image['zoom'] * 100 self.imageZoom.setStringValue_("%i%%" % pct) @IBAction From 2f616bf2f66e79a22c6fb80c96e937beed13f514 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 23 May 2025 14:37:13 -0400 Subject: [PATCH 46/59] scale PDF page dimensions using `zoom` param - previously the content was scaled, but within the original page size, clipping contents if zoom was > 1 - fixes #70 --- plotdevice/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotdevice/context.py b/plotdevice/context.py index 5f6b3d17..799a9fb4 100644 --- a/plotdevice/context.py +++ b/plotdevice/context.py @@ -1777,7 +1777,7 @@ def _getImageData(self, format, zoom=1.0, cmyk=False): w, h = self.pagesize cgData = NSMutableData.data() dataConsumer = CGDataConsumerCreateWithCFData(cgData) - pdfContext = CGPDFContextCreate(dataConsumer, CGRectMake(0, 0, w, h), None) + pdfContext = CGPDFContextCreate(dataConsumer, CGRectMake(0, 0, w*zoom, h*zoom), None) CGPDFContextBeginPage(pdfContext, None) self._render_to_context(pdfContext, zoom) CGPDFContextEndPage(pdfContext) From 89c91e465a1a9b74c7e226906b40befb520bde58 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 23 May 2025 14:49:08 -0400 Subject: [PATCH 47/59] use wildcard to find compiled swift module - newer versions of the swift compiler generate a file named `SwiftDraw_Module.o` rather than `SwiftDraw.o` - fixes #62 --- deps/extensions/svg/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/extensions/svg/Makefile b/deps/extensions/svg/Makefile index 46f37382..df4cbda9 100644 --- a/deps/extensions/svg/Makefile +++ b/deps/extensions/svg/Makefile @@ -7,7 +7,7 @@ all: SwiftDraw.o SwiftDraw.o: SwiftDraw cd SwiftDraw && swift build -c release --target SwiftDraw --arch arm64 --arch x86_64 - cp SwiftDraw/.build/apple/Products/Release/SwiftDraw.o . + cp SwiftDraw/.build/apple/Products/Release/SwiftDraw*.o $@ SwiftDraw: git clone --depth 1 --branch $(TAG) $(REPO) From 047422afb77803c4e0bee62c4e7637a9067f8dac Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 23 May 2025 17:46:19 -0400 Subject: [PATCH 48/59] bump PyObjC to 8.5.1 - adds support for python 3.11 --- deps/frameworks/requirements.txt | 2 +- setup.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deps/frameworks/requirements.txt b/deps/frameworks/requirements.txt index d04f012d..8c847935 100644 --- a/deps/frameworks/requirements.txt +++ b/deps/frameworks/requirements.txt @@ -2,6 +2,6 @@ xattr cachecontrol[filecache] cffi -pyobjc==8.5 +pyobjc==8.5.1 requests six \ No newline at end of file diff --git a/setup.py b/setup.py index ebd2f714..22165f71 100644 --- a/setup.py +++ b/setup.py @@ -506,10 +506,10 @@ def codesign(root, name=None, exec=False, entitlement=False): install_requires = [ 'requests', 'cachecontrol[filecache]', - 'pyobjc-core==8.5', - 'pyobjc-framework-Quartz==8.5', - 'pyobjc-framework-LaunchServices==8.5', - 'pyobjc-framework-WebKit==8.5', + 'pyobjc-core==8.5.1', + 'pyobjc-framework-Quartz==8.5.1', + 'pyobjc-framework-LaunchServices==8.5.1', + 'pyobjc-framework-WebKit==8.5.1', ], scripts = ["app/plotdevice"], zip_safe=False, From 86627cb6dfba8905cec4056abac2be924fb7d060 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 23 May 2025 18:25:14 -0400 Subject: [PATCH 49/59] bump embedded python framework to 3.11.9 --- app/python.xcconfig | 8 ++++---- deps/frameworks/Makefile | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/python.xcconfig b/app/python.xcconfig index 3f55206e..697f55dc 100644 --- a/app/python.xcconfig +++ b/app/python.xcconfig @@ -1,7 +1,7 @@ // Generated by deps/frameworks/config.py PYTHON_FRAMEWORK = $(PROJECT_DIR)/deps/frameworks/Python.framework -PYTHON = $(PYTHON_FRAMEWORK)/Versions/3.10/bin/python3 -LIBRARY_SEARCH_PATHS = $(inherited) $(PYTHON_FRAMEWORK)/Versions/3.10/lib/python3.10/config-3.10-darwin -HEADER_SEARCH_PATHS = $(inherited) $(PYTHON_FRAMEWORK)/Versions/3.10/include/python3.10 -OTHER_LDFLAGS = $(inherited) -lpython3.10 +PYTHON = $(PYTHON_FRAMEWORK)/Versions/3.11/bin/python3 +LIBRARY_SEARCH_PATHS = $(inherited) $(PYTHON_FRAMEWORK)/Versions/3.11/lib/python3.11/config-3.11-darwin +HEADER_SEARCH_PATHS = $(inherited) $(PYTHON_FRAMEWORK)/Versions/3.11/include/python3.11 +OTHER_LDFLAGS = $(inherited) -lpython3.11 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) PYTHON_BIN="$(PYTHON)" PY3K=1 \ No newline at end of file diff --git a/deps/frameworks/Makefile b/deps/frameworks/Makefile index 151b4fb9..3a481608 100644 --- a/deps/frameworks/Makefile +++ b/deps/frameworks/Makefile @@ -1,4 +1,4 @@ -PYTHON_VERSION = 3.10.5 +PYTHON_VERSION = 3.11.9 FRAMEWORK_REPO = https://github.com/gregneagle/relocatable-python.git BUILD_OPTS = --os-version=11 --python-version=$(PYTHON_VERSION) --upgrade-pip --pip-requirements=requirements.txt BIN = ./Python.framework/Versions/Current/bin @@ -6,8 +6,11 @@ BIN = ./Python.framework/Versions/Current/bin # Use PIP_NO_CACHE_DIR from environment if set, otherwise empty PIP_ENV = $(if $(PIP_NO_CACHE_DIR),PIP_NO_CACHE_DIR=1,) +# Use the framework's headers (otherwise clang defaults to looking in /Library/Frameworks/Python.framework) +PIP_INCLUDES = $(shell $(BIN)/python3-config --includes) + all: Python.framework - $(PIP_ENV) $(BIN)/pip3 install --upgrade ../.. + $(PIP_ENV) CFLAGS="$(PIP_INCLUDES)" $(BIN)/pip3 install --upgrade ../.. Python.framework: relocatable-python PYTHONNOUSERSITE=1 $(PIP_ENV) python3 ./relocatable-python/make_relocatable_python_framework.py $(BUILD_OPTS) From be7dc22993447ab97ccbf0323d0f8d075009baff Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 23 May 2025 18:25:47 -0400 Subject: [PATCH 50/59] replace deprecated `getargspec` method --- plotdevice/run/sandbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotdevice/run/sandbox.py b/plotdevice/run/sandbox.py index 52e4462f..254298f2 100644 --- a/plotdevice/run/sandbox.py +++ b/plotdevice/run/sandbox.py @@ -1,7 +1,7 @@ import os, sys, re, ast from os.path import dirname, basename, abspath, relpath, isdir from functools import partial -from inspect import getargspec +from inspect import getfullargspec from collections import namedtuple from PyObjCTools import AppHelper from ..lib.io import MovieExportSession, ImageExportSession @@ -214,7 +214,7 @@ def run(self, method=None, cmyk=False): func = self.namespace.get(routine) # replace each such routine with a partial application passing # the dict. this means we can .call() it without any explicit args - if callable(func) and getargspec(func).args: + if callable(func) and getfullargspec(func).args: self.namespace[routine] = partial(self.namespace[routine], self._anim) elif method=='draw': From 0a1d0fa7e276a2a29cb5a91ce974bbd38476826e Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 25 May 2025 17:37:24 -0400 Subject: [PATCH 51/59] drop unused imports - `cgi` was removed in python 3.13 causing an import error --- plotdevice/gui/editor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plotdevice/gui/editor.py b/plotdevice/gui/editor.py index 3f2393c3..7d8044b3 100644 --- a/plotdevice/gui/editor.py +++ b/plotdevice/gui/editor.py @@ -2,13 +2,10 @@ import os import re import json -import cgi import objc from io import open from objc import super -from pprint import pprint from time import time -from bisect import bisect from ..lib.cocoa import * from plotdevice.gui.preferences import get_default, editor_info from plotdevice.gui import bundle_path, set_timeout From 09e75b814d65b0b6487cb52ad5a918cebe420673 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 25 May 2025 17:37:51 -0400 Subject: [PATCH 52/59] fix typo in exported symbols --- plotdevice/run/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotdevice/run/__init__.py b/plotdevice/run/__init__.py index c1e02f3e..0e94e4df 100644 --- a/plotdevice/run/__init__.py +++ b/plotdevice/run/__init__.py @@ -28,4 +28,4 @@ # expose the script-runner object from .sandbox import Sandbox -__all__ = ('objc', 'encoding', 'Sandbox') \ No newline at end of file +__all__ = ('objc', 'encoded', 'Sandbox') \ No newline at end of file From 9d4e46adbc9d28469c0764c93a02b13d6a29afac Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 25 May 2025 17:39:20 -0400 Subject: [PATCH 53/59] bump PyObjC to 11.0 - `objc` is no longer reexported by Foundation so pull it in manually --- deps/frameworks/requirements.txt | 3 ++- plotdevice/run/__init__.py | 2 ++ setup.py | 8 ++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/deps/frameworks/requirements.txt b/deps/frameworks/requirements.txt index 8c847935..150f105c 100644 --- a/deps/frameworks/requirements.txt +++ b/deps/frameworks/requirements.txt @@ -2,6 +2,7 @@ xattr cachecontrol[filecache] cffi -pyobjc==8.5.1 +pyobjc==11.0 +py2app requests six \ No newline at end of file diff --git a/plotdevice/run/__init__.py b/plotdevice/run/__init__.py index 0e94e4df..5dc8ec27 100644 --- a/plotdevice/run/__init__.py +++ b/plotdevice/run/__init__.py @@ -5,6 +5,7 @@ try: # test the sys.path by attempting to load a PyObjC submodule... from Foundation import * + import objc except ImportError: # detect whether we're being run from the repository and set up a local env if so repo = abspath(join(dirname(__file__), '../..')) @@ -17,6 +18,7 @@ call([sys.executable, setup_py, 'dev']) site.addsitedir(local_libs) from Foundation import * + import objc else: from pprint import pformat missing = "Searched for PyObjC libraries in:\n%s\nto no avail..."%pformat(sys.path) diff --git a/setup.py b/setup.py index 22165f71..61b181a6 100644 --- a/setup.py +++ b/setup.py @@ -506,10 +506,10 @@ def codesign(root, name=None, exec=False, entitlement=False): install_requires = [ 'requests', 'cachecontrol[filecache]', - 'pyobjc-core==8.5.1', - 'pyobjc-framework-Quartz==8.5.1', - 'pyobjc-framework-LaunchServices==8.5.1', - 'pyobjc-framework-WebKit==8.5.1', + 'pyobjc-core==11.0', + 'pyobjc-framework-Quartz==11.0', + 'pyobjc-framework-LaunchServices==11.0', + 'pyobjc-framework-WebKit==11.0', ], scripts = ["app/plotdevice"], zip_safe=False, From 52fef48145e5186b5156c146f7ee4f9436fd8cdb Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 25 May 2025 17:39:59 -0400 Subject: [PATCH 54/59] bump embedded python framework to 3.13.3 --- app/python.xcconfig | 8 ++++---- deps/frameworks/Makefile | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/python.xcconfig b/app/python.xcconfig index 697f55dc..868ff609 100644 --- a/app/python.xcconfig +++ b/app/python.xcconfig @@ -1,7 +1,7 @@ // Generated by deps/frameworks/config.py PYTHON_FRAMEWORK = $(PROJECT_DIR)/deps/frameworks/Python.framework -PYTHON = $(PYTHON_FRAMEWORK)/Versions/3.11/bin/python3 -LIBRARY_SEARCH_PATHS = $(inherited) $(PYTHON_FRAMEWORK)/Versions/3.11/lib/python3.11/config-3.11-darwin -HEADER_SEARCH_PATHS = $(inherited) $(PYTHON_FRAMEWORK)/Versions/3.11/include/python3.11 -OTHER_LDFLAGS = $(inherited) -lpython3.11 +PYTHON = $(PYTHON_FRAMEWORK)/Versions/3.13/bin/python3 +LIBRARY_SEARCH_PATHS = $(inherited) $(PYTHON_FRAMEWORK)/Versions/3.13/lib/python3.13/config-3.13-darwin +HEADER_SEARCH_PATHS = $(inherited) $(PYTHON_FRAMEWORK)/Versions/3.13/include/python3.13 +OTHER_LDFLAGS = $(inherited) -lpython3.13 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) PYTHON_BIN="$(PYTHON)" PY3K=1 \ No newline at end of file diff --git a/deps/frameworks/Makefile b/deps/frameworks/Makefile index 3a481608..83ca9edc 100644 --- a/deps/frameworks/Makefile +++ b/deps/frameworks/Makefile @@ -1,4 +1,4 @@ -PYTHON_VERSION = 3.11.9 +PYTHON_VERSION = 3.13.3 FRAMEWORK_REPO = https://github.com/gregneagle/relocatable-python.git BUILD_OPTS = --os-version=11 --python-version=$(PYTHON_VERSION) --upgrade-pip --pip-requirements=requirements.txt BIN = ./Python.framework/Versions/Current/bin From 448dbfa93425b067a26cb8bb70f634abe9b3c54e Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 25 May 2025 17:41:08 -0400 Subject: [PATCH 55/59] target macOS 10.13+ --- PlotDevice.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PlotDevice.xcodeproj/project.pbxproj b/PlotDevice.xcodeproj/project.pbxproj index ba4bca4d..9b87e2c2 100644 --- a/PlotDevice.xcodeproj/project.pbxproj +++ b/PlotDevice.xcodeproj/project.pbxproj @@ -607,7 +607,7 @@ INFOPLIST_FILE = app/info.plist; INSTALL_PATH = /Applications; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../Frameworks/Python.framework"; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = io.plotdevice.PlotDevice; @@ -637,7 +637,7 @@ INFOPLIST_FILE = app/info.plist; INSTALL_PATH = /Applications; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../Frameworks/Python.framework"; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = io.plotdevice.PlotDevice; PRODUCT_NAME = PlotDevice; From a4fcc8e27935fa454ae0099225c16a9d77463f16 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 25 May 2025 17:46:49 -0400 Subject: [PATCH 56/59] disable code-stripping to prevent build failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A number of the .so files installed by PyObjC trip up Xcode's `/usr/bin/strip`, failing with the error: `internal error: reloc_has_pair() called with unknown cputype (18)` if `COPY_STRIP_PHASE` is set to `YES` - it's not clear to me whether this is a (temporary?) bug in Xcode 16.3 or something that should be fixed in the Python.framework build process… --- PlotDevice.xcodeproj/project.pbxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/PlotDevice.xcodeproj/project.pbxproj b/PlotDevice.xcodeproj/project.pbxproj index 9b87e2c2..ac55fb0c 100644 --- a/PlotDevice.xcodeproj/project.pbxproj +++ b/PlotDevice.xcodeproj/project.pbxproj @@ -625,6 +625,7 @@ CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; CONFIGURATION_BUILD_DIR = dist; + COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( From 3fdc89c5c0338ff4a89141467a580364209cfe1d Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 30 May 2025 11:02:53 -0400 Subject: [PATCH 57/59] fix tests on python 3.13 --- tests/compositing.py | 2 +- tests/drawing.py | 2 +- tests/geometry.py | 2 +- tests/module.py | 4 ++-- tests/primitives.py | 2 +- tests/typography.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/compositing.py b/tests/compositing.py index 6c73e1b8..5db32747 100644 --- a/tests/compositing.py +++ b/tests/compositing.py @@ -111,5 +111,5 @@ def test_beginclip(self): def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(CompositingTests)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(CompositingTests)) return suite diff --git a/tests/drawing.py b/tests/drawing.py index 8769369e..1d0dbe14 100644 --- a/tests/drawing.py +++ b/tests/drawing.py @@ -479,5 +479,5 @@ def test_strokewidth(self): def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(DrawingTests)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(DrawingTests)) return suite diff --git a/tests/geometry.py b/tests/geometry.py index cbbcec2b..48197987 100644 --- a/tests/geometry.py +++ b/tests/geometry.py @@ -247,5 +247,5 @@ def test_push(self): def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(GeometryTests)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(GeometryTests)) return suite diff --git a/tests/module.py b/tests/module.py index 9a13100f..77a30bba 100644 --- a/tests/module.py +++ b/tests/module.py @@ -35,9 +35,9 @@ def test_cli(self): def suite(): - from unittest import TestSuite, makeSuite + from unittest import TestSuite, defaultTestLoader suite = TestSuite() - suite.addTest(makeSuite(ModuleTests)) + suite.addTest(defaultTestLoader.loadTestsFromTestCase(ModuleTests)) return suite \ No newline at end of file diff --git a/tests/primitives.py b/tests/primitives.py index 1c31dd36..4fcf41c8 100644 --- a/tests/primitives.py +++ b/tests/primitives.py @@ -160,5 +160,5 @@ def test_star(self): def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(PrimitivesTests)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(PrimitivesTests)) return suite diff --git a/tests/typography.py b/tests/typography.py index bc30adb9..2f777dc4 100644 --- a/tests/typography.py +++ b/tests/typography.py @@ -494,5 +494,5 @@ def test_line_fragment(self): def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TypographyTests)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TypographyTests)) return suite From bdb55df728a86c7b952bb9c460f318c9b70c5244 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 30 May 2025 11:03:26 -0400 Subject: [PATCH 58/59] use lazy http session when loading images --- plotdevice/gfx/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotdevice/gfx/image.py b/plotdevice/gfx/image.py index c413681a..8817cf07 100644 --- a/plotdevice/gfx/image.py +++ b/plotdevice/gfx/image.py @@ -10,7 +10,7 @@ from plotdevice import DeviceError from ..util import _copy_attrs, autorelease -from ..util.readers import HTTP, last_modified +from ..util.readers import get_http_session, last_modified from ..lib.io import MovieExportSession, ImageExportSession from .geometry import Region, Size, Point, Transform, CENTER from .atoms import TransformMixin, EffectsMixin, FrameMixin, Grob @@ -123,7 +123,7 @@ def _lazyload(self, path=None, data=None): if re.match(r'https?:', path): # load from url key = err_info = path - resp = HTTP.get(path) + resp = get_http_session().get(path) mtime = last_modified(resp) # return a cached image if possible... if path in _cache and _cache[path][1] >= mtime: From 23e234f20942d42dd5f4dd7395fe71e37ae50be1 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Fri, 30 May 2025 11:16:44 -0400 Subject: [PATCH 59/59] remove problematic `py2app` binaries - Xcode was objecting to the legacy fat binaries in the prebuilt dirs - now keeping only the `main-universal2` binary and reenabling symbol stripping in the production build process --- PlotDevice.xcodeproj/project.pbxproj | 2 +- deps/frameworks/Makefile | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/PlotDevice.xcodeproj/project.pbxproj b/PlotDevice.xcodeproj/project.pbxproj index ac55fb0c..1464ae4a 100644 --- a/PlotDevice.xcodeproj/project.pbxproj +++ b/PlotDevice.xcodeproj/project.pbxproj @@ -625,7 +625,7 @@ CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; CONFIGURATION_BUILD_DIR = dist; - COPY_PHASE_STRIP = NO; + COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( diff --git a/deps/frameworks/Makefile b/deps/frameworks/Makefile index 83ca9edc..788b5389 100644 --- a/deps/frameworks/Makefile +++ b/deps/frameworks/Makefile @@ -11,6 +11,7 @@ PIP_INCLUDES = $(shell $(BIN)/python3-config --includes) all: Python.framework $(PIP_ENV) CFLAGS="$(PIP_INCLUDES)" $(BIN)/pip3 install --upgrade ../.. + find $(shell find ./Python.framework -name py2app -type d) -name main-\* -not -name \*universal2 -delete Python.framework: relocatable-python PYTHONNOUSERSITE=1 $(PIP_ENV) python3 ./relocatable-python/make_relocatable_python_framework.py $(BUILD_OPTS)