diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f2e0f2..becf7f5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# pypa:pywwt 0.23.0 (2023-11-28) + +- Update the Jupyter widget implementation to use ES6 class syntax (#368, + @Carifio24). This should fix up compatibility with the 8.x series of + `ipywidgets`. + +We have temporarily disabled DOI registration with this release (#369, @pkgw). +The associated release artifacts will report (obviously) bogus DOIs. This is due +to a recent major update to Zenodo, which seems to have broken their APIs. Our +Cranko release automation tool has not yet been updated to handle the new +system. We intend to address that soon, but we have internal motivations to get +this release out soon (see above), so we are going ahead without the Zenodo +deposition. + + # pypa:pywwt 0.22.0 (2023-09-15) - When using the bundled version of the WWT research app, make sure that we pull diff --git a/ci/azure-build-and-test.yml b/ci/azure-build-and-test.yml index 962bb45f..777de085 100644 --- a/ci/azure-build-and-test.yml +++ b/ci/azure-build-and-test.yml @@ -213,11 +213,31 @@ jobs: parameters: setupBuild: true + # We install some extra deps here to make the docs build happy (it needs to + # import everything) while avoiding clashes between Conda and pip. - bash: | set -euo pipefail source activate-conda.sh set -x - \conda create -y -n build setuptools matplotlib pip pyopengl 'pyqt>=5.12' pyqtwebengine python=3.9 qtpy scipy shapely + \conda create -y -n build \ + astropy \ + astropy-sphinx-theme \ + ipykernel \ + jupyter_sphinx \ + matplotlib \ + nbclassic \ + numpydoc \ + pip \ + pyopengl \ + 'pyqt>=5.12' \ + pyqtwebengine \ + python=3.9 \ + qtpy \ + scipy \ + setuptools \ + shapely \ + sphinx \ + sphinx-automodapi conda activate build pip install $BASH_WORKSPACE/sdist/*.tar.gz displayName: Install from sdist @@ -230,7 +250,6 @@ jobs: source activate-conda.sh conda activate build set -x - \conda install -y astropy astropy-sphinx-theme ipykernel jupyter_sphinx nbclassic numpydoc sphinx sphinx-automodapi cd docs make html displayName: Build docs diff --git a/ci/azure-deployment.yml b/ci/azure-deployment.yml index 23725ba3..9bf955a7 100644 --- a/ci/azure-deployment.yml +++ b/ci/azure-deployment.yml @@ -111,33 +111,35 @@ jobs: - bash: shred ~/.npmrc displayName: Clean up credentials - - job: zenodo_publish - pool: - vmImage: ubuntu-latest - variables: - - group: Deployment Credentials - - steps: - - template: azure-job-setup.yml - parameters: - setupCranko: true - - - bash: | - set -xeuo pipefail - - if cranko show if-released --exit-code pypa:pywwt ; then - cranko zenodo upload-artifacts --metadata=ci/zenodo.json5 $BASH_WORKSPACE/sdist/*.tar.gz - fi - displayName: Upload source tarball - env: - ZENODO_TOKEN: $(ZENODO_TOKEN) - - - bash: | - set -xeuo pipefail - - if cranko show if-released --exit-code pypa:pywwt ; then - cranko zenodo publish --metadata=ci/zenodo.json5 - fi - displayName: Publish to Zenodo - env: - ZENODO_TOKEN: $(ZENODO_TOKEN) + # 2023 Nov: temporarily disabling Zenodo; they have just updated their API and broken + # everything, and we want to ge a release out. + #- job: zenodo_publish + # pool: + # vmImage: ubuntu-latest + # variables: + # - group: Deployment Credentials + # + # steps: + # - template: azure-job-setup.yml + # parameters: + # setupCranko: true + # + # - bash: | + # set -xeuo pipefail + # + # if cranko show if-released --exit-code pypa:pywwt ; then + # cranko zenodo upload-artifacts --metadata=ci/zenodo.json5 $BASH_WORKSPACE/sdist/*.tar.gz + # fi + # displayName: Upload source tarball + # env: + # ZENODO_TOKEN: $(ZENODO_TOKEN) + # + # - bash: | + # set -xeuo pipefail + # + # if cranko show if-released --exit-code pypa:pywwt ; then + # cranko zenodo publish --metadata=ci/zenodo.json5 + # fi + # displayName: Publish to Zenodo + # env: + # ZENODO_TOKEN: $(ZENODO_TOKEN) diff --git a/ci/azure-sdist.yml b/ci/azure-sdist.yml index 859ebdc6..ea790a7a 100644 --- a/ci/azure-sdist.yml +++ b/ci/azure-sdist.yml @@ -29,12 +29,14 @@ jobs: - bash: cranko release-workflow apply-versions displayName: Apply Cranko versions - - bash: | - cranko zenodo preregister --metadata=ci/zenodo.json5 pypa:pywwt pywwt/_version.py CHANGELOG.md - displayName: "Preregister Zenodo DOI" - ${{ if and(eq(variables['Build.SourceBranchName'], 'rc'), ne(variables['build.reason'], 'PullRequest')) }}: - env: - ZENODO_TOKEN: $(ZENODO_TOKEN) + # 2023 Nov: temporarily disabling Zenodo; they have just updated their API and broken + # everything, and we want to ge a release out. + # - bash: | + # cranko zenodo preregister --metadata=ci/zenodo.json5 pypa:pywwt pywwt/_version.py CHANGELOG.md + # displayName: "Preregister Zenodo DOI" + # ${{ if and(eq(variables['Build.SourceBranchName'], 'rc'), ne(variables['build.reason'], 'PullRequest')) }}: + # env: + # ZENODO_TOKEN: $(ZENODO_TOKEN) - bash: | set -xeuo pipefail diff --git a/ci/zenodo.json5 b/ci/zenodo.json5 index 4f177a5d..4fd5ed97 100644 --- a/ci/zenodo.json5 +++ b/ci/zenodo.json5 @@ -1,73 +1,85 @@ +// See https://pkgw.github.io/cranko/book/latest/integrations/zenodo.html +// and https://developers.zenodo.org/#representation + { - "conceptrecid": "7164147", - "metadata": { - "access_right": "open", - "creators": [ + conceptrecid: '7164147', + + metadata: { + upload_type: 'software', + language: 'eng', + + // ** Keep this alphabetical by family name!!! ** + creators: [ { - "affiliation": "Center for Astrophysics | Harvard & Smithsonian", - "name": "Carifio, Jonathan", - "orcid": "0000-0002-7759-2601" + affiliation: 'Center for Astrophysics | Harvard & Smithsonian', + name: 'Carifio, Jonathan', + orcid: '0000-0002-7759-2601', }, { - "affiliation": "Heidelberg Institute for Theoretical Studies", - "name": "Gaibler, Volker", - "orcid": "0000-0001-7581-7574" + affiliation: 'Heidelberg Institute for Theoretical Studies', + name: 'Gaibler, Volker', + orcid: '0000-0001-7581-7574', }, { - "affiliation": "Aperio Software", - "name": "Homeier, Derek", - "orcid": "0000-0002-8546-9128" + affiliation: 'Aperio Software', + name: 'Homeier, Derek', + orcid: '0000-0002-8546-9128', }, { - "affiliation": "Winter Way", - "name": "Norman, Henrik", - "orcid": "0000-0003-4189-3450" + affiliation: 'Winter Way', + name: 'Norman, Henrik', + orcid: '0000-0003-4189-3450', }, { - "affiliation": "Space Telescope Science Institute", - "name": "Otor, O. Justin", - "orcid": "0000-0002-4679-5692" + affiliation: 'Space Telescope Science Institute', + name: 'Otor, O. Justin', + orcid: '0000-0002-4679-5692', }, { - "affiliation": "Aperio Software", - "name": "Robitaille, Thomas P.", - "orcid": "0000-0002-8642-1329" + affiliation: 'Aperio Software', + name: 'Robitaille, Thomas P.', + orcid: '0000-0002-8642-1329', }, { - "name": "Subbarao, Jeffrey" + name: 'Subbarao, Jeffrey', }, { - "affiliation": "Center for Astrophysics | Harvard & Smithsonian", - "name": "Williams, Peter K. G.", - "orcid": "0000-0003-3734-3587" + affiliation: 'Center for Astrophysics | Harvard & Smithsonian', + name: 'Williams, Peter K. G.', + orcid: '0000-0003-3734-3587', }, { - "affiliation": "Center for Astrophysics | Harvard & Smithsonian", - "name": "ZuHone, John", - "orcid": "0000-0003-3175-2347" - } + affiliation: 'Center for Astrophysics | Harvard & Smithsonian', + name: 'ZuHone, John', + orcid: '0000-0003-3175-2347', + }, ], - "description": "pywwt is the official toolkit for accessing WorldWide Telescope (WWT) from Python. Learn more at the pywwt website.", - "grants": [ - { - "id": "10.13039/100000001::1550701" - } + + // Subset of HTML allowed here: + description: 'pywwt is the official toolkit for accessing WorldWide \ +Telescope (WWT) from Python. Learn more at the pywwt website.', + + access_right: 'open', + license: 'BSD-3-Clause', // see https://spdx.org/licenses/ + + grants: [ + // National Science Foundation: + {id: '10.13039/100000001::1550701'}, + + // As of 2022 August these grants are not in the OpenAIRE "Research Graph" + // and so Zenodo won't let us record them here. Test existence by + // GET'ing `https://zenodo.org/api/grants/$id`. + // + //{id: '10.13039/100000001::1642446'}, + //{id: '10.13039/100000001::2004840'}, ], - "keywords": [ - "Astronomy", - "Python", - "Visualization", - "WorldWide Telescope" + + // Keywords use an uncontrolled vocabulary (and so are of limited usefulness): + keywords: [ + 'Astronomy', + 'Python', + 'Visualization', + 'WorldWide Telescope', ], - "language": "eng", - "license": "BSD-3-Clause", - "publication_date": "2023-09-15", - "title": "pypa:pywwt 0.22.0", - "upload_type": "software", - "version": "0.22.0" }, - "conceptdoi": "10.5281/zenodo.7164147", - "record_id": "8350627", - "doi": "10.5281/zenodo.8350627", - "bucket_link": "https://zenodo.org/api/files/4d1eeef7-6d48-44b1-a8eb-ee7127b1eba4" } diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index 3be11fa0..388fb452 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -1,3 +1,10 @@ +# npm:pywwt 1.7.0 (2023-11-28) + +- Update the Jupyter widget implementation to use ES6 class syntax (#368, + @Carifio24). This should fix up compatibility with the 8.x series of + `ipywidgets`. + + # npm:pywwt 1.6.0 (2023-09-15) - Require the new ESM-based engine through the 0.16.x series of the research app diff --git a/frontend/lib/widget.js b/frontend/lib/widget.js index 44377918..d10b27f5 100644 --- a/frontend/lib/widget.js +++ b/frontend/lib/widget.js @@ -67,21 +67,24 @@ var version = require('./index').version; // Which is a hairy busines, because not only does our one widget potentially // have multiple views, but there are also potentially multiple active widgets, // and we'll see all of their messages. -var WWTModel = widgets.DOMWidgetModel.extend({ - defaults: _.extend(widgets.DOMWidgetModel.prototype.defaults(), { - _model_name: 'WWTModel', - _model_module: 'pywwt', - _model_module_version: version, - - _view_name: 'WWTView', - _view_module: 'pywwt', - _view_module_version: version, - - _appUrl: '' - }), +class WWTModel extends widgets.DOMWidgetModel { + defaults() { + return { + ...super.defaults(), + _model_name: 'WWTModel', + _model_module: 'pywwt', + _model_module_version: version, + + _view_name: 'WWTView', + _view_module: 'pywwt', + _view_module_version: version, + + _appUrl: '' + } + } - initialize: function () { - WWTModel.__super__.initialize.apply(this, arguments); + initialize() { + widgets.DOMWidgetModel.prototype.initialize.apply(this, arguments); // NOTE: we deliberately call the following twice to make sure that it // is properly set, due to a caching bug in some versions of JupyterLab. @@ -112,12 +115,12 @@ var WWTModel = widgets.DOMWidgetModel.extend({ function (event) { self.processDomWindowMessage(event); }, false ); - }, + } // The kernel can generate partial URLs, but doesn't (and can't) know the // full URL where data are ultimately exposed. So in various places we need // to edit URLs emerging from the client to make them complete. - canonicalizeUrl: function (url) { + canonicalizeUrl(url) { // Sketchy heuristic to deal with the Jupyter "base URL", which still // isn't an absolute URL. It's a URL path used by multi-user Jupyter // servers and the like. The Python kernel code can determine the base @@ -132,19 +135,19 @@ var WWTModel = widgets.DOMWidgetModel.extend({ } return new URL(url, location.toString()).toString(); - }, + } // Get a unique ID and sequence number for distinguishing views. Note that // while each model might have multiple views, there might also be multiple // widget models too, and we have to distinguish them all. - mintViewIds: function() { + mintViewIds() { var seq = this._nextViewSeqNumber; this._nextViewSeqNumber++; return [this.model_id + "v" + seq, seq]; - }, + } // Called by a widget view when the "liveness" state of its app changes. - onViewStatusChange: function(view, alive) { + onViewStatusChange(view, alive) { if (alive) { // Should this view become the current view? // @@ -187,14 +190,14 @@ var WWTModel = widgets.DOMWidgetModel.extend({ } } } - }, + } // Relay a message from the kernel to the active view. In order to keep // things tractable, we only route messages to the "current" view. For // instance, if the client were to issue a data-request message and we // routed it to multiple views, we'd get multiple responses, with no // sensible way to know which to prefer. - processIpyWidgetsMessage: function (msg) { + processIpyWidgetsMessage(msg) { if (this._currentView === null) { // We could queue up messages here. The kernel "shouldn't" send us // any messages until a view is ready, but it's always possible that @@ -224,7 +227,7 @@ var WWTModel = widgets.DOMWidgetModel.extend({ } this._currentView.relayIpyWidgetsMessage(msg); - }, + } // Process messages from the WWT apps, potentially relaying them to the // kernel. @@ -234,7 +237,7 @@ var WWTModel = widgets.DOMWidgetModel.extend({ // // The message is relayed to the kernel using ipywidgets "custom" messages, // which is basically trivial once we've dealt with the above. - processDomWindowMessage: function (event) { + processDomWindowMessage(event) { var payload = event.data; if (event.origin !== this._appOrigin) @@ -270,7 +273,7 @@ var WWTModel = widgets.DOMWidgetModel.extend({ payload['_pywwtExpedite'] = true; this.send(payload); } -}); +} // The pywwt ipywidget view implementation. // @@ -282,8 +285,8 @@ var WWTModel = widgets.DOMWidgetModel.extend({ // destroy the element. However, re-adding an iframe to the DOM causes it to // reload, so hiding and re-showing a WWT view causes its internal state to be // reset :-( -var WWTView = widgets.DOMWidgetView.extend({ - render: function () { +class WWTView extends widgets.DOMWidgetView { + render() { this._appUrl = this.model.canonicalizeUrl(this.model.get('_appUrl')); this._appOrigin = new URL(this._appUrl).origin; @@ -324,9 +327,9 @@ var WWTView = widgets.DOMWidgetView.extend({ ); setInterval(function () { self.checkApp(); }, 1000); - }, + } - checkApp: function() { + checkApp() { // Send our next ping ... var window = this.tryGetWindow(); @@ -346,13 +349,13 @@ var WWTView = widgets.DOMWidgetView.extend({ this._alive = alive; this.model.onViewStatusChange(this, alive); } - }, + } // Process a message sent to the browser window. This function's only job is // to look for responses to our pings. It will be called for messages from // all views of all widgets, though, so it needs to be careful about which // messages to process. - processDomWindowMessage: function (event) { + processDomWindowMessage(event) { var payload = event.data; if (event.origin !== this._appOrigin) @@ -365,12 +368,12 @@ var WWTView = widgets.DOMWidgetView.extend({ this._lastPongTimestamp = ts; } } - }, + } // Called by the model when there's a message from the kernel that should go // to this view. The model "shouldn't" give us any messages if/when our // window is nonfunctional, but the window might always die underneath us. - relayIpyWidgetsMessage: function (msg) { + relayIpyWidgetsMessage(msg) { var window = this.tryGetWindow(); if (!window) { // TODO? Tell the model that we failed? @@ -378,38 +381,34 @@ var WWTView = widgets.DOMWidgetView.extend({ } window.postMessage(msg, this._appUrl); - }, + } // Note: processPhosphorMessage is needed for Jupyter Lab <2 and // processLuminoMessage is needed for Jupyter Lab 2.0+ - processPhosphorMessage: function (msg) { + // See https://ipywidgets.readthedocs.io/en/latest/migration_guides.html#phosphor-lumino + _processLuminoMessage(msg, _super) { // We listen for phosphor resize events so that when Jupyter Lab is // used, we adjust the canvas size to the tab/panel in Jupyter Lab. // See relayout for more details. - WWTView.__super__.processPhosphorMessage.apply(this, arguments); + _super.call(this, msg); switch (msg.type) { case 'resize': case 'after-show': this.relayout(); break; } - }, + } - processLuminoMessage: function (msg) { - // We listen for lumino resize events so that when Jupyter Lab is - // used, we adjust the canvas size to the tab/panel in Jupyter Lab. - // See relayout for more details. - WWTView.__super__.processLuminoMessage.apply(this, arguments); - switch (msg.type) { - case 'resize': - case 'after-show': - this.relayout(); - break; - } - }, + processPhosphorMessage(msg) { + this._processLuminoMessage(msg, super.processPhosphorMessage); + } - relayout: function () { + processLuminoMessage(msg) { + this._processLuminoMessage(msg, super.processLuminoMessage); + } + + relayout() { // Only do resizing if we are not in the notebook context but in a // split panel context. We find this out by checking if one of the // parents of the current element has the jp-MainAreaWidget class -- @@ -459,13 +458,13 @@ var WWTView = widgets.DOMWidgetView.extend({ // need to find a better solution in the long term. iframe.width = width - 10; iframe.height = height - 10; - }, + } // Get the WWT window, if it is actually fully initialized. Note that in // JupyterLab if this widget view is hidden and then re-shown, the iframe // will reload, and the contentWindow will acquire a new value. So we can't // cache too aggressively. - tryGetWindow: function () { + tryGetWindow() { var iframe = this.el.getElementsByTagName('iframe')[0]; if (!iframe) return null; @@ -476,7 +475,7 @@ var WWTView = widgets.DOMWidgetView.extend({ return window; } -}); +} module.exports = { WWTModel: WWTModel, diff --git a/frontend/package.json b/frontend/package.json index dc028255..5e217c1d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,5 +42,5 @@ "clean": "shx rm -rf dist", "pywwt-export": "npm run build && npm pack && node pywwt-export.js" }, - "version": "1.6.0" + "version": "1.7.0" } diff --git a/pywwt/_version.py b/pywwt/_version.py index 64ad4248..5d52bd0a 100644 --- a/pywwt/_version.py +++ b/pywwt/_version.py @@ -1,4 +1,4 @@ -version_info = (0, 22, 0, 'final', 0) # cranko project-version tuple +version_info = (0, 23, 0, 'final', 0) # cranko project-version tuple _specifier_ = { "alpha": ".a", @@ -18,5 +18,5 @@ ) # The strings are auto-updated by Cranko during formal releases: -version_doi = "10.5281/zenodo.8350627" -concept_doi = "10.5281/zenodo.7164147" +version_doi = "xx.xxxx/dev-build.pypa:pywwt.version" +concept_doi = "xx.xxxx/dev-build.pypa:pywwt.concept" diff --git a/pywwt/core.py b/pywwt/core.py index cc29fd7e..87b2fc60 100644 --- a/pywwt/core.py +++ b/pywwt/core.py @@ -610,8 +610,9 @@ def instruments(self): ).tag(wwt="altAzGridColor", wwt_reset=True) alt_az_text = Bool( - False, help='Whether to show labels for the altitude-azimuth grid\'s text ' '(`bool`)' - ).tag(wwt='showAltAzGridText', wwt_reset=True) + False, + help="Whether to show labels for the altitude-azimuth grid's text " "(`bool`)", + ).tag(wwt="showAltAzGridText", wwt_reset=True) background = Unicode( "Hydrogen Alpha Full Sky Map", @@ -651,13 +652,15 @@ def instruments(self): ).tag(wwt="showConstellationSelection", wwt_reset=True) constellation_pictures = Bool( - False, help='Whether to show pictures of the constellations\' ' - 'mythological representations ' '(`bool`)' - ).tag(wwt='showConstellationPictures', wwt_reset=True) + False, + help="Whether to show pictures of the constellations' " + "mythological representations " + "(`bool`)", + ).tag(wwt="showConstellationPictures", wwt_reset=True) constellation_labels = Bool( - False, help='Whether to show labels for constellations ' '(`bool`)' - ).tag(wwt='showConstellationLabels', wwt_reset=True) + False, help="Whether to show labels for constellations " "(`bool`)" + ).tag(wwt="showConstellationLabels", wwt_reset=True) crosshairs = Bool( False, help="Whether to show crosshairs at the center of " "the field (`bool`)" @@ -711,8 +714,8 @@ def instruments(self): ).tag(wwt="galacticGridColor", wwt_reset=True) galactic_text = Bool( - False, help='Whether to show labels for the galactic grid\'s text ' '(`bool`)' - ).tag(wwt='showGalacticGridText', wwt_reset=True) + False, help="Whether to show labels for the galactic grid's text " "(`bool`)" + ).tag(wwt="showGalacticGridText", wwt_reset=True) grid = Bool(False, help="Whether to show the equatorial grid " "(`bool`)").tag( wwt="showGrid", wwt_reset=True @@ -871,11 +874,13 @@ def center_on_coordinates(self, coord, fov=60 * u.deg, roll=None, instant=True): desired location. """ coord_icrs = coord.icrs - msg = dict(event="center_on_coordinates", - ra=coord_icrs.ra.deg, - dec=coord_icrs.dec.deg, - fov=fov.to(u.deg).value, - instant=instant) + msg = dict( + event="center_on_coordinates", + ra=coord_icrs.ra.deg, + dec=coord_icrs.dec.deg, + fov=fov.to(u.deg).value, + instant=instant, + ) if roll is not None: msg["roll"] = roll.to(u.deg).value self._send_msg(**msg) @@ -1069,11 +1074,11 @@ def load_image_collection(self, url, recursive=False, remote_only=False): nest_asyncio.apply() loop = asyncio.get_event_loop() - loop.run_until_complete(self._send_into_future( - event="load_image_collection", - url=url, - loadChildFolders=recursive - )) + loop.run_until_complete( + self._send_into_future( + event="load_image_collection", url=url, loadChildFolders=recursive + ) + ) @property def available_layers(self): @@ -1341,7 +1346,7 @@ def _serialize_state(self, title, max_width, max_height): "ra": center.icrs.ra.deg, "dec": center.icrs.dec.deg, "fov": fov.to_value(u.deg), - "roll": roll.to_value(u.deg) + "roll": roll.to_value(u.deg), } state["foreground_settings"] = {