From ba5b4d0bc5499e8218a2983b14dae3d1e4682149 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Thu, 13 Feb 2025 10:32:26 +0100 Subject: [PATCH 01/10] 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 f085083..613289c 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 02/10] 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 9a3550f..2cdaa6e 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 03/10] 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 4823c92..3d9ce21 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 04/10] 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 749ea83..2b18441 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 05/10] 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 613289c..151b4fb 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 2b18441..f511056 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 217daf0c6b16ecbababb8c8f0aba981c293cd0d2 Mon Sep 17 00:00:00 2001 From: Christian Swinehart Date: Sun, 6 Apr 2025 16:45:55 -0400 Subject: [PATCH 06/10] 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 3d9ce21..7529ddc 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 f511056..9395675 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 07/10] 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 9395675..cb11db4 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 08/10] 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 7529ddc..d04f012 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 09/10] 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 fde13dc..e051121 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 10/10] drop trailing whitespace --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index cb11db4..ebd2f71 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