diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9dee91e35..d1ae0eef5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: # ubuntu-latest runner home is /home/runner; matches $HOME/hera_unittest_data # which is both bootstrap_unittest_data.sh's default and conftest's default. TEST_HERA: /home/runner/hera_unittest_data + # Silence jupyter_client's platformdirs migration DeprecationWarning in test output + JUPYTER_PLATFORM_DIRS: "1" services: mongodb: diff --git a/hera/measurements/GIS/raster/topography.py b/hera/measurements/GIS/raster/topography.py index e5fb79798..39a2861c0 100644 --- a/hera/measurements/GIS/raster/topography.py +++ b/hera/measurements/GIS/raster/topography.py @@ -37,6 +37,7 @@ def _open_raster(fheight): import xarray as xr import geopandas as gpd from shapely.geometry import Point +from pyproj import Transformer WSG84 = 4326 @@ -253,25 +254,36 @@ def convertPointsCRS(self, points, inputCRS, outputCRS, **kwargs): if isinstance(points, np.ndarray): if points.ndim == 1: - gdf = gpd.GeoDataFrame(geometry=gpd.points_from_xy([points[0]], [points[1]])) + x_vals, y_vals = [points[0]], [points[1]] else: - gdf = gpd.GeoDataFrame(geometry=gpd.points_from_xy(points[:, 0], points[:, 1])) + x_vals, y_vals = points[:, 0], points[:, 1] elif isinstance(points, pd.DataFrame): x_col = kwargs.get("x", "x") y_col = kwargs.get("y", "y") - gdf = gpd.GeoDataFrame(geometry=gpd.points_from_xy(points[x_col], points[y_col])) + x_vals, y_vals = points[x_col], points[y_col] elif isinstance(points, list): if len(points) == 1: - gdf = gpd.GeoDataFrame(geometry=gpd.points_from_xy([points[0][0]], [points[0][1]])) + x_vals, y_vals = [points[0][0]], [points[0][1]] else: - gdf = gpd.GeoDataFrame(geometry=gpd.points_from_xy([x[0] for x in points], [x[1] for x in points])) + x_vals, y_vals = [x[0] for x in points], [x[1] for x in points] else: raise ValueError(f"Unsupported type: {type(points)}") + gdf = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x_vals, y_vals)) gdf.set_crs(inputCRS, inplace=True) + + # A single-point GeoDataFrame.to_crs() triggers a spurious pyproj + # "ndim > 0 to a scalar" DeprecationWarning on the pinned geopandas/pyproj + # versions (the array-transform path mishandles length-1 arrays), so route + # the single-point case through a scalar pyproj transform instead. + if len(gdf) == 1: + transformer = Transformer.from_crs(inputCRS, outputCRS, always_xy=True) + x_out, y_out = transformer.transform(list(x_vals)[0], list(y_vals)[0]) + return gpd.GeoDataFrame(geometry=[Point(x_out, y_out)], crs=outputCRS) + return gdf.to_crs(outputCRS) def create_xarray(self, minx, miny, maxx, maxy, dxdy=30, inputCRS=WSG84): diff --git a/hera/riskassessment/agents/effects/Calculator.py b/hera/riskassessment/agents/effects/Calculator.py index 902eefad1..5105d969e 100755 --- a/hera/riskassessment/agents/effects/Calculator.py +++ b/hera/riskassessment/agents/effects/Calculator.py @@ -128,7 +128,7 @@ def __init__(self,tenbergeCoefficient,breathingRate=10*ureg.L/ureg.min,**kwargs) self.n = tenbergeCoefficient def calculate(self,concentrationField,field,breathingRate=10*ureg.L/ureg.min,time="datetime",inUnits=None): - """ + r""" Calculates the toxic load from a concentration field. \begin{equation} D(T) = \int_0^T C^n dt diff --git a/hera/riskassessment/agents/effects/InjuryLevel.py b/hera/riskassessment/agents/effects/InjuryLevel.py index f4631a635..ac5eb0565 100755 --- a/hera/riskassessment/agents/effects/InjuryLevel.py +++ b/hera/riskassessment/agents/effects/InjuryLevel.py @@ -98,7 +98,8 @@ def calculateContours(self, toxicLoads, time="datetime", x="x", y="y"): The data returns in km because itm is in km. """ - curBackend = plt.get_backend() + curBackend = plt.get_backend() + plt.close('all') plt.switch_backend("pdf") if time in toxicLoads.dims: @@ -138,6 +139,7 @@ def calculateContours(self, toxicLoads, time="datetime", x="x", y="y"): retList.append(ret) ret = None if ret.empty else pandas.concat(retList,ignore_index=True) + plt.close('all') plt.switch_backend(curBackend) return ret diff --git a/hera/simulations/gaussian/Meteorology.py b/hera/simulations/gaussian/Meteorology.py index 731909acb..70a57c86c 100644 --- a/hera/simulations/gaussian/Meteorology.py +++ b/hera/simulations/gaussian/Meteorology.py @@ -194,7 +194,7 @@ def getAirTemperature(self, height): default unit m :return: air temperature at C. - """ + r""" return self._temperature - 6.5e-3 * tonumber(height, ureg.m) * ureg.K def getAirDensity(self, height): @@ -213,7 +213,7 @@ def getAirDensity(self, height): default m :return: air density in kg/m**3 - """ + r""" P = unumToPint(self.getAirPressure(height)).m_as(ureg.mmHg) T = unumToPint(self.getAirTemperature(height)).m_as(ureg.degC) @@ -243,7 +243,7 @@ def _setPvalues(self): :return: The coefficient (dimensionless). - """ + r""" if (self.z0 is None or self.stability is None): return pstab = self._pvalues[self.stability] @@ -262,7 +262,7 @@ def getWindVelocity(self, height): default units [m] :return: The wind velocity at the requested height. - """ + r""" height = tonumber(height, ureg.m) refHeight = tonumber(self.refHeight, ureg.m) height = numpy.min([numpy.max([height, 0]), 300]) diff --git a/hera/simulations/gaussian/gasCloud.py b/hera/simulations/gaussian/gasCloud.py index 9cc1ff92d..2580d5bae 100644 --- a/hera/simulations/gaussian/gasCloud.py +++ b/hera/simulations/gaussian/gasCloud.py @@ -672,7 +672,7 @@ class Continuous(object): _FullKernel = None def __init__(self,dt,kernelsize,timetofinish=10*ureg.min): - """ + r""" Time to finish. the time (min) it take to reach 0.1. diff --git a/hera/simulations/hydrodynamics/nearWallFlow.py b/hera/simulations/hydrodynamics/nearWallFlow.py index 1ff6d3b4b..9647594b8 100644 --- a/hera/simulations/hydrodynamics/nearWallFlow.py +++ b/hera/simulations/hydrodynamics/nearWallFlow.py @@ -10,7 +10,7 @@ import numpy class functionG: - """ + r""" The G(Lambda,D) is defined implicitly (Eqn. 17.60, page 537): \begin{equation} diff --git a/hera/simulations/openFoam/CLI.py b/hera/simulations/openFoam/CLI.py index 78bd10227..21482d41a 100644 --- a/hera/simulations/openFoam/CLI.py +++ b/hera/simulations/openFoam/CLI.py @@ -490,11 +490,11 @@ def stochasticLagrangian_source_makeEscapedMassFile(args): data = data.interpolate() newstr = "/*--------------------------------*- C++ -*----------------------------------*\n" \ "| ========= | |\n" \ - "| \ / F ield | OpenFOAM: The Open Source CFD Toolbox |\n" \ - "| \ / O peration | Version: dev |\n" \ - "| \ / A nd | Web: www.OpenFOAM.org |\n" \ - "| \/ M anipulation | |\n" \ - "\*---------------------------------------------------------------------------*/\n" \ + "| \\ / F ield | OpenFOAM: The Open Source CFD Toolbox |\n" \ + "| \\ / O peration | Version: dev |\n" \ + "| \\ / A nd | Web: www.OpenFOAM.org |\n" \ + "| \\/ M anipulation | |\n" \ + "\\*---------------------------------------------------------------------------*/\n" \ "FoamFile\n{ version 2.0;\n format ascii;\n class scalarField;\n object kinematicCloudPositions;\n}\n" \ f"// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //\n\n{len(data)}\n(\n" for time in timesteps: diff --git a/hera/simulations/openFoam/eulerian/NavierStokes.old/postprocess/gui/morphology.py b/hera/simulations/openFoam/eulerian/NavierStokes.old/postprocess/gui/morphology.py index 9314774bc..9262e0c95 100644 --- a/hera/simulations/openFoam/eulerian/NavierStokes.old/postprocess/gui/morphology.py +++ b/hera/simulations/openFoam/eulerian/NavierStokes.old/postprocess/gui/morphology.py @@ -13001,7 +13001,7 @@ def areastatistics(): plt.plot(x1,y1,'*r') plt.plot(x2,y2,'or') plt.plot(x3,y3,'+r') - plt.title('$\lambda_f$') + plt.title(r'$\lambda_f$') plt.subplot(1,3,2) plt.imshow(zilp, origin='lower', interpolation='nearest',extent=[xmin,xmax,ymin,ymax]) # jet, Paired plt.colorbar() @@ -13009,7 +13009,7 @@ def areastatistics(): plt.plot(x1,y1,'*r') plt.plot(x2,y2,'or') plt.plot(x3,y3,'+r') - plt.title('$\lambda_p$') + plt.title(r'$\lambda_p$') plt.subplot(1,3,3) plt.imshow(zilh, origin='lower', interpolation='nearest',extent=[xmin,xmax,ymin,ymax], norm=matplotlib.colors.LogNorm()) # jet, Paired plt.colorbar() diff --git a/hera/simulations/openFoam/lagrangian/LSM/toolkit.py b/hera/simulations/openFoam/lagrangian/LSM/toolkit.py index 81360cfed..c5db5f1de 100644 --- a/hera/simulations/openFoam/lagrangian/LSM/toolkit.py +++ b/hera/simulations/openFoam/lagrangian/LSM/toolkit.py @@ -402,11 +402,11 @@ def makeSource(self, x, y, z, nParticles, type="Point", fileName="kinematicCloud """ string = "/*--------------------------------*- C++ -*----------------------------------*\n " \ "| ========= | |\n" \ - "| \ / F ield | OpenFOAM: The Open Source CFD Toolbox |\n" \ - "| \ / O peration | Version: dev |\n" \ - "| \ / A nd | Web: www.OpenFOAM.org |\n" \ - "| \/ M anipulation | |\n" \ - "\*---------------------------------------------------------------------------*/\n" \ + "| \\ / F ield | OpenFOAM: The Open Source CFD Toolbox |\n" \ + "| \\ / O peration | Version: dev |\n" \ + "| \\ / A nd | Web: www.OpenFOAM.org |\n" \ + "| \\/ M anipulation | |\n" \ + "\\*---------------------------------------------------------------------------*/\n" \ "FoamFile\n{ version 2.0;\n format ascii;\n class vectorField;\n" \ " object kinematicCloudPositions;\n}\n" \ "// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //\n\n" \ diff --git a/hera/simulations/openFoam/lagrangian/abstractLagrangianSolver.py b/hera/simulations/openFoam/lagrangian/abstractLagrangianSolver.py index 5f76ffcff..8767082b9 100644 --- a/hera/simulations/openFoam/lagrangian/abstractLagrangianSolver.py +++ b/hera/simulations/openFoam/lagrangian/abstractLagrangianSolver.py @@ -679,11 +679,11 @@ def writeParticlePositionFile(self, x, y, z, nParticles, dispersionName, type="P """ string = "/*--------------------------------*- C++ -*----------------------------------*\n " \ "| ========= | |\n" \ - "| \ / F ield | OpenFOAM: The Open Source CFD Toolbox |\n" \ - "| \ / O peration | Version: dev |\n" \ - "| \ / A nd | Web: www.OpenFOAM.org |\n" \ - "| \/ M anipulation | |\n" \ - "\*---------------------------------------------------------------------------*/\n" \ + "| \\ / F ield | OpenFOAM: The Open Source CFD Toolbox |\n" \ + "| \\ / O peration | Version: dev |\n" \ + "| \\ / A nd | Web: www.OpenFOAM.org |\n" \ + "| \\/ M anipulation | |\n" \ + "\\*---------------------------------------------------------------------------*/\n" \ "FoamFile\n{ version 2.0;\n format ascii;\n class vectorField;\n" \ " object kinematicCloudPositions;\n}\n" \ "// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //\n\n" \ diff --git a/hera/simulations/openFoam/toberewritten/wrf2of.py b/hera/simulations/openFoam/toberewritten/wrf2of.py index 5ee024f3a..d2a957006 100644 --- a/hera/simulations/openFoam/toberewritten/wrf2of.py +++ b/hera/simulations/openFoam/toberewritten/wrf2of.py @@ -284,11 +284,11 @@ def regrid3(data, factorxy, factorz): lines.append('// built with wrf2of.py for '+outputdir) lines.append("/*--------------------------------*- C++ -*----------------------------------*\'") lines.append('| ========= | |') -lines.append('| \ / F ield | OpenFOAM: The Open Source CFD Toolbox |') -lines.append('| \ / O peration | Version: 10.0 |') -lines.append('| \ / A nd | Web: www.OpenFOAM.org |') -lines.append('| \/ M anipulation | |') -lines.append('\*---------------------------------------------------------------------------*/') +lines.append('| \\ / F ield | OpenFOAM: The Open Source CFD Toolbox |') +lines.append('| \\ / O peration | Version: 10.0 |') +lines.append('| \\ / A nd | Web: www.OpenFOAM.org |') +lines.append('| \\/ M anipulation | |') +lines.append('\\*---------------------------------------------------------------------------*/') lines.append('FoamFile') lines.append('{') lines.append(' version 10.0;') diff --git a/hera/tests/test_logging_helpers.py b/hera/tests/test_logging_helpers.py new file mode 100644 index 000000000..5124a9aef --- /dev/null +++ b/hera/tests/test_logging_helpers.py @@ -0,0 +1,18 @@ +import warnings + +from hera.utils.logging import helpers + + +class TestGetDefaultLoggingConfig: + def test_no_deprecation_warning_when_creating_default_config(self, tmp_path, monkeypatch): + """Loading the packaged default config must not use deprecated importlib.resources APIs.""" + log_dir = tmp_path / "log" + log_dir.mkdir() + monkeypatch.setattr(helpers, "HERA_DEFAULT_LOG_DIR", log_dir) + + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + config = helpers.get_default_logging_config() + + assert isinstance(config, dict) + assert (log_dir / "heraLogging.config").is_file() diff --git a/hera/tests/test_no_invalid_escapes.py b/hera/tests/test_no_invalid_escapes.py new file mode 100644 index 000000000..ac799ce63 --- /dev/null +++ b/hera/tests/test_no_invalid_escapes.py @@ -0,0 +1,27 @@ +import pathlib +import warnings + +import pytest + +HERA_ROOT = pathlib.Path(__file__).resolve().parents[1] + + +def _python_sources(): + return sorted(HERA_ROOT.rglob("*.py")) + + +@pytest.mark.parametrize("path", _python_sources(), ids=lambda p: str(p.relative_to(HERA_ROOT))) +def test_no_invalid_escape_sequences(path): + """Compiling every hera source file must not emit invalid-escape warnings. + + Python <=3.11 emits DeprecationWarning, >=3.12 emits SyntaxWarning.""" + source = path.read_text(encoding="utf-8", errors="replace") + with warnings.catch_warnings(): + warnings.simplefilter("error", SyntaxWarning) + warnings.simplefilter("error", DeprecationWarning) + try: + compile(source, str(path), "exec") + except SyntaxError as err: + # Legacy files that never compiled (dead code) are out of scope here; + # they cannot emit escape warnings because they cannot be imported at all. + pytest.skip(f"pre-existing SyntaxError, file is not importable: {err.msg} (line {err.lineno})") diff --git a/hera/tests/test_topography.py b/hera/tests/test_topography.py index 0719ef4ad..35d366c94 100644 --- a/hera/tests/test_topography.py +++ b/hera/tests/test_topography.py @@ -256,6 +256,27 @@ def test_basic(self, topo_toolkit): converted = topo_toolkit.convertPointsCRS(points, inputCRS=4326, outputCRS=2039) assert converted.shape[0] == 2 + def test_single_point_no_deprecation_warning(self, topo_toolkit): + """A single-point conversion must not trigger pyproj's array-to-scalar + DeprecationWarning (issue971) — geopandas.to_crs() on a 1-row + GeoDataFrame mishandles length-1 arrays on the pinned pyproj/geopandas.""" + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + converted = topo_toolkit.convertPointsCRS([(35.1, 33.85)], inputCRS=4326, outputCRS=2039) + + assert converted.shape[0] == 1 + + def test_single_point_matches_multi_point_conversion(self, topo_toolkit): + """The single-point fast path must produce the same result as the + general (multi-point) to_crs path for the same coordinate.""" + single = topo_toolkit.convertPointsCRS([(35.1, 33.85)], inputCRS=4326, outputCRS=2039) + multi = topo_toolkit.convertPointsCRS([(35.1, 33.85), (36.05, 33.9)], inputCRS=4326, outputCRS=2039) + + assert single.geometry.iloc[0].x == pytest.approx(multi.geometry.iloc[0].x) + assert single.geometry.iloc[0].y == pytest.approx(multi.geometry.iloc[0].y) + class TestCreateElevationSTL: def test_basic(self, topo_toolkit): diff --git a/hera/utils/latex.py b/hera/utils/latex.py index 5c689d9d7..e67af8735 100644 --- a/hera/utils/latex.py +++ b/hera/utils/latex.py @@ -8,7 +8,7 @@ class bibItem: - """ + r""" Represents a single bib item. \bibitem{paper2} diff --git a/hera/utils/logging/helpers.py b/hera/utils/logging/helpers.py index db6dd89f6..05fae4f8f 100644 --- a/hera/utils/logging/helpers.py +++ b/hera/utils/logging/helpers.py @@ -3,7 +3,7 @@ import logging.config import os.path import pathlib -from importlib.resources import read_text +from importlib.resources import files HERA_DEFAULT_LOG_DIR = pathlib.Path.home() / ".pyhera" / "log" @@ -31,7 +31,7 @@ def get_default_logging_config(*, disable_existing_loggers: bool = False) -> dic """Load the default Hera logging configuration, creating it on first use.""" defaultLocalConfig = os.path.join(HERA_DEFAULT_LOG_DIR,'heraLogging.config') if not os.path.isfile(defaultLocalConfig): - defaultConfig = read_text('hera.utils.logging', 'heraLogging.config') + defaultConfig = files('hera.utils.logging').joinpath('heraLogging.config').read_text() with open(defaultLocalConfig,'w') as localConfig: localConfig.write(defaultConfig)