CreativePython 1.1.1__tar.gz → 1.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {creativepython-1.1.1/src/CreativePython.egg-info → creativepython-1.1.2}/PKG-INFO +1 -1
  2. {creativepython-1.1.1 → creativepython-1.1.2}/pyproject.toml +1 -1
  3. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/GuiHandler.py +38 -25
  4. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/GuiRenderer.py +69 -89
  5. {creativepython-1.1.1 → creativepython-1.1.2/src/CreativePython.egg-info}/PKG-INFO +1 -1
  6. {creativepython-1.1.1 → creativepython-1.1.2}/src/gui.py +8 -8
  7. creativepython-1.1.2/src/image.py +123 -0
  8. creativepython-1.1.1/src/image.py +0 -70
  9. {creativepython-1.1.1 → creativepython-1.1.2}/LICENSE +0 -0
  10. {creativepython-1.1.1 → creativepython-1.1.2}/LICENSE-PSF +0 -0
  11. {creativepython-1.1.1 → creativepython-1.1.2}/MANIFEST.in +0 -0
  12. {creativepython-1.1.1 → creativepython-1.1.2}/README.md +0 -0
  13. {creativepython-1.1.1 → creativepython-1.1.2}/setup.cfg +0 -0
  14. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/RealtimeAudioPlayer.py +0 -0
  15. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/__init__.py +0 -0
  16. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/advMetricRunner.pythonSurvey.py +0 -0
  17. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/compareMetrics_Java-Vs-Python.py +0 -0
  18. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/RunMetrics.py +0 -0
  19. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/Surveyor.py +0 -0
  20. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/__init__.py +0 -0
  21. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Confidence.py +0 -0
  22. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Contig.py +0 -0
  23. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/ExtendedNote.py +0 -0
  24. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Histogram.py +0 -0
  25. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Judgement.py +0 -0
  26. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Measurement.py +0 -0
  27. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/PianoRoll.py +0 -0
  28. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/PianoRollOld.py +0 -0
  29. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/__init__.py +0 -0
  30. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_ExtendedNote.py +0 -0
  31. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_Histogram.py +0 -0
  32. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_Measurement.py +0 -0
  33. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_assertions.py +0 -0
  34. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_integration.py +0 -0
  35. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_quantization.py +0 -0
  36. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py +0 -0
  37. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/Metric.py +0 -0
  38. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py +0 -0
  39. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/__init__.py +0 -0
  40. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDensityMetric.py +0 -0
  41. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDistanceMetric.py +0 -0
  42. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordMetric.py +0 -0
  43. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordNormalizedMetric.py +0 -0
  44. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChromaticToneMetric.py +0 -0
  45. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationMetric.py +0 -0
  46. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationQuantizedMetric.py +0 -0
  47. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslinePitchMetric.py +0 -0
  48. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationMetric.py +0 -0
  49. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationQuantizedMetric.py +0 -0
  50. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyPitchMetric.py +0 -0
  51. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationBigramMetric.py +0 -0
  52. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationDistanceMetric.py +0 -0
  53. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationMetric.py +0 -0
  54. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedBigramMetric.py +0 -0
  55. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedDistanceMetric.py +0 -0
  56. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedMetric.py +0 -0
  57. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicBigramMetric.py +0 -0
  58. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicConsonanceMetric.py +0 -0
  59. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicIntervalMetric.py +0 -0
  60. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicBigramMetric.py +0 -0
  61. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicConsonanceMetric.py +0 -0
  62. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicIntervalMetric.py +0 -0
  63. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDistanceMetric.py +0 -0
  64. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationMetric.py +0 -0
  65. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationQuantizedMetric.py +0 -0
  66. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchMetric.py +0 -0
  67. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/RestMetric.py +0 -0
  68. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/__init__.py +0 -0
  69. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_DurationMetric.py +0 -0
  70. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_BasicIntervalsAndBigrams.py +0 -0
  71. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ChordsAndConsonance.py +0 -0
  72. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ContoursAndChromatic.py +0 -0
  73. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_QuantizedDurationsAndDistances.py +0 -0
  74. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_PitchMetric.py +0 -0
  75. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_RestMetric.py +0 -0
  76. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/test_Metric.py +0 -0
  77. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/CSVWriter.py +0 -0
  78. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py +0 -0
  79. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/__init__.py +0 -0
  80. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython/notationRenderer.py +0 -0
  81. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython.egg-info/SOURCES.txt +0 -0
  82. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython.egg-info/dependency_links.txt +0 -0
  83. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython.egg-info/requires.txt +0 -0
  84. {creativepython-1.1.1 → creativepython-1.1.2}/src/CreativePython.egg-info/top_level.txt +0 -0
  85. {creativepython-1.1.1 → creativepython-1.1.2}/src/bin/libportaudio.2.dylib +0 -0
  86. {creativepython-1.1.1 → creativepython-1.1.2}/src/iannix.py +0 -0
  87. {creativepython-1.1.1 → creativepython-1.1.2}/src/markov.py +0 -0
  88. {creativepython-1.1.1 → creativepython-1.1.2}/src/midi.py +0 -0
  89. {creativepython-1.1.1 → creativepython-1.1.2}/src/music.py +0 -0
  90. {creativepython-1.1.1 → creativepython-1.1.2}/src/osc.py +0 -0
  91. {creativepython-1.1.1 → creativepython-1.1.2}/src/timer.py +0 -0
  92. {creativepython-1.1.1 → creativepython-1.1.2}/src/zipf.py +0 -0
  93. {creativepython-1.1.1 → creativepython-1.1.2}/tests/testAnimate.py +0 -0
  94. {creativepython-1.1.1 → creativepython-1.1.2}/tests/testPeer.py +0 -0
  95. {creativepython-1.1.1 → creativepython-1.1.2}/tests/test_keyEvent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CreativePython
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: A Python-based software environment for developing algorithmic art projects.
5
5
  Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "CreativePython"
