diff --git a/.github/ISSUE_TEMPLATE/4-release_checklist.md b/.github/ISSUE_TEMPLATE/4-release_checklist.md index 12dde7459b6..379af1a6b72 100644 --- a/.github/ISSUE_TEMPLATE/4-release_checklist.md +++ b/.github/ISSUE_TEMPLATE/4-release_checklist.md @@ -8,49 +8,51 @@ assignees: '' --- **Release**: [v0.x.x](https://github.com/GenericMappingTools/pygmt/milestones/?) -**Scheduled Date**: YYYY/MM/DD -**Pull request due date**: YYYY/MM/DD +**Scheduled Date**: 20YY/MM/DD +**Pull request due date**: 20YY/MM/DD +**DOI**: `10.5281/zenodo.XXXXXXX` **Priority PRs/issues to complete prior to release** + - [ ] Wrap X () - [ ] Wrap Y () **Before release**: + - [ ] Check [SPEC 0](https://scientific-python.org/specs/spec-0000/) to see if we need to bump the minimum supported versions of GMT, Python and core package dependencies (NumPy/Pandas/Xarray) -- [ ] Run `make codespell` to check common misspellings. If there are any, either fix them or add them to `ignore-words-list` in `pyproject.toml` +- [ ] Review the ["PyGMT Team" page](https://www.pygmt.org/dev/team.html) - [ ] Check to ensure that: + - [ ] Deprecations and related tests are removed for this version by running `grep --include="*.py" -r vX.Y.Z` from the base of the repository - [ ] All tests pass in the ["GMT Legacy Tests" workflow](https://github.com/GenericMappingTools/pygmt/actions/workflows/ci_tests_legacy.yaml) - [ ] All tests pass in the ["GMT Dev Tests" workflow](https://github.com/GenericMappingTools/pygmt/actions/workflows/ci_tests_dev.yaml) - [ ] All tests pass in the ["Doctests" workflow](https://github.com/GenericMappingTools/pygmt/actions/workflows/ci_doctests.yaml) - - [ ] Deprecations and related tests are removed for this version by running `grep --include="*.py" -r 'remove_version="vX.Y.Z"' pygmt` from the base of the repository -- [ ] Update warnings in `pygmt.show_versions()` as well as notes in [Common installation issues](https://www.pygmt.org/dev/install.html#not-working-transparency) - and [Testing your install]((https://www.pygmt.org/dev/install.html#testing-your-install) regarding GMT-Ghostscript incompatibility +- [ ] Update warnings in `pygmt/_show_versions.py` as well as notes in + [Not working transparency](https://www.pygmt.org/dev/install.html#not-working-transparency) + regarding GMT-Ghostscript incompatibility - [ ] Reserve a DOI on [Zenodo](https://zenodo.org) by clicking on "New Version" -- [ ] Review the ["PyGMT Team" page](https://www.pygmt.org/dev/team.html) -- [ ] Finish up 'Changelog entry for v0.x.x' Pull Request: - - [ ] Add a new entry in `doc/_static/version_switch.js` for documentation switcher - - [ ] Update `CITATION.cff` and BibTeX at https://github.com/GenericMappingTools/pygmt#citing-pygmt - - [ ] Update authorship list - - [ ] Update DOI (and url for BibTeX) - - [ ] Update version - - [ ] Update date released - - [ ] Add the documentation link `doc/minversions.md` - - [ ] Add minimum required version information `doc/minversions.md` - - [ ] Copy draft changelog from Release Drafter and edit it to look nice ([see maintainers guide for details](https://www.pygmt.org/dev/maintenance.html#updating-the-changelog)) +- [ ] Finish up the "Changelog entry for v0.x.x" Pull Request (Use the previous changelog PR as a reference) +- [ ] Run `make codespell` to check common misspellings. If there are any, either fix them or add them to `ignore-words-list` in `pyproject.toml` +- [ ] Draft the announcement on https://hackmd.io/@pygmt **Release**: + - [ ] At the [PyGMT release page on GitHub](https://github.com/GenericMappingTools/pygmt/releases): - [ ] Edit the draft release notes with the finalized changelog - [ ] Set the tag version and release title to vX.Y.Z - [ ] Make a release by clicking the 'Publish Release' button, this will automatically create a tag too -- [ ] Manually upload the pygmt-vX.Y.Z.zip and baseline-images.zip files to https://zenodo.org/deposit, ensure that it is filed under the correct reserved DOI +- [ ] Download pygmt-X.Y.Z.zip (rename to pygmt-vX.Y.Z.zip) and baseline-images.zip from + the release page, and upload the two zip files to https://zenodo.org/deposit, + ensure that they are filed under the correct reserved DOI **After release**: -- [ ] Update conda-forge [pygmt-feedstock](https://github.com/conda-forge/pygmt-feedstock) [Done automatically by conda-forge's bot, but remember to pin SPEC0 versions] + +- [ ] Update conda-forge [pygmt-feedstock](https://github.com/conda-forge/pygmt-feedstock) + [Done automatically by conda-forge's bot. Remember to pin Python and SPEC0 versions] - [ ] Bump PyGMT version on https://github.com/GenericMappingTools/try-gmt (after conda-forge update) - [ ] Announce the release on: - - [ ] GMT [forum](https://forum.generic-mapping-tools.org/c/news/) (do this announcement first! draft on https://hackmd.io/@pygmt. requires moderator status) + - [ ] GMT [forum](https://forum.generic-mapping-tools.org/c/news/) (do this announcement first! Requires moderator status) - [ ] [ResearchGate](https://www.researchgate.net) (after forum announcement, add new version as research item via the **code** category, be sure to include the corresponding new Zenodo DOI) + --- - [ ] Party :tada: (don't tick before all other checkboxes are ticked!) diff --git a/.github/workflows/cache_data.yaml b/.github/workflows/cache_data.yaml index 6e3d774a191..c2a309b22e9 100644 --- a/.github/workflows/cache_data.yaml +++ b/.github/workflows/cache_data.yaml @@ -58,7 +58,7 @@ jobs: xarray netCDF4 packaging - build + python-build # Install the package that we want to test - name: Install the package @@ -76,9 +76,10 @@ jobs: # Upload the downloaded files as artifacts to GitHub - name: Upload artifacts to GitHub - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.4.0 with: name: gmt-cache + include-hidden-files: true path: | ~/.gmt/cache ~/.gmt/server diff --git a/.github/workflows/ci_docs.yml b/.github/workflows/ci_docs.yml index 7d435683b1a..89dba96fd9d 100644 --- a/.github/workflows/ci_docs.yml +++ b/.github/workflows/ci_docs.yml @@ -94,7 +94,7 @@ jobs: create-args: >- python=3.12 gmt=6.5.0 - ghostscript=10.03.1 + ghostscript=10.04.0 numpy pandas xarray @@ -104,9 +104,9 @@ jobs: geopandas<1.0 ipython rioxarray - build make pip + python-build myst-nb panel sphinx diff --git a/.github/workflows/ci_doctests.yaml b/.github/workflows/ci_doctests.yaml index 45bdcb47311..e5c07d21e6d 100644 --- a/.github/workflows/ci_doctests.yaml +++ b/.github/workflows/ci_doctests.yaml @@ -62,9 +62,9 @@ jobs: ipython pyarrow rioxarray - build make pip + python-build pytest pytest-doctestplus pytest-mpl diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml index 732442ef43b..e271c3cd51c 100644 --- a/.github/workflows/ci_tests.yaml +++ b/.github/workflows/ci_tests.yaml @@ -119,16 +119,16 @@ jobs: create-args: >- python=${{ matrix.python-version }}${{ matrix.optional-packages }} gmt=6.5.0 - ghostscript=10.03.1 + ghostscript=10.04.0 numpy=${{ matrix.numpy-version }} pandas${{ matrix.pandas-version }} xarray${{ matrix.xarray-version }} netCDF4 packaging - build dvc make pip + python-build pytest pytest-cov pytest-doctestplus @@ -161,7 +161,7 @@ jobs: # Upload diff images on test failure - name: Upload diff images if any test fails - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.4.0 if: failure() with: name: artifact-${{ runner.os }}-${{ matrix.python-version }} diff --git a/.github/workflows/ci_tests_dev.yaml b/.github/workflows/ci_tests_dev.yaml index eeeafe93118..7ffb34cc19c 100644 --- a/.github/workflows/ci_tests_dev.yaml +++ b/.github/workflows/ci_tests_dev.yaml @@ -75,7 +75,7 @@ jobs: ninja curl fftw - ghostscript=10.03.1 + ghostscript=10.04.0 glib hdf5 libblas @@ -179,7 +179,7 @@ jobs: # Upload diff images on test failure - name: Upload diff images if any test fails - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.4.0 if: ${{ failure() }} with: name: artifact-GMT-${{ matrix.gmt_git_ref }}-${{ runner.os }} diff --git a/.github/workflows/ci_tests_legacy.yaml b/.github/workflows/ci_tests_legacy.yaml index ef8efc6e4fe..d38e3acde7e 100644 --- a/.github/workflows/ci_tests_legacy.yaml +++ b/.github/workflows/ci_tests_legacy.yaml @@ -72,9 +72,9 @@ jobs: pyarrow rioxarray sphinx-gallery - build make pip + python-build pytest pytest-doctestplus pytest-mpl diff --git a/.github/workflows/format-command.yml b/.github/workflows/format-command.yml index a33d2eb2cdc..6744ebc34ef 100644 --- a/.github/workflows/format-command.yml +++ b/.github/workflows/format-command.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: # Generate token from GenericMappingTools bot - - uses: actions/create-github-app-token@v1.10.3 + - uses: actions/create-github-app-token@v1.11.0 id: generate-token with: app-id: ${{ secrets.APP_ID }} @@ -25,7 +25,7 @@ jobs: ref: ${{ github.event.client_payload.pull_request.head.ref }} # Setup Python environment - - uses: actions/setup-python@v5.1.1 + - uses: actions/setup-python@v5.2.0 with: python-version: '3.12' diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 995fd0ce13d..03777be4b44 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -51,7 +51,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.2.0 with: python-version: '3.12' @@ -75,10 +75,10 @@ jobs: ls -lh dist/ - name: Publish to Test PyPI - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 with: repository-url: https://test.pypi.org/legacy/ - name: Publish to PyPI if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 diff --git a/.github/workflows/slash-command-dispatch.yml b/.github/workflows/slash-command-dispatch.yml index c5d10cf5be8..c551b14954f 100644 --- a/.github/workflows/slash-command-dispatch.yml +++ b/.github/workflows/slash-command-dispatch.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v4 + uses: peter-evans/slash-command-dispatch@v4.0.0 with: token: ${{ github.token }} commands: | diff --git a/.github/workflows/style_checks.yaml b/.github/workflows/style_checks.yaml index 119f1200066..f63236a2cb0 100644 --- a/.github/workflows/style_checks.yaml +++ b/.github/workflows/style_checks.yaml @@ -28,7 +28,7 @@ jobs: # Setup Python - name: Set up Python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.2.0 with: python-version: '3.12' diff --git a/.github/workflows/type_checks.yml b/.github/workflows/type_checks.yml index 28d54993ee8..b973e951c5a 100644 --- a/.github/workflows/type_checks.yml +++ b/.github/workflows/type_checks.yml @@ -37,7 +37,7 @@ jobs: # Setup Python - name: Set up Python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.2.0 with: python-version: '3.12' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d758c6b5187..1deeef23169 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,3 +15,9 @@ repos: - id: remove-crlf - id: chmod args: ['644'] + +# https://pre-commit.ci/#configuration +ci: + skip: [chmod] + autofix_prs: false + autoupdate_schedule: quarterly diff --git a/CITATION.cff b/CITATION.cff index ee4d5ded691..e6d79e2530d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -76,9 +76,9 @@ authors: family-names: Wessel affiliation: University of Hawaiʻi at Mānoa, USA orcid: https://orcid.org/0000-0001-5708-7336 -date-released: 2024-05-01 -doi: 10.5281/zenodo.11062720 +date-released: 2024-09-05 +doi: 10.5281/zenodo.13679420 license: BSD-3-Clause repository-code: https://github.com/GenericMappingTools/pygmt type: software -version: 0.12.0 +version: 0.13.0 diff --git a/README.md b/README.md index 7fd56461d74..4808f482eac 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ research using the following BibTeX: ``` @software{ - pygmt_2024_11062720, + pygmt_2024_13679420, author = {Tian, Dongdong and Uieda, Leonardo and Leong, Wei Ji and @@ -152,12 +152,12 @@ research using the following BibTeX: Quinn, Jamie and Wessel, Paul}, title = {{PyGMT: A Python interface for the Generic Mapping Tools}}, - month = may, + month = sep, year = 2024, publisher = {Zenodo}, - version = {0.12.0}, - doi = {10.5281/zenodo.11062720}, - url = {https://doi.org/10.5281/zenodo.11062720} + version = {0.13.0}, + doi = {10.5281/zenodo.13679420}, + url = {https://doi.org/10.5281/zenodo.13679420} } ``` @@ -186,12 +186,6 @@ Other official wrappers for GMT: - [GMT.jl](https://github.com/GenericMappingTools/GMT.jl): A Julia wrapper for GMT. - [gmtmex](https://github.com/GenericMappingTools/gmtmex): A Matlab/Octave wrapper for GMT. -Other non-official Python wrappers for GMT (not maintained): - -- [gmtpy](https://github.com/emolch/gmtpy) by [Sebastian Heimann](https://github.com/emolch) -- [pygmt](https://github.com/ian-r-rose/pygmt) by [Ian Rose](https://github.com/ian-r-rose) -- [PyGMT](https://github.com/glimmer-cism/PyGMT) by [Magnus Hagdorn](https://github.com/mhagdorn) - ## Minimum supported versions diff --git a/ci/requirements/docs.yml b/ci/requirements/docs.yml index 605e5b750d5..8214dbac8fe 100644 --- a/ci/requirements/docs.yml +++ b/ci/requirements/docs.yml @@ -6,7 +6,7 @@ dependencies: # Required dependencies - python=3.12 - gmt=6.5.0 - - ghostscript=10.03.1 + - ghostscript=10.04.0 - numpy - pandas - xarray @@ -18,9 +18,9 @@ dependencies: - ipython - rioxarray # Development dependencies (general) - - build - make - pip + - python-build # Dev dependencies (building documentation) - myst-nb - panel diff --git a/doc/_static/style.css b/doc/_static/style.css index 77546baa72b..2681160e4b8 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -203,3 +203,19 @@ a.copybtn { .sphx-glr-single-img { max-width: 80%!important; } + +/* + * Styles for aligning table cells. + * https://myst-parser.readthedocs.io/en/latest/syntax/tables.html#markdown-syntax + */ +th.text-left, td.text-left { + text-align: left !important; +} + +th.text-center, td.text-center { + text-align: center !important; +} + +th.text-right, td.text-right { + text-align: right !important; +} diff --git a/doc/_static/version_switch.js b/doc/_static/version_switch.js index f79874cb998..ac6f81a5eff 100644 --- a/doc/_static/version_switch.js +++ b/doc/_static/version_switch.js @@ -12,6 +12,7 @@ var all_versions = { 'latest': 'latest', 'dev': 'dev', + 'v0.13.0': 'v0.13.0', 'v0.12.0': 'v0.12.0', 'v0.11.0': 'v0.11.0', 'v0.10.0': 'v0.10.0', diff --git a/doc/_templates/breadcrumbs.html b/doc/_templates/breadcrumbs.html index 686d5f6172a..ac9e949330e 100644 --- a/doc/_templates/breadcrumbs.html +++ b/doc/_templates/breadcrumbs.html @@ -14,7 +14,7 @@ {% set gallery_path = gallery_dir[pagename.split("/")[0]] %} {% set example_script = pagename|replace(pagename.split("/")[0], gallery_dir[pagename.split("/")[0]]) %} {% if pagename.split("/")[-1] == "index" %} - {% set example_script = example_script|replace("index", "README.txt") %} + {% set example_script = example_script|replace("index", "GALLERY_HEADER.rst") %} {% else %} {% set example_script = example_script + ".py" %} {% endif %} diff --git a/doc/api/index.rst b/doc/api/index.rst index cff460ce2ff..1c80d8d163f 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -317,5 +317,6 @@ Low level access (these are mostly used by the :mod:`pygmt.clib` package): clib.Session.get_libgmt_func clib.Session.virtualfile_from_data clib.Session.virtualfile_from_grid + clib.Session.virtualfile_from_stringio clib.Session.virtualfile_from_matrix clib.Session.virtualfile_from_vectors diff --git a/doc/changes.md b/doc/changes.md index 761ee93201f..cc760f4e0f1 100644 --- a/doc/changes.md +++ b/doc/changes.md @@ -1,5 +1,93 @@ # Changelog +## Release v0.13.0 (2024/09/05) + +[![Digital Object Identifier for PyGMT v0.13.0](https://zenodo.org/badge/DOI/10.5281/zenodo.13679420.svg)](https://doi.org/10.5281/zenodo.13679420) + +### Highlights + +* 🎉 **Thirteenth minor release of PyGMT** 🎉 +* Add new documentation section "Technical Reference" and document the supported encodings and fonts +* Tutorial for "Draping a dataset on top of a topographic surface" ([#3316](https://github.com/GenericMappingTools/pygmt/pull/3316)) +* Tutorial for "Typesetting non-ASCII characters" ([#3389](https://github.com/GenericMappingTools/pygmt/pull/3389)) + +### New Features + +* Wrap the GMT API function GMT_Read_Data to read data into GMT data containers ([#3324](https://github.com/GenericMappingTools/pygmt/pull/3324)) +* Wrap GMT's standard data type GMT_IMAGE for images ([#3338](https://github.com/GenericMappingTools/pygmt/pull/3338)) + +### Enhancements + +* **Breaking**: pygmt.x2sys_cross: Refactor to use virtualfiles for output tables ([#3182](https://github.com/GenericMappingTools/pygmt/pull/3182)) +* pygmt.show_versions: Warn about incompatible Ghostscript versions ([#3244](https://github.com/GenericMappingTools/pygmt/pull/3244)) +* pygmt.show_versions: Show GDAL version ([#3364](https://github.com/GenericMappingTools/pygmt/pull/3364), [#3376](https://github.com/GenericMappingTools/pygmt/pull/3376)) +* pygmt.datasets.load_*: Add autocompletion support for the 'resolution' parameter ([#3260](https://github.com/GenericMappingTools/pygmt/pull/3260)) +* clib.Session: Refactor the `__getitem__` special method to avoid calling API function GMT_Get_Enum repeatedly ([#3261](https://github.com/GenericMappingTools/pygmt/pull/3261)) +* clib: Refactor to avoid checking GMT version repeatedly and only check once when loading the GMT library ([#3254](https://github.com/GenericMappingTools/pygmt/pull/3254)) +* Support non-ASCII characters in ISO-8859-x charsets ([#3310](https://github.com/GenericMappingTools/pygmt/pull/3310)) +* Refactor to improve the user experience with non-ASCII characters ([#3206](https://github.com/GenericMappingTools/pygmt/pull/3206)) + +### Deprecations + +* SPEC 0: Bump minimum supported version to xarray 2022.09 ([#3372](https://github.com/GenericMappingTools/pygmt/pull/3372)) +* SPEC 0: Bump minimum supported version to NumPy 1.24 ([#3286](https://github.com/GenericMappingTools/pygmt/pull/3286)) +* clib: Deprecate API function 'Session.virtualfile_from_data', use 'Session.virtualfile_in' instead (will be removed in v0.15.0) ([#3225](https://github.com/GenericMappingTools/pygmt/pull/3225)) +* Remove the unused pygmt.print_clib_info function ([#3257](https://github.com/GenericMappingTools/pygmt/pull/3257)) +* Figure.timestamp: Remove deprecated parameter 'justification', use 'justify' instead (deprecated since v0.11.0) ([#3222](https://github.com/GenericMappingTools/pygmt/pull/3222)) + +### Bug Fixes + +* pygmt.set_display: Fix the bug that `method=None` doesn't reset to the default display method ([#3396](https://github.com/GenericMappingTools/pygmt/pull/3396)) +* load_tile_map: Register the rio accessor by importing rioxarray, so the returned raster has CRS ([#3323](https://github.com/GenericMappingTools/pygmt/pull/3323)) +* load_tile_map: Fix the raster band indexing, should start from 1 ([#3322](https://github.com/GenericMappingTools/pygmt/pull/3322)) +* load_tile_map: Replace deprecated rio.set_crs with rio.write_crs ([#3321](https://github.com/GenericMappingTools/pygmt/pull/3321)) +* PYGMT_USE_EXTERNAL_DISPLAY should NOT disable image display in Jupyter notebook environment ([#3418](https://github.com/GenericMappingTools/pygmt/pull/3418)) + +### Documentation + +* External Resources: Add tutorial in Portuguese and using PyGMT in Google Colab ([#3360](https://github.com/GenericMappingTools/pygmt/pull/3360)) +* Remove the non-official GMT wrappers from README ([#3413](https://github.com/GenericMappingTools/pygmt/pull/3413)) +* Give recommendations about GMT-Ghostscript incompatibility and the testing example ([#3249](https://github.com/GenericMappingTools/pygmt/pull/3249)) +* Document the supported 35 standard Postscript fonts in the Technical Reference section ([#3378](https://github.com/GenericMappingTools/pygmt/pull/3378)) +* Add an offboarding access checklist for maintainers ([#3411](https://github.com/GenericMappingTools/pygmt/pull/3411)) +* Update the onboarding access checklist in Maintainers Guides ([#3404](https://github.com/GenericMappingTools/pygmt/pull/3404)) +* Add sphinx extension myst-nb to enable writing executable Markdown notebooks ([#3379](https://github.com/GenericMappingTools/pygmt/pull/3379)) + +### Maintenance + +* pygmt.grd2cpt & pygmt.makecpt: Simplify the logic for dealing with CPT output ([#3334](https://github.com/GenericMappingTools/pygmt/pull/3334)) +* geopandas: Use io.StringIO to read geojson data and handle compatibility with geopandas v0.x and v1.x ([#3247](https://github.com/GenericMappingTools/pygmt/pull/3247)) +* Simplify the "Minimum Supported Versions" page using MyST customized URL schemes ([#3383](https://github.com/GenericMappingTools/pygmt/pull/3383)) +* build_arg_list: Raise an exception if an invalid output file name is given ([#3336](https://github.com/GenericMappingTools/pygmt/pull/3336)) +* sphinx-gallery: Temporarily pin to < 0.17.0 ([#3350](https://github.com/GenericMappingTools/pygmt/pull/3350)) +* Run pytest with `--color=yes` to force GitHub Actions logs to have color ([#3330](https://github.com/GenericMappingTools/pygmt/pull/3330)) +* Patch the callback print function to suppress the UnicodeDecodeError ([#3367](https://github.com/GenericMappingTools/pygmt/pull/3367)) +* Move Will from Active Maintainers to Distinguished Contributors ([#3388](https://github.com/GenericMappingTools/pygmt/pull/3388)) +* Enable ruff's unspecified-encoding (PLW1514) rule and fix violations ([#3319](https://github.com/GenericMappingTools/pygmt/pull/3319)) +* Enable ruff's literal-membership (PLR6201) rule and fix violations ([#3317](https://github.com/GenericMappingTools/pygmt/pull/3317)) +* Determine the minimum required versions of dependencies from package metadata for docs ([#3380](https://github.com/GenericMappingTools/pygmt/pull/3380)) +* CI: Use OIDC token for codecov uploading ([#3163](https://github.com/GenericMappingTools/pygmt/pull/3163)) +* CI: Test NumPy 2.1 in the GMT Tests workflow ([#3401](https://github.com/GenericMappingTools/pygmt/pull/3401)) +* CI: Set GMT_ENABLE_OPENMP to TRUE to enable OpenMP support on macOS ([#3266](https://github.com/GenericMappingTools/pygmt/pull/3266)) +* CI: Fix the name of the 'build' package to 'python-build' on conda-forge ([#3408](https://github.com/GenericMappingTools/pygmt/pull/3408)) +* CI: Bump to ubuntu-24.04 and mambaforge-23.11 in ReadTheDocs ([#3296](https://github.com/GenericMappingTools/pygmt/pull/3296)) +* CI: Build GMT dev source code with OpenMP enabled on Linux and GThreads enabled on Linux/macOS ([#3011](https://github.com/GenericMappingTools/pygmt/pull/3011)) +* CI: Add pytest plugins pytest-xdist and pytest-rerunfailures ([#3193](https://github.com/GenericMappingTools/pygmt/pull/3193), [#3267](https://github.com/GenericMappingTools/pygmt/pull/3267)) +* Add pre-commit config with pre-commit-hooks and enable pre-commit.ci to update hooks quarterly ([#3283](https://github.com/GenericMappingTools/pygmt/pull/3283), [#3414](https://github.com/GenericMappingTools/pygmt/pull/3414)) +* Add a test to make sure PyGMT works with paths that contain non-ASCII characters ([#3280](https://github.com/GenericMappingTools/pygmt/pull/3280)) + +**Full Changelog**: + +### Contributors + +* [Dongdong Tian](https://github.com/seisman) +* [Yvonne Fröhlich](https://github.com/yvonnefroehlich) +* [Wei Ji Leong](https://github.com/weiji14) +* [Michael Grund](https://github.com/michaelgrund) +* [Andre L. Belem](https://github.com/andrebelem) + +--- + ## Release v0.12.0 (2024/05/01) [![Digital Object Identifier for PyGMT v0.12.0](https://zenodo.org/badge/DOI/10.5281/zenodo.11062720.svg)](https://doi.org/10.5281/zenodo.11062720) @@ -35,6 +123,7 @@ ### Deprecations +* SPEC 0: Set minimum supported versions to Python>=3.10, pandas>=1.5 and xarray>=2022.06 ([#3043](https://github.com/GenericMappingTools/pygmt/pull/3043), [#3039](https://github.com/GenericMappingTools/pygmt/pull/3039), [#3151](https://github.com/GenericMappingTools/pygmt/pull/3151)) * Figure.plot/plot3d/rose: Remove deprecated parameter "color", use "fill" instead (deprecated since v0.8.0) ([#3032](https://github.com/GenericMappingTools/pygmt/pull/3032)) * Figure.velo: Remove deprecated parameters "color"/"uncertaintycolor", use "fill"/"uncertaintyfill" instead (deprecated since v0.8.0) ([#3034](https://github.com/GenericMappingTools/pygmt/pull/3034)) * Figure.wiggle: Remove deprecated parameter "color", use "fillpositive"/"fillnegative" instead (deprecated since v0.8.0) ([#3035](https://github.com/GenericMappingTools/pygmt/pull/3035)) @@ -68,7 +157,6 @@ * Figure.psconvert: Ignore the unrecognized "metadata" parameter added by pytest-mpl v0.17.0 ([#3054](https://github.com/GenericMappingTools/pygmt/pull/3054)) * Remote Datasets: Adjust attributes - remove "title", use default of "name" and "long_name", introduce "description" ([#3048](https://github.com/GenericMappingTools/pygmt/pull/3048)) * Adopt SPEC 0 policy and drop NEP 29 policy ([#3037](https://github.com/GenericMappingTools/pygmt/pull/3037)) -* SPEC 0: Set minimum supported versions to Python>=3.10, pandas>=1.5 and xarray>=2022.06 ([#3043](https://github.com/GenericMappingTools/pygmt/pull/3043), [#3039](https://github.com/GenericMappingTools/pygmt/pull/3039), [#3151](https://github.com/GenericMappingTools/pygmt/pull/3151)) * Document the support policy for minimum required GMT versions ([#3070](https://github.com/GenericMappingTools/pygmt/pull/3070)) * Bump to ghostscript 10.03.0 ([#3112](https://github.com/GenericMappingTools/pygmt/pull/3112)) * Bump to ruff 0.3.0 ([#3081](https://github.com/GenericMappingTools/pygmt/pull/3081)) diff --git a/doc/conf.py b/doc/conf.py index 4fbdb8aacf9..fc26641bd83 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -61,6 +61,9 @@ "requires": requirements, } +# MyST-NB configurations. +# Reference: https://myst-nb.readthedocs.io/en/latest/configuration.html +nb_render_markdown_format = "myst" # The format to use for text/markdown rendering # Make the list of returns arguments and attributes render the same as the # parameters list diff --git a/doc/maintenance.md b/doc/maintenance.md index 2029f006e5d..fb551f0e0f0 100644 --- a/doc/maintenance.md +++ b/doc/maintenance.md @@ -3,11 +3,9 @@ This page contains instructions for project maintainers about how our setup works, making releases, creating packages, etc. -If you want to make a contribution to the project, see the -[Contributing Guide](https://github.com/GenericMappingTools/pygmt/blob/main/CONTRIBUTING.md) -instead. +If you want to make a contribution to the project, see the {doc}`contributing` instead. -## Onboarding Access Checklist +## Onboarding/Offboarding Access Checklist Note that anyone can contribute to PyGMT, even without being added to the [GenericMappingTools team](https://github.com/orgs/GenericMappingTools/teams). @@ -17,25 +15,31 @@ communication tools we use. ### As a Contributor -- Added to the [pygmt-contributors team](https://github.com/orgs/GenericMappingTools/teams/pygmt-contributors) (gives 'write' permission to the repository) -- Added as a collaborator on [DAGsHub](https://dagshub.com/GenericMappingTools/pygmt/settings/collaboration) (gives 'write' permission to dvc remote storage) -- Added to the [PyGMT devs Slack channel](https://pygmtdevs.slack.com) (for casual conversations) -- Added to the {doc}`Team Gallery page ` -- Added as a member on [HackMD](https://hackmd.io/@pygmt) (for draft announcements) [optional] +- Add to the [pygmt-contributors team](https://github.com/orgs/GenericMappingTools/teams/pygmt-contributors) (gives 'write' permission to the repository) +- Add as a collaborator on [DAGsHub](https://dagshub.com/GenericMappingTools/pygmt) (gives 'write' permission to dvc remote storage) +- Add as a member on [HackMD](https://hackmd.io/@pygmt) (for draft announcements) [optional] ### As a Maintainer -- Added to the [pygmt-maintainers team](https://github.com/orgs/GenericMappingTools/teams/pygmt-maintainers) (gives 'maintain' permission to the repository) -- Update the role on the {doc}`Team Gallery page ` -- Added as a moderator on the [GMT forum](https://forum.generic-mapping-tools.org) (to see mod-only discussions) [optional] -- Added as a maintainer on [ReadtheDocs](https://readthedocs.org/projects/pygmt-dev) [optional] -- Added as a curator to the [GMT community](https://zenodo.org/communities/generic-mapping-tools/) on Zenodo (for making releases) [optional] +- Add to the [pygmt-maintainers team](https://github.com/orgs/GenericMappingTools/teams/pygmt-maintainers) (gives 'maintain' permission to the repository) +- Add to "Active Maintainers" on the {doc}`Team Gallery page ` +- Add as a moderator on the [GMT forum](https://forum.generic-mapping-tools.org) (to see mod-only discussions) [optional] +- Add as a maintainer on [ReadtheDocs](https://readthedocs.org/projects/pygmt-dev) [optional] +- Add as a curator to the [GMT community](https://zenodo.org/communities/generic-mapping-tools/) on Zenodo (for making releases) [optional] ### As an Administrator -- Added to the [pygmt-admin team](https://github.com/orgs/GenericMappingTools/teams/pygmt-admin) (gives 'admin' permission to the repository) -- Added as an admin on [DAGsHub](https://www.dagshub.com/GenericMappingTools/pygmt/settings/collaboration) -- Added as a maintainer on [PyPI](https://pypi.org/project/pygmt/) and [Test PyPI](https://test.pypi.org/project/pygmt) [optional] +- Add to the [pygmt-admin team](https://github.com/orgs/GenericMappingTools/teams/pygmt-admin) (gives 'admin' permission to the repository) +- Add as an admin on [DAGsHub](https://www.dagshub.com/GenericMappingTools/pygmt) +- Add as a maintainer on [PyPI](https://pypi.org/project/pygmt/) and [Test PyPI](https://test.pypi.org/project/pygmt) [optional] + +**Note**: When a maintainer is no longer active (no activity in one year), we will mirror +the onboarding access checklist: + +- Move from the [pygmt-maintainers team](https://github.com/orgs/GenericMappingTools/teams/pygmt-maintainers) + to the [pygmt-contributors team](https://github.com/orgs/GenericMappingTools/teams/pygmt-contributors) +- Move from "Active Maintainers" to "Distinguished Contributors" on the {doc}`Team Gallery page ` +- Remove 'maintain' permission from GMT forum, ReadTheDocs, Zenodo ## Branches diff --git a/doc/minversions.md b/doc/minversions.md index 025defe3042..888cbf28a08 100644 --- a/doc/minversions.md +++ b/doc/minversions.md @@ -30,6 +30,7 @@ after their initial release. | PyGMT Version | GMT | Python | NumPy | Pandas | Xarray | |---|---|---|---|---|---| | [Dev][]* [] | {{ requires.gmt }} | {{ requires.python }} | {{ requires.numpy }} | {{ requires.pandas }} | {{ requires.xarray }} | +| [] | >=6.3.0 | >=3.10 | >=1.24 | >=1.5 | >=2022.09 | | [] | >=6.3.0 | >=3.10 | >=1.23 | >=1.5 | >=2022.06 | | [] | >=6.3.0 | >=3.9 | >=1.23 | | | | [] | >=6.3.0 | >=3.9 | >=1.22 | | | diff --git a/doc/techref/encodings.md b/doc/techref/encodings.md index f96d0163deb..18f0a80f20b 100644 --- a/doc/techref/encodings.md +++ b/doc/techref/encodings.md @@ -1,130 +1,94 @@ +--- +file_format: mystnb +--- + +```{code-cell} +--- +tags: [remove-input] +--- +from IPython.display import display, Markdown +from pygmt.encodings import charset + + +def get_charset_mdtable(name): + """ + Create a markdown table for a charset. + """ + mappings = charset[name] + + text = "| Octal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |\n" + text += "|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n" + for i in range(0o00, 0o400, 8): + chars = [mappings.get(j) for j in range(i, i + 8)] + if all(v is None for v in chars): # All characters in this row are undefined + continue + row = f"\\{i:03o}"[:-1] + "x" + chars = [f"&#x{ord(char):04x};" for char in chars] + text += f"| **{row}** | {' | '.join(chars)} |\n" + text += "\n" + return Markdown(text) +``` + # Supported Encodings and Non-ASCII Characters -GMT supports a number of encodings and each encoding contains a set of ASCII and +PyGMT supports a number of encodings and each encoding contains a set of ASCII and non-ASCII characters. In PyGMT, you can use any of these ASCII and non-ASCII characters in arguments and text strings. When using non-ASCII characters in PyGMT, the easiest way is to copy and paste the character from the encoding tables below. **Note**: The special character � (REPLACEMENT CHARACTER) is used to indicate -that the character is not defined in the encoding. +that the character is undefined in the encoding. ## Adobe ISOLatin1+ Encoding -| octal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -|---|---|---|---|---|---|---|---|---| -| **\03x** | � | • | … | ™ | — | – | fi | ž | -| **\04x** | | ! | " | # | $ | % | & | ’ | -| **\05x** | ( | ) | * | + | , | - | . | / | -| **\06x** | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -| **\07x** | 8 | 9 | : | ; | < | = | > | ? | -| **\10x** | @ | A | B | C | D | E | F | G | -| **\11x** | H | I | J | K | L | M | N | O | -| **\12x** | P | Q | R | S | T | U | V | W | -| **\13x** | X | Y | Z | [ | \ | ] | ^ | _ | -| **\14x** | ‘ | a | b | c | d | e | f | g | -| **\15x** | h | i | j | k | l | m | n | o | -| **\16x** | p | q | r | s | t | u | v | w | -| **\17x** | x | y | z | { | | | } | ~ | š | -| **\20x** | Œ | † | ‡ | Ł | ⁄ | ‹ | Š | › | -| **\21x** | œ | Ÿ | Ž | ł | ‰ | „ | “ | ” | -| **\22x** | ı | ` | ´ | ^ | ˜ | ¯ | ˘ | ˙ | -| **\23x** | ¨ | ‚ | ˚ | ¸ | ' | ˝ | ˛ | ˇ | -| **\24x** | � | ¡ | ¢ | £ | ¤ | ¥ | ¦ | § | -| **\25x** | ¨ | © | ª | « | ¬ | ­ | ® | ¯ | -| **\26x** | ° | ± | ² | ³ | ´ | µ | ¶ | · | -| **\27x** | ¸ | ¹ | º | » | ¼ | ½ | ¾ | ¿ | -| **\30x** | À | Á |  | à | Ä | Å | Æ | Ç | -| **\31x** | È | É | Ê | Ë | Ì | Í | Î | Ï | -| **\32x** | Ð | Ñ | Ò | Ó | Ô | Õ | Ö | × | -| **\33x** | Ø | Ù | Ú | Û | Ü | Ý | Þ | ß | -| **\34x** | à | á | â | ã | ä | å | æ | ç | -| **\35x** | è | é | ê | ë | ì | í | î | ï | -| **\36x** | ð | ñ | ò | ó | ô | õ | ö | ÷ | -| **\37x** | ø | ù | ú | û | ü | ý | þ | ÿ | +```{code-cell} +--- +tags: [remove-input] +--- +display(get_charset_mdtable("ISOLatin1+")) +``` ## Adobe Symbol Encoding -| octal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -|---|---|---|---|---|---|---|---|---| -| **\04x** | | ! | ∀ | # | ∃ | % | & | ∋ | -| **\05x** | ( | ) | ∗ | + | , | − | . | / | -| **\06x** | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -| **\07x** | 8 | 9 | : | ; | < | = | > | ? | -| **\10x** | ≅ | Α | Β | Χ | ∆ | Ε | Φ | Γ | -| **\11x** | Η | Ι | ϑ | Κ | Λ | Μ | Ν | Ο | -| **\12x** | Π | Θ | Ρ | Σ | Τ | Υ | ς | Ω | -| **\13x** | Ξ | Ψ | Ζ | [ | ∴ | ] | ⊥ | _ | -| **\14x** |  | α | β | χ | δ | ε | φ | γ | -| **\15x** | η | ι | ϕ | κ | λ | μ | ν | ο | -| **\16x** | π | θ | ρ | σ | τ | υ | ϖ | ω | -| **\17x** | ξ | ψ | ζ | { | | | } | ∼ | � | -| **\24x** | € | ϒ | ′ | ≤ | ∕ | ∞ | ƒ | ♣ | -| **\25x** | ♦ | ♥ | ♠ | ↔ | ← | ↑ | → | ↓ | -| **\26x** | ° | ± | ″ | ≥ | × | ∝ | ∂ | • | -| **\27x** | ÷ | ≠ | ≡ | ≈ | … | ⏐ | ⎯ | ↵ | -| **\30x** | ℵ | ℑ | ℜ | ℘ | ⊗ | ⊕ | ∅ | ∩ | -| **\31x** | ∪ | ⊃ | ⊇ | ⊄ | ⊂ | ⊆ | ∈ | ∉ | -| **\32x** | ∠ | ∇ | ® | © | ™ | ∏ | √ | ⋅ | -| **\33x** | ¬ | ∧ | ∨ | ⇔ | ⇐ | ⇑ | ⇒ | ⇓ | -| **\34x** | ◊ | 〈 | ® | © | ™ | ∑ | ⎛ | ⎜ | -| **\35x** | ⎝ | ⎡ | ⎢ | ⎣ | ⎧ | ⎨ | ⎩ | ⎪ | -| **\36x** | � | 〉 | ∫ | ⌠ | ⎮ | ⌡ | ⎞ | ⎟ | -| **\37x** | ⎠ | ⎤ | ⎥ | ⎦ | ⎫ | ⎬ | ⎭ | � | - -**Note**: The octal code `\140` represents the RADICAL EXTENDER character, which is not available in -the Unicode character set. +```{code-cell} +--- +tags: [remove-input] +--- +display(get_charset_mdtable("Symbol")) +``` + +**Note**: The octal code `\140` represents the RADICAL EXTENDER character, which is not +available in the Unicode character set. ## Adobe ZapfDingbats Encoding -| octal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -|---|---|---|---|---|---|---|---|---| -| **\04x** | | ✁ | ✂ | ✃ | ✄ | ☎ | ✆ | ✇ | -| **\05x** | ✈ | ✉ | ☛ | ☞ | ✌ | ✍ | ✎ | ✏ | -| **\06x** | ✐ | ✑ | ✒ | ✓ | ✔ | ✕ | ✖ | ✗ | -| **\07x** | ✘ | ✙ | ✚ | ✛ | ✜ | ✝ | ✞ | ✟ | -| **\10x** | ✠ | ✡ | ✢ | ✣ | ✤ | ✥ | ✦ | ✧ | -| **\11x** | ★ | ✩ | ✪ | ✫ | ✬ | ✭ | ✮ | ✯ | -| **\12x** | ✰ | ✱ | ✲ | ✳ | ✴ | ✵ | ✶ | ✷ | -| **\13x** | ✸ | ✹ | ✺ | ✻ | ✼ | ✽ | ✾ | ✿ | -| **\14x** | ❀ | ❁ | ❂ | ❃ | ❄ | ❅ | ❆ | ❇ | -| **\15x** | ❈ | ❉ | ❊ | ❋ | ● | ❍ | ■ | ❏ | -| **\16x** | ❐ | ❑ | ❒ | ▲ | ▼ | ◆ | ❖ | ◗ | -| **\17x** | ❘ | ❙ | ❚ | ❛ | ❜ | ❝ | ❞ | � | -| **\20x** | ❨ | ❩ | ❪ | ❫ | ❬ | ❭ | ❮ | ❯ | -| **\21x** | ❰ | ❱ | ❲ | ❳ | ❴ | ❵ | � | � | -| **\24x** | � | ❡ | ❢ | ❣ | ❤ | ❥ | ❦ | ❧ | -| **\25x** | ♣ | ♦ | ♥ | ♠ | ① | ② | ③ | ④ | -| **\26x** | ⑤ | ⑥ | ⑦ | ⑧ | ⑨ | ⑩ | ❶ | ❷ | -| **\27x** | ❸ | ❹ | ❺ | ❻ | ❼ | ❽ | ❾ | ❿ | -| **\30x** | ➀ | ➁ | ➂ | ➃ | ➄ | ➅ | ➆ | ➇ | -| **\31x** | ➈ | ➉ | ➊ | ➋ | ➌ | ➍ | ➎ | ➏ | -| **\32x** | ➐ | ➑ | ➒ | ➓ | ➔ | → | ↔ | ↕ | -| **\33x** | ➘ | ➙ | ➚ | ➛ | ➜ | ➝ | ➞ | ➟ | -| **\34x** | ➠ | ➡ | ➢ | ➣ | ➤ | ➥ | ➦ | ➧ | -| **\35x** | ➨ | ➩ | ➪ | ➫ | ➬ | ➭ | ➮ | ➯ | -| **\36x** | � | ➱ | ➲ | ➳ | ➴ | ➵ | ➶ | ➷ | -| **\37x** | ➸ | ➹ | ➺ | ➻ | ➼ | ➽ | ➾ | � | +```{code-cell} +--- +tags: [remove-input] +--- +display(get_charset_mdtable("ZapfDingbats")) +``` ## ISO/IEC 8859 PyGMT also supports the ISO/IEC 8859 standard for 8-bit character encodings. Refer to - for descriptions of the different parts of -the standard. +[ISO/IEC 8859](https://en.wikipedia.org/wiki/ISO/IEC_8859) for descriptions of the +different parts of the standard. For a list of the characters in each part of the standard, refer to the following links: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +- [ISO/IEC 8859-1](https://en.wikipedia.org/wiki/ISO/IEC_8859-1) +- [ISO/IEC 8859-2](https://en.wikipedia.org/wiki/ISO/IEC_8859-2) +- [ISO/IEC 8859-3](https://en.wikipedia.org/wiki/ISO/IEC_8859-3) +- [ISO/IEC 8859-4](https://en.wikipedia.org/wiki/ISO/IEC_8859-4) +- [ISO/IEC 8859-5](https://en.wikipedia.org/wiki/ISO/IEC_8859-5) +- [ISO/IEC 8859-6](https://en.wikipedia.org/wiki/ISO/IEC_8859-6) +- [ISO/IEC 8859-7](https://en.wikipedia.org/wiki/ISO/IEC_8859-7) +- [ISO/IEC 8859-8](https://en.wikipedia.org/wiki/ISO/IEC_8859-8) +- [ISO/IEC 8859-9](https://en.wikipedia.org/wiki/ISO/IEC_8859-9) +- [ISO/IEC 8859-10](https://en.wikipedia.org/wiki/ISO/IEC_8859-10) +- [ISO/IEC 8859-11](https://en.wikipedia.org/wiki/ISO/IEC_8859-11) +- [ISO/IEC 8859-13](https://en.wikipedia.org/wiki/ISO/IEC_8859-13) +- [ISO/IEC 8859-14](https://en.wikipedia.org/wiki/ISO/IEC_8859-14) +- [ISO/IEC 8859-15](https://en.wikipedia.org/wiki/ISO/IEC_8859-15) +- [ISO/IEC 8859-16](https://en.wikipedia.org/wiki/ISO/IEC_8859-16) diff --git a/doc/techref/fonts.md b/doc/techref/fonts.md index eaec7768c26..0d10a911c09 100644 --- a/doc/techref/fonts.md +++ b/doc/techref/fonts.md @@ -1,3 +1,7 @@ +--- +file_format: mystnb +--- + # Supported Fonts PyGMT supports the 35 standard PostScript fonts. The table below lists them with their @@ -7,25 +11,104 @@ either `"Helvetica"` or `"0"`. For the special fonts "Symbol" (**12**) and "ZapfDingbats" (**34**), see the {doc}`/techref/encodings` for the character set. The image below the table shows a visual sample for each font. -| Font No. | Font Name | Font No. | Font Name | -|----------|------------------------|----------|------------------------------| -| 0 | Helvetica | 17 | Bookman-Demi | -| 1 | Helvetica-Bold | 18 | Bookman-DemiItalic | -| 2 | Helvetica-Oblique | 19 | Bookman-Light | -| 3 | Helvetica-BoldOblique | 20 | Bookman-LightItalic | -| 4 | Times-Roman | 21 | Helvetica-Narrow | -| 5 | Times-Bold | 22 | Helvetica-Narrow-Bold | -| 6 | Times-Italic | 23 | Helvetica-Narrow-Oblique | -| 7 | Times-BoldItalic | 24 | Helvetica-Narrow-BoldOblique | -| 8 | Courier | 25 | NewCenturySchlbk-Roman | -| 9 | Courier-Bold | 26 | NewCenturySchlbk-Italic | -| 10 | Courier-Oblique | 27 | NewCenturySchlbk-Bold | -| 11 | Courier-BoldOblique | 28 | NewCenturySchlbk-BoldItalic | -| 12 | Symbol | 29 | Palatino-Roman | -| 13 | AvantGarde-Book | 30 | Palatino-Italic | -| 14 | AvantGarde-BookOblique | 31 | Palatino-Bold | -| 15 | AvantGarde-Demi | 32 | Palatino-BoldItalic | -| 16 | AvantGarde-DemiOblique | 33 | ZapfChancery-MediumItalic | -| | | 34 | ZapfDingbats | - -![Standard PostScript Fonts](https://docs.generic-mapping-tools.org/dev/_images/GMT_App_G.png){width="67.5%"} +```{code-cell} +--- +tags: [remove-input] +--- +from IPython.display import display, Markdown + +fonts = [ + "Helvetica", + "Helvetica-Bold", + "Helvetica-Oblique", + "Helvetica-BoldOblique", + "Times-Roman", + "Times-Bold", + "Times-Italic", + "Times-BoldItalic", + "Courier", + "Courier-Bold", + "Courier-Oblique", + "Courier-BoldOblique", + "Symbol", + "AvantGarde-Book", + "AvantGarde-BookOblique", + "AvantGarde-Demi", + "AvantGarde-DemiOblique", + "Bookman-Demi", + "Bookman-DemiItalic", + "Bookman-Light", + "Bookman-LightItalic", + "Helvetica-Narrow", + "Helvetica-Narrow-Bold", + "Helvetica-Narrow-Oblique", + "Helvetica-Narrow-BoldOblique", + "NewCenturySchlbk-Roman", + "NewCenturySchlbk-Italic", + "NewCenturySchlbk-Bold", + "NewCenturySchlbk-BoldItalic", + "Palatino-Roman", + "Palatino-Italic", + "Palatino-Bold", + "Palatino-BoldItalic", + "ZapfChancery-MediumItalic", + "ZapfDingbats", +] + +text = "| Font No. | Font Name | Font No. | Font Name |\n" +text += "|:---:|:---|:---:|:---|\n" +for i in range(17): + j = i + 17 + text += f"| {i} | {fonts[i]} | {j} | {fonts[j]} |\n" +text += f"| | | 34 | {fonts[34]} |\n" + +display(Markdown(text)) +``` + +```{code-cell} +--- +tags: [remove-input] +--- +""" +Script to generate visual samples of the fonts. +""" +import pygmt + +x1, x2, dx = 0, 7, 0.75 + +fig = pygmt.Figure() +# Draw the table +fig.basemap(region=[-0.5, 14, -1.5, 18], projection="X14c/-10c", frame=0) +fig.plot(x=[-0.5, 14], y=[-0.5, -0.5]) +for x in (0.5, 6.5, 7.5): + fig.plot(x=[x, x], y=[-1.5, 18]) +# Table header +fig.text( + x=[x1, x1 + dx, x2, x2 + dx], + y=[-1] * 4, + text=["#", "Font Name"] * 2, + justify=["MC", "ML"] * 2, + font="Helvetica-Bold", +) +# Fonts +for i, font in enumerate(fonts): + x0 = x1 if i < 17 else x2 + y0 = i % 17 + font_no, font_name = i, font + + # Deal with special cases + if font in ["Symbol", "ZapfDingbats"]: + font_name = f"{font} @%0%({font})@%%" + if font == "ZapfDingbats": + font_no = "@%0%34@%%" + y0 = 17 + + fig.text( + x=[x0, x0 + dx], + y=[y0] * 2, + text=[font_no, font_name], + justify=["MC", "ML"], + font=font, + ) +fig.show(width=600) +``` diff --git a/doc/techref/projections.md b/doc/techref/projections.md index 4f41daf38f9..ac28f305847 100644 --- a/doc/techref/projections.md +++ b/doc/techref/projections.md @@ -16,7 +16,7 @@ The table below shows the projection codes for the 31 GMT map projections: | PyGMT Projection Argument | Projection Name | | --- | --- | -| **A**{{ lon0 }}/{{ lat0 }}[/*horizon*]/*width* | {doc}`Lambert azimuthal equal area ` | +| **A**{{ lon0 }}/{{ lat0 }}[/*horizon*]/*width* | {doc}`Lambert azimuthal equal area ` | | **B**{{ lon0 }}/{{ lat0 }}/{{ lat1 }}/{{ lat2 }}/*width* | {doc}`Albers conic equal area ` | | **C**{{ lon0 }}/{{ lat0 }}/*width* | {doc}`Cassini cylindrical ` | | **Cyl_stere**/[{{ lon0 }}/[{{ lat0 }}/]]*width* | {doc}`Cylindrical stereographic ` | diff --git a/environment.yml b/environment.yml index 3d66bce5a5b..3b8d97b8ca8 100644 --- a/environment.yml +++ b/environment.yml @@ -6,7 +6,7 @@ dependencies: - python=3.12 # Required dependencies - gmt=6.5.0 - - ghostscript=10.03.1 + - ghostscript=10.04.0 - numpy>=1.24 - pandas>=1.5 - xarray>=2022.09 @@ -18,11 +18,11 @@ dependencies: - ipython - rioxarray # Development dependencies (general) - - build - dvc - jupyter - make - pip + - python-build # Dev dependencies (style checks) - codespell - pre-commit @@ -40,7 +40,7 @@ dependencies: - sphinx-autodoc-typehints - sphinx-copybutton - sphinx-design - - sphinx-gallery + - sphinx-gallery>=0.17.0 - sphinx_rtd_theme # Dev dependencies (type hints) - mypy diff --git a/examples/gallery/3d_plots/README.txt b/examples/gallery/3d_plots/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/3d_plots/README.txt rename to examples/gallery/3d_plots/GALLERY_HEADER.rst diff --git a/examples/gallery/README.txt b/examples/gallery/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/README.txt rename to examples/gallery/GALLERY_HEADER.rst diff --git a/examples/gallery/basemaps/README.txt b/examples/gallery/basemaps/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/basemaps/README.txt rename to examples/gallery/basemaps/GALLERY_HEADER.rst diff --git a/examples/gallery/basemaps/ternary.py b/examples/gallery/basemaps/ternary.py index 47edad5ec56..8ad9031f406 100644 --- a/examples/gallery/basemaps/ternary.py +++ b/examples/gallery/basemaps/ternary.py @@ -4,7 +4,7 @@ The :meth:`pygmt.Figure.ternary` method can draw ternary diagrams. The example shows how to plot circles with a diameter of 0.1 centimeters -(``style="c0.1c"``) on a 10-centimeter-wide (``width="10c"``) ternary diagram +(``style="c0.1c"``) on a 10-centimeters-wide (``width="10c"``) ternary diagram at the positions listed in the first three columns of the sample dataset ``rock_compositions``, with default annotations and gridline spacings, using the specified labeling defined via ``alabel``, ``blabel``, and ``clabel``. @@ -36,7 +36,7 @@ frame=[ "aafg+lLimestone component+u %", "bafg+lWater component+u %", - "cagf+lAir component+u %", + "cafg+lAir component+u %", ], ) diff --git a/examples/gallery/embellishments/README.txt b/examples/gallery/embellishments/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/embellishments/README.txt rename to examples/gallery/embellishments/GALLERY_HEADER.rst diff --git a/examples/gallery/embellishments/colorbar.py b/examples/gallery/embellishments/colorbar.py index aad985911c0..84010591e4a 100644 --- a/examples/gallery/embellishments/colorbar.py +++ b/examples/gallery/embellishments/colorbar.py @@ -21,8 +21,8 @@ from anchor point. - **n**: using normalized (0-1) coordinates, e.g. ``position="n0.4/0.8"``. -Note that the anchor point defaults to the bottom left (**BL**). Append ``+h`` -to ``position`` to get a horizontal colorbar instead of a vertical one (``+v``). +Note that the anchor point defaults to Bottom Left (**BL**). Append ``+h`` to +``position`` to get a horizontal colorbar instead of a vertical one (``+v``). """ # %% diff --git a/examples/gallery/histograms/README.txt b/examples/gallery/histograms/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/histograms/README.txt rename to examples/gallery/histograms/GALLERY_HEADER.rst diff --git a/examples/gallery/images/README.txt b/examples/gallery/images/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/images/README.txt rename to examples/gallery/images/GALLERY_HEADER.rst diff --git a/examples/gallery/lines/README.txt b/examples/gallery/lines/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/lines/README.txt rename to examples/gallery/lines/GALLERY_HEADER.rst diff --git a/examples/gallery/lines/vector_heads_tails.py b/examples/gallery/lines/vector_heads_tails.py index 401f6222952..58f61a7dbbf 100644 --- a/examples/gallery/lines/vector_heads_tails.py +++ b/examples/gallery/lines/vector_heads_tails.py @@ -24,7 +24,7 @@ In the following we use the :meth:`pygmt.Figure.plot` method to plot vectors with individual heads and tails. We must specify the modifiers (together with -the vector type, here ``"v"`` for Cartesian vector, see also +the vector type, here ``"v"`` for Cartesian vector, see also the :doc:`Vector types example `) by passing the corresponding shortcuts to the ``style`` parameter. diff --git a/examples/gallery/lines/vector_styles.py b/examples/gallery/lines/vector_styles.py index 726bfabd642..a1882df105b 100644 --- a/examples/gallery/lines/vector_styles.py +++ b/examples/gallery/lines/vector_styles.py @@ -2,16 +2,16 @@ Cartesian, circular, and geographic vectors =========================================== -The :meth:`pygmt.Figure.plot` method can plot Cartesian, circular, and -geographic vectors. The ``style`` parameter controls vector attributes. -See also :doc:`Vector attributes example `. +The :meth:`pygmt.Figure.plot` method can plot Cartesian, circular, and geographic +vectors. The ``style`` parameter controls vector attributes. See also the +:doc:`Vector attributes example `. """ # %% import numpy as np import pygmt -# create a plot with coast, Mercator projection (M) over the continental US +# Create a plot with coast, Mercator projection (M) over the continental US fig = pygmt.Figure() fig.coast( region=[-127, -64, 24, 53], @@ -28,8 +28,8 @@ y = np.linspace(33.5, 42.5, 12) # y vector coordinates direction = np.zeros(x.shape) # direction of vectors length = np.linspace(0.5, 2.4, 12) # length of vectors -# Cartesian vectors (v) with red fill and pen (+g, +p), vector head at -# end (+e), and 40 degree angle (+a) with no indentation for vector head (+h) +# Cartesian vectors (v) with red fill and pen (+g, +p), vector head at the end (+e), and +# 40 degree angle (+a) with no indentation for the vector head (+h) style = "v0.2c+e+a40+gred+h0+p1p,red" fig.plot(x=x, y=y, style=style, pen="1p,red", direction=[direction, length]) fig.text(text="CARTESIAN", x=-112, y=44.2, font="13p,Helvetica-Bold,red", fill="white") @@ -44,7 +44,7 @@ stopdir = 180 + 40 * np.arange(0, num) # stop direction in degrees # data for circular vectors data = np.column_stack([x, y, radius, startdir, stopdir]) -arcstyle = "m0.5c+ea" # Circular vector (m) with an arrow at end +arcstyle = "m0.5c+ea" # Circular vector (m) with an arrow at the end fig.plot(data=data, style=arcstyle, fill="red3", pen="1.5p,black") fig.text(text="CIRCULAR", x=-95, y=44.2, font="13p,Helvetica-Bold,black", fill="white") @@ -54,9 +54,8 @@ CHI = [-87.6298, 41.8781] SEA = [-122.3321, 47.6062] NO = [-90.0715, 29.9511] -# `=` means geographic vectors. -# With the modifier '+s', the input data should contain coordinates of start -# and end points +# '=' means geographic vectors. With the modifier '+s', the input data should contain +# coordinates of start and end points style = "=0.5c+s+e+a30+gblue+h0.5+p1p,blue" data = np.array([NYC + CHI, NYC + SEA, NYC + NO]) fig.plot(data=data, style=style, pen="1.0p,blue") diff --git a/examples/gallery/maps/README.txt b/examples/gallery/maps/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/maps/README.txt rename to examples/gallery/maps/GALLERY_HEADER.rst diff --git a/examples/gallery/seismology/README.txt b/examples/gallery/seismology/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/seismology/README.txt rename to examples/gallery/seismology/GALLERY_HEADER.rst diff --git a/examples/gallery/symbols/README.txt b/examples/gallery/symbols/GALLERY_HEADER.rst similarity index 100% rename from examples/gallery/symbols/README.txt rename to examples/gallery/symbols/GALLERY_HEADER.rst diff --git a/examples/get_started/README.txt b/examples/get_started/GALLERY_HEADER.rst similarity index 100% rename from examples/get_started/README.txt rename to examples/get_started/GALLERY_HEADER.rst diff --git a/examples/projections/README.txt b/examples/projections/GALLERY_HEADER.rst similarity index 100% rename from examples/projections/README.txt rename to examples/projections/GALLERY_HEADER.rst diff --git a/examples/projections/azim/README.txt b/examples/projections/azim/GALLERY_HEADER.rst similarity index 100% rename from examples/projections/azim/README.txt rename to examples/projections/azim/GALLERY_HEADER.rst diff --git a/examples/projections/conic/README.txt b/examples/projections/conic/GALLERY_HEADER.rst similarity index 100% rename from examples/projections/conic/README.txt rename to examples/projections/conic/GALLERY_HEADER.rst diff --git a/examples/projections/cyl/README.txt b/examples/projections/cyl/GALLERY_HEADER.rst similarity index 100% rename from examples/projections/cyl/README.txt rename to examples/projections/cyl/GALLERY_HEADER.rst diff --git a/examples/projections/misc/README.txt b/examples/projections/misc/GALLERY_HEADER.rst similarity index 100% rename from examples/projections/misc/README.txt rename to examples/projections/misc/GALLERY_HEADER.rst diff --git a/examples/projections/nongeo/README.txt b/examples/projections/nongeo/GALLERY_HEADER.rst similarity index 100% rename from examples/projections/nongeo/README.txt rename to examples/projections/nongeo/GALLERY_HEADER.rst diff --git a/examples/tutorials/README.txt b/examples/tutorials/GALLERY_HEADER.rst similarity index 100% rename from examples/tutorials/README.txt rename to examples/tutorials/GALLERY_HEADER.rst diff --git a/examples/tutorials/advanced/README.txt b/examples/tutorials/advanced/GALLERY_HEADER.rst similarity index 100% rename from examples/tutorials/advanced/README.txt rename to examples/tutorials/advanced/GALLERY_HEADER.rst diff --git a/examples/tutorials/advanced/draping_on_3d_surface.py b/examples/tutorials/advanced/draping_on_3d_surface.py new file mode 100644 index 00000000000..5a91985b8bc --- /dev/null +++ b/examples/tutorials/advanced/draping_on_3d_surface.py @@ -0,0 +1,137 @@ +""" +Draping a dataset on top of a topographic surface +================================================== + +It can be visually appealing to "drape" a dataset over a topographic surface. This can +be accomplished using the ``drapegrid`` parameter of :meth:`pygmt.Figure.grdview`. + +This tutorial consists of two examples: + +1. Draping a grid + +2. Draping an image +""" + +# %% + +# Load the required packages +import pygmt +import rasterio +import xarray as xr + +# %% +# 1. Drapping a grid +# ------------------ +# +# In the first example, the seafloor crustal age is plotted with color-coding on top of +# the topographic map of an area of the Mid-Atlantic Ridge. + +# Define the study area in degrees East or North +region_2d = [-50, 0, 36, 70] # [lon_min, lon_max, lat_min, lat_max] + +# Download elevation and crustal age grids for the study region with a resolution of 10 +# arc-minutes and load them into xarray.DataArrays +grd_relief = pygmt.datasets.load_earth_relief(resolution="10m", region=region_2d) +grd_age = pygmt.datasets.load_earth_age(resolution="10m", region=region_2d) + +# Determine the 3-D region from the minimum and maximum values of the relief grid +region_3d = [*region_2d, grd_relief.min().to_numpy(), grd_relief.max().to_numpy()] + +# %% +# The topographic surface is created based on the grid passed to the ``grid`` parameter +# of :meth:`pygmt.Figure.grdview`; here we use a grid of the Earth relief. To add a +# color-coding based on *another* grid we have to pass a second grid to the +# ``drapegrid`` parameter; here we use a grid of the crustal age. In this case the +# colormap specified via the ``cmap`` parameter applies to the grid passed to +# ``drapegrid``, not to ``grid``. The azimuth and elevation of the 3-D plot are set via +# the ``perspective`` parameter. + +fig = pygmt.Figure() + +# Set up colormap for the crustal age +pygmt.config(COLOR_NAN="lightgray") +pygmt.makecpt(cmap="batlow", series=[0, 200, 1], reverse=True, overrule_bg=True) + +fig.grdview( + projection="M12c", # Mercator projection with a width of 12 centimeters + region=region_3d, + grid=grd_relief, # Use elevation grid for z values + drapegrid=grd_age, # Use crustal age grid for color-coding + cmap=True, # Use colormap created for the crustal age + surftype="i", # Create an image plot + # Use an illumination from the azimuthal directions 0° (north) and 270° + # (west) with a normalization via a cumulative Laplace distribution for + # the shading + shading="+a0/270+ne0.6", + perspective=[157.5, 30], # Azimuth and elevation for the 3-D plot + zsize="1.5c", + plane="+gdarkgray", + frame=True, +) + +# Add colorbar for the crustal age +fig.colorbar(frame=["x+lseafloor crustal age", "y+lMyr"], position="+n") + +fig.show() + + +# %% +# 2. Draping an image +# ------------------- +# +# In the second example, the flag of the European Union (EU) is plotted on top of a +# topographic map of northwest Europe. This example is modified from +# :gmt-docs:`GMT example 32 `. +# We have to consider the dimension of the image we want to drap. The image we will +# download in this example has 1000 x 667 pixels, i.e. an aspect ratio of 3 x 2. + +# Define the study area in degrees East or North, with an extend of 6 degrees for +# the longitude and 4 degrees for the latitude +region_2d = [3, 9, 50, 54] # [lon_min, lon_max, lat_min, lat_max] + +# Download elevation grid for the study region with a resolution of 30 arc-seconds and +# pixel registration and load it into an xarray.DataArray +grd_relief = pygmt.datasets.load_earth_relief(resolution="30s", region=region_2d) + +# Determine the 3-D region from the minimum and maximum values of the relief grid +region_3d = [*region_2d, grd_relief.min().to_numpy(), grd_relief.max().to_numpy()] + +# Download an PNG image of the flag of the EU using rasterio and load it into a +# xarray.DataArray +url_to_image = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Flag_of_Europe.svg/1000px-Flag_of_Europe.svg.png" +with rasterio.open(url_to_image) as dataset: + data = dataset.read() + drapegrid = xr.DataArray(data, dims=("band", "y", "x")) + +# %% +# Again we create a 3-D plot with :meth:`pygmt.Figure.grdview` and pass an Earth relief +# grid to the ``grid`` parameter to create the topographic surface. But now we pass the +# PNG image which was loaded into an :class:`xarray.DataArray` to the ``drapgrid`` +# parameter. + +fig = pygmt.Figure() + +# Set up a colormap with two colors for the EU flag: blue (0/51/153) for the background +# (value 0 in the nedCDF file -> lower half of 0-255 range) and yellow (255/204/0) for +# the stars (value 255 -> upper half) +pygmt.makecpt(cmap="0/51/153,255/204/0", series=[0, 256, 128]) + +fig.grdview( + projection="M12c", # Mercator projection with a width of 12 centimeters + region=region_3d, + grid=grd_relief, # Use elevation grid for z values + drapegrid=drapegrid, # Drap image grid for the EU flag on top + cmap=True, # Use colormap defined for the EU flag + surftype="i", # Create an image plot + # Use an illumination from the azimuthal directions 0° (north) and 270° (west) with + # a normalization via a cumulative Laplace distribution for the shading + shading="+a0/270+ne0.6", + perspective=[157.5, 30], # Define azimuth, elevation for the 3-D plot + zsize="1c", + plane="+gdarkgray", + frame=True, +) + +fig.show() + +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/tutorials/advanced/non_ascii_text.py b/examples/tutorials/advanced/non_ascii_text.py index d51a8084281..fb88c50e46f 100644 --- a/examples/tutorials/advanced/non_ascii_text.py +++ b/examples/tutorials/advanced/non_ascii_text.py @@ -32,14 +32,14 @@ ) fig.text( - x=[0.2, 0.2, 0.2, 0.2, 0.2], + x=[0.2] * 5, y=[5, 4, 3, 2, 1], text=["ASCII:", "ISOLatin1+:", "Symbol:", "ZapfDingbats:", "Mixed:"], font="20p,Helvetica-Bold,red", justify="LM", ) fig.text( - x=[2, 2, 2, 2, 2], + x=[2] * 5, y=[5, 4, 3, 2, 1], text=[ "ABCDE12345!#$:;<=>?", # ASCII only diff --git a/examples/tutorials/basics/README.txt b/examples/tutorials/basics/GALLERY_HEADER.rst similarity index 100% rename from examples/tutorials/basics/README.txt rename to examples/tutorials/basics/GALLERY_HEADER.rst diff --git a/pygmt/_show_versions.py b/pygmt/_show_versions.py index b27033a0ed2..6371c3ea309 100644 --- a/pygmt/_show_versions.py +++ b/pygmt/_show_versions.py @@ -52,9 +52,7 @@ def _get_ghostscript_version() -> str | None: Get Ghostscript version. """ match sys.platform: - case "linux" | "darwin": - cmds = ["gs"] - case os_name if os_name.startswith("freebsd"): + case name if name in {"linux", "darwin"} or name.startswith("freebsd"): cmds = ["gs"] case "win32": cmds = ["gswin64c.exe", "gswin32c.exe"] diff --git a/pygmt/clib/loading.py b/pygmt/clib/loading.py index 9b785fe826e..60541c7186d 100644 --- a/pygmt/clib/loading.py +++ b/pygmt/clib/loading.py @@ -111,14 +111,12 @@ def clib_names(os_name: str) -> list[str]: If the operating system is not supported yet. """ match os_name: - case "linux": # Linux + case name if name == "linux" or name.startswith("freebsd"): # Linux or FreeBSD libnames = ["libgmt.so"] case "darwin": # macOS libnames = ["libgmt.dylib"] case "win32": # Windows libnames = ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"] - case name if name.startswith("freebsd"): # FreeBSD - libnames = ["libgmt.so"] case _: raise GMTOSError(f"Operating system '{os_name}' is not supported.") return libnames diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 8daee550421..395a60a025a 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -7,6 +7,7 @@ import contextlib import ctypes as ctp +import io import pathlib import sys import warnings @@ -60,6 +61,7 @@ "GMT_IS_PLP", # items could be any one of POINT, LINE, or POLY "GMT_IS_SURFACE", # items are 2-D grid "GMT_IS_VOLUME", # items are 3-D grid + "GMT_IS_TEXT", # Text strings which triggers ASCII text reading ] METHODS = [ @@ -70,6 +72,11 @@ DIRECTIONS = ["GMT_IN", "GMT_OUT"] MODES = ["GMT_CONTAINER_ONLY", "GMT_IS_OUTPUT"] +MODE_MODIFIERS = [ + "GMT_GRID_IS_CARTESIAN", + "GMT_GRID_IS_GEO", + "GMT_WITH_STRINGS", +] REGISTRATIONS = ["GMT_GRID_PIXEL_REG", "GMT_GRID_NODE_REG"] @@ -728,7 +735,7 @@ def create_data( mode_int = self._parse_constant( mode, valid=MODES, - valid_modifiers=["GMT_GRID_IS_CARTESIAN", "GMT_GRID_IS_GEO"], + valid_modifiers=MODE_MODIFIERS, ) geometry_int = self._parse_constant(geometry, valid=GEOMETRIES) registration_int = self._parse_constant(registration, valid=REGISTRATIONS) @@ -1604,6 +1611,100 @@ def virtualfile_from_grid(self, grid): with self.open_virtualfile(*args) as vfile: yield vfile + @contextlib.contextmanager + def virtualfile_from_stringio(self, stringio: io.StringIO): + r""" + Store a :class:`io.StringIO` object in a virtual file. + + Store the contents of a :class:`io.StringIO` object in a GMT_DATASET container + and create a virtual file to pass to a GMT module. + + For simplicity, currently we make following assumptions in the StringIO object + + - ``"#"`` indicates a comment line. + - ``">"`` indicates a segment header. + + Parameters + ---------- + stringio + The :class:`io.StringIO` object containing the data to be stored in the + virtual file. + + Yields + ------ + fname + The name of the virtual file. + + Examples + -------- + >>> import io + >>> from pygmt.clib import Session + >>> # A StringIO object containing legend specifications + >>> stringio = io.StringIO( + ... "# Comment\n" + ... "H 24p Legend\n" + ... "N 2\n" + ... "S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" + ... ) + >>> with Session() as lib: + ... with lib.virtualfile_from_stringio(stringio) as fin: + ... lib.virtualfile_to_dataset(vfname=fin, output_type="pandas") + 0 + 0 H 24p Legend + 1 N 2 + 2 S 0.1i c 0.15i p300/12 0.25p 0.3i My circle + """ + # Parse the io.StringIO object. + segments = [] + current_segment = {"header": "", "data": []} + for line in stringio.getvalue().splitlines(): + if line.startswith("#"): # Skip comments + continue + if line.startswith(">"): # Segment header + if current_segment["data"]: # If we have data, start a new segment + segments.append(current_segment) + current_segment = {"header": "", "data": []} + current_segment["header"] = line.strip(">").lstrip() + else: + current_segment["data"].append(line) # type: ignore[attr-defined] + if current_segment["data"]: # Add the last segment if it has data + segments.append(current_segment) + + # One table with one or more segments. + # n_rows is the maximum number of rows/records for all segments. + # n_columns is the number of numeric data columns, so it's 0 here. + n_tables = 1 + n_segments = len(segments) + n_rows = max(len(segment["data"]) for segment in segments) + n_columns = 0 + + # Create the GMT_DATASET container + family, geometry = "GMT_IS_DATASET", "GMT_IS_TEXT" + dataset = self.create_data( + family, + geometry, + mode="GMT_CONTAINER_ONLY|GMT_WITH_STRINGS", + dim=[n_tables, n_segments, n_rows, n_columns], + ) + dataset = ctp.cast(dataset, ctp.POINTER(_GMT_DATASET)) + table = dataset.contents.table[0].contents + for i, segment in enumerate(segments): + seg = table.segment[i].contents + if segment["header"]: + seg.header = segment["header"].encode() # type: ignore[attr-defined] + seg.text = strings_to_ctypes_array(segment["data"]) + + with self.open_virtualfile(family, geometry, "GMT_IN", dataset) as vfile: + try: + yield vfile + finally: + # Must set the pointers to None to avoid double freeing the memory. + # Maybe upstream bug. + for i in range(n_segments): + seg = table.segment[i].contents + seg.header = None + seg.text = None + def virtualfile_in( # noqa: PLR0912 self, check_kind=None, @@ -1697,6 +1798,7 @@ def virtualfile_in( # noqa: PLR0912 "geojson": tempfile_from_geojson, "grid": self.virtualfile_from_grid, "image": tempfile_from_image, + "stringio": self.virtualfile_from_stringio, # Note: virtualfile_from_matrix is not used because a matrix can be # converted to vectors instead, and using vectors allows for better # handling of string type inputs (e.g. for datetime data types) @@ -1705,7 +1807,7 @@ def virtualfile_in( # noqa: PLR0912 }[kind] # Ensure the data is an iterable (Python list or tuple) - if kind in {"geojson", "grid", "image", "file", "arg"}: + if kind in {"geojson", "grid", "image", "file", "arg", "stringio"}: if kind == "image" and data.dtype != "uint8": msg = ( f"Input image has dtype: {data.dtype} which is unsupported, " diff --git a/pygmt/figure.py b/pygmt/figure.py index ce8693a43f3..91a4193f6f0 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -4,8 +4,9 @@ import base64 import os -from pathlib import Path +from pathlib import Path, PurePath from tempfile import TemporaryDirectory +from typing import Literal try: import IPython @@ -26,27 +27,49 @@ use_alias, ) + +def _get_default_display_method() -> Literal["external", "notebook", "none"]: + """ + Get the default method to display preview images. + + The function checks the current environment and determines the most suitable method + to display preview images when calling :meth:`pygmt.Figure.show`. Valid display + methods are: + + - ``"external"``: External PDF preview using the default PDF viewer + - ``"notebook"``: Inline PNG preview in the current notebook + - ``"none"``: Disable image preview + + The default display method is ``"notebook"`` in the Jupyter notebook environment, + and ``"external"`` in other cases. + + Setting environment variable **PYGMT_USE_EXTERNAL_DISPLAY** to ``"false"`` can + disable image preview in external viewers. It's useful when running the tests and + building the documentation to avoid popping up windows. + + Returns + ------- + method + The default display method. + """ + # Check if an IPython kernel is running. + if _HAS_IPYTHON and (ipy := IPython.get_ipython()) and "IPKernelApp" in ipy.config: + return "notebook" + # Check if the environment variable PYGMT_USE_EXTERNAL_DISPLAY is set to "false". + if os.environ.get("PYGMT_USE_EXTERNAL_DISPLAY", "true").lower() == "false": + return "none" + # Fallback to using the external viewer. + return "external" + + # A registry of all figures that have had "show" called in this session. # This is needed for the sphinx-gallery scraper in pygmt/sphinx_gallery.py SHOWED_FIGURES = [] - -# Configurations for figure display +# Configurations for figure display. SHOW_CONFIG = { - "method": "external", # Open in an external viewer by default + "method": _get_default_display_method(), # The image preview display method. } -# Show figures in Jupyter notebooks if available -if _HAS_IPYTHON: - get_ipython = IPython.get_ipython() - if get_ipython and "IPKernelApp" in get_ipython.config: # Jupyter Notebook enabled - SHOW_CONFIG["method"] = "notebook" - -# Set environment variable PYGMT_USE_EXTERNAL_DISPLAY to 'false' to disable -# external display. Use it when running the tests and building the docs to -# avoid popping up windows. -if os.environ.get("PYGMT_USE_EXTERNAL_DISPLAY", "true").lower() == "false": - SHOW_CONFIG["method"] = "none" - class Figure: """ @@ -253,12 +276,12 @@ def psconvert(self, **kwargs): def savefig( # noqa: PLR0912 self, - fname, - transparent=False, - crop=True, - anti_alias=True, - show=False, - worldfile=False, + fname: str | PurePath, + transparent: bool = False, + crop: bool = True, + anti_alias: bool = True, + show: bool = False, + worldfile: bool = False, **kwargs, ): """ @@ -280,45 +303,39 @@ def savefig( # noqa: PLR0912 - EPS (``.eps``) - PDF (``.pdf``) - Beside the above formats, you can also save the figure to a KML file - (``.kml``), with a companion PNG file generated automatically. The KML - file can be viewed in Google Earth. + Besides the above formats, you can also save the figure to a KML file + (``.kml``), with a companion PNG file generated automatically. The KML file can + be viewed in Google Earth. - You can pass in any keyword arguments that - :meth:`pygmt.Figure.psconvert` accepts. + You can pass in any keyword arguments that :meth:`pygmt.Figure.psconvert` + accepts. Parameters ---------- - fname : str - The desired figure file name, including the extension. See the list - of supported formats and their extensions above. - transparent : bool - If ``True``, will use a transparent background for the figure. - Only valid for PNG format. - crop : bool - If ``True``, will crop the figure canvas (page) to the plot area. - anti_alias: bool - If ``True``, will use anti-aliasing when creating raster images. - More specifically, it passes the arguments ``"t2"`` and ``"g2"`` - to the ``anti_aliasing`` parameter of - :meth:`pygmt.Figure.psconvert`. Ignored if creating vector images. - show: bool - If ``True``, will open the figure in an external viewer. - worldfile : bool - If ``True``, will create a companion - `world file `__ for the - figure. The world file will have the same name as the figure file - but with different extension (e.g. tfw for tif). See - https://en.wikipedia.org/wiki/World_file#Filename_extension - for the convention of world file extensions. This parameter only - works for raster image formats (except GeoTIFF). - dpi : int - Set raster resolution in dpi [Default is ``720`` for PDF, ``300`` - for others]. + fname + The desired figure file name, including the extension. See the list of + supported formats and their extensions above. + transparent + Use a transparent background for the figure. Only valid for PNG format. + crop + Crop the figure canvas (page) to the plot area. + anti_alias + Use anti-aliasing when creating raster images. Ignored if creating vector + images. More specifically, it passes the arguments ``"t2"`` and ``"g2"`` to + the ``anti_aliasing`` parameter of :meth:`pygmt.Figure.psconvert`. + show + Display the figure in an external viewer. + worldfile + Create a companion `world file `__ + for the figure. The world file will have the same name as the figure file + but with different extension (e.g., ``.tfw`` for ``.tif``). See + https://en.wikipedia.org/wiki/World_file#Filename_extension for the + convention of world file extensions. This parameter only works for raster + image formats (except GeoTIFF). **kwargs : dict - Additional keyword arguments passed to - :meth:`pygmt.Figure.psconvert`. Valid parameters are ``gs_path``, - ``gs_option``, ``resize``, ``bb_style``, and ``verbose``. + Additional keyword arguments passed to :meth:`pygmt.Figure.psconvert`. Valid + parameters are ``dpi``, ``gs_path``, ``gs_option``, ``resize``, + ``bb_style``, and ``verbose``. """ # All supported formats fmts = { @@ -337,26 +354,24 @@ def savefig( # noqa: PLR0912 prefix, suffix = fname.with_suffix("").as_posix(), fname.suffix ext = suffix[1:].lower() # Remove the . and normalize to lowercase - if ext == "jpeg": # Alias jpeg to jpg - ext = "jpg" - elif ext == "tiff": # GeoTIFF - kwargs["W"] = "+g" - elif ext == "kml": # KML - kwargs["W"] = "+k" + match ext: + case "jpeg": # Alias jpeg to jpg + ext = "jpg" + case "tiff": # GeoTIFF + kwargs["W"] = "+g" + case "kml": # KML + kwargs["W"] = "+k" + case "ps": + msg = "Extension '.ps' is not supported. Use '.eps' or '.pdf' instead." + raise GMTInvalidInput(msg) + case ext if ext not in fmts: + raise GMTInvalidInput(f"Unknown extension '.{ext}'.") - if ext not in fmts: - if ext == "ps": - raise GMTInvalidInput( - "Extension '.ps' is not supported. " - "Please use '.eps' or '.pdf' instead." - ) - raise GMTInvalidInput(f"Unknown extension '.{ext}'.") fmt = fmts[ext] if transparent: if fmt != "g": - raise GMTInvalidInput( - f"Transparency unavailable for '{ext}', only for png." - ) + msg = f"Transparency unavailable for '{ext}', only for png." + raise GMTInvalidInput(msg) fmt = fmt.upper() if anti_alias: kwargs["Qt"] = 2 @@ -364,14 +379,13 @@ def savefig( # noqa: PLR0912 if worldfile: if ext in {"eps", "kml", "pdf", "tiff"}: - raise GMTInvalidInput( - f"Saving a world file is not supported for '{ext}' format." - ) + msg = f"Saving a world file is not supported for '{ext}' format." + raise GMTInvalidInput(msg) kwargs["W"] = True self.psconvert(prefix=prefix, fmt=fmt, crop=crop, **kwargs) - # Remove the .pgw world file if exists + # Remove the .pgw world file if exists. # Not necessary after GMT 6.5.0. # See upstream fix https://github.com/GenericMappingTools/gmt/pull/7865 if ext == "tiff": @@ -382,91 +396,98 @@ def savefig( # noqa: PLR0912 fname.with_suffix("." + ext).rename(fname) if show: - launch_external_viewer(fname) + launch_external_viewer(str(fname)) - def show(self, dpi=300, width=500, method=None, waiting=0.5, **kwargs): + def show( + self, + method: Literal["external", "notebook", "none", None] = None, + dpi: int = 300, + width: int = 500, + waiting: float = 0.5, + **kwargs, + ): """ Display a preview of the figure. - Inserts the preview in the Jupyter notebook output if available, - otherwise opens it in the default viewer for your operating system - (falls back to the default web browser). + Inserts the preview in the Jupyter notebook output if available, otherwise opens + it in the default viewer for your operating system (falls back to the default + web browser). - :func:`pygmt.set_display` can select the default display method + Use :func:`pygmt.set_display` to select the default display method (``"notebook"``, ``"external"``, ``"none"`` or ``None``). - The ``method`` parameter can also override the default display method - for the current figure. Parameters ``dpi`` and ``width`` can be used - to control the resolution and dimension of the figure in the notebook. + The ``method`` parameter allows to override the default display method for the + current figure. The parameters ``dpi`` and ``width`` can be used to control the + resolution and dimension of the figure in the notebook. - **Note**: The external viewer can be disabled by setting the - PYGMT_USE_EXTERNAL_DISPLAY environment variable to **false**. - This is useful when running unit tests and building the documentation - in consoles without a Graphical User Interface. + The external viewer can be disabled by setting the environment variable + **PYGMT_USE_EXTERNAL_DISPLAY** to ``"false"``. This is useful when running tests + and building the documentation to avoid popping up windows. - Note that the external viewer does not block the current process, thus - it's necessary to suspend the execution of the current process for a - short while after launching the external viewer, so that the preview - image won't be deleted before the external viewer tries to open it. Set - the ``waiting`` parameter to a larger number if your computer is slow. + The external viewer does not block the current process, thus it's necessary to + suspend the execution of the current process for a short while after launching + the external viewer, so that the preview image won't be deleted before the + external viewer tries to open it. Set the ``waiting`` parameter to a larger + number if the image viewer on your computer is slow to open the figure. Parameters ---------- - dpi : int - The image resolution (dots per inch) in Jupyter notebooks. - width : int - The image width (in pixels) in Jupyter notebooks. - method : str or None - How the current figure will be displayed. Choose from: + method + The method to display the current image preview. Choose from: - ``"external"``: External PDF preview using the default PDF viewer - ``"notebook"``: Inline PNG preview in the current notebook - ``"none"``: Disable image preview - ``None``: Reset to the default display method - The default display method is ``"external"`` in Python consoles or + The default display method is ``"external"`` in Python consoles and ``"notebook"`` in Jupyter notebooks, but can be changed by :func:`pygmt.set_display`. - waiting : float - Suspend the execution of the current process for a given number of - seconds after launching an external viewer. - Only works if ``method="external"``. + + dpi + The image resolution (dots per inch) in Jupyter notebooks. + width + The image width (in pixels) in Jupyter notebooks. + waiting + Suspend the execution of the current process for a given number of seconds + after launching an external viewer. Only works if ``method="external"``. **kwargs : dict - Additional keyword arguments passed to - :meth:`pygmt.Figure.psconvert`. Valid parameters are ``gs_path``, - ``gs_option``, ``resize``, ``bb_style``, and ``verbose``. + Additional keyword arguments passed to :meth:`pygmt.Figure.psconvert`. Valid + parameters are ``gs_path``, ``gs_option``, ``resize``, ``bb_style``, and + ``verbose``. """ - # Module level variable to know which figures had their show method - # called. Needed for the sphinx-gallery scraper. + # Module level variable to know which figures had their show method called. + # Needed for the sphinx-gallery scraper. SHOWED_FIGURES.append(self) # Set the display method if method is None: method = SHOW_CONFIG["method"] - if method not in {"external", "notebook", "none"}: - raise GMTInvalidInput( - f"Invalid display method '{method}', " - "should be either 'notebook', 'external', or 'none'." - ) - - if method == "notebook": - if not _HAS_IPYTHON: - raise GMTError( - "Notebook display is selected, but IPython is not available. " - "Make sure you have IPython installed, " - "or run the script in a Jupyter notebook." + match method: + case "notebook": + if not _HAS_IPYTHON: + raise GMTError( + "Notebook display is selected, but IPython is not available. " + "Make sure you have IPython installed, " + "or run the script in a Jupyter notebook." + ) + png = self._preview( + fmt="png", dpi=dpi, anti_alias=True, as_bytes=True, **kwargs + ) + IPython.display.display(IPython.display.Image(data=png, width=width)) + case "external": + pdf = self._preview( + fmt="pdf", dpi=dpi, anti_alias=False, as_bytes=False, **kwargs + ) + launch_external_viewer(pdf, waiting=waiting) + case "none": + pass # Do nothing + case _: + raise GMTInvalidInput( + f"Invalid display method '{method}'. Valid values are 'external', " + "'notebook', 'none' or None." ) - png = self._preview( - fmt="png", dpi=dpi, anti_alias=True, as_bytes=True, **kwargs - ) - IPython.display.display(IPython.display.Image(data=png, width=width)) - - if method == "external": - pdf = self._preview( - fmt="pdf", dpi=dpi, anti_alias=False, as_bytes=False, **kwargs - ) - launch_external_viewer(pdf, waiting=waiting) def _preview(self, fmt, dpi, as_bytes=False, **kwargs): """ @@ -545,22 +566,20 @@ def _repr_html_(self): ) -def set_display(method=None): +def set_display(method: Literal["external", "notebook", "none", None] = None): """ Set the display method when calling :meth:`pygmt.Figure.show`. Parameters ---------- - method : str or None + method The method to display an image preview. Choose from: - ``"external"``: External PDF preview using the default PDF viewer - ``"notebook"``: Inline PNG preview in the current notebook - ``"none"``: Disable image preview - - ``None``: Reset to the default display method - - The default display method is ``"external"`` in Python consoles or - ``"notebook"`` in Jupyter notebooks. + - ``None``: Reset to the default display method, which is either ``"external"`` + in Python consoles or ``"notebook"`` in Jupyter notebooks. Examples -------- @@ -583,10 +602,13 @@ def set_display(method=None): >>> pygmt.set_display(method=None) >>> fig.show() # again, will show a PNG image in the current notebook """ - if method in {"notebook", "external", "none"}: - SHOW_CONFIG["method"] = method - elif method is not None: - raise GMTInvalidInput( - f"Invalid display mode '{method}', " - "should be either 'notebook', 'external', 'none' or None." - ) + match method: + case "external" | "notebook" | "none": + SHOW_CONFIG["method"] = method # type: ignore[assignment] + case None: + SHOW_CONFIG["method"] = _get_default_display_method() # type: ignore[assignment] + case _: + raise GMTInvalidInput( + f"Invalid display method '{method}'. Valid values are 'external'," + "'notebook', 'none' or None." + ) diff --git a/pygmt/helpers/__init__.py b/pygmt/helpers/__init__.py index 08583896b6c..0d074061ba1 100644 --- a/pygmt/helpers/__init__.py +++ b/pygmt/helpers/__init__.py @@ -19,7 +19,6 @@ _validate_data_input, args_in_kwargs, build_arg_list, - build_arg_string, data_kind, is_nonstr_iter, launch_external_viewer, diff --git a/pygmt/helpers/tempfile.py b/pygmt/helpers/tempfile.py index 469f387fc5f..0177f17d435 100644 --- a/pygmt/helpers/tempfile.py +++ b/pygmt/helpers/tempfile.py @@ -12,17 +12,17 @@ from packaging.version import Version -def unique_name(): +def unique_name() -> str: """ Generate a unique name. - Useful for generating unique names for figures (otherwise GMT will plot - everything on the same figure instead of creating a new one). + Useful for generating unique names for figures. Otherwise GMT will plot everything + on the same figure instead of creating a new one. Returns ------- - name : str - A unique name generated by :func:`uuid.uuid4` + name + A unique name generated by :func:`uuid.uuid4`. """ return uuid.uuid4().hex @@ -31,15 +31,14 @@ class GMTTempFile: """ Context manager for creating closed temporary files. - This class does not return a file-like object. So, you can't do - ``for line in GMTTempFile()``, for example, or pass it to things that - need file objects. + This class does not return a file-like object. So, you can't iterate over the object + like ``for line in GMTTempFile()``, or pass it to things that need a file object. Parameters ---------- - prefix : str + prefix The temporary file name begins with the prefix. - suffix : str + suffix The temporary file name ends with the suffix. Examples @@ -60,7 +59,10 @@ class GMTTempFile: [0. 0. 0.] [1. 1. 1.] [2. 2. 2.] """ - def __init__(self, prefix="pygmt-", suffix=".txt"): + def __init__(self, prefix: str = "pygmt-", suffix: str = ".txt"): + """ + Initialize the object. + """ with NamedTemporaryFile(prefix=prefix, suffix=suffix, delete=False) as tmpfile: self.name = tmpfile.name @@ -76,18 +78,18 @@ def __exit__(self, *args): """ Path(self.name).unlink(missing_ok=True) - def read(self, keep_tabs=False): + def read(self, keep_tabs: bool = False) -> str: """ Read the entire contents of the file as a Unicode string. Parameters ---------- - keep_tabs : bool + keep_tabs If False, replace the tabs that GMT uses with spaces. Returns ------- - content : str + content Content of the temporary file as a Unicode string. """ content = Path(self.name).read_text(encoding="utf8") @@ -95,14 +97,14 @@ def read(self, keep_tabs=False): content = content.replace("\t", " ") return content - def loadtxt(self, **kwargs): + def loadtxt(self, **kwargs) -> np.ndarray: """ Load data from the temporary file using numpy.loadtxt. Parameters ---------- - kwargs : dict - Any keyword arguments that can be passed to numpy.loadtxt. + kwargs + Any keyword arguments that can be passed to :func:`np.loadtxt`. Returns ------- diff --git a/pygmt/helpers/testing.py b/pygmt/helpers/testing.py index 28027e82f64..77e2a952bf3 100644 --- a/pygmt/helpers/testing.py +++ b/pygmt/helpers/testing.py @@ -89,6 +89,8 @@ def wrapper(*args, ext="png", request=None, **kwargs): file_name = "".join(c for c in request.node.name if c in allowed_chars) except AttributeError: # 'NoneType' object has no attribute 'node' file_name = func.__name__ + + fig_ref, fig_test = None, None try: fig_ref, fig_test = func(*args, **kwargs) ref_image_path = Path(result_dir) / f"{file_name}-expected.{ext}" diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index cd54d6fc18e..6585bb7566b 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -2,6 +2,7 @@ Utilities and common tasks for wrapping the GMT modules. """ +import io import os import pathlib import shutil @@ -9,7 +10,6 @@ import subprocess import sys import time -import warnings import webbrowser from collections.abc import Iterable, Sequence from typing import Any, Literal @@ -189,8 +189,10 @@ def _check_encoding( def data_kind( data: Any = None, required: bool = True -) -> Literal["arg", "file", "geojson", "grid", "image", "matrix", "vectors"]: - """ +) -> Literal[ + "arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors" +]: + r""" Check the kind of data that is provided to a module. The ``data`` argument can be in any type, but only following types are supported: @@ -223,6 +225,7 @@ def data_kind( >>> import numpy as np >>> import xarray as xr >>> import pathlib + >>> import io >>> data_kind(data=None) 'vectors' >>> data_kind(data=np.arange(10).reshape((5, 2))) @@ -241,8 +244,12 @@ def data_kind( 'grid' >>> data_kind(data=xr.DataArray(np.random.rand(3, 4, 5))) 'image' + >>> data_kind(data=io.StringIO("TEXT1\nTEXT23\n")) + 'stringio' """ - kind: Literal["arg", "file", "geojson", "grid", "image", "matrix", "vectors"] + kind: Literal[ + "arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors" + ] if isinstance(data, str | pathlib.PurePath) or ( isinstance(data, list | tuple) and all(isinstance(_file, str | pathlib.PurePath) for _file in data) @@ -251,6 +258,8 @@ def data_kind( kind = "file" elif isinstance(data, bool | int | float) or (data is None and not required): kind = "arg" + elif isinstance(data, io.StringIO): + kind = "stringio" elif isinstance(data, xr.DataArray): kind = "image" if len(data.dims) == 3 else "grid" elif hasattr(data, "__geo_interface__"): @@ -461,139 +470,6 @@ def build_arg_list( # noqa: PLR0912 return gmt_args -def build_arg_string(kwdict, confdict=None, infile=None, outfile=None): - r""" - Convert keyword dictionaries and input/output files into a GMT argument string. - - Make sure all values in ``kwdict`` have been previously converted to a - string representation using the ``kwargs_to_strings`` decorator. The only - exceptions are True, False and None. - - Any lists or tuples left will be interpreted as multiple entries for the - same command line option. For example, the kwargs entry ``'B': ['xa', - 'yaf']`` will be converted to ``-Bxa -Byaf`` in the argument string. - - Note that spaces `` `` in arguments are converted to the equivalent octal - code ``\040``, except in the case of -J (projection) arguments where PROJ4 - strings (e.g. "+proj=longlat +datum=WGS84") will have their spaces removed. - See https://github.com/GenericMappingTools/pygmt/pull/1487 for more info. - - .. deprecated:: 0.12.0 - - Use :func:`build_arg_list` instead. - - Parameters - ---------- - kwdict : dict - A dictionary containing parsed keyword arguments. - confdict : dict - A dictionary containing configurable GMT parameters. - infile : str or pathlib.Path - The input file. - outfile : str or pathlib.Path - The output file. - - Returns - ------- - args : str - The space-delimited argument string with '-' inserted before each - keyword, or '--' inserted before GMT configuration key-value pairs. - The keyword arguments are sorted alphabetically, followed by GMT - configuration key-value pairs, with optional input file at the - beginning and optional output file at the end. - - Examples - -------- - - >>> print( - ... build_arg_string( - ... dict( - ... A=True, - ... B=False, - ... E=200, - ... J="+proj=longlat +datum=WGS84", - ... P="", - ... R="1/2/3/4", - ... X=None, - ... Y=None, - ... Z=0, - ... ) - ... ) - ... ) - -A -E200 -J+proj=longlat+datum=WGS84 -P -R1/2/3/4 -Z0 - >>> print( - ... build_arg_string( - ... dict( - ... R="1/2/3/4", - ... J="X4i", - ... B=["xaf", "yaf", "WSen"], - ... I=("1/1p,blue", "2/0.25p,blue"), - ... ) - ... ) - ... ) - -BWSen -Bxaf -Byaf -I1/1p,blue -I2/0.25p,blue -JX4i -R1/2/3/4 - >>> print(build_arg_string(dict(R="1/2/3/4", J="X4i", watre=True))) - Traceback (most recent call last): - ... - pygmt.exceptions.GMTInvalidInput: Unrecognized parameter 'watre'. - >>> print( - ... build_arg_string( - ... dict( - ... B=["af", "WSne+tBlank Space"], - ... F='+t"Empty Spaces"', - ... l="'Void Space'", - ... ), - ... ) - ... ) - -BWSne+tBlank\040Space -Baf -F+t"Empty\040\040Spaces" -l'Void\040Space' - >>> print( - ... build_arg_string( - ... dict(A="0", B=True, C="rainbow"), - ... confdict=dict(FORMAT_DATE_MAP="o dd"), - ... infile="input.txt", - ... outfile="output.txt", - ... ) - ... ) - input.txt -A0 -B -Crainbow --FORMAT_DATE_MAP="o dd" ->output.txt - """ - msg = ( - "Utility function 'build_arg_string()' is deprecated in v0.12.0 and will be " - "removed in v0.14.0. Use 'build_arg_list()' instead." - ) - warnings.warn(msg, category=FutureWarning, stacklevel=2) - - gmt_args = [] - for key in kwdict: - if len(key) > 2: # raise an exception for unrecognized options - raise GMTInvalidInput(f"Unrecognized parameter '{key}'.") - if kwdict[key] is None or kwdict[key] is False: - pass # Exclude arguments that are None and False - elif is_nonstr_iter(kwdict[key]): - for value in kwdict[key]: - _value = str(value).replace(" ", r"\040") - gmt_args.append(rf"-{key}{_value}") - elif kwdict[key] is True: - gmt_args.append(f"-{key}") - else: - if key != "J": # non-projection parameters - _value = str(kwdict[key]).replace(" ", r"\040") - else: - # special handling if key == "J" (projection) - # remove any spaces in PROJ4 string - _value = str(kwdict[key]).replace(" ", "") - gmt_args.append(rf"-{key}{_value}") - gmt_args = sorted(gmt_args) - - if confdict: - gmt_args.extend(f'--{key}="{value}"' for key, value in confdict.items()) - - if infile: - gmt_args = [str(infile), *gmt_args] - if outfile: - gmt_args.append("->" + str(outfile)) - return non_ascii_to_octal(" ".join(gmt_args)) - - def is_nonstr_iter(value): """ Check if the value is not a string but is iterable (list, tuple, array) @@ -630,42 +506,43 @@ def is_nonstr_iter(value): return isinstance(value, Iterable) and not isinstance(value, str) -def launch_external_viewer(fname, waiting=0): +def launch_external_viewer(fname: str, waiting: float = 0): """ Open a file in an external viewer program. - Uses the ``xdg-open`` command on Linux, the ``open`` command on macOS, the - associated application on Windows, and the default web browser on other - systems. + Uses the ``xdg-open`` command on Linux/FreeBSD, the ``open`` command on macOS, the + associated application on Windows, and the default web browser on other systems. Parameters ---------- - fname : str + fname The file name of the file (preferably a full path). + waiting + Wait for a few seconds before exiting the function, to allow the external viewer + open the file before it's deleted. """ - # Redirect stdout and stderr to devnull so that the terminal isn't filled - # with noise + # Redirect stdout and stderr to devnull so that the terminal isn't filled with noise run_args = { "stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, } - # Open the file with the default viewer. - # Fall back to the browser if can't recognize the operating system. - os_name = sys.platform - if os_name.startswith(("linux", "freebsd")) and ( - xdgopen := shutil.which("xdg-open") - ): - subprocess.run([xdgopen, fname], check=False, **run_args) - elif os_name == "darwin": # Darwin is macOS - subprocess.run([shutil.which("open"), fname], check=False, **run_args) - elif os_name == "win32": - os.startfile(fname) # noqa: S606 - else: - webbrowser.open_new_tab(f"file://{fname}") + match sys.platform: + case name if ( + (name == "linux" or name.startswith("freebsd")) + and (xdgopen := shutil.which("xdg-open")) + ): # Linux/FreeBSD + subprocess.run([xdgopen, fname], check=False, **run_args) # type:ignore[call-overload] + case "darwin": # macOS + subprocess.run([shutil.which("open"), fname], check=False, **run_args) # type:ignore[call-overload] + case "win32": # Windows + os.startfile(fname) # type:ignore[attr-defined] # noqa: S606 + case _: # Fall back to the browser if can't recognize the operating system. + webbrowser.open_new_tab(f"file://{fname}") if waiting > 0: - # suspend the execution for a few seconds to avoid the images being - # deleted when a Python script exits + # Preview images will be deleted when a GMT modern-mode session ends, but the + # external viewer program may take a few seconds to open the images. + # Suspend the execution for a few seconds. time.sleep(waiting) diff --git a/pygmt/src/grdcontour.py b/pygmt/src/grdcontour.py index 0c461330acf..53c328f8264 100644 --- a/pygmt/src/grdcontour.py +++ b/pygmt/src/grdcontour.py @@ -2,8 +2,6 @@ grdcontour - Plot a contour figure. """ -import warnings - from pygmt.clib import Session from pygmt.helpers import ( build_arg_list, @@ -140,20 +138,6 @@ def grdcontour(self, grid, **kwargs): """ kwargs = self._preprocess(**kwargs) - # Backward compatibility with the old syntax for the annotation parameter, e.g., - # [100, "e", "f10p", "gred"]. - if is_nonstr_iter(kwargs.get("A")) and any( - i[0] in "acdefgijlLnoprtuvwx=" for i in kwargs["A"] if isinstance(i, str) - ): - msg = ( - "Argument of the parameter 'annotation'/'A' is using the old, deprecated " - "syntax. Please refer to the PyGMT documentation for the new syntax. " - "The warning will be removed in v0.14.0 and the old syntax will no longer " - "be supported. " - ) - warnings.warn(msg, category=FutureWarning, stacklevel=2) - kwargs["A"] = "+".join(f"{item}" for item in kwargs["A"]) - # Specify levels for the annotation and levels parameters. # One level is converted to a string with a trailing comma to separate it from # specifying an interval. diff --git a/pygmt/src/legend.py b/pygmt/src/legend.py index e5a7ebefab0..ed34bc0d797 100644 --- a/pygmt/src/legend.py +++ b/pygmt/src/legend.py @@ -2,6 +2,9 @@ legend - Plot a legend. """ +import io +import pathlib + from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( @@ -26,7 +29,13 @@ t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwargs): +def legend( + self, + spec: str | pathlib.PurePath | io.StringIO | None = None, + position="JTR+jTR+o0.2c", + box="+gwhite+p1p", + **kwargs, +): r""" Plot legends on maps. @@ -42,10 +51,16 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg Parameters ---------- - spec : None or str - Either ``None`` [Default] for using the automatically generated legend - specification file, or a *filename* pointing to the legend - specification file. + spec + The legend specification. It can be: + + - ``None`` which means using the automatically generated legend specification + file + - A string or a :class:`pathlib.PurePath` object pointing to the legend + specification file + - A :class:`io.StringIO` object containing the legend specification. + + See :gmt-docs:`legend.html` for the definition of the legend specification. {projection} {region} position : str @@ -75,12 +90,12 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg if kwargs.get("F") is None: kwargs["F"] = box + kind = data_kind(spec) + if kind not in {"vectors", "file", "stringio"}: # kind="vectors" means spec is None + raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}") + if kind == "file" and is_nonstr_iter(spec): + raise GMTInvalidInput("Only one legend specification file is allowed.") + with Session() as lib: - if spec is None: - specfile = "" - elif data_kind(spec) == "file" and not is_nonstr_iter(spec): - # Is a file but not a list of files - specfile = spec - else: - raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}") - lib.call_module(module="legend", args=build_arg_list(kwargs, infile=specfile)) + with lib.virtualfile_in(data=spec, required_data=False) as vintbl: + lib.call_module(module="legend", args=build_arg_list(kwargs, infile=vintbl)) diff --git a/pygmt/tests/test_clib_virtualfiles.py b/pygmt/tests/test_clib_virtualfiles.py index b8b5ee0500d..2a966de7c05 100644 --- a/pygmt/tests/test_clib_virtualfiles.py +++ b/pygmt/tests/test_clib_virtualfiles.py @@ -2,6 +2,7 @@ Test the C API functions related to virtual files. """ +import io from importlib.util import find_spec from itertools import product from pathlib import Path @@ -407,3 +408,106 @@ def test_inquire_virtualfile(): ]: with lib.open_virtualfile(family, geometry, "GMT_OUT", None) as vfile: assert lib.inquire_virtualfile(vfile) == lib[family] + + +class TestVirtualfileFromStringIO: + """ + Test the virtualfile_from_stringio method. + """ + + def _stringio_to_dataset(self, data: io.StringIO): + """ + A helper function for check the virtualfile_from_stringio method. + + The function does the following: + + 1. Creates a virtual file from the input StringIO object. + 2. Pass the virtual file to the ``read`` module, which reads the virtual file + and writes it to another virtual file. + 3. Reads the output virtual file as a GMT_DATASET object. + 4. Extracts the header and the trailing text from the dataset and returns it as + a string. + """ + with clib.Session() as lib: + with ( + lib.virtualfile_from_stringio(data) as vintbl, + lib.virtualfile_out(kind="dataset") as vouttbl, + ): + lib.call_module("read", args=[vintbl, vouttbl, "-Td"]) + ds = lib.read_virtualfile(vouttbl, kind="dataset").contents + + output = [] + table = ds.table[0].contents + for segment in table.segment[: table.n_segments]: + seg = segment.contents + output.append(f"> {seg.header.decode()}" if seg.header else ">") + output.extend(np.char.decode(seg.text[: seg.n_rows])) + return "\n".join(output) + "\n" + + def test_virtualfile_from_stringio(self): + """ + Test the virtualfile_from_stringio method. + """ + data = io.StringIO( + "# Comment\n" + "H 24p Legend\n" + "N 2\n" + "S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" + ) + expected = ( + ">\n" + "H 24p Legend\n" + "N 2\n" + "S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" + ) + assert self._stringio_to_dataset(data) == expected + + def test_one_segment(self): + """ + Test the virtualfile_from_stringio method with one segment. + """ + data = io.StringIO( + "# Comment\n" + "> Segment 1\n" + "1 2 3 ABC\n" + "4 5 DE\n" + "6 7 8 9 FGHIJK LMN OPQ\n" + "RSTUVWXYZ\n" + ) + expected = ( + "> Segment 1\n" + "1 2 3 ABC\n" + "4 5 DE\n" + "6 7 8 9 FGHIJK LMN OPQ\n" + "RSTUVWXYZ\n" + ) + assert self._stringio_to_dataset(data) == expected + + def test_multiple_segments(self): + """ + Test the virtualfile_from_stringio method with multiple segments. + """ + data = io.StringIO( + "# Comment line 1\n" + "# Comment line 2\n" + "> Segment 1\n" + "1 2 3 ABC\n" + "4 5 DE\n" + "6 7 8 9 FG\n" + "# Comment line 3\n" + "> Segment 2\n" + "1 2 3 ABC\n" + "4 5 DE\n" + "6 7 8 9 FG\n" + ) + expected = ( + "> Segment 1\n" + "1 2 3 ABC\n" + "4 5 DE\n" + "6 7 8 9 FG\n" + "> Segment 2\n" + "1 2 3 ABC\n" + "4 5 DE\n" + "6 7 8 9 FG\n" + ) + assert self._stringio_to_dataset(data) == expected diff --git a/pygmt/tests/test_figure.py b/pygmt/tests/test_figure.py index 042c9876e71..4c8e184d118 100644 --- a/pygmt/tests/test_figure.py +++ b/pygmt/tests/test_figure.py @@ -4,7 +4,6 @@ Doesn't include the plotting commands which have their own test files. """ -import importlib from pathlib import Path import numpy as np @@ -12,9 +11,15 @@ import pytest from pygmt import Figure, set_display from pygmt.exceptions import GMTError, GMTInvalidInput +from pygmt.figure import SHOW_CONFIG, _get_default_display_method from pygmt.helpers import GMTTempFile -HAS_IPYTHON = bool(importlib.util.find_spec("IPython")) +try: + import IPython + + _HAS_IPYTHON = True +except ImportError: + _HAS_IPYTHON = False def test_figure_region(): @@ -307,7 +312,7 @@ def test_figure_savefig_worldfile(): fig.savefig(fname=imgfile.name, worldfile=True) -@pytest.mark.skipif(not HAS_IPYTHON, reason="run when IPython is installed") +@pytest.mark.skipif(not _HAS_IPYTHON, reason="run when IPython is installed") def test_figure_show(): """ Test that show creates the correct file name and deletes the temp dir. @@ -347,7 +352,7 @@ def test_figure_show_invalid_method(): fig.show(method="test") -@pytest.mark.skipif(HAS_IPYTHON, reason="run without IPython installed") +@pytest.mark.skipif(_HAS_IPYTHON, reason="run without IPython installed") def test_figure_show_notebook_error_without_ipython(): """ Test to check if an error is raised when display method is 'notebook', but IPython @@ -368,12 +373,31 @@ def test_figure_display_external(): fig.show(method="external") -def test_figure_set_display_invalid(): +class TestSetDisplay: """ - Test to check if an error is raised when an invalid method is passed to set_display. + Test the pygmt.set_display method. """ - with pytest.raises(GMTInvalidInput): - set_display(method="invalid") + + def test_set_display(self): + """ + Test if pygmt.set_display updates the SHOW_CONFIG variable correctly. + """ + default_method = SHOW_CONFIG["method"] # Current default method + + for method in ("notebook", "external", "none"): + set_display(method=method) + assert SHOW_CONFIG["method"] == method + + # Setting method to None should revert it to the default method. + set_display(method=None) + assert SHOW_CONFIG["method"] == default_method + + def test_invalid_method(self): + """ + Test if an error is raised when an invalid method is passed. + """ + with pytest.raises(GMTInvalidInput): + set_display(method="invalid") def test_figure_unsupported_xshift_yshift(): @@ -390,3 +414,48 @@ def test_figure_unsupported_xshift_yshift(): fig.plot(x=1, y=1, style="c3c", yshift="3c") with pytest.raises(GMTInvalidInput): fig.plot(x=1, y=1, style="c3c", Y="3c") + + +class TestGetDefaultDisplayMethod: + """ + Test the _get_default_display_method function. + """ + + def test_default_display_method(self, monkeypatch): + """ + Default display method is "external" if PYGMT_USE_EXTERNAL_DISPLAY is undefined. + """ + monkeypatch.delenv("PYGMT_USE_EXTERNAL_DISPLAY", raising=False) + assert _get_default_display_method() == "external" + + def test_disable_external_display(self, monkeypatch): + """ + Setting PYGMT_USE_EXTERNAL_DISPLAY to "false" should disable external display. + """ + monkeypatch.setenv("PYGMT_USE_EXTERNAL_DISPLAY", "false") + assert _get_default_display_method() == "none" + + @pytest.mark.skipif(not _HAS_IPYTHON, reason="Run when IPython is installed") + def test_notebook_display(self, monkeypatch): + """ + Default display method is "notebook" when an IPython kernel is running. + """ + + class MockIPython: + """ + A simple mock class to simulate an IPython instance. + """ + + def __init__(self): + self.config = {"IPKernelApp": True} + + # Mock IPython.get_ipython() to return a MockIPython instance. + mock_ipython = MockIPython() + monkeypatch.setattr(IPython, "get_ipython", lambda: mock_ipython) + + # Default display method should be "notebook" when an IPython kernel is running. + assert _get_default_display_method() == "notebook" + + # PYGMT_USE_EXTERNAL_DISPLAY should not affect notebook display. + monkeypatch.setenv("PYGMT_USE_EXTERNAL_DISPLAY", "false") + assert _get_default_display_method() == "notebook" diff --git a/pygmt/tests/test_grdcontour.py b/pygmt/tests/test_grdcontour.py index 14d43e849cf..b2e1558db86 100644 --- a/pygmt/tests/test_grdcontour.py +++ b/pygmt/tests/test_grdcontour.py @@ -45,23 +45,6 @@ def test_grdcontour_one_level(grid): return fig -@pytest.mark.mpl_image_compare(filename="test_grdcontour_one_level.png") -def test_grdcontour_old_annotations(grid): - """ - Test the old syntax for the annotation parameter using "sequence_plus". - Modified from the "test_grdcontour_one_level()" test. Can be removed in v0.14.0. - """ - fig = Figure() - fig.grdcontour( - grid=grid, - levels=[400], - annotation=["570,", "gwhite"], - projection="M10c", - frame=True, - ) - return fig - - @pytest.mark.mpl_image_compare def test_grdcontour_multiple_levels(grid): """ diff --git a/pygmt/tests/test_legend.py b/pygmt/tests/test_legend.py index 5280c131cda..3a63d74166e 100644 --- a/pygmt/tests/test_legend.py +++ b/pygmt/tests/test_legend.py @@ -2,6 +2,7 @@ Test Figure.legend. """ +import io from pathlib import Path import pytest @@ -10,12 +11,44 @@ from pygmt.helpers import GMTTempFile +@pytest.fixture(scope="module", name="legend_spec") +def fixture_legend_spec(): + """ + A string contains a legend specification. + """ + return """ +G -0.1i +H 24 Times-Roman My Map Legend +D 0.2i 1p +N 2 +V 0 1p +S 0.1i c 0.15i p300/12 0.25p 0.3i This circle is hachured +S 0.1i e 0.15i yellow 0.25p 0.3i This ellipse is yellow +S 0.1i w 0.15i green 0.25p 0.3i This wedge is green +S 0.1i f0.1i+l+t 0.25i blue 0.25p 0.3i This is a fault +S 0.1i - 0.15i - 0.25p,- 0.3i A dashed contour +S 0.1i v0.1i+a40+e 0.25i magenta 0.25p 0.3i This is a vector +S 0.1i i 0.15i cyan 0.25p 0.3i This triangle is boring +V 0 1p +D 0.2i 1p +N 1 +G 0.05i +G 0.05i +G 0.05i +L 9 4 R Smith et al., @%5%J. Geophys. Res., 99@%%, 2000 +G 0.1i +P +T Let us just try some simple text that can go on a few lines. +T There is no easy way to predetermine how many lines will be required, +T so we may have to adjust the box height to get the right size box. +""" + + @pytest.mark.mpl_image_compare def test_legend_position(): """ - Test that plots a position with each of the four legend coordinate systems. + Test positioning the legend with different coordinate systems. """ - fig = Figure() fig.basemap(region=[-2, 2, -2, 2], frame=True) positions = ["jTR+jTR", "g0/1", "n0.2/0.2", "x4i/2i/2i"] @@ -30,14 +63,10 @@ def test_legend_default_position(): """ Test using the default legend position. """ - fig = Figure() - fig.basemap(region=[-1, 1, -1, 1], frame=True) - fig.plot(x=[0], y=[0], style="p10p", label="Default") fig.legend() - return fig @@ -45,7 +74,7 @@ def test_legend_default_position(): @pytest.mark.mpl_image_compare def test_legend_entries(): """ - Test different marker types/shapes. + Test legend using the automatically generated legend entries. """ fig = Figure() fig.basemap(projection="x1i", region=[0, 7, 3, 7], frame=True) @@ -59,48 +88,31 @@ def test_legend_entries(): fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines") fig.plot(data="@Table_5_11.txt", style="t0.15i", fill="orange", label="Oranges") fig.legend(position="JTR+jTR") - return fig @pytest.mark.mpl_image_compare -def test_legend_specfile(): +def test_legend_specfile(legend_spec): """ - Test specfile functionality. + Test passing a legend specification file. """ - - specfile_contents = """ -G -0.1i -H 24 Times-Roman My Map Legend -D 0.2i 1p -N 2 -V 0 1p -S 0.1i c 0.15i p300/12 0.25p 0.3i This circle is hachured -S 0.1i e 0.15i yellow 0.25p 0.3i This ellipse is yellow -S 0.1i w 0.15i green 0.25p 0.3i This wedge is green -S 0.1i f0.1i+l+t 0.25i blue 0.25p 0.3i This is a fault -S 0.1i - 0.15i - 0.25p,- 0.3i A dashed contour -S 0.1i v0.1i+a40+e 0.25i magenta 0.25p 0.3i This is a vector -S 0.1i i 0.15i cyan 0.25p 0.3i This triangle is boring -V 0 1p -D 0.2i 1p -N 1 -G 0.05i -G 0.05i -G 0.05i -L 9 4 R Smith et al., @%5%J. Geophys. Res., 99@%%, 2000 -G 0.1i -P -T Let us just try some simple text that can go on a few lines. -T There is no easy way to predetermine how many lines will be required, -T so we may have to adjust the box height to get the right size box. -""" - with GMTTempFile() as specfile: - Path(specfile.name).write_text(specfile_contents, encoding="utf-8") + Path(specfile.name).write_text(legend_spec, encoding="utf-8") fig = Figure() fig.basemap(projection="x6i", region=[0, 1, 0, 1], frame=True) fig.legend(specfile.name, position="JTM+jCM+w5i") + return fig + + +@pytest.mark.mpl_image_compare(filename="test_legend_specfile.png") +def test_legend_stringio(legend_spec): + """ + Test passing a legend specification via an io.StringIO object. + """ + spec = io.StringIO(legend_spec) + fig = Figure() + fig.basemap(projection="x6i", region=[0, 1, 0, 1], frame=True) + fig.legend(spec, position="JTM+jCM+w5i") return fig @@ -111,3 +123,6 @@ def test_legend_fails(): fig = Figure() with pytest.raises(GMTInvalidInput): fig.legend(spec=["@Table_5_11.txt"]) + + with pytest.raises(GMTInvalidInput): + fig.legend(spec=[1, 2]) diff --git a/pygmt/tests/test_show_versions.py b/pygmt/tests/test_show_versions.py index 4ef7ad1804a..41a0132c2b8 100644 --- a/pygmt/tests/test_show_versions.py +++ b/pygmt/tests/test_show_versions.py @@ -5,16 +5,43 @@ import io import pygmt +import pytest def test_show_versions(): """ - Check that pygmt.show_versions() reports version information from PyGMT, the - operating system, dependencies and the GMT library. + Check that pygmt.show_versions reports version information of PyGMT, the operating + system, dependencies and the GMT library. """ buf = io.StringIO() pygmt.show_versions(file=buf) - assert "PyGMT information:" in buf.getvalue() - assert "System information:" in buf.getvalue() - assert "Dependency information:" in buf.getvalue() - assert "GMT library information:" in buf.getvalue() + output = buf.getvalue() + + assert "PyGMT information:" in output + assert "System information:" in output + assert "Dependency information:" in output + assert "GMT library information:" in output + assert "WARNING:" not in output # No GMT-Ghostscript incompatibility warnings. + + +@pytest.mark.parametrize( + ("gs_version", "gmt_version"), + [ + ("9.52", "6.4.0"), + ("10.01", "6.4.0"), + ("10.02", "6.4.0"), + (None, "6.5.0"), + ], +) +def test_show_versions_ghostscript_warnings(gs_version, gmt_version, monkeypatch): + """ + Check that pygmt.show_versions reports warnings for GMT-Ghostscript incompatibility. + """ + monkeypatch.setattr("pygmt._show_versions.__gmt_version__", gmt_version) + monkeypatch.setattr( + "pygmt._show_versions._get_ghostscript_version", lambda: gs_version + ) + + buf = io.StringIO() + pygmt.show_versions(file=buf) + assert "WARNING:" in buf.getvalue() diff --git a/pygmt/tests/test_sphinx_gallery.py b/pygmt/tests/test_sphinx_gallery.py index c40d4732a0b..1fc7d7f260c 100644 --- a/pygmt/tests/test_sphinx_gallery.py +++ b/pygmt/tests/test_sphinx_gallery.py @@ -9,7 +9,7 @@ from pygmt.figure import SHOWED_FIGURES, Figure from pygmt.sphinx_gallery import PyGMTScraper -pytest.importorskip("sphinx-gallery", reason="Requires sphinx-gallery to be installed") +pytest.importorskip("sphinx_gallery", reason="Requires sphinx-gallery to be installed") pytest.importorskip("IPython", reason="Requires IPython to be installed")