From af66e2ab85da2f0a1c7b7f2d64f0927ddf5d6cd4 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 6 Jan 2025 00:05:44 +0800 Subject: [PATCH 01/20] Add the missing separators in changelog (#3745) --- doc/changes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/changes.md b/doc/changes.md index 21250127374..8e7142d2fd8 100644 --- a/doc/changes.md +++ b/doc/changes.md @@ -102,6 +102,8 @@ * [Will Schlitzer](https://github.com/willschlitzer) * [Jiayuan Yao](https://github.com/core-man) +--- + ## 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) @@ -280,6 +282,8 @@ * [Michael Grund](https://github.com/michaelgrund) * [Wei Ji Leong](https://github.com/weiji14) +--- + ## Release v0.11.0 (2024/02/01) [![Digital Object Identifier for PyGMT v0.11.0](https://zenodo.org/badge/DOI/10.5281/zenodo.10578540.svg)](https://doi.org/10.5281/zenodo.10578540) From 7c38ce75df74695e9add69fc97c38a9c7215f354 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 6 Jan 2025 08:28:20 +0800 Subject: [PATCH 02/20] Use well-known labels in project URLs following PEP753 (#3743) --- pyproject.toml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2150af08d7b..9af670df32c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,10 +48,11 @@ all = [ ] [project.urls] -homepage = "https://www.pygmt.org" -documentation = "https://www.pygmt.org" -repository = "https://github.com/GenericMappingTools/pygmt" -changelog = "https://www.pygmt.org/latest/changes.html" +"Homepage" = "https://www.pygmt.org" +"Documentation" = "https://www.pygmt.org" +"Source Code" = "https://github.com/GenericMappingTools/pygmt" +"Changelog" = "https://www.pygmt.org/latest/changes.html" +"Issue Tracker" = "https://github.com/GenericMappingTools/pygmt/issues" [tool.setuptools] platforms = ["Any"] From 4c03bab6fbc90f7354fad28d663fd98f9df6c268 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 6 Jan 2025 22:57:33 +0800 Subject: [PATCH 03/20] Update the release checklist post v0.14.0 release (#3692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> Co-authored-by: Michael Grund <23025878+michaelgrund@users.noreply.github.com> Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/4-release_checklist.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/4-release_checklist.md b/.github/ISSUE_TEMPLATE/4-release_checklist.md index 83fe809724a..87a164b15c8 100644 --- a/.github/ISSUE_TEMPLATE/4-release_checklist.md +++ b/.github/ISSUE_TEMPLATE/4-release_checklist.md @@ -19,17 +19,15 @@ assignees: '' **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) +- [ ] 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) - [ ] Review the ["PyGMT Team" page](https://www.pygmt.org/dev/team.html) +- [ ] README looks good on TestPyPI. Visit [TestPyPI](https://test.pypi.org/project/pygmt/#history), click the latest pre-release, and check the homepage. - [ ] Check to ensure that: - [ ] Deprecated workarounds/codes/tests are removed. Run `grep "# TODO" **/*.py` to find all potential TODOs. - [ ] 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) -- [ ] 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 +- [ ] 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" - [ ] 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` @@ -41,18 +39,16 @@ assignees: '' - [ ] 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 -- [ ] 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 +- [ ] 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. Remember to pin Python and SPEC0 versions] +- [ ] Update conda-forge [pygmt-feedstock](https://github.com/conda-forge/pygmt-feedstock) [Done automatically by conda-forge's bot. Remember to pin GMT, 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! 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) + - [ ] [ResearchGate](https://www.researchgate.net) (after forum announcement; download the ZIP file of the new release from the release page and add it as research item via the **code** category, be sure to include the corresponding new Zenodo DOI) +- [ ] Update release checklist template with any additional bullet points that may have arisen during the release --- From e0f8e84b623149dac9a0bfe2cbf62b72cd4a4941 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 7 Jan 2025 07:53:54 +0800 Subject: [PATCH 04/20] Figure.plot/Figure.plot3d: Improve the docstrings for straight_line (#3720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/src/plot.py | 49 +++++++++++++++++++++++++++++++++------------ pygmt/src/plot3d.py | 42 +++++++++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py index ebfb2f22f3c..23c5bde12fd 100644 --- a/pygmt/src/plot.py +++ b/pygmt/src/plot.py @@ -2,6 +2,8 @@ plot - Plot in two dimensions. """ +from typing import Literal + from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( @@ -49,7 +51,15 @@ ) @kwargs_to_strings(R="sequence", c="sequence_comma", i="sequence_comma", p="sequence") def plot( - self, data=None, x=None, y=None, size=None, symbol=None, direction=None, **kwargs + self, + data=None, + x=None, + y=None, + size=None, + symbol=None, + direction=None, + straight_line: bool | Literal["x", "y"] = False, # noqa: ARG001 + **kwargs, ): r""" Plot lines, polygons, and symbols in 2-D. @@ -98,18 +108,29 @@ def plot( depending on the style options chosen. {projection} {region} - straight_line : bool or str - [**m**\|\ **p**\|\ **x**\|\ **y**]. - By default, geographic line segments are drawn as great circle - arcs. To draw them as straight lines, use - ``straight_line=True``. - Alternatively, add **m** to draw the line by first following a - meridian, then a parallel. Or append **p** to start following a - parallel, then a meridian. (This can be practical to draw a line - along parallels, for example). For Cartesian data, points are - simply connected, unless you append **x** or **y** to draw - stair-case curves that whose first move is along *x* or *y*, - respectively. + straight_line + By default, line segments are drawn as straight lines in the Cartesian and polar + coordinate systems, and as great circle arcs (by resampling coarse input data + along such arcs) in the geographic coordinate system. The ``straight_line`` + parameter can control the drawing of line segments. Valid values are: + + - ``True``: Draw line segments as straight lines in geographic coordinate + systems. + - ``"x"``: Draw line segments by first along *x*, then along *y*. + - ``"y"``: Draw line segments by first along *y*, then along *x*. + + Here, *x* and *y* have different meanings depending on the coordinate system: + + - **Cartesian** coordinate system: *x* and *y* are the X- and Y-axes. + - **Polar** coordinate system: *x* and *y* are theta and radius. + - **Geographic** coordinate system: *x* and *y* are parallels and meridians. + + .. attention:: + + There exits a bug in GMT<=6.5.0 that, in geographic coordinate systems, the + meaning of *x* and *y* is reversed, i.e., *x* means meridians and *y* means + parallels. The bug is fixed by upstream + `PR #8648 `__. {frame} {cmap} offset : str @@ -206,6 +227,8 @@ def plot( ``x``/``y``. {wrap} """ + # TODO(GMT>6.5.0): Remove the note for the upstream bug of the "straight_line" + # parameter. kwargs = self._preprocess(**kwargs) kind = data_kind(data) diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py index f7f2b08290a..e8e75382d74 100644 --- a/pygmt/src/plot3d.py +++ b/pygmt/src/plot3d.py @@ -2,6 +2,8 @@ plot3d - Plot in three dimensions. """ +from typing import Literal + from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( @@ -58,6 +60,7 @@ def plot3d( size=None, symbol=None, direction=None, + straight_line: bool | Literal["x", "y"] = False, # noqa: ARG001 **kwargs, ): r""" @@ -108,18 +111,31 @@ def plot3d( zscale/zsize : float or str Set z-axis scaling or z-axis size. {region} - straight_line : bool or str - [**m**\|\ **p**\|\ **x**\|\ **y**]. - By default, geographic line segments are drawn as great circle - arcs. To draw them as straight lines, use ``straight_line``. - Alternatively, add **m** to draw the line by first following a - meridian, then a parallel. Or append **p** to start following a - parallel, then a meridian. (This can be practical to draw a line - along parallels, for example). For Cartesian data, points are - simply connected, unless you append **x** or **y** to draw - stair-case curves that whose first move is along *x* or *y*, - respectively. **Note**: The ``straight_line`` parameter requires - constant *z*-coordinates. + straight_line + By default, line segments are drawn as straight lines in the Cartesian and polar + coordinate systems, and as great circle arcs (by resampling coarse input data + along such arcs) in the geographic coordinate system. The ``straight_line`` + parameter can control the drawing of line segments. Valid values are: + + - ``True``: Draw line segments as straight lines in geographic coordinate + systems. + - ``"x"``: Draw line segments by first along *x*, then along *y*. + - ``"y"``: Draw line segments by first along *y*, then along *x*. + + Here, *x* and *y* have different meanings depending on the coordinate system: + + - **Cartesian** coordinate system: *x* and *y* are the X- and Y-axes. + - **Polar** coordinate system: *x* and *y* are theta and radius. + - **Geographic** coordinate system: *x* and *y* are parallels and meridians. + + **NOTE**: The ``straight_line`` parameter requires constant *z*-coordinates. + + .. attention:: + + There exits a bug in GMT<=6.5.0 that, in geographic coordinate systems, the + meaning of *x* and *y* is reversed, i.e., *x* means meridians and *y* means + parallels. The bug is fixed by upstream + `PR #8648 `__. {frame} {cmap} offset : str @@ -189,6 +205,8 @@ def plot3d( ``x``/``y``/``z``. {wrap} """ + # TODO(GMT>6.5.0): Remove the note for the upstream bug of the "straight_line" + # parameter. kwargs = self._preprocess(**kwargs) kind = data_kind(data) From 448e44c2a7f6d78202666488ee58f8c839a82930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yvonne=20Fr=C3=B6hlich?= <94163266+yvonnefroehlich@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:03:49 +0100 Subject: [PATCH 05/20] DOC/Gallery example "Choropleth map": Fix typo (#3751) --- examples/gallery/maps/choropleth_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gallery/maps/choropleth_map.py b/examples/gallery/maps/choropleth_map.py index 19376f3c61c..b4b3bf54e69 100644 --- a/examples/gallery/maps/choropleth_map.py +++ b/examples/gallery/maps/choropleth_map.py @@ -6,7 +6,7 @@ polygons which are stored in a :class:`geopandas.GeoDataFrame` object. Use :func:`geopandas.read_file` to load data from any supported OGR format such as a shapefile (.shp), GeoJSON (.geojson), geopackage (.gpkg), etc. You can also use a full -URL pointing to your desired data source. Then, pass the class:`geopandas.GeoDataFrame` +URL pointing to your desired data source. Then, pass the :class:`geopandas.GeoDataFrame` as an argument to the ``data`` parameter of :meth:`pygmt.Figure.plot`, and style the geometry using the ``pen`` parameter. To fill the polygons based on a corresponding column you need to set ``fill="+z"`` as well as select the appropriate column using the From aeaf33fc352561cdb4881804aa0ec01efd9e91cf Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 8 Jan 2025 08:35:25 +0800 Subject: [PATCH 06/20] _load_remote_dataset: Add the "kind" attribute to explicitly specify if data is a grid or image (#3688) --- pygmt/datasets/load_remote_dataset.py | 36 +++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/pygmt/datasets/load_remote_dataset.py b/pygmt/datasets/load_remote_dataset.py index b19c055425c..41fe729d0e0 100644 --- a/pygmt/datasets/load_remote_dataset.py +++ b/pygmt/datasets/load_remote_dataset.py @@ -39,7 +39,9 @@ class GMTRemoteDataset(NamedTuple): Attributes ---------- description - The name assigned as an attribute to the DataArray. + The name assigned as an attribute to the DataArray. + kind + The kind of the dataset source. Valid values are ``"grid"`` and ``"image"``. units The units of the values in the DataArray. resolutions @@ -49,6 +51,7 @@ class GMTRemoteDataset(NamedTuple): """ description: str + kind: Literal["grid", "image"] units: str | None resolutions: dict[str, Resolution] extra_attributes: dict[str, Any] @@ -57,6 +60,7 @@ class GMTRemoteDataset(NamedTuple): datasets = { "earth_age": GMTRemoteDataset( description="EarthByte Earth seafloor crustal age", + kind="grid", units="Myr", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -75,6 +79,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_day": GMTRemoteDataset( description="NASA Day Images", + kind="image", units=None, extra_attributes={"long_name": "blue_marble", "horizontal_datum": "WGS84"}, resolutions={ @@ -94,6 +99,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_dist": GMTRemoteDataset( description="GSHHG Earth distance to shoreline", + kind="grid", units="kilometers", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -112,6 +118,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_edefl": GMTRemoteDataset( description="IGPP Earth east-west deflection", + kind="grid", units="micro-radians", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -130,6 +137,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_faa": GMTRemoteDataset( description="IGPP Earth free-air anomaly", + kind="grid", units="mGal", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -148,6 +156,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_faaerror": GMTRemoteDataset( description="IGPP Earth free-air anomaly errors", + kind="grid", units="mGal", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -166,6 +175,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_gebco": GMTRemoteDataset( description="GEBCO Earth relief", + kind="grid", units="meters", extra_attributes={"vertical_datum": "EGM96", "horizontal_datum": "WGS84"}, resolutions={ @@ -188,6 +198,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_geoid": GMTRemoteDataset( description="EGM2008 Earth geoid", + kind="grid", units="meters", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -206,6 +217,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_igpp": GMTRemoteDataset( description="IGPP Earth relief", + kind="grid", units="meters", extra_attributes={"vertical_datum": "EGM96", "horizontal_datum": "WGS84"}, resolutions={ @@ -228,6 +240,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_mag": GMTRemoteDataset( description="EMAG2 Earth Magnetic Anomaly Model", + kind="grid", units="nT", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -245,6 +258,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_mask": GMTRemoteDataset( description="GSHHG Earth mask", + kind="grid", units=None, extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -265,6 +279,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_mss": GMTRemoteDataset( description="CNES Earth mean sea surface", + kind="grid", units="meters", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -283,6 +298,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_night": GMTRemoteDataset( description="NASA Night Images", + kind="image", units=None, extra_attributes={"long_name": "black_marble", "horizontal_datum": "WGS84"}, resolutions={ @@ -302,6 +318,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_mdt": GMTRemoteDataset( description="CNES Earth mean dynamic topography", + kind="grid", units="meters", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -315,6 +332,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_ndefl": GMTRemoteDataset( description="IGPP Earth north-south deflection", + kind="grid", units="micro-radians", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -333,6 +351,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_vgg": GMTRemoteDataset( description="IGPP Earth vertical gravity gradient", + kind="grid", units="Eotvos", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -351,6 +370,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_wdmam": GMTRemoteDataset( description="WDMAM World Digital Magnetic Anomaly Map", + kind="grid", units="nT", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -367,6 +387,7 @@ class GMTRemoteDataset(NamedTuple): ), "mars_relief": GMTRemoteDataset( description="NASA Mars (MOLA) relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -388,6 +409,7 @@ class GMTRemoteDataset(NamedTuple): ), "moon_relief": GMTRemoteDataset( description="USGS Moon (LOLA) relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -409,6 +431,7 @@ class GMTRemoteDataset(NamedTuple): ), "mercury_relief": GMTRemoteDataset( description="USGS Mercury relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -428,6 +451,7 @@ class GMTRemoteDataset(NamedTuple): ), "pluto_relief": GMTRemoteDataset( description="USGS Pluto relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -447,6 +471,7 @@ class GMTRemoteDataset(NamedTuple): ), "venus_relief": GMTRemoteDataset( description="NASA Magellan Venus relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -545,15 +570,16 @@ def _load_remote_dataset( raise GMTInvalidInput(msg) fname = f"@{prefix}_{resolution}_{reg}" - kind = "image" if name in {"earth_day", "earth_night"} else "grid" - kwdict = {"R": region, "T": {"grid": "g", "image": "i"}[kind]} + kwdict = {"R": region, "T": {"grid": "g", "image": "i"}[dataset.kind]} with Session() as lib: - with lib.virtualfile_out(kind=kind) as voutgrd: + with lib.virtualfile_out(kind=dataset.kind) as voutgrd: lib.call_module( module="read", args=[fname, voutgrd, *build_arg_list(kwdict)], ) - grid = lib.virtualfile_to_raster(kind=kind, outgrid=None, vfname=voutgrd) + grid = lib.virtualfile_to_raster( + kind=dataset.kind, outgrid=None, vfname=voutgrd + ) # Full path to the grid if not tiled grids. source = which(fname, download="a") if not resinfo.tiled else None From a9d26a8248b2579f0f1a889d6d3258137fe7c0e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 08:36:33 +0800 Subject: [PATCH 07/20] Build(deps): Bump lycheeverse/lychee-action from 2.1.0 to 2.2.0 (#3753) Bumps [lycheeverse/lychee-action](https://github.com/lycheeverse/lychee-action) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/lycheeverse/lychee-action/releases) - [Commits](https://github.com/lycheeverse/lychee-action/compare/v2.1.0...v2.2.0) --- updated-dependencies: - dependency-name: lycheeverse/lychee-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index b1f45b73df8..0ed425da8d6 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -35,7 +35,7 @@ jobs: - name: Link Checker id: lychee - uses: lycheeverse/lychee-action@v2.1.0 + uses: lycheeverse/lychee-action@v2.2.0 with: fail: false # Don't fail action on broken links output: /tmp/lychee-out.md From 1d50ffb9f62f97fa861755e7b937c3d363bf4511 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 8 Jan 2025 21:24:38 +0800 Subject: [PATCH 08/20] TYP: Add return type hints for functions that return None (#3754) --- pygmt/_show_versions.py | 2 +- pygmt/clib/session.py | 18 ++++++++++++------ pygmt/figure.py | 12 ++++++------ pygmt/helpers/caching.py | 2 +- pygmt/helpers/tempfile.py | 2 +- pygmt/helpers/utils.py | 4 ++-- pygmt/session_management.py | 4 ++-- 7 files changed, 25 insertions(+), 19 deletions(-) diff --git a/pygmt/_show_versions.py b/pygmt/_show_versions.py index e529f053e46..d15bf0799c6 100644 --- a/pygmt/_show_versions.py +++ b/pygmt/_show_versions.py @@ -85,7 +85,7 @@ def _check_ghostscript_version(gs_version: str | None) -> str | None: return None -def show_versions(file: TextIO | None = sys.stdout): +def show_versions(file: TextIO | None = sys.stdout) -> None: """ Print various dependency versions which are useful when submitting bug reports. diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index f35161b7b02..179737d35f9 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -327,7 +327,7 @@ def get_libgmt_func( function.restype = restype return function - def create(self, name: str): + def create(self, name: str) -> None: """ Create a new GMT C API session. @@ -594,7 +594,7 @@ def get_common(self, option: str) -> bool | int | float | np.ndarray: case _: # 'status' is the option value (in integer type). return status - def call_module(self, module: str, args: str | list[str]): + def call_module(self, module: str, args: str | list[str]) -> None: """ Call a GMT module with the given arguments. @@ -946,7 +946,9 @@ def _check_dtype_and_dim(self, array: np.ndarray, ndim: int) -> int: raise GMTInvalidInput(msg) return self[DTYPES[dtype]] - def put_vector(self, dataset: ctp.c_void_p, column: int, vector: np.ndarray): + def put_vector( + self, dataset: ctp.c_void_p, column: int, vector: np.ndarray + ) -> None: r""" Attach a 1-D numpy array as a column on a GMT dataset. @@ -1005,7 +1007,9 @@ def put_vector(self, dataset: ctp.c_void_p, column: int, vector: np.ndarray): ) raise GMTCLibError(msg) - def put_strings(self, dataset: ctp.c_void_p, family: str, strings: np.ndarray): + def put_strings( + self, dataset: ctp.c_void_p, family: str, strings: np.ndarray + ) -> None: """ Attach a 1-D numpy array of dtype str as a column on a GMT dataset. @@ -1059,7 +1063,9 @@ def put_strings(self, dataset: ctp.c_void_p, family: str, strings: np.ndarray): msg = f"Failed to put strings of type {strings.dtype} into dataset." raise GMTCLibError(msg) - def put_matrix(self, dataset: ctp.c_void_p, matrix: np.ndarray, pad: int = 0): + def put_matrix( + self, dataset: ctp.c_void_p, matrix: np.ndarray, pad: int = 0 + ) -> None: """ Attach a 2-D numpy array to a GMT dataset. @@ -1204,7 +1210,7 @@ def read_data( raise GMTCLibError(msg) return ctp.cast(data_ptr, ctp.POINTER(dtype)) - def write_data(self, family, geometry, mode, wesn, output, data): + def write_data(self, family, geometry, mode, wesn, output, data) -> None: """ Write a GMT data container to a file. diff --git a/pygmt/figure.py b/pygmt/figure.py index 374eb1d8fee..5c5d4734ce6 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -95,19 +95,19 @@ class Figure: 122.94, 145.82, 20.53, 45.52 """ - def __init__(self): + def __init__(self) -> None: self._name = unique_name() self._preview_dir = TemporaryDirectory(prefix=f"{self._name}-preview-") self._activate_figure() - def __del__(self): + def __del__(self) -> None: """ Clean up the temporary directory that stores the previews. """ if hasattr(self, "_preview_dir"): self._preview_dir.cleanup() - def _activate_figure(self): + def _activate_figure(self) -> None: """ Start and/or activate the current figure. @@ -144,7 +144,7 @@ def savefig( show: bool = False, worldfile: bool = False, **kwargs, - ): + ) -> None: """ Save the figure to an image file. @@ -268,7 +268,7 @@ def show( width: int = 500, waiting: float = 0.5, **kwargs, - ): + ) -> None: """ Display a preview of the figure. @@ -442,7 +442,7 @@ def _repr_html_(self) -> str: ) -def set_display(method: Literal["external", "notebook", "none", None] = None): +def set_display(method: Literal["external", "notebook", "none", None] = None) -> None: """ Set the display method when calling :meth:`pygmt.Figure.show`. diff --git a/pygmt/helpers/caching.py b/pygmt/helpers/caching.py index 19d3eae0559..ea6bed8d4cf 100644 --- a/pygmt/helpers/caching.py +++ b/pygmt/helpers/caching.py @@ -5,7 +5,7 @@ from pygmt.src import which -def cache_data(): +def cache_data() -> None: """ Download GMT remote data files used in PyGMT tests and docs to cache folder. """ diff --git a/pygmt/helpers/tempfile.py b/pygmt/helpers/tempfile.py index 6d8dfd74fec..6995be1db98 100644 --- a/pygmt/helpers/tempfile.py +++ b/pygmt/helpers/tempfile.py @@ -59,7 +59,7 @@ class GMTTempFile: [0. 0. 0.] [1. 1. 1.] [2. 2. 2.] """ - def __init__(self, prefix: str = "pygmt-", suffix: str = ".txt"): + def __init__(self, prefix: str = "pygmt-", suffix: str = ".txt") -> None: """ Initialize the object. """ diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 32fad37e4ff..b53942818c5 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -43,7 +43,7 @@ def _validate_data_input( data=None, x=None, y=None, z=None, required_z=False, required_data=True, kind=None -): +) -> None: """ Check if the combination of data/x/y/z is valid. @@ -552,7 +552,7 @@ def is_nonstr_iter(value): return isinstance(value, Iterable) and not isinstance(value, str) -def launch_external_viewer(fname: str, waiting: float = 0): +def launch_external_viewer(fname: str, waiting: float = 0) -> None: """ Open a file in an external viewer program. diff --git a/pygmt/session_management.py b/pygmt/session_management.py index be3cff9539c..ac18218c858 100644 --- a/pygmt/session_management.py +++ b/pygmt/session_management.py @@ -9,7 +9,7 @@ from pygmt.helpers import unique_name -def begin(): +def begin() -> None: """ Initiate a new GMT modern mode session. @@ -28,7 +28,7 @@ def begin(): lib.call_module(module="set", args=["GMT_COMPATIBILITY=6"]) -def end(): +def end() -> None: """ Terminate the GMT modern mode session created by :func:`pygmt.begin`. From 0ba87ba369466de605c39a020d7564be1bc3ef1f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 8 Jan 2025 21:33:21 +0800 Subject: [PATCH 09/20] CI: Update the doc deploy script to generate CNAME and index.html (#3749) --- .github/workflows/ci_docs.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_docs.yml b/.github/workflows/ci_docs.yml index a74d4fbfcd5..06c7debb426 100644 --- a/.github/workflows/ci_docs.yml +++ b/.github/workflows/ci_docs.yml @@ -163,9 +163,14 @@ jobs: # to get the right commit hash. message="Deploy $version from $(git rev-parse --short HEAD)" cd deploy - # Need to have this file so that GitHub doesn't try to run Jekyll + # Create some files in the root directory. + # .nojekyll: Need to have this file so that GitHub doesn't try to run Jekyll touch .nojekyll - # Delete all the files and replace with our new set + # CNAME: Set the custom domain name + echo "www.pygmt.org" > CNAME + # index.html: Redirect to the latest version + echo '' > index.html + # Delete all the files and replace with our new set echo -e "\nRemoving old files from previous builds of ${version}:" rm -rvf ${version} echo -e "\nCopying HTML files to ${version}:" From 664a9eda6fa03adc24b9a1752c601c438528e5e3 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 8 Jan 2025 21:43:31 +0800 Subject: [PATCH 10/20] ruff: Ignore the B018 rule (useless expressions) in examples (#3750) --- examples/gallery/images/rgb_image.py | 2 +- examples/gallery/lines/linestrings.py | 2 +- examples/gallery/maps/choropleth_map.py | 2 +- examples/tutorials/basics/plot.py | 6 +++--- pyproject.toml | 5 ++++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/gallery/images/rgb_image.py b/examples/gallery/images/rgb_image.py index 56d7b9d2f70..8afb2c93e25 100644 --- a/examples/gallery/images/rgb_image.py +++ b/examples/gallery/images/rgb_image.py @@ -28,7 +28,7 @@ # Subset to area of Lāhainā in EPSG:32604 coordinates image = img.rio.clip_box(minx=738000, maxx=755000, miny=2300000, maxy=2318000) image = image.load() # Force loading the DataArray into memory -image # noqa: B018 +image # %% # Plot the RGB imagery: diff --git a/examples/gallery/lines/linestrings.py b/examples/gallery/lines/linestrings.py index d3793645c84..18f94502f16 100644 --- a/examples/gallery/lines/linestrings.py +++ b/examples/gallery/lines/linestrings.py @@ -25,7 +25,7 @@ # Convert object to EPSG 4326 coordinate system gdf = gdf.to_crs("EPSG:4326") -print(gdf.head()) +gdf.head() # %% fig = pygmt.Figure() diff --git a/examples/gallery/maps/choropleth_map.py b/examples/gallery/maps/choropleth_map.py index b4b3bf54e69..f1cce8c3014 100644 --- a/examples/gallery/maps/choropleth_map.py +++ b/examples/gallery/maps/choropleth_map.py @@ -20,7 +20,7 @@ # Read the example dataset provided by geodatasets. gdf = gpd.read_file(geodatasets.get_path("geoda airbnb")) -print(gdf) +print(gdf.head()) # %% fig = pygmt.Figure() diff --git a/examples/tutorials/basics/plot.py b/examples/tutorials/basics/plot.py index babfb60d771..42cb8aad5ae 100644 --- a/examples/tutorials/basics/plot.py +++ b/examples/tutorials/basics/plot.py @@ -18,7 +18,9 @@ # The data are loaded as a :class:`pandas.DataFrame`. data = pygmt.datasets.load_sample_data(name="japan_quakes") +data.head() +# %% # Set the region for the plot to be slightly larger than the data bounds. region = [ data.longitude.min() - 1, @@ -26,9 +28,7 @@ data.latitude.min() - 1, data.latitude.max() + 1, ] - -print(region) -print(data.head()) +region # %% # We'll use the :meth:`pygmt.Figure.plot` method to plot circles on the diff --git a/pyproject.toml b/pyproject.toml index 9af670df32c..61c6a541fef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,7 +160,10 @@ known-third-party = ["pygmt"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # Ignore `F401` (unused-import) in all `__init__.py` files "*/tests/test_*.py" = ["S101"] # Ignore `S101` (use of assert) in all tests files -"examples/**/*.py" = ["T201"] # Allow `print` in examples +"examples/**/*.py" = [ # Ignore rules in examples + "B018", # Allow useless expressions in Jupyter Notebooks + "T201", # Allow `print` statements +] [tool.ruff.lint.pycodestyle] max-doc-length = 88 From c7198e6f2d38edca746b87886f2e7c0933d3f2c9 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 9 Jan 2025 08:20:52 +0800 Subject: [PATCH 11/20] DOC: Make bullet point lists consistent in docstrings (":" and lower-case) Part II (#3752) --- pygmt/helpers/decorators.py | 70 ++++++++++++++++++------------------- pygmt/src/grdfilter.py | 14 ++++---- pygmt/src/grdgradient.py | 14 ++++---- pygmt/src/grdview.py | 10 +++--- pygmt/src/sphdistance.py | 6 ++-- pygmt/src/x2sys_cross.py | 6 ++-- pygmt/src/x2sys_init.py | 14 ++++---- 7 files changed, 67 insertions(+), 67 deletions(-) diff --git a/pygmt/helpers/decorators.py b/pygmt/helpers/decorators.py index a22d49334af..08822d42a86 100644 --- a/pygmt/helpers/decorators.py +++ b/pygmt/helpers/decorators.py @@ -37,17 +37,17 @@ (using ``binary="o"``), where *ncols* is the number of data columns of *type*, which must be one of: - - **c** - int8_t (1-byte signed char) - - **u** - uint8_t (1-byte unsigned char) - - **h** - int16_t (2-byte signed int) - - **H** - uint16_t (2-byte unsigned int) - - **i** - int32_t (4-byte signed int) - - **I** - uint32_t (4-byte unsigned int) - - **l** - int64_t (8-byte signed int) - - **L** - uint64_t (8-byte unsigned int) - - **f** - 4-byte single-precision float - - **d** - 8-byte double-precision float - - **x** - use to skip *ncols* anywhere in the record + - **c**: int8_t (1-byte signed char) + - **u**: uint8_t (1-byte unsigned char) + - **h**: int16_t (2-byte signed int) + - **H**: uint16_t (2-byte unsigned int) + - **i**: int32_t (4-byte signed int) + - **I**: uint32_t (4-byte unsigned int) + - **l**: int64_t (8-byte signed int) + - **L**: uint64_t (8-byte unsigned int) + - **f**: 4-byte single-precision float + - **d**: 8-byte double-precision float + - **x**: use to skip *ncols* anywhere in the record For records with mixed types, append additional comma-separated combinations of *ncols* *type* (no space). The following modifiers @@ -84,9 +84,9 @@ **e**\|\ **f**\|\ **g**. Determine how spherical distances are calculated. - - **e** - Ellipsoidal (or geodesic) mode - - **f** - Flat Earth mode - - **g** - Great circle distance [Default] + - **e**: Ellipsoidal (or geodesic) mode + - **f**: Flat Earth mode + - **g**: Great circle distance [Default] All spherical distance calculations depend on the current ellipsoid (:gmt-term:`PROJ_ELLIPSOID`), the definition of the mean radius @@ -118,16 +118,16 @@ a list with each item containing a string describing one set of criteria. - - **x**\|\ **X** - define a gap when there is a large enough + - **x**\|\ **X**: define a gap when there is a large enough change in the x coordinates (upper case to use projected coordinates). - - **y**\|\ **Y** - define a gap when there is a large enough + - **y**\|\ **Y**: define a gap when there is a large enough change in the y coordinates (upper case to use projected coordinates). - - **d**\|\ **D** - define a gap when there is a large enough + - **d**\|\ **D**: define a gap when there is a large enough distance between coordinates (upper case to use projected coordinates). - - **z** - define a gap when there is a large enough change in + - **z**: define a gap when there is a large enough change in the z data. Use **+c**\ *col* to change the z data column [Default *col* is 2 (i.e., 3rd column)]. @@ -146,9 +146,9 @@ One of the following modifiers can be appended: - - **+n** - specify that the previous value minus the current + - **+n**: specify that the previous value minus the current column value must exceed *gap* for a break to be imposed. - - **+p** - specify that the current value minus the previous + - **+p**: specify that the current value minus the previous value must exceed *gap* for a break to be imposed.""", "grid": r""" grid : str or xarray.DataArray @@ -367,13 +367,13 @@ Select verbosity level [Default is **w**], which modulates the messages written to stderr. Choose among 7 levels of verbosity: - - **q** - Quiet, not even fatal error messages are produced - - **e** - Error messages only - - **w** - Warnings [Default] - - **t** - Timings (report runtimes for time-intensive algorithms) - - **i** - Informational messages (same as ``verbose=True``) - - **c** - Compatibility warnings - - **d** - Debugging messages""", + - **q**: Quiet, not even fatal error messages are produced + - **e**: Error messages only + - **w**: Warnings [Default] + - **t**: Timings (report runtimes for time-intensive algorithms) + - **i**: Informational messages (same as ``verbose=True``) + - **c**: Compatibility warnings + - **d**: Debugging messages""", "wrap": r""" wrap : str **y**\|\ **a**\|\ **w**\|\ **d**\|\ **h**\|\ **m**\|\ **s**\|\ @@ -382,14 +382,14 @@ different column if selected via **+c**\ *col*. The following cyclical coordinate transformations are supported: - - **y** - yearly cycle (normalized) - - **a** - annual cycle (monthly) - - **w** - weekly cycle (day) - - **d** - daily cycle (hour) - - **h** - hourly cycle (minute) - - **m** - minute cycle (second) - - **s** - second cycle (second) - - **c** - custom cycle (normalized) + - **y**: yearly cycle (normalized) + - **a**: annual cycle (monthly) + - **w**: weekly cycle (day) + - **d**: daily cycle (hour) + - **h**: hourly cycle (minute) + - **m**: minute cycle (second) + - **s**: second cycle (second) + - **c**: custom cycle (normalized) Full documentation is at :gmt-docs:`gmt.html#w-full`.""", } diff --git a/pygmt/src/grdfilter.py b/pygmt/src/grdfilter.py index 1ad044d0b5e..786e280dd61 100644 --- a/pygmt/src/grdfilter.py +++ b/pygmt/src/grdfilter.py @@ -48,13 +48,13 @@ def grdfilter(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None [/*width2*\][*modifiers*]. Name of the filter type you wish to apply, followed by the *width*: - - **b** - Box Car - - **c** - Cosine Arch - - **g** - Gaussian - - **o** - Operator - - **m** - Median - - **p** - Maximum Likelihood probability - - **h** - Histogram + - **b**: Box Car + - **c**: Cosine Arch + - **g**: Gaussian + - **o**: Operator + - **m**: Median + - **p**: Maximum Likelihood probability + - **h**: Histogram distance : str State how the grid (x,y) relates to the filter *width*: diff --git a/pygmt/src/grdgradient.py b/pygmt/src/grdgradient.py index a793be04df0..96c4c61937f 100644 --- a/pygmt/src/grdgradient.py +++ b/pygmt/src/grdgradient.py @@ -69,11 +69,11 @@ def grdgradient(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | No Find the direction of the positive (up-slope) gradient of the data. The following options are supported: - - **a** - Find the aspect (i.e., the down-slope direction) - - **c** - Use the conventional Cartesian angles measured + - **a**: Find the aspect (i.e., the down-slope direction) + - **c**: Use the conventional Cartesian angles measured counterclockwise from the positive x (east) direction. - - **o** - Report orientations (0-180) rather than directions (0-360). - - **n** - Add 90 degrees to all angles (e.g., to give local strikes of + - **o**: Report orientations (0-180) rather than directions (0-360). + - **n**: Add 90 degrees to all angles (e.g., to give local strikes of the surface). radiance : str or list [**m**\|\ **s**\|\ **p**]\ *azim/elev*\ [**+a**\ *ambient*][**+d**\ @@ -102,14 +102,14 @@ def grdgradient(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | No given, it is set to the average of :math:`g`. The following forms are supported: - - **True** - Normalize using :math:`g_n = \mbox{{amp}}\ + - **True**: Normalize using :math:`g_n = \mbox{{amp}}\ (\frac{{g - \mbox{{offset}}}}{{max(|g - \mbox{{offset}}|)}})` - - **e** - Normalize using a cumulative Laplace distribution yielding: + - **e**: Normalize using a cumulative Laplace distribution yielding: :math:`g_n = \mbox{{amp}}(1 - \ \exp{{(\sqrt{{2}}\frac{{g - \mbox{{offset}}}}{{\sigma}}))}}`, where :math:`\sigma` is estimated using the L1 norm of :math:`(g - \mbox{{offset}})` if it is not given. - - **t** - Normalize using a cumulative Cauchy distribution yielding: + - **t**: Normalize using a cumulative Cauchy distribution yielding: :math:`g_n = \ \frac{{2(\mbox{{amp}})}}{{\pi}}(\tan^{{-1}}(\frac{{g - \ \mbox{{offset}}}}{{\sigma}}))` where :math:`\sigma` is estimated diff --git a/pygmt/src/grdview.py b/pygmt/src/grdview.py index ba7341046e0..77ace573a30 100644 --- a/pygmt/src/grdview.py +++ b/pygmt/src/grdview.py @@ -76,12 +76,12 @@ def grdview(self, grid, **kwargs): Specify cover type of the grid. Select one of following settings: - - **m** - mesh plot [Default]. - - **mx** or **my** - waterfall plots (row or column profiles). - - **s** - surface plot, and optionally append **m** to have mesh lines + - **m**: mesh plot [Default]. + - **mx** or **my**: waterfall plots (row or column profiles). + - **s**: surface plot, and optionally append **m** to have mesh lines drawn on top of the surface. - - **i** - image plot. - - **c** - Same as **i** but will make nodes with z = NaN transparent. + - **i**: image plot. + - **c**: Same as **i** but will make nodes with z = NaN transparent. For any of these choices, you may force a monochrome image by appending the modifier **+m**. diff --git a/pygmt/src/sphdistance.py b/pygmt/src/sphdistance.py index ab49f961e03..279db4e3590 100644 --- a/pygmt/src/sphdistance.py +++ b/pygmt/src/sphdistance.py @@ -65,10 +65,10 @@ def sphdistance( Specify the quantity that should be assigned to the grid nodes [Default is **d**]: - - **d** - compute distances to the nearest data point - - **n** - assign the ID numbers of the Voronoi polygons that each + - **d**: compute distances to the nearest data point + - **n**: assign the ID numbers of the Voronoi polygons that each grid node is inside - - **z** - assign all nodes inside the polygon the z-value of the center + - **z**: assign all nodes inside the polygon the z-value of the center node for a natural nearest-neighbor grid. Optionally, append the resampling interval along Voronoi arcs in diff --git a/pygmt/src/x2sys_cross.py b/pygmt/src/x2sys_cross.py index f7d9f8e1843..4e209d33d18 100644 --- a/pygmt/src/x2sys_cross.py +++ b/pygmt/src/x2sys_cross.py @@ -145,9 +145,9 @@ def x2sys_cross( Sets the interpolation mode for estimating values at the crossover. Choose among: - - **l** - Linear interpolation [Default]. - - **a** - Akima spline interpolation. - - **c** - Cubic spline interpolation. + - **l**: Linear interpolation [Default]. + - **a**: Akima spline interpolation. + - **c**: Cubic spline interpolation. coe : str Use **e** for external COEs only, and **i** for internal COEs only diff --git a/pygmt/src/x2sys_init.py b/pygmt/src/x2sys_init.py index 99af7211424..6e36263fe0e 100644 --- a/pygmt/src/x2sys_init.py +++ b/pygmt/src/x2sys_init.py @@ -85,13 +85,13 @@ def x2sys_init(tag, **kwargs): programs. Append **d** for distance or **s** for speed, then give the desired *unit* as: - - **c** - Cartesian userdist or userdist/usertime - - **e** - meters or m/s - - **f** - feet or ft/s - - **k** - kilometers or km/hr - - **m** - miles or mi/hr - - **n** - nautical miles or knots - - **u** - survey feet or sft/s + - **c**: Cartesian userdist or userdist/usertime + - **e**: meters or m/s + - **f**: feet or ft/s + - **k**: kilometers or km/hr + - **m**: miles or mi/hr + - **n**: nautical miles or knots + - **u**: survey feet or sft/s [Default is ``units=["dk", "se"]`` (km and m/s) if ``discontinuity`` is set, and ``units=["dc", "sc"]`` otherwise (e.g., for Cartesian units)]. From d33e5ac5286e260db534c1c8925ee544ecfa4a9e Mon Sep 17 00:00:00 2001 From: Michael Grund <23025878+michaelgrund@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:54:14 +0100 Subject: [PATCH 12/20] DOC: Make usage of "lowercase" and "uppercase" consistent across all docs (#3756) --- doc/techref/patterns.md | 2 +- examples/gallery/basemaps/double_y_axes.py | 2 +- examples/gallery/embellishments/scalebar.py | 5 +++-- examples/gallery/images/cross_section.py | 2 +- examples/gallery/lines/decorated_lines.py | 2 +- examples/gallery/lines/quoted_lines.py | 2 +- examples/gallery/symbols/multi_parameter_symbols.py | 2 +- examples/projections/cyl/cyl_oblique_mercator.py | 2 +- examples/projections/nongeo/cartesian_linear.py | 2 +- examples/projections/nongeo/cartesian_logarithmic.py | 2 +- examples/projections/nongeo/cartesian_power.py | 2 +- examples/projections/nongeo/polar.py | 2 +- examples/tutorials/basics/frames.py | 4 ++-- pygmt/helpers/decorators.py | 6 +++--- pygmt/src/filter1d.py | 2 +- pygmt/src/project.py | 2 +- pygmt/src/text.py | 8 ++++---- 17 files changed, 25 insertions(+), 24 deletions(-) diff --git a/doc/techref/patterns.md b/doc/techref/patterns.md index 17deb045aa3..de7df02d047 100644 --- a/doc/techref/patterns.md +++ b/doc/techref/patterns.md @@ -11,7 +11,7 @@ image raster file. The former will result in one of the 90 predefined 64x64 bit- provided by GMT (see the figure below). The latter allows the user to create customized, repeating images using image raster files. -By specifying upper case **P** instead of **p** the image will be bit-reversed, i.e., +By specifying uppercase **P** instead of **p** the image will be bit-reversed, i.e., white and black areas will be interchanged (only applies to 1-bit images or predefined bit-image patterns). For these patterns and other 1-bit images one may specify alternative **b**ackground and **f**oreground colors (by appending **+b**_color_ and/or diff --git a/examples/gallery/basemaps/double_y_axes.py b/examples/gallery/basemaps/double_y_axes.py index aa8ba8a6815..c6f970e59ca 100644 --- a/examples/gallery/basemaps/double_y_axes.py +++ b/examples/gallery/basemaps/double_y_axes.py @@ -5,7 +5,7 @@ The ``frame`` parameter of the plotting methods of the :class:`pygmt.Figure` class can control which axes should be plotted and optionally show annotations, tick marks, and gridlines. By default, all 4 axes are plotted, along with -annotations and tick marks (denoted **W**, **S**, **E**, **N**). Lower case +annotations and tick marks (denoted **W**, **S**, **E**, **N**). Lowercase versions (**w**, **s**, **e**, **n**) can be used to denote to only plot the axes with tick marks. We can also only plot the axes without annotations and tick marks using **l** (left axis), **r** (right axis), **t** (top axis), diff --git a/examples/gallery/embellishments/scalebar.py b/examples/gallery/embellishments/scalebar.py index bca7d0b9703..4b829165fe5 100644 --- a/examples/gallery/embellishments/scalebar.py +++ b/examples/gallery/embellishments/scalebar.py @@ -12,8 +12,9 @@ - **g**: Give map coordinates as *longitude*\/\ *latitude*. - **j**\|\ **J**: Specify a two-character (order independent) code. Choose from vertical **T**\(op), **M**\(iddle), or **B**\(ottom) and - horizontal **L**\(eft), **C**\(entre), or **R**\(ight). Lower / upper - case **j** / **J** mean inside / outside of the map bounding box. + horizontal **L**\(eft), **C**\(entre), or **R**\(ight). Lower / + uppercase **j** / **J** mean inside / outside of the map bounding + box. - **n**: Give normalized bounding box coordinates as *nx*\/\ *ny*. - **x**: Give plot coordinates as *x*\/\ *y*. diff --git a/examples/gallery/images/cross_section.py b/examples/gallery/images/cross_section.py index 0f0cd8e352e..d451c8c3ee8 100644 --- a/examples/gallery/images/cross_section.py +++ b/examples/gallery/images/cross_section.py @@ -37,7 +37,7 @@ # Add a colorbar for the elevation fig.colorbar( - # Place the colorbar inside the plot (lower-case "j") in the Bottom Right (BR) + # Place the colorbar inside the plot (lowercase "j") in the Bottom Right (BR) # corner with an offset ("+o") of 0.7 centimeters and 0.3 centimeters in x or y # directions, respectively; move the x label above the horizontal colorbar ("+ml") position="jBR+o0.7c/0.8c+h+w5c/0.3c+ml", diff --git a/examples/gallery/lines/decorated_lines.py b/examples/gallery/lines/decorated_lines.py index 42ad5dc7be9..cbe6b6b510d 100644 --- a/examples/gallery/lines/decorated_lines.py +++ b/examples/gallery/lines/decorated_lines.py @@ -51,7 +51,7 @@ "~d1c:+sd0.5c+gtan+p1p,black+n-0.2c/0.1c", # Give the number of equally spaced symbols by using "n" instead of "d" "~n6:+sn0.5c+gtan+p1p,black", - # Use upper-case "N" to have symbols at the start and end of the line + # Use uppercase "N" to have symbols at the start and end of the line "~N6:+sh0.5c+gtan+p1p,black", # Suppress the main decorated line by appending "+i" "~d1c:+sg0.5c+gtan+p1p,black+i", diff --git a/examples/gallery/lines/quoted_lines.py b/examples/gallery/lines/quoted_lines.py index 9e70ec15c4a..2ccfb1309a6 100644 --- a/examples/gallery/lines/quoted_lines.py +++ b/examples/gallery/lines/quoted_lines.py @@ -33,7 +33,7 @@ "qd1c:+ltext+i", # Give the number of equally spaced labels by using "n" instead of "d" "qn5:+ltext", - # Use upper-case "N" to have labels at the start and end of the line + # Use uppercase "N" to have labels at the start and end of the line "qN5:+ltext", # To only plot a label at the start of the line use "N-1" "qN-1:+ltext", diff --git a/examples/gallery/symbols/multi_parameter_symbols.py b/examples/gallery/symbols/multi_parameter_symbols.py index 6baa607f1c5..72480aba063 100644 --- a/examples/gallery/symbols/multi_parameter_symbols.py +++ b/examples/gallery/symbols/multi_parameter_symbols.py @@ -33,7 +33,7 @@ # directions given in degrees counter-clockwise from horizontal. Append **+i** and the # desired value to apply an inner diameter. # -# Upper-case versions **E**, **J**, and **W** are similar to **e**, **j**, and **w** +# Uppercase versions **E**, **J**, and **W** are similar to **e**, **j**, and **w** # but expect geographic azimuths and distances. fig = pygmt.Figure() diff --git a/examples/projections/cyl/cyl_oblique_mercator.py b/examples/projections/cyl/cyl_oblique_mercator.py index 17bf589d48b..22db4f973da 100644 --- a/examples/projections/cyl/cyl_oblique_mercator.py +++ b/examples/projections/cyl/cyl_oblique_mercator.py @@ -9,7 +9,7 @@ The projection is set with **o** or **O**. There are three different specification ways (**a**\|\ **A**, **b**\|\ **B**, **c**\|\ **C**) available. For all three -definitions, the upper case letter mean the projection pole is set in the southern +definitions, the uppercase letter mean the projection pole is set in the southern hemisphere [Default is northern hemisphere]. Align the y-axis with the optional modifier **+v**. The figure size is set with *scale* or *width*. """ diff --git a/examples/projections/nongeo/cartesian_linear.py b/examples/projections/nongeo/cartesian_linear.py index 021d080ae94..83e986f6739 100644 --- a/examples/projections/nongeo/cartesian_linear.py +++ b/examples/projections/nongeo/cartesian_linear.py @@ -4,7 +4,7 @@ **X**\ *width*\ [/*height*] or **x**\ *x-scale*\ [/*y-scale*] -Give the *width* of the figure and the optional *height*. The lower-case version +Give the *width* of the figure and the optional *height*. The lowercase version **x** is similar to **X** but expects an *x-scale* and an optional *y-scale*. The Cartesian linear projection is primarily designed for regular floating point diff --git a/examples/projections/nongeo/cartesian_logarithmic.py b/examples/projections/nongeo/cartesian_logarithmic.py index ef354dba73f..0e4619075ea 100644 --- a/examples/projections/nongeo/cartesian_logarithmic.py +++ b/examples/projections/nongeo/cartesian_logarithmic.py @@ -6,7 +6,7 @@ **x**\ *x-scale*\ [**l**][/*y-scale*\ [**l**]] Give the *width* of the figure and the optional *height*. -The lower-case version **x** is similar to **X** but expects +The lowercase version **x** is similar to **X** but expects an *x-scale* and an optional *y-scale*. Each axis with a logarithmic transformation requires **l** after its size argument. diff --git a/examples/projections/nongeo/cartesian_power.py b/examples/projections/nongeo/cartesian_power.py index 862ceba8595..b5f856713ed 100644 --- a/examples/projections/nongeo/cartesian_power.py +++ b/examples/projections/nongeo/cartesian_power.py @@ -6,7 +6,7 @@ **x**\ *x-scale*\ [**p**\ *pvalue*][/*y-scale*\ [**p**\ *pvalue*]] Give the *width* of the figure and the optional argument *height*. -The lower-case version **x** is similar to **X** but expects +The lowercase version **x** is similar to **X** but expects an *x-scale* and an optional *y-scale*. Each axis with a power transformation requires **p** and the exponent for that axis after its size argument. diff --git a/examples/projections/nongeo/polar.py b/examples/projections/nongeo/polar.py index 5c71e517c17..760838b34ac 100644 --- a/examples/projections/nongeo/polar.py +++ b/examples/projections/nongeo/polar.py @@ -12,7 +12,7 @@ Limits are set via the ``region`` parameter ([*theta_min*, *theta_max*, *radius_min*, *radius_max*]). When using **P**\ *width* you -have to give the *width* of the figure. The lower-case version **p** is similar to **P** +have to give the *width* of the figure. The lowercase version **p** is similar to **P** but expects a *scale* instead of a width (**p**\ *scale*). The following customizing modifiers are available: diff --git a/examples/tutorials/basics/frames.py b/examples/tutorials/basics/frames.py index 56b4e9ac130..b3fb466fa23 100644 --- a/examples/tutorials/basics/frames.py +++ b/examples/tutorials/basics/frames.py @@ -90,9 +90,9 @@ # :meth:`pygmt.Figure.basemap`. The map boundaries (or plot axes) are named as # West/west/left (**W**, **w**, **l**), South/south/bottom # (**S**, **s**, **b**), North/north/top (**N**, **n**, **t**), and -# East/east/right (**E**, **e**, **r**) sides of a figure. If an upper-case +# East/east/right (**E**, **e**, **r**) sides of a figure. If an uppercase # letter (**W**, **S**, **N**, **E**) is passed, the axis is plotted with -# tick marks and annotations. The lower-case version +# tick marks and annotations. The lowercase version # (**w**, **s**, **n**, **e**) plots the axis only with tick marks. # To only plot the axis pass **l**, **b**, **t**, **r**. By default # (``frame=True`` or ``frame="af"``), the West and the South axes are diff --git a/pygmt/helpers/decorators.py b/pygmt/helpers/decorators.py index 08822d42a86..3c4f9dd5510 100644 --- a/pygmt/helpers/decorators.py +++ b/pygmt/helpers/decorators.py @@ -119,13 +119,13 @@ criteria. - **x**\|\ **X**: define a gap when there is a large enough - change in the x coordinates (upper case to use projected + change in the x coordinates (uppercase to use projected coordinates). - **y**\|\ **Y**: define a gap when there is a large enough - change in the y coordinates (upper case to use projected + change in the y coordinates (uppercase to use projected coordinates). - **d**\|\ **D**: define a gap when there is a large enough - distance between coordinates (upper case to use projected + distance between coordinates (uppercase to use projected coordinates). - **z**: define a gap when there is a large enough change in the z data. Use **+c**\ *col* to change the z data column diff --git a/pygmt/src/filter1d.py b/pygmt/src/filter1d.py index 206c3a2fd48..6d0e97938cf 100644 --- a/pygmt/src/filter1d.py +++ b/pygmt/src/filter1d.py @@ -75,7 +75,7 @@ def filter1d( - **u**: upper (absolute). Return maximum of all values. - **U**: upper. Return maximum of all negative values only. - Upper case type **B**, **C**, **G**, **M**, **P**, **F** will use + Uppercase type **B**, **C**, **G**, **M**, **P**, **F** will use robust filter versions: i.e., replace outliers (2.5 L1 scale off median, using 1.4826 \* median absolute deviation [MAD]) with median during filtering. diff --git a/pygmt/src/project.py b/pygmt/src/project.py index f90e517b202..a49d5a1ad1f 100644 --- a/pygmt/src/project.py +++ b/pygmt/src/project.py @@ -136,7 +136,7 @@ def project( convention : str Specify the desired output using any combination of **xyzpqrs**, in any order [Default is **xypqrsz**]. Do not space between the letters. - Use lower case. The output will be columns of values corresponding to + Use lowercase. The output will be columns of values corresponding to your ``convention``. The **z** flag is special and refers to all numerical columns beyond the leading **x** and **y** in your input record. The **z** flag also includes any trailing text (which is diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 75b2653043c..b507510f620 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -138,11 +138,11 @@ def text_( # noqa: PLR0912 **i** for inches, or **p** for points; if not given we consult :gmt-term:`PROJ_LENGTH_UNIT`) or *%* for a percentage of the font size. Optionally, use modifier **+t** to set the shape of the text - box when using ``fill`` and/or ``pen``. Append lower case **o** - to get a straight rectangle [Default is **o**]. Append upper case + box when using ``fill`` and/or ``pen``. Append lowercase **o** + to get a straight rectangle [Default is **o**]. Append uppercase **O** to get a rounded rectangle. In paragraph mode (*paragraph*) - you can also append lower case **c** to get a concave rectangle or - append upper case **C** to get a convex rectangle. + you can also append lowercase **c** to get a concave rectangle or + append uppercase **C** to get a convex rectangle. fill : str Set color for filling text boxes [Default is no fill]. offset : str From 0bf733f54ca6f2e387954be2cbc146f7ca59739d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 9 Jan 2025 08:55:21 +0800 Subject: [PATCH 13/20] clib.conversion._to_numpy: Add tests for numpy array with np.datetime64 dtypes (#3687) --- pygmt/tests/test_clib_to_numpy.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pygmt/tests/test_clib_to_numpy.py b/pygmt/tests/test_clib_to_numpy.py index c3bc413a0b1..79858bc04d4 100644 --- a/pygmt/tests/test_clib_to_numpy.py +++ b/pygmt/tests/test_clib_to_numpy.py @@ -153,6 +153,36 @@ def test_to_numpy_numpy_string(dtype): npt.assert_array_equal(result, array) +@pytest.mark.parametrize( + "dtype", + [ + np.datetime64, # The expected dtype is "datetime64[D]" for this test. + "datetime64[Y]", + "datetime64[M]", + "datetime64[W]", + "datetime64[D]", + "datetime64[h]", + "datetime64[m]", + "datetime64[s]", + "datetime64[ms]", + "datetime64[us]", + "datetime64[ns]", + ], +) +def test_to_numpy_numpy_datetime(dtype): + """ + Test the _to_ndarray function with 1-D NumPy arrays of datetime. + + Time units "fs", "as", "ps" are not tested here because they can only represent a + small range of times in 1969-1970. + """ + array = np.array(["2024-01-01", "2024-01-02", "2024-01-03"], dtype=dtype) + result = _to_numpy(array) + _check_result(result, np.datetime64) + assert result.dtype == (dtype if isinstance(dtype, str) else "datetime64[D]") + npt.assert_array_equal(result, array) + + ######################################################################################## # Test the _to_numpy function with pandas.Series. # From 9e912baec890b1af009c0eac2b344229a9285e36 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 9 Jan 2025 10:38:40 +0800 Subject: [PATCH 14/20] clib.converison._to_numpy: Add tests for pandas.Series with datetime dtypes (#3670) --- pygmt/clib/conversion.py | 11 +++ pygmt/tests/test_clib_to_numpy.py | 107 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/pygmt/clib/conversion.py b/pygmt/clib/conversion.py index c54923705dc..52eec0d2479 100644 --- a/pygmt/clib/conversion.py +++ b/pygmt/clib/conversion.py @@ -192,6 +192,17 @@ def _to_numpy(data: Any) -> np.ndarray: numpy_dtype = np.float64 data = data.to_numpy(na_value=np.nan) + # Deal with timezone-aware datetime dtypes. + if isinstance(dtype, pd.DatetimeTZDtype): # pandas.DatetimeTZDtype + numpy_dtype = getattr(dtype, "base", None) + elif isinstance(dtype, pd.ArrowDtype) and hasattr(dtype.pyarrow_dtype, "tz"): + # pd.ArrowDtype[pa.Timestamp] + numpy_dtype = getattr(dtype, "numpy_dtype", None) + # TODO(pandas>=2.1): Remove the workaround for pandas<2.1. + if Version(pd.__version__) < Version("2.1"): + # In pandas 2.0, dtype.numpy_type is dtype("O"). + numpy_dtype = np.dtype(f"M8[{dtype.pyarrow_dtype.unit}]") # type: ignore[assignment, attr-defined] + array = np.ascontiguousarray(data, dtype=numpy_dtype) # Check if a np.object_ array can be converted to np.str_. diff --git a/pygmt/tests/test_clib_to_numpy.py b/pygmt/tests/test_clib_to_numpy.py index 79858bc04d4..31b6c2421e8 100644 --- a/pygmt/tests/test_clib_to_numpy.py +++ b/pygmt/tests/test_clib_to_numpy.py @@ -365,6 +365,113 @@ def test_to_numpy_pandas_date(dtype, expected_dtype): ) +pandas_old_version = pytest.mark.xfail( + condition=Version(pd.__version__) < Version("2.1"), + reason="pandas 2.0 bug reported in https://github.com/pandas-dev/pandas/issues/52705", +) + + +@pytest.mark.parametrize( + ("dtype", "expected_dtype"), + [ + # NumPy datetime64 types. Only unit 's'/'ms'/'us'/'ns' are supported. + pytest.param("datetime64[s]", "datetime64[s]", id="datetime64[s]"), + pytest.param("datetime64[ms]", "datetime64[ms]", id="datetime64[ms]"), + pytest.param("datetime64[us]", "datetime64[us]", id="datetime64[us]"), + pytest.param("datetime64[ns]", "datetime64[ns]", id="datetime64[ns]"), + # pandas.DatetimeTZDtype can be given in two ways [tz is required]: + # 1. pandas.DatetimeTZDtype(unit, tz) + # 2. String aliases: "datetime64[unit, tz]" + pytest.param( + "datetime64[s, UTC]", + "datetime64[s]", + id="datetime64[s, tz=UTC]", + marks=pandas_old_version, + ), + pytest.param( + "datetime64[s, America/New_York]", + "datetime64[s]", + id="datetime64[s, tz=America/New_York]", + marks=pandas_old_version, + ), + pytest.param( + "datetime64[s, +07:30]", + "datetime64[s]", + id="datetime64[s, +07:30]", + marks=pandas_old_version, + ), + # PyArrow timestamp types can be given in two ways [tz is optional]: + # 1. pd.ArrowDtype(pyarrow.Timestamp(unit, tz=tz)) + # 2. String aliases: "timestamp[unit, tz][pyarrow]" + pytest.param( + "timestamp[s][pyarrow]", + "datetime64[s]", + id="timestamp[s][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + pytest.param( + "timestamp[ms][pyarrow]", + "datetime64[ms]", + id="timestamp[ms][pyarrow]", + marks=[skip_if_no(package="pyarrow"), pandas_old_version], + ), + pytest.param( + "timestamp[us][pyarrow]", + "datetime64[us]", + id="timestamp[us][pyarrow]", + marks=[skip_if_no(package="pyarrow"), pandas_old_version], + ), + pytest.param( + "timestamp[ns][pyarrow]", + "datetime64[ns]", + id="timestamp[ns][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + pytest.param( + "timestamp[s, UTC][pyarrow]", + "datetime64[s]", + id="timestamp[s, UTC][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + pytest.param( + "timestamp[s, America/New_York][pyarrow]", + "datetime64[s]", + id="timestamp[s, America/New_York][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + pytest.param( + "timestamp[s, +08:00][pyarrow]", + "datetime64[s]", + id="timestamp[s, +08:00][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + ], +) +def test_to_numpy_pandas_datetime(dtype, expected_dtype): + """ + Test the _to_numpy function with pandas.Series of datetime types. + """ + series = pd.Series( + [pd.Timestamp("2024-01-02T03:04:05"), pd.Timestamp("2024-01-02T03:04:06")], + dtype=dtype, + ) + result = _to_numpy(series) + _check_result(result, np.datetime64) + assert result.dtype == expected_dtype + + # Convert to UTC if the dtype is timezone-aware + if "," in str(dtype): # A hacky way to decide if the dtype is timezone-aware. + # TODO(pandas>=2.1): Simplify the if-else statement. + if Version(pd.__version__) < Version("2.1") and dtype.startswith("timestamp"): + # pandas 2.0 doesn't have the dt.tz_convert method for pyarrow.Timestamp. + series = pd.to_datetime(series, utc=True) + else: + series = series.dt.tz_convert("UTC") + # Remove time zone information and preserve local time. + expected_series = series.dt.tz_localize(tz=None) + npt.assert_array_equal(result, np.array(expected_series, dtype=expected_dtype)) + + ######################################################################################## # Test the _to_numpy function with PyArrow arrays. # From 504138458b9fde3d26f45abdce7b617c49c13e41 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:37:33 +1300 Subject: [PATCH 15/20] CI: Separate jobs for publishing to TestPyPI and PyPI (#3742) --- .github/workflows/publish-to-pypi.yml | 58 ++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 2dbc12cbef1..15c66e1842b 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -35,13 +35,9 @@ on: # - main jobs: - publish-pypi: - name: Publish to PyPI + build: + name: Build distribution 📦 runs-on: ubuntu-latest - permissions: - # This permission is mandatory for OIDC publishing - id-token: write - if: github.repository == 'GenericMappingTools/pygmt' steps: - name: Checkout @@ -49,6 +45,7 @@ jobs: with: # fetch all history so that setuptools-scm works fetch-depth: 0 + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5.3.0 @@ -74,11 +71,54 @@ jobs: echo "Generated files:" ls -lh dist/ - - name: Publish to Test PyPI + - name: Store the distribution packages + uses: actions/upload-artifact@v4.5.0 + with: + name: python-package-distributions + path: dist/ + + publish-to-testpypi: + name: Publish Python 🐍 distribution 📦 to TestPyPI + if: github.repository == 'GenericMappingTools/pygmt' + needs: + - build + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/project/pygmt + permissions: + id-token: write # IMPORTANT: mandatory for trusted OIDC publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4.1.8 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution 📦 to TestPyPI uses: pypa/gh-action-pypi-publish@v1.12.3 with: repository-url: https://test.pypi.org/legacy/ - - name: Publish to PyPI - if: startsWith(github.ref, 'refs/tags') + publish-pypi: + name: Publish Python 🐍 distribution 📦 to PyPI + if: github.repository == 'GenericMappingTools/pygmt' && startsWith(github.ref, 'refs/tags/') + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/pygmt/ + permissions: + id-token: write # IMPORTANT: mandatory for trusted OIDC publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4.1.8 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@v1.12.3 From 753a06ed6b3485af95dfff833bebfc3fc55493f2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 Jan 2025 17:46:59 +0800 Subject: [PATCH 16/20] Bump to ruff 0.9.0, apply ruff 2025 style, and ignore A005 (stdlib-module-shadowing) violations (#3763) * Ignore A005 errors * Apply ruff styling changes in 2025 * Bump to ruff 0.9.0 --- doc/conf.py | 6 ++---- environment.yml | 2 +- .../tutorials/advanced/cartesian_histograms.py | 4 ++-- pygmt/_show_versions.py | 2 +- pygmt/encodings.py | 1 + pygmt/helpers/tempfile.py | 1 + pygmt/helpers/utils.py | 5 ++--- pygmt/io.py | 1 + pygmt/src/grd2xyz.py | 3 +-- pygmt/src/select.py | 1 + .../test_clib_virtualfile_from_stringio.py | 17 +++-------------- pygmt/tests/test_grdview.py | 2 +- pygmt/tests/test_info.py | 10 ++-------- 13 files changed, 19 insertions(+), 36 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 2b41acd3f61..f3cb59228c2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -210,12 +210,10 @@ repository = "GenericMappingTools/pygmt" repository_url = "https://github.com/GenericMappingTools/pygmt" if __commit__: - commit_link = ( - f'{ __commit__[:8] }' - ) + commit_link = f'{__commit__[:8]}' else: commit_link = ( - f'{ __version__ }' + f'{__version__}' ) html_context = { "menu_links": [ diff --git a/environment.yml b/environment.yml index ce30de4825c..c51b2967fc2 100644 --- a/environment.yml +++ b/environment.yml @@ -27,7 +27,7 @@ dependencies: # Dev dependencies (style checks) - codespell - pre-commit - - ruff>=0.8.2 + - ruff>=0.9.0 # Dev dependencies (unit testing) - matplotlib-base - pytest>=6.0 diff --git a/examples/tutorials/advanced/cartesian_histograms.py b/examples/tutorials/advanced/cartesian_histograms.py index 7191d18cbfd..3a547fd92f5 100644 --- a/examples/tutorials/advanced/cartesian_histograms.py +++ b/examples/tutorials/advanced/cartesian_histograms.py @@ -348,7 +348,7 @@ # of the bin width # Offset ("+o") the bars to align each bar with the left limit of the corresponding # bin - barwidth=f"{binwidth/2}+o-{binwidth/4}", + barwidth=f"{binwidth / 2}+o-{binwidth / 4}", label="data01", ) @@ -359,7 +359,7 @@ fill="orange", pen="1p,darkgray,solid", histtype=0, - barwidth=f"{binwidth/2}+o{binwidth/4}", + barwidth=f"{binwidth / 2}+o{binwidth / 4}", label="data02", ) diff --git a/pygmt/_show_versions.py b/pygmt/_show_versions.py index d15bf0799c6..f0d4b4e3c2f 100644 --- a/pygmt/_show_versions.py +++ b/pygmt/_show_versions.py @@ -16,7 +16,7 @@ from pygmt.clib import Session, __gmt_version__ # Get semantic version through setuptools-scm -__version__ = f'v{version("pygmt")}' # e.g. v0.1.2.dev3+g0ab3cd78 +__version__ = f"v{version('pygmt')}" # e.g. v0.1.2.dev3+g0ab3cd78 __commit__ = __version__.split("+g")[-1] if "+g" in __version__ else "" # 0ab3cd78 diff --git a/pygmt/encodings.py b/pygmt/encodings.py index 0c7b7ddc895..09c749c4c82 100644 --- a/pygmt/encodings.py +++ b/pygmt/encodings.py @@ -1,3 +1,4 @@ +# noqa: A005 """ Character encodings supported by GMT. diff --git a/pygmt/helpers/tempfile.py b/pygmt/helpers/tempfile.py index 6995be1db98..70cc688156a 100644 --- a/pygmt/helpers/tempfile.py +++ b/pygmt/helpers/tempfile.py @@ -1,3 +1,4 @@ +# noqa: A005 """ Utilities for dealing with temporary file management. """ diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index b53942818c5..e32f5bbe03f 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -574,9 +574,8 @@ def launch_external_viewer(fname: str, waiting: float = 0) -> None: } match sys.platform: - case name if ( - (name == "linux" or name.startswith("freebsd")) - and (xdgopen := shutil.which("xdg-open")) + 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 diff --git a/pygmt/io.py b/pygmt/io.py index 9451de36c8f..a4ba289c7d9 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -1,3 +1,4 @@ +# noqa: A005 """ PyGMT input/output (I/O) utilities. """ diff --git a/pygmt/src/grd2xyz.py b/pygmt/src/grd2xyz.py index a44dc996c6d..b31d0013a25 100644 --- a/pygmt/src/grd2xyz.py +++ b/pygmt/src/grd2xyz.py @@ -145,8 +145,7 @@ def grd2xyz( if kwargs.get("o") is not None and output_type == "pandas": msg = ( - "If 'outcols' is specified, 'output_type' must be either 'numpy' " - "or 'file'." + "If 'outcols' is specified, 'output_type' must be either 'numpy' or 'file'." ) raise GMTInvalidInput(msg) # Set the default column names for the pandas DataFrame header. diff --git a/pygmt/src/select.py b/pygmt/src/select.py index ecd6d12bfad..a7db421a210 100644 --- a/pygmt/src/select.py +++ b/pygmt/src/select.py @@ -1,3 +1,4 @@ +# noqa: A005 """ select - Select data table subsets based on multiple spatial criteria. """ diff --git a/pygmt/tests/test_clib_virtualfile_from_stringio.py b/pygmt/tests/test_clib_virtualfile_from_stringio.py index ce6de238a88..62daaa688c8 100644 --- a/pygmt/tests/test_clib_virtualfile_from_stringio.py +++ b/pygmt/tests/test_clib_virtualfile_from_stringio.py @@ -43,14 +43,9 @@ def test_virtualfile_from_stringio(): 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" + "# Comment\nH 24p Legend\nN 2\nS 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" ) + expected = ">\nH 24p Legend\nN 2\nS 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" assert _stringio_to_dataset(data) == expected @@ -66,13 +61,7 @@ def test_one_segment(): "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" - ) + expected = "> Segment 1\n1 2 3 ABC\n4 5 DE\n6 7 8 9 FGHIJK LMN OPQ\nRSTUVWXYZ\n" assert _stringio_to_dataset(data) == expected diff --git a/pygmt/tests/test_grdview.py b/pygmt/tests/test_grdview.py index f73b1150e54..3be4ed7aa42 100644 --- a/pygmt/tests/test_grdview.py +++ b/pygmt/tests/test_grdview.py @@ -161,7 +161,7 @@ def test_grdview_with_perspective_and_zaxis_frame(xrgrid, region): a Transverse Mercator (T) projection. """ fig = Figure() - projection = f"T{(region[0]+region[1])/2}/{abs((region[2]+region[3])/2)}" + projection = f"T{(region[0] + region[1]) / 2}/{abs((region[2] + region[3]) / 2)}" fig.grdview( grid=xrgrid, projection=projection, diff --git a/pygmt/tests/test_info.py b/pygmt/tests/test_info.py index 3ac9f27c4e1..d055abb61ec 100644 --- a/pygmt/tests/test_info.py +++ b/pygmt/tests/test_info.py @@ -23,10 +23,7 @@ def test_info(): """ output = info(data=POINTS_DATA) expected_output = ( - f"{POINTS_DATA}: N = 20 " - "<11.5309/61.7074> " - "<-2.9289/7.8648> " - "<0.1412/0.9338>\n" + f"{POINTS_DATA}: N = 20 <11.5309/61.7074> <-2.9289/7.8648> <0.1412/0.9338>\n" ) assert output == expected_output @@ -57,10 +54,7 @@ def test_info_path(table): """ output = info(data=table) expected_output = ( - f"{POINTS_DATA}: N = 20 " - "<11.5309/61.7074> " - "<-2.9289/7.8648> " - "<0.1412/0.9338>\n" + f"{POINTS_DATA}: N = 20 <11.5309/61.7074> <-2.9289/7.8648> <0.1412/0.9338>\n" ) assert output == expected_output From 2bd524eb1e3780e598b3e8789a11be36042ee335 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 11 Jan 2025 13:58:20 +0800 Subject: [PATCH 17/20] Simplify doc/Makefile with sphinx-build make-mode (#3761) --- doc/Makefile | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index b82215c44c4..5656fe80ab6 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,14 +1,12 @@ # Makefile for Sphinx documentation # You can set these variables from the command line. -SPHINXOPTS = -j auto -SPHINXBUILD = sphinx-build +SPHINXOPTS ?= -j auto +SPHINXBUILD ?= sphinx-build SPHINXAUTOGEN = sphinx-autogen +SOURCEDIR = . BUILDDIR = _build -# Internal variables. -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . - .PHONY: help all api html server clean help: @@ -28,20 +26,20 @@ api: @echo $(SPHINXAUTOGEN) -i -t _templates -o api/generated api/*.rst -html: api +html latex: api @echo - @echo "Building HTML files." + @echo "Building "$@" files." @echo # Set PYGMT_USE_EXTERNAL_DISPLAY to "false" to disable external display - PYGMT_USE_EXTERNAL_DISPLAY="false" $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + PYGMT_USE_EXTERNAL_DISPLAY="false" $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + @echo "Build finished. The files are in $(BUILDDIR)/$@." html-noplot: api @echo @echo "Building HTML files without example plots." @echo - $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + $(SPHINXBUILD) -D plot_gallery=0 -M html $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." From 77a2d5929dcbad02c8b10e40f3920f5f3131344d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 12 Jan 2025 14:05:24 +0800 Subject: [PATCH 18/20] Fix the 'html-noplot' make target for docs (#3766) --- doc/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Makefile b/doc/Makefile index 5656fe80ab6..04b1c1ab549 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -39,7 +39,7 @@ html-noplot: api @echo @echo "Building HTML files without example plots." @echo - $(SPHINXBUILD) -D plot_gallery=0 -M html $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) + $(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) -D plot_gallery=0 @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." From 8b9ec5a6cea7b1352b52783534fe76bc9f99dd77 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 13 Jan 2025 08:11:09 +0800 Subject: [PATCH 19/20] Fix the bug of converting Python sequence of datetime-like objects (#3760) Convert unrecognized objects to datetime before converting to string dtype --- pygmt/clib/conversion.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pygmt/clib/conversion.py b/pygmt/clib/conversion.py index 52eec0d2479..7823aa32103 100644 --- a/pygmt/clib/conversion.py +++ b/pygmt/clib/conversion.py @@ -205,6 +205,11 @@ def _to_numpy(data: Any) -> np.ndarray: array = np.ascontiguousarray(data, dtype=numpy_dtype) + # Check if a np.object_ or np.str_ array can be converted to np.datetime64. + if array.dtype.type in {np.object_, np.str_}: + with contextlib.suppress(TypeError, ValueError): + return np.ascontiguousarray(array, dtype=np.datetime64) + # Check if a np.object_ array can be converted to np.str_. if array.dtype == np.object_: with contextlib.suppress(TypeError, ValueError): From 900e8d4f3cda1f29eb0a5c6d7cabe7cc6ae1f655 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 13 Jan 2025 08:22:32 +0800 Subject: [PATCH 20/20] clib.conversion._to_numpy: Add tests for Python sequence of datetime-like objects (#3758) Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> --- .../tests/baseline/test_plot_datetime.png.dvc | 4 +- pygmt/tests/test_clib_to_numpy.py | 77 +++++++++++++++++-- pygmt/tests/test_plot.py | 7 +- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/pygmt/tests/baseline/test_plot_datetime.png.dvc b/pygmt/tests/baseline/test_plot_datetime.png.dvc index 714104995ba..1450b29ef82 100644 --- a/pygmt/tests/baseline/test_plot_datetime.png.dvc +++ b/pygmt/tests/baseline/test_plot_datetime.png.dvc @@ -1,5 +1,5 @@ outs: -- md5: 583947facaa873122f0bf18137809cd4 - size: 12695 +- md5: 0a2eae0da1e3d5b71d7392de1c081346 + size: 13124 path: test_plot_datetime.png hash: md5 diff --git a/pygmt/tests/test_clib_to_numpy.py b/pygmt/tests/test_clib_to_numpy.py index 31b6c2421e8..40b45e466d8 100644 --- a/pygmt/tests/test_clib_to_numpy.py +++ b/pygmt/tests/test_clib_to_numpy.py @@ -2,8 +2,8 @@ Tests for the _to_numpy function in the clib.conversion module. """ +import datetime import sys -from datetime import date, datetime import numpy as np import numpy.testing as npt @@ -80,6 +80,70 @@ def test_to_numpy_python_types(data, expected_dtype): npt.assert_array_equal(result, data) +@pytest.mark.parametrize( + "data", + [ + pytest.param( + ["2018", "2018-02", "2018-03-01", "2018-04-01T01:02:03"], id="iso8601" + ), + pytest.param( + [ + datetime.date(2018, 1, 1), + datetime.datetime(2018, 2, 1), + datetime.date(2018, 3, 1), + datetime.datetime(2018, 4, 1, 1, 2, 3), + ], + id="datetime", + ), + pytest.param( + [ + np.datetime64("2018"), + np.datetime64("2018-02"), + np.datetime64("2018-03-01"), + np.datetime64("2018-04-01T01:02:03"), + ], + id="np_datetime64", + ), + pytest.param( + [ + pd.Timestamp("2018-01-01"), + pd.Timestamp("2018-02-01"), + pd.Timestamp("2018-03-01"), + pd.Timestamp("2018-04-01T01:02:03"), + ], + id="pd_timestamp", + ), + pytest.param( + [ + "2018-01-01", + np.datetime64("2018-02-01"), + datetime.datetime(2018, 3, 1), + pd.Timestamp("2018-04-01T01:02:03"), + ], + id="mixed", + ), + ], +) +def test_to_numpy_python_datetime(data): + """ + Test the _to_numpy function with Python sequence of datetime types. + """ + result = _to_numpy(data) + assert result.dtype.type == np.datetime64 + npt.assert_array_equal( + result, + np.array( + [ + "2018-01-01T00:00:00", + "2018-02-01T00:00:00", + "2018-03-01T00:00:00", + "2018-04-01T01:02:03", + ], + dtype="datetime64[s]", + ), + ) + + ######################################################################################## # Test the _to_numpy function with NumPy arrays. # @@ -603,9 +667,9 @@ def test_to_numpy_pyarrow_date(dtype, expected_dtype): Here we explicitly check the dtype and date unit of the result. """ data = [ - date(2024, 1, 1), - datetime(2024, 1, 2), - datetime(2024, 1, 3), + datetime.date(2024, 1, 1), + datetime.datetime(2024, 1, 2), + datetime.datetime(2024, 1, 3), ] array = pa.array(data, type=dtype) result = _to_numpy(array) @@ -649,7 +713,10 @@ def test_to_numpy_pyarrow_timestamp(dtype, expected_dtype): Reference: https://arrow.apache.org/docs/python/generated/pyarrow.timestamp.html """ - data = [datetime(2024, 1, 2, 3, 4, 5), datetime(2024, 1, 2, 3, 4, 6)] + data = [ + datetime.datetime(2024, 1, 2, 3, 4, 5), + datetime.datetime(2024, 1, 2, 3, 4, 6), + ] array = pa.array(data, type=dtype) result = _to_numpy(array) _check_result(result, np.datetime64) diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index 721b7841307..c2f2b846724 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -467,9 +467,14 @@ def test_plot_datetime(): fig.plot(x=x, y=y, style="a0.2c", pen="1p") # the Python built-in datetime and date - x = [datetime.date(2018, 1, 1), datetime.datetime(2019, 1, 1)] + x = [datetime.date(2018, 1, 1), datetime.datetime(2019, 1, 1, 0, 0, 0)] y = [8.5, 9.5] fig.plot(x=x, y=y, style="i0.2c", pen="1p") + + # Python sequence of pd.Timestamp + x = [pd.Timestamp("2018-01-01"), pd.Timestamp("2019-01-01")] + y = [5.5, 6.5] + fig.plot(x=x, y=y, style="d0.2c", pen="1p") return fig