7
- version = "1.1.1"
7
+ version = "1.1.2"
8
8
  description = "A Python-based software environment for developing algorithmic art projects."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -61,16 +61,25 @@ def _createEvent(eventType, target, args=None):
61
61
  # Child process entry point
62
62
  #######################################################################################
63
63
 
64
- def _launchRenderer(childConn):
64
+ def _launchRenderer(childConn, shutdownReadConn, shutdownWriteConn):
65
65
  """
66
66
  Entry point for the GuiRenderer child process.
67
67
  Defined here so GuiHandler can reference it without importing from GuiRenderer
68
68
  globally (which would also import Qt/PySide6 in the parent process).
69
69
 
70
+ shutdownReadConn is the read end of the dedicated shutdown pipe; the child
71
+ watches it with QSocketNotifier and shuts down when it becomes readable (EOF).
72
+ shutdownWriteConn is the write end inherited from the parent (fork) or passed
73
+ via pickling (forkserver/spawn); it is closed immediately so that only the
74
+ parent's copy of the write end remains open — ensuring the child sees EOF when
75
+ the parent closes its copy.
76
+
70
77
  At runtime this function only executes in the child process.
71
78
  """
72
79
  import sys, os
73
80
 
81
+ shutdownWriteConn.close() # child must not hold the write end open
82
+
74
83
  # Suppress macOS system noise (stale XPC connections inherited via fork)
75
84
  # that would confuse students. Redirect at the OS level (fd 2) so that
76
85
  # C-level libraries (Cocoa, XPC) are also silenced.
@@ -80,7 +89,7 @@ def _launchRenderer(childConn):
80
89
  sys.stderr = open(os.devnull, 'w')
81
90
 
82
91
  from CreativePython.GuiRenderer import GuiRenderer
83
- renderer = GuiRenderer(childConn)
92
+ renderer = GuiRenderer(childConn, shutdownReadConn)
84
93
  renderer.run()
