From 2b67be0ff06c69f779bc6bedb4b17910180ec9bc Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:41:50 -0400 Subject: [PATCH 01/11] move raven_hydro/raven model management to __init__.py --- src/ravenpy/__init__.py | 29 ++++++++++++++++++++++++++++- src/ravenpy/config/defaults.py | 4 ++-- src/ravenpy/config/rvs.py | 11 +++++++---- src/ravenpy/ravenpy.py | 20 ++++++++------------ 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/ravenpy/__init__.py b/src/ravenpy/__init__.py index da5670bc..ee8c0941 100644 --- a/src/ravenpy/__init__.py +++ b/src/ravenpy/__init__.py @@ -24,9 +24,36 @@ # SOFTWARE. ################################################################################### +import os +import shutil + from .ravenpy import Emulator, EnsembleReader, OutputReader, RavenWarning, run -__all__ = ["Emulator", "EnsembleReader", "OutputReader", "RavenWarning", "run"] +__all__ = [ + "RAVEN_EXEC_PATH", + "Emulator", + "EnsembleReader", + "OutputReader", + "RavenWarning", + "__author__", + "__email__", + "__raven_version__", + "__version__", + "run", +] + + +RAVEN_EXEC_PATH = os.getenv("RAVENPY_RAVEN_BINARY_PATH") or shutil.which("raven") + +if not RAVEN_EXEC_PATH: + raise RuntimeError( + "Could not find raven binary in PATH and RAVENPY_RAVEN_BINARY_PATH env variable is not set." + ) + +try: + from raven_hydro import __raven_version__ +except ImportError: + __raven_version__ = "0.0.0" __author__ = """David Huard""" __email__ = "huard.david@ouranos.ca" diff --git a/src/ravenpy/config/defaults.py b/src/ravenpy/config/defaults.py index 3b5eee6e..bd063bca 100644 --- a/src/ravenpy/config/defaults.py +++ b/src/ravenpy/config/defaults.py @@ -1,4 +1,4 @@ -from raven_hydro import __raven_version__ +from ravenpy import __raven_version__ units = { "PRECIP": "mm/d", @@ -65,5 +65,5 @@ def default_nc_attrs(): return { "history": f"Created on {now} by Raven {version}", "references": "Craig, J.R., and the Raven Development Team, Raven user's and developer's manual " - f"(Version {version}), URL: https://raven.uwaterloo.ca/ (2025).", + f"(Version {version}), URL: https://raven.uwaterloo.ca/ ({dt.datetime.today().year}).", } diff --git a/src/ravenpy/config/rvs.py b/src/ravenpy/config/rvs.py index 5c4683ca..7c944ecc 100644 --- a/src/ravenpy/config/rvs.py +++ b/src/ravenpy/config/rvs.py @@ -6,13 +6,18 @@ import cftime from pydantic import ConfigDict, Field, ValidationInfo, field_validator -from raven_hydro import __raven_version__ from ..config import commands as rc from ..config import options as o from ..config import processes as rp from .base import RV, Sym, optfield, parse_symbolic +try: + from raven_hydro import __raven_version__ +except ImportError: + __raven_version__ = "0.0.0" + + """ Generic Raven model configuration. @@ -268,12 +273,10 @@ def header(rv): import ravenpy - version = __raven_version__ - return dedent( f""" ########################################################################################################### - :FileType {rv.upper()} Raven {version} + :FileType {rv.upper()} Raven {__raven_version__} :WrittenBy RavenPy {ravenpy.__version__} based on setups provided by James Craig and Juliane Mai :CreationDate {dt.datetime.now().isoformat(timespec="seconds")} #---------------------------------------------------------------------------------------------------------- diff --git a/src/ravenpy/ravenpy.py b/src/ravenpy/ravenpy.py index 7b5e299d..707b111f 100644 --- a/src/ravenpy/ravenpy.py +++ b/src/ravenpy/ravenpy.py @@ -12,11 +12,11 @@ import xarray as xr +from ravenpy import RAVEN_EXEC_PATH + from .config import parsers from .config.rvs import Config -RAVEN_EXEC_PATH = os.getenv("RAVENPY_RAVEN_BINARY_PATH") or shutil.which("raven") - class Emulator: def __init__( @@ -278,18 +278,14 @@ def run( overwrite : bool If True, overwrite existing files. verbose : bool - If True, always display Raven warnings. If False, warnings will only be printed if an error occurs. + If True, always display Raven warnings. + If False, warnings will only be printed if an error occurs. Returns ------- Path - Path to model outputs. + The path to the model outputs. """ - if not RAVEN_EXEC_PATH: - raise RuntimeError( - "Could not find raven binary in PATH, and RAVENPY_RAVEN_BINARY_PATH env variable is not set" - ) - # Confirm configdir exists configdir = Path(configdir).absolute() if not configdir.exists(): @@ -318,7 +314,7 @@ def run( ) stdout, stderr = process.communicate(input="\n") - returncode = process.wait() + return_code = process.wait() # Deal with errors and warnings messages = parsers.parse_raven_messages(outputdir / "Raven_errors.txt") @@ -332,8 +328,8 @@ def run( "\n".join([f"Config directory: {configdir}"] + messages["ERROR"]) ) - if returncode != 0: - raise OSError(f"Raven Error (code: {returncode}): \n{stdout}\n{stderr}") + if return_code != 0: + raise OSError(f"Raven Error (code: {return_code}): \n{stdout}\n{stderr}") return outputdir From b7977a0df3fee9bb5b2f4495729e50e9f55d9f78 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:43:47 -0400 Subject: [PATCH 02/11] add test using monkeypatch for testing missing raven-hydro behaviour --- tests/test_missing.py | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_missing.py diff --git a/tests/test_missing.py b/tests/test_missing.py new file mode 100644 index 00000000..c8283d3a --- /dev/null +++ b/tests/test_missing.py @@ -0,0 +1,70 @@ +import os +import shutil +import sys + +import pytest + + +def filter_raven(): + # Find the real conda-installed tool path + real_tool = shutil.which("raven") + assert real_tool is not None, "raven must be installed for this test" + + # Get the directory the conda tool is in + conda_tool_dir = os.path.dirname(real_tool) + + # Create a new PATH that includes everything *except* the conda env's tool directory + filtered_path = os.pathsep.join( + [ + p + for p in os.environ["PATH"].split(os.pathsep) + if os.path.abspath(p) != os.path.abspath(conda_tool_dir) + ] + ) + + return filtered_path + + +@pytest.fixture +def hide_module(monkeypatch): + def _hide(name): + monkeypatch.setitem(sys.modules, name, None) + + return _hide + + +class TestMissing: + + def test_missing_raven_binary(self, monkeypatch, tmpdir, hide_module): + """Test for behaviour when binary is missing from the system path.""" + + # Set up a temporary directory to simulate the absence of the raven binary + filtered_path = filter_raven() + monkeypatch.setenv("PATH", f"{tmpdir}{os.pathsep}{filtered_path}") + + # Hide the raven_hydro module + hide_module("raven_hydro") + hide_module("raven_hydro._version") + hide_module("raven_hydro.libraven") + + # Force the raven modules to be reloaded + del sys.modules["ravenpy"] + del sys.modules["ravenpy.config.defaults"] + + # Now the tool should be "missing" + assert shutil.which("raven") is None + + # Loading the module should raise a RuntimeError + with pytest.raises(RuntimeError): + import ravenpy # noqa: F401 + + # Check that setting the RAVENPY_RAVEN_BINARY_PATH environment variable works + monkeypatch.setenv("RAVENPY_RAVEN_BINARY_PATH", f"some/path/to/raven") + import ravenpy + + assert ravenpy.RAVEN_EXEC_PATH == "some/path/to/raven" + + # Check that the raven_hydro library is not imported + from ravenpy.config.defaults import __raven_version__ + + assert __raven_version__ == "0.0.0" From bb3ecc3ba9f0602eb8fc5d6865cd34200ef98dc5 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:44:36 -0400 Subject: [PATCH 03/11] make raven-hydro installation optional --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 428a4e25..1a9cb527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,6 @@ dependencies = [ "pydantic >=2.0", "pydap >=3.4.0", # Note: As of 2025-03-18 (v3.5.4) does not support Python 3.13 "pymbolic >=2024.2", - "raven-hydro >=0.4.0,<1.0", "scipy >=1.11.0", "spotpy >=1.6.1", "statsmodels >=0.14.2", @@ -137,10 +136,14 @@ gis = [ "setuptools >=71.0", "shapely >=2.0" ] +raven-hydro = [ + "raven-hydro >=0.4.0,<1.0" +] all = [ "ravenpy[dev]", "ravenpy[docs]", - "ravenpy[gis]" + "ravenpy[gis]", + "ravenpy[raven-hydro]" ] [project.scripts] From d3c1a93a113cccb9f03a36a3c0cd5a381f967c93 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:44:51 -0400 Subject: [PATCH 04/11] innocuous fix --- tests/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 856f8078..9e8ff7aa 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -20,8 +20,8 @@ def test_nc_specs_bad(bad_netcdf): @pytest.mark.online def test_dap_specs(): # Link to THREDDS Data Server netCDF testdata - TDS = "https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/testdata/raven" - fn = f"{TDS}/raven-gr4j-cemaneige/Salmon-River-Near-Prince-George_meteo_daily.nc" + tds = "https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/testdata/raven" + fn = f"{tds}/raven-gr4j-cemaneige/Salmon-River-Near-Prince-George_meteo_daily.nc" attrs = nc_specs(fn, "PRECIP", station_idx=1, alt_names=("rain",), engine="pydap") assert "units" in attrs From a47fa76ea1edb9036dfa7019e40260a09ec51bd2 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:45:02 -0400 Subject: [PATCH 05/11] update CHANGELOG.rst --- CHANGELOG.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b67d04e3..b1d24dec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,14 @@ Changelog ========= +v0.18.1 (2025-04-16) +-------------------- + +New features +^^^^^^^^^^^^ +* RavenPy no longer requires `raven-hydro` to be installed. The Raven model executable can now be provided by explicitly setting the `RAVENPY_RAVEN_BINARY_PATH` environment variable. (PR #482). + + v0.18.0 (2025-04-03) -------------------- From 04072db787955109fa60e3ed5407627b06566c38 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:31:22 -0400 Subject: [PATCH 06/11] move raven-hydro handling to hidden module --- src/ravenpy/__init__.py | 30 +----------------------------- src/ravenpy/_raven.py | 16 ++++++++++++++++ tests/test_missing.py | 1 + 3 files changed, 18 insertions(+), 29 deletions(-) create mode 100644 src/ravenpy/_raven.py diff --git a/src/ravenpy/__init__.py b/src/ravenpy/__init__.py index ee8c0941..ad8fc5b9 100644 --- a/src/ravenpy/__init__.py +++ b/src/ravenpy/__init__.py @@ -24,37 +24,9 @@ # SOFTWARE. ################################################################################### -import os -import shutil - +from ._raven import RAVEN_EXEC_PATH, __raven_version__ # noqa: F401 from .ravenpy import Emulator, EnsembleReader, OutputReader, RavenWarning, run -__all__ = [ - "RAVEN_EXEC_PATH", - "Emulator", - "EnsembleReader", - "OutputReader", - "RavenWarning", - "__author__", - "__email__", - "__raven_version__", - "__version__", - "run", -] - - -RAVEN_EXEC_PATH = os.getenv("RAVENPY_RAVEN_BINARY_PATH") or shutil.which("raven") - -if not RAVEN_EXEC_PATH: - raise RuntimeError( - "Could not find raven binary in PATH and RAVENPY_RAVEN_BINARY_PATH env variable is not set." - ) - -try: - from raven_hydro import __raven_version__ -except ImportError: - __raven_version__ = "0.0.0" - __author__ = """David Huard""" __email__ = "huard.david@ouranos.ca" __version__ = "0.18.0" diff --git a/src/ravenpy/_raven.py b/src/ravenpy/_raven.py new file mode 100644 index 00000000..886c0187 --- /dev/null +++ b/src/ravenpy/_raven.py @@ -0,0 +1,16 @@ +"""Configurations for raven-hydro.""" + +import os +import shutil + +RAVEN_EXEC_PATH = os.getenv("RAVENPY_RAVEN_BINARY_PATH") or shutil.which("raven") + +if not RAVEN_EXEC_PATH: + raise RuntimeError( + "Could not find raven binary in PATH and RAVENPY_RAVEN_BINARY_PATH env variable is not set." + ) + +try: + from raven_hydro import __raven_version__ +except ImportError: + __raven_version__ = "0.0.0" diff --git a/tests/test_missing.py b/tests/test_missing.py index c8283d3a..055c65eb 100644 --- a/tests/test_missing.py +++ b/tests/test_missing.py @@ -49,6 +49,7 @@ def test_missing_raven_binary(self, monkeypatch, tmpdir, hide_module): # Force the raven modules to be reloaded del sys.modules["ravenpy"] + del sys.modules["ravenpy._raven"] del sys.modules["ravenpy.config.defaults"] # Now the tool should be "missing" From ffd8bfdf3b54c8682984f440fd3edd002ec0e74f Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:32:37 -0400 Subject: [PATCH 07/11] pin pydap temporarily --- CHANGELOG.rst | 5 ++++- environment-dev.yml | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b1d24dec..a6369bf9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,8 +7,11 @@ v0.18.1 (2025-04-16) New features ^^^^^^^^^^^^ -* RavenPy no longer requires `raven-hydro` to be installed. The Raven model executable can now be provided by explicitly setting the `RAVENPY_RAVEN_BINARY_PATH` environment variable. (PR #482). +* RavenPy no longer requires `raven-hydro` to be installed. The Raven model executable can now be provided by explicitly setting the `RAVENPY_RAVEN_BINARY_PATH` environment variable. (PR #486). +Internal changes +^^^^^^^^^^^^^^^^ +* `pydap` has been pinned below v3.5.5 temporarily until `xarray` offers support for it. (PR #486). v0.18.0 (2025-04-03) -------------------- diff --git a/environment-dev.yml b/environment-dev.yml index d5f56dc6..53442ad7 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -24,7 +24,7 @@ dependencies: - pint >=0.24.4 - platformdirs >=4.3.6 - pydantic >=2.0 - - pydap >=3.4.0 # Note: As of 2025-03-18 (v3.5.4) does not support Python 3.13 + - pydap >=3.4.0,<3.5.5 # pydap 3.5.5 is not currently supported by `xarray` (v2025.3.1) - pymetalink >=6.5.2 - pymbolic >=2024.2 - pyproj >=3.3.0 diff --git a/pyproject.toml b/pyproject.toml index 1a9cb527..cae6d918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pint >=0.24.4", "platformdirs >=4.3.6", "pydantic >=2.0", - "pydap >=3.4.0", # Note: As of 2025-03-18 (v3.5.4) does not support Python 3.13 + "pydap >=3.4.0,<3.5.5", # pydap 3.5.5 is not currently supported by `xarray` (v2025.3.1) "pymbolic >=2024.2", "scipy >=1.11.0", "spotpy >=1.6.1", From 2514677035131565a1ecb1694c3d3a20f66ac855 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:02:31 -0400 Subject: [PATCH 08/11] add raven-hydro to `tox` config, update installation.rst --- docs/installation.rst | 8 +++++++- tox.ini | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 8b06ac15..191bb88d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -57,10 +57,16 @@ Then, from your python environment, run: .. code-block:: console - python -m pip install ravenpy[gis] + python -m pip install ravenpy[gis,raven-hydro] If desired, the core functions of `RavenPy` can be installed without its GIS functionalities as well. This implementation of RavenPy is much lighter on dependencies and can be installed easily with `pip`, without the need for `conda` or `virtualenv`. + .. code-block:: console + + python -m pip install ravenpy[raven-hydro] + +Finally, if you wish to provide your own `Raven` binary, you can install `RavenPy` without installing the `raven-hydro` package: + .. code-block:: console python -m pip install ravenpy diff --git a/tox.ini b/tox.ini index 39b66dff..4ed94689 100644 --- a/tox.ini +++ b/tox.ini @@ -58,6 +58,7 @@ passenv = extras = dev gis + raven-hydro download = true install_command = python -m pip install --no-user {opts} {packages} From 7ea43d2502c8ca7eebf3e6de995fe248ab157e10 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:44:00 -0400 Subject: [PATCH 09/11] update pydantic to 2.11+, address DeprecationWarnings --- environment-dev.yml | 2 +- environment-docs.yml | 2 +- pyproject.toml | 2 +- src/ravenpy/config/base.py | 8 +++++--- tests/test_rvs.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/environment-dev.yml b/environment-dev.yml index 53442ad7..2c058ebb 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -23,7 +23,7 @@ dependencies: - pandas >=2.2.0 - pint >=0.24.4 - platformdirs >=4.3.6 - - pydantic >=2.0 + - pydantic >=2.11 - pydap >=3.4.0,<3.5.5 # pydap 3.5.5 is not currently supported by `xarray` (v2025.3.1) - pymetalink >=6.5.2 - pymbolic >=2024.2 diff --git a/environment-docs.yml b/environment-docs.yml index 88383d81..c680058d 100644 --- a/environment-docs.yml +++ b/environment-docs.yml @@ -31,7 +31,7 @@ dependencies: - netCDF4 >=1.7.2 - numpy >=1.24.0 - notebook - - pydantic >=2.0 + - pydantic >=2.11 - pymetalink >=6.5.2 - s3fs - salib diff --git a/pyproject.toml b/pyproject.toml index cae6d918..ecede85c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dependencies = [ "pandas >=2.2.0", "pint >=0.24.4", "platformdirs >=4.3.6", - "pydantic >=2.0", + "pydantic >=2.11", "pydap >=3.4.0,<3.5.5", # pydap 3.5.5 is not currently supported by `xarray` (v2025.3.1) "pymbolic >=2024.2", "scipy >=1.11.0", diff --git a/src/ravenpy/config/base.py b/src/ravenpy/config/base.py index 3bc46716..585b9085 100644 --- a/src/ravenpy/config/base.py +++ b/src/ravenpy/config/base.py @@ -144,7 +144,8 @@ def __subcommands__(self) -> tuple[dict[str, str], list]: """Return dictionary of class attributes that are Raven models.""" cmds = {} recs = [] - for key, field in self.model_fields.items(): + cls = self.__class__ + for key, field in cls.model_fields.items(): obj = self.__dict__[key] if obj is not None: if issubclass(obj.__class__, _Record): @@ -225,8 +226,9 @@ class LineCommand(FlatCommand): """ def to_rv(self): - out = [f":{self.__class__.__name__:<20}"] - for field in self.model_fields.keys(): + cls = self.__class__ + out = [f":{cls.__name__:<20}"] + for field in cls.model_fields.keys(): out.append(str(getattr(self, field))) # noqa: PERF401 return " ".join(out) + "\n" diff --git a/tests/test_rvs.py b/tests/test_rvs.py index c59589eb..d142cbb0 100644 --- a/tests/test_rvs.py +++ b/tests/test_rvs.py @@ -18,7 +18,7 @@ class Test(RV): a: bool = optfield(alias="a") t = Test() - assert not t.model_fields["a"].is_required() + assert not t.__class__.model_fields["a"].is_required() def test_rvi_datetime(): From 138978ddc84ea6cf5c27c95a81ef43247a02136a Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 15 Apr 2025 11:22:18 -0400 Subject: [PATCH 10/11] update CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a6369bf9..7a58497e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,7 @@ New features Internal changes ^^^^^^^^^^^^^^^^ * `pydap` has been pinned below v3.5.5 temporarily until `xarray` offers support for it. (PR #486). +* More than 7500 DeprecationWarnings emitted during the testing suite have been addressed. Minimum supported `pydantic` has been raised to v2.11. (PR #487). v0.18.0 (2025-04-03) -------------------- From 8c40e55ca12e8df630243a024489e09f5c86ede3 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 15 Apr 2025 11:44:55 -0400 Subject: [PATCH 11/11] fix imports --- src/ravenpy/config/rvs.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ravenpy/config/rvs.py b/src/ravenpy/config/rvs.py index 7c944ecc..460d4b84 100644 --- a/src/ravenpy/config/rvs.py +++ b/src/ravenpy/config/rvs.py @@ -1,7 +1,9 @@ import datetime as dt +import zipfile from collections.abc import Sequence from dataclasses import asdict, fields, is_dataclass from pathlib import Path +from textwrap import dedent from typing import Any, Optional, Union import cftime @@ -269,8 +271,6 @@ class Config(RVI, RVC, RVH, RVT, RVP, RVE): @staticmethod def header(rv): """Return the header to print at the top of each RV file.""" - from textwrap import dedent - import ravenpy return dedent( @@ -495,8 +495,6 @@ def zip( overwrite : bool If True, overwrite existing configuration zip file. """ - import zipfile - workdir = Path(workdir) if not workdir.exists(): workdir.mkdir(parents=True)