85
94
 
86
95
 
@@ -140,6 +149,15 @@ class GuiHandler:
140
149
  ctx = multiprocessing.get_context('forkserver')
141
150
  else:
142
151
  ctx = multiprocessing.get_context('fork')
152
+
153
+ # Dedicated one-directional shutdown pipe. The parent holds the write
154
+ # end; the child holds the read end. Closing the write end in the parent
155
+ # sends EOF to the child's read end, which Qt detects immediately via
156
+ # QSocketNotifier — independent of the command queue and independent of
157
+ # whether atexit runs (the OS closes the fd automatically on process death).
158
+ shutdownReadConn, shutdownWriteConn = multiprocessing.Pipe(duplex=False)
159
+ self._shutdownPipe = shutdownWriteConn
160
+
143
161
  # With 'spawn' on POSIX (fork+exec), the forked helper calls
144
162
  # get_preparation_data() which reads sys.modules['__main__'].__file__.
145
163
  # When a user script is running in PENCIL's subprocess, __main__.__file__
@@ -159,7 +177,7 @@ class GuiHandler:
159
177
  try:
160
178
  self.childProcess = ctx.Process(
161
179
  target = _launchRenderer,
162
- args = (childConn,),
180
+ args = (childConn, shutdownReadConn, shutdownWriteConn),
163
181
  daemon = True # child is killed automatically if parent dies unexpectedly
164
182
  )
165
183
  self.childProcess.start()
@@ -169,7 +187,8 @@ class GuiHandler:
169
187
  _main_mod.__file__ = _saved_file
170
188
  if _saved_spec is not _UNSET:
171
189
  _main_mod.__spec__ = _saved_spec
172
- childConn.close() # parent no longer needs the child end of the Pipe
190
+ childConn.close() # parent no longer needs the child end of the command pipe
191
+ shutdownReadConn.close() # parent no longer needs the read end of the shutdown pipe
173
192
 
174
193
  # responseId counter — each sendQuery() call gets a unique ID so the
175
194
  # listener thread can route the response back to the correct caller
@@ -214,11 +233,10 @@ class GuiHandler:
214
233
 
215
234
  def _listenForMessages(self):
216
235
  """
217
- Background thread. Reads all incoming messages from GuiRenderer and
218
- routes each one based on its type:
219
- - 'responseId' key present response for a waiting sendQuery() call
220
- - 'type' key present → event for a registered callback
221
- Exits when the pipe is closed (EOFError).
236
+ Background thread. Reads incoming messages from GuiRenderer and routes
237
+ each one: responses unblock waiting sendQuery() callers; events invoke
238
+ registered callbacks. Exits when the child closes the pipe (EOFError)
239
+ and releases any sendQuery() callers still waiting for a response.
222
240
  """
223
241
  while True:
224
242
  try:
@@ -228,7 +246,11 @@ class GuiHandler:
228
246
  elif 'type' in message:
229
247
  self._dispatchEvent(message)
230
248
  except EOFError:
231
- break # pipe closed; thread exits cleanly
249
+ break
250
+ with self._pendingLock:
251
+ for slot in self._pendingResponses.values():
252
+ slot['values'] = ['shutdown']
253
+ slot['event'].set()
232
254
 
233
255
  def _handleResponse(self, responseDict):
234
256
  """
@@ -334,23 +356,14 @@ class GuiHandler:
334
356
 
335
357
  def _shutdown(self):
336
358
  """
337
- Kills the child process immediately and closes the pipe.
359
+ Signals the child to shut down and waits for it to exit.
360
+ Closing the shutdown pipe delivers EOF to the child's QSocketNotifier,
361
+ which fires _onShutdownSignal on Qt's main thread immediately — even if
362
+ atexit does not run, since the OS closes the pipe fd on process death.
338
363
  Called automatically via atexit when the parent process exits.
339
- The child is killed with SIGKILL so it does not drain its event queue —
340
- exit() in the parent should return promptly.
341
364
  """
342
- if self.childProcess.is_alive():
343
- # SIGTERM is overridden in the child to call os._exit(0) immediately,
344
- # bypassing Qt's graceful shutdown (which drains the event queue).
345
- # Python delivers SIGTERM between bytecodes, so it fires within one
346
- # iteration of the child's _pollPipe loop — far faster than SIGKILL,
347
- # which macOS may defer while a Metal/GPU operation is in flight.
348
- self.childProcess.terminate()
349
- self.childProcess.join(timeout=0.5) # should die in < 1 ms
350
- if self.childProcess.is_alive():
351
- self.childProcess.kill() # backup: SIGKILL
352
- self.childProcess.join(timeout=2.0)
353
- self.connection.close()
365
+ self._shutdownPipe.close()
366
+ self.childProcess.join(timeout=1.0)
354
367
 
355
368
 
356
369
  #######################################################################################
@@ -75,30 +75,20 @@ class GuiRenderer:
75
75
  Unknown targets and unknown actions are silently ignored.
76
76
  """
77
77
 
78
- def __init__(self, childConn):
78
+ def __init__(self, childConn, shutdownReadConn):
79
79
  """
80
80
  Creates the QApplication and initializes all registries.
81
81
  """
82
- self.connection = childConn
82
+ self.connection = childConn
83
+ self._shutdownConn = shutdownReadConn
83
84
 
84
85
  # QApplication must be created first; it owns the Qt event loop.
85
86
  self._app = QtWidgets.QApplication([])
86
87
 
87
- # Do not quit when the last window is closed. The default behaviour
88
- # (quitOnLastWindowClosed = True) would exit the child process the
89
- # moment the user closes a Display, breaking the IPC pipe and making
90
- # it impossible to open new Displays without restarting Python.
91
- # The child only exits when the parent sends a 'shutdown' command or
92
- # kills the process directly.
88
+ # Do not quit when the last window is closed the child stays alive
89
+ # until the parent closes the shutdown pipe, triggering _onShutdownSignal.
93
90
  self._app.setQuitOnLastWindowClosed(False)
94
91
 
95
- # Qt installs a SIGTERM handler that drains the event queue before
96
- # exiting (graceful shutdown). When the parent kills this process,
97
- # we want immediate exit — override Qt's handler after QApplication
98
- # is constructed so ours takes precedence.
99
- import signal, os as _os
100
- signal.signal(signal.SIGTERM, lambda sig, frame: _os._exit(0))
101
-
102
92
  # maps objectId (int) -> mirror object (e.g. DisplayMirror, RectangleMirror)
103
93
  self._objectRegistry = {}
104
94
 
@@ -108,87 +98,58 @@ class GuiRenderer:
108
98
 
109
99
  def run(self):
110
100
  """
111
- Starts the pipe-polling timer and enters Qt's event loop.
112
- The QTimer fires _pollPipe() every 8ms (~120 Hz), draining any pending
113
- commands without blocking the Qt event loop between frames.
101
+ Wires up the command-polling timer and the shutdown notifier, then enters
102
+ Qt's event loop.
103
+ _pollPipe fires every 8ms to process incoming commands in fixed batches.
104
+ _shutdownNotifier watches the dedicated shutdown pipe fd; when the parent
105
+ closes its write end (via _shutdown() or process death), Qt fires
106
+ _onShutdownSignal on the main thread immediately — independent of the
107
+ command queue and of whether atexit runs in the parent.
114
108
  """
115
109
  self._timer = QtCore.QTimer()
116
110
  self._timer.timeout.connect(self._pollPipe)
117
111
  self._timer.start(8)
118
- self._app.exec()
119
112
 
120
- _polling = False # reentrancy guard
113
+ self._shutdownNotifier = QtCore.QSocketNotifier(
114
+ self._shutdownConn.fileno(),
115
+ QtCore.QSocketNotifier.Type.Read
116
+ )
117
+ self._shutdownNotifier.activated.connect(self._onShutdownSignal)
118
+
119
+ self._app.exec()
121
120
 
122
- # # ── Option A: no batching (drain entire queue before rendering) ─────────
123
- # def _pollPipe(self):
124
- # """
125
- # Drains all pending commands before returning to Qt's event loop.
126
- # """
127
- # while self.connection.poll():
128
- # message = self.connection.recv()
129
- # keepRunning = self._routeCommand(message)
130
- # if not keepRunning:
131
- # break
121
+ # ── Command polling ───────────────────────────────────────────────────────
132
122
 
133
- # ── Option B: fixed batch (let Qt render every N commands) ──────────────
134
123
  _BATCH_SIZE = 10
135
-
124
+
136
125
  def _pollPipe(self):
137
126
  """
138
- Drains pending commands, letting Qt process events every _BATCH_SIZE
139
- commands so the display updates incrementally during large batches.
140
- """
141
- if self._polling:
142
- return # processEvents() fired the timer again — skip
143
- self._polling = True
144
- try:
145
- count = 0
146
- while self.connection.poll():
147
- message = self.connection.recv()
148
- keepRunning = self._routeCommand(message)
149
- if not keepRunning:
150
- break
151
- count += 1
152
- if count % self._BATCH_SIZE == 0:
153
- self._app.processEvents()
154
- finally:
155
- self._polling = False
156
-
157
- # # ── Option C: adaptive batch (scales with scene complexity) ─────────────
158
- # _BATCH_MIN = 10 # batch size when scene is empty
159
- # _BATCH_MAX = 1000 # batch size ceiling
160
- # _BATCH_PER = 25 # add this many to the batch per 1000 objects in scene
161
-
162
- # def _pollPipe(self):
163
- # """
164
- # Drains pending commands, letting Qt process events at adaptive
165
- # intervals. Small scenes get frequent updates (responsive feel);
166
- # large scenes batch more commands between repaints (less overhead
167
- # from increasingly expensive repaints).
168
- # """
169
- # if self._polling:
170
- # return # processEvents() fired the timer again — skip
171
- # self._polling = True
172
- # try:
173
- # objectCount = len(self._objectRegistry)
174
- # batchSize = min(self._BATCH_MAX,
175
- # self._BATCH_MIN + objectCount * self._BATCH_PER)
176
- # count = 0
177
- # while self.connection.poll():
178
- # message = self.connection.recv()
179
- # keepRunning = self._routeCommand(message)
180
- # if not keepRunning:
181
- # break
182
- # count += 1
183
- # if count % batchSize == 0:
184
- # self._app.processEvents()
185
- # finally:
186
- # self._polling = False
127
+ Processes up to _BATCH_SIZE pending commands per timer tick, then returns
128
+ control to Qt's event loop. Keeping batches small lets Qt handle input
129
+ events and repaints between each batch, keeping the UI responsive during
130
+ heavy animation.
131
+ """
132
+ count = 0
133
+ while count < self._BATCH_SIZE and self.connection.poll():
134
+ try:
135
+ message = self.connection.recv()
136
+ except (EOFError, OSError):
137
+ # Command pipe closed — parent process died without sending a shutdown
138
+ # signal (e.g. killed by SIGTERM/SIGKILL, atexit didn't run).
139
+ # _onShutdownSignal may also fire if the OS released the shutdown pipe
140
+ # write end, but _app.quit() is safe to call from either path.
141
+ self._timer.stop()
142
+ self._shutdownNotifier.setEnabled(False)
143
+ self._app.quit()
144
+ return
145
+ self._routeCommand(message)
146
+ count += 1
187
147
 
188
148
  def _routeCommand(self, commandDict):
189
149
  """
190
150
  Routes an incoming command to the appropriate handler.
191
- Returns False if the renderer should shut down, True otherwise.
151
+ Shutdown is handled out-of-band by _onShutdownSignal; unknown actions are
152
+ silently ignored.
192
153
  """
193
154
  action = commandDict.get('action')
194
155
  target = commandDict.get('target')
@@ -196,11 +157,7 @@ class GuiRenderer:
196
157
  responseId = commandDict.get('responseId')
197
158
 
198
159
  # ── Built-in actions ──────────────────────────────────────────────────
199
- if action == 'shutdown':
200
- self._handleShutdown()
201
- return False
202
-
203
- elif action == 'ping':
160
+ if action == 'ping':
204
161
  response = _createResponse(responseId, ['pong'])
205
162
  self.connection.send(response)
206
163
 
@@ -356,9 +313,32 @@ class GuiRenderer:
356
313
  event = _createEvent(eventType, objectId, args)
357
314
  self.connection.send(event)
358
315
 
359
- def _handleShutdown(self):
360
- """Stops the polling timer and quits the Qt event loop."""
316
+ def _onShutdownSignal(self):
317
+ """
318
+ Called by Qt when the shutdown pipe becomes readable — meaning the parent
319
+ has closed its write end (via _shutdown() or process death).
320
+ Fires immediately on the Qt main thread, independent of the command queue.
321
+ Drains any remaining commands from the command pipe, sending 'shutdown'
322
+ responses to release any sendQuery() callers blocking in the parent.
323
+ Fire-and-forget commands are discarded. Then quits Qt's event loop
324
+ normally so Qt can clean up its own resources.
325
+ """
326
+ self._shutdownNotifier.setEnabled(False)
361
327
  self._timer.stop()
328
+
329
+ # Drain remaining commands. Queries get a 'shutdown' response so that
330
+ # any sendQuery() call blocking in the parent is released immediately.
331
+ # Fire-and-forget commands are discarded.
332
+ # If the command pipe is already closed, the drain is skipped gracefully.
333
+ try:
334
+ while self.connection.poll():
335
+ message = self.connection.recv()
336
+ responseId = message.get('responseId')
337
+ if responseId is not None:
338
+ self.connection.send(_createResponse(responseId, ['shutdown']))
339
+ except (EOFError, OSError):
340
+ pass # command pipe already closed; listener releases pending queries
341
+
362
342
  self._app.quit()
363
343
 
364
344
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CreativePython
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: A Python-based software environment for developing algorithmic art projects.
5
5
  Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
6
6
  License: MIT License
@@ -933,19 +933,19 @@ class Display(_Interactable):
933
933
  raise TypeError(f'{type(self).__name__}.addPopupMenu(): menu should be a Menu object (it was {type(menu).__name__})')
934
934
  _handler().sendCommand('addPopupMenu', self._objectId, {'menuId': menu._objectId})
935
935
 
936
- def write(self, filename, width=None, height=None):
936
+ def save(self, filename, width=None, height=None):
937
937
  """
938
938
  Saves the display's currently visible canvas to an image file.
939
939
  The file format is determined by the filename extension (e.g. .png, .jpg).
940
940
  Optional width and height resize the saved image; omitting one preserves the aspect ratio.
941
- Prints whether the write succeeded and the resolved file path.
941
+ Prints whether the save succeeded and the resolved file path.
942
942
  """
943
943
  result = _handler().sendQuery('write', self._objectId, {'filename': filename, 'width': width, 'height': height})
944
944
  success, resolvedPath = result[0], result[1]
945
945
  if success:
946
- print(f'{type(self).__name__}.write(): saved to "{resolvedPath}"')
946
+ print(f'{type(self).__name__}.save(): saved canvas to "{resolvedPath}"')
947
947
  else:
948
- print(f'{type(self).__name__}.write(): failed to save "{resolvedPath}"')
948
+ print(f'{type(self).__name__}.save(): failed to save to "{resolvedPath}"')
949
949
 
950
950
  def onClose(self, action):
951
951
  """
@@ -2296,9 +2296,9 @@ class Icon(_Graphics):
2296
2296
  rotation = self.getRotation()
2297
2297
  return f'Icon(filename = "{filename}", width = {width}, height = {height}, rotation = {rotation})'
2298
2298
 
2299
- # ── Write ────────────────────────────────────────────────────────────────
2299
+ # ── Save ────────────────────────────────────────────────────────────────
2300
2300
 
2301
- def write(self, filename, width=None, height=None):
2301
+ def save(self, filename, width=None, height=None):
2302
2302
  """
2303
2303
  Saves the icon to an image file.
2304
2304
  The file format is determined by the filename extension (e.g. .png, .jpg).
@@ -2308,9 +2308,9 @@ class Icon(_Graphics):
2308
2308
  result = _handler().sendQuery('write', self._objectId, {'filename': filename, 'width': width, 'height': height})
2309
2309
  success, resolvedPath = result[0], result[1]
2310
2310
  if success:
2311
- print(f'{type(self).__name__}.write(): saved to "{resolvedPath}"')
2311
+ print(f'{type(self).__name__}.save(): saved canvas to "{resolvedPath}"')
2312
2312
  else:
2313
- print(f'{type(self).__name__}.write(): failed to save "{resolvedPath}"')
2313
+ print(f'{type(self).__name__}.save(): failed to save "{resolvedPath}"')
2314
2314
 
2315
2315
  # ── Crop ────────────────────────────────────────────────────────────────
2316
2316
 
@@ -0,0 +1,123 @@
1
+ ################################################################################
2
+ # image.py Version 1.0 30-Jan-2025
3
+ # Taj Ballinger, Trevor Ritchie, and Bill Manaris
4
+ #
5
+ ##############################################################
6
+ ##################
7
+
8
+ from gui import Display, Icon
9
+
10
+ class Image:
11
+ """
12
+ Display window for rendering images.
13
+ """
14
+ def __init__(self, arg1, arg2=None):
15
+ """
16
+ Creates a new image display.
17
+ """
18
+ # JythonMusic's Image class has overloaded constructors,
19
+ # but Python doesn't allow that. We replicate an overload based on the
20
+ # data types of arg1 and arg2.
21
+ #
22
+ # (str, None) -> a filename for an image to load
23
+ # (str, int) -> a filename for an image to load, scaled to a width
24
+ # (int, None) -> the width of a square, blank canvas
25
+ # (int, int) -> the width and height of a blank canvas
26
+
27
+ self._display = Display() # create a new, blank Display
28
+ self.read(arg1, arg2) # read() handles loading the icon, plus setting the title and size of the Display
29
+
30
+ def show(self):
31
+ """
32
+ Shows the display window.
33
+ """
34
+ self._display.show()
35
+
36
+ def hide(self):
37
+ """
38
+ Hides the display window.
39
+ """
40
+ self._display.hide()
41
+
42
+ def getWidth(self):
43
+ """
44
+ Returns the display's canvas width (in pixels).
45
+ """
46
+ return self._display.getWidth()
47
+
48
+ def getHeight(self):
49
+ """
50
+ Returns the display's canvas height (in pixels).
51
+ """
52
+ return self._display.getHeight()
53
+
54
+ # pixel manipulation wrappers
55
+
56
+ def getPixel(self, col, row):
57
+ """
58
+ Returns the [r, g, b] color of a given pixel in the image.
59
+ """
60
+ return self._icon.getPixel(col, row)
61
+
62
+ def setPixel(self, col, row, RGBList):
63
+ """
64
+ Sets the [r, g, b] color of a given pixel in the image.
65
+ """
66
+ self._icon.setPixel(col, row, RGBList)
67
+
68
+ def getPixels(self):
69
+ """
70
+ Returns the [r, g, b] color of all pixels in the icon as a 2-dimensional array.
71
+ """
72
+ return self._icon.getPixels()
73
+
74
+ def setPixels(self, pixels):
75
+ """
76
+ Sets the [r, g, b] color of all pixels in the icon from a 2-dimensional array.
77
+ """
78
+ self._icon.setPixels(pixels)
79
+
80
+ def write(self, filename):
81
+ """
82
+ Saves the display's currently visible canvas to an image file.
83
+ The file format is determined by the filename extension (e.g. .png, .jpg).
84
+ """
85
+ self._display.save(filename)
86
+
87
+ def read(self, arg1, arg2=None):
88
+ """
89
+ Updates the image with a new filename.
90
+ arg1 and arg2 have the same overloaded behavior as Image's constructor.
91
+ (str, None) -> a filename for an image to load
92
+ (str, int) -> a filename for an image to load, scaled to a width
93
+ (int, None) -> the width of a square, blank canvas
94
+ (int, int) -> the width and height of a blank canvas
95
+ """
96
+ # first, load the icon
97
+ if isinstance(arg1, str):
98
+ # arg1 is a filename
99
+ title = arg1
100
+ icon = Icon(arg1, arg2) # load icon with optional resizing
101
+ else:
102
+ # arg1 is a image size
103
+ title = "Image"
104
+ icon = Icon("", arg1, arg2) # load blank icon, sized to given dimensions
105
+
106
+ # remove the previous icon, if needed
107
+ if self._icon is not None:
108
+ self._display.remove(self._icon)
109
+
110
+ # next, update the Display
111
+ width, height = icon.getSize()
112
+ self._display.setSize(width, height)
113
+ self._display.setTitle(title)
114
+
115
+ # finally, add new icon to the display
116
+ self._icon = icon
117
+ self._display.add(icon)
118
+
119
+
120
+ ###### Unit Tests ###################################
121
+
122
+ if __name__ == "__main__":
123
+ pass
@@ -1,70 +0,0 @@
1
- ################################################################################
2
- # image.py Version 1.0 30-Jan-2025
3
- # Taj Ballinger, Trevor Ritchie, and Bill Manaris
4
- #
5
- ##############################################################
6
- ##################
7
-
8
- from gui import Display, Icon
9
-
10
- class Image(Display):
11
- """
12
- Display window for rendering images.
13
- """
14
- def __init__(self, arg1, arg2=None):
15
- """
16
- Creates a new image display.
17
- """
18
- # JythonMusic's Image class has overloaded constructors,
19
- # but Python doesn't allow that. We replicate an overload based on the
20
- # data types of arg1 and arg2.
21
- #
22
- # (str, None) -> a filename for an image to load
23
- # (str, int) -> a filename for an image to load, scaled to a width
24
- # (int, None) -> the width of a square, blank canvas
25
- # (int, int) -> the width and height of a blank canvas
26
-
27
- if isinstance(arg1, str):
28
- # this is a filename
29
- title = arg1
30
- self._icon = Icon(arg1, arg2) # load icon with optional parameter
31
-
32
- else:
33
- title = "Image"
34
- self._icon = Icon("", arg1, arg2) # load blank icon with given dimensions
35
-
36
- width, height = self._icon.getSize()
37
-
38
- Display.__init__(self, title, width, height)
39
- self.add(self._icon)
40
-
41
- # pixel manipulation wrappers
42
-
43
- def getPixel(self, col, row):
44
- """
45
- Returns the [r, g, b] color of a given pixel in the image.
46
- """
47
- return self._icon.getPixel(col, row)
48
-
49
- def setPixel(self, col, row, RGBList):
50
- """
51
- Sets the [r, g, b] color of a given pixel in the image.
52
- """
53
- self._icon.setPixel(col, row, RGBList)
54
-
55
- def getPixels(self):
56
- """
57
- Returns the [r, g, b] color of all pixels in the icon as a 2-dimensional array.
58
- """
59
- return self._icon.getPixels()
60
-
61
- def setPixels(self, pixels):
62
- """
63
- Sets the [r, g, b] color of all pixels in the icon from a 2-dimensional array.
64
- """
65
- self._icon.setPixels(pixels)
66
-
67
- ###### Unit Tests ###################################
68
-
69
- if __name__ == "__main__":
70
- pass
File without changes
File without changes
File without changes