From c0aa657924eb677420145a1b903de929d14ccef5 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 26 Oct 2025 07:58:17 +0100 Subject: [PATCH 1/7] Add chemicals-based extended component database option --- pyproject.toml | 2 + src/neqsim/thermo/thermoTools.py | 113 +++++++++++++++++++++++++++++++ tests/test_extended_database.py | 26 +++++++ 3 files changed, 141 insertions(+) create mode 100644 tests/test_extended_database.py diff --git a/pyproject.toml b/pyproject.toml index 50466d44..87d8ddc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ pandas = [ { version = "^1.3.5", markers = "python_version == '3.8'" }, { version = "^2.0.3", markers = "python_version > '3.8'" }, ] +chemicals = { version = "^1.1.5", optional = true } matplotlib = { version = "^3.7.0", optional = true } jupyter = { version = "^1.0.0", optional = true } tabulate = { version = "^0.9.0", optional = true } @@ -30,6 +31,7 @@ pre-commit = "^3.5.0" # Higher versions require python 3.9+ [tool.poetry.extras] interactive = ["matplotlib", "jupyter", "tabulate"] +extended-database = ["chemicals"] [build-system] requires = ["poetry-core"] diff --git a/src/neqsim/thermo/thermoTools.py b/src/neqsim/thermo/thermoTools.py index 73457423..f34f6b1c 100644 --- a/src/neqsim/thermo/thermoTools.py +++ b/src/neqsim/thermo/thermoTools.py @@ -266,6 +266,7 @@ """ import logging +from dataclasses import dataclass from typing import List, Optional, Union import jpype import pandas @@ -315,6 +316,91 @@ } +class ExtendedDatabaseError(Exception): + """Raised when a component cannot be resolved in the extended database.""" + + +@dataclass +class _ChemicalComponentData: + name: str + CAS: str + tc: float + pc: float + omega: float + + +def _create_extended_database_provider(): + """Create a chemicals database provider.""" + + return _ChemicalsDatabaseProvider() + + +class _ChemicalsDatabaseProvider: + """Lookup component data from the `chemicals` package.""" + + def __init__(self): + try: + from chemicals.identifiers import CAS_from_any + from chemicals.critical import Pc, Tc, omega + except ImportError as exc: # pragma: no cover - import guard + raise ModuleNotFoundError( + "The 'chemicals' package is required to use the extended component database." + ) from exc + + self._cas_from_any = CAS_from_any + self._tc = Tc + self._pc = Pc + self._omega = omega + + def get_component(self, name: str) -> _ChemicalComponentData: + cas = self._cas_from_any(name) + if not cas: + raise ExtendedDatabaseError( + f"Component '{name}' was not found in the chemicals database." + ) + + tc = self._tc(cas) + pc = self._pc(cas) + omega = self._omega(cas) + + if None in (tc, pc, omega): + raise ExtendedDatabaseError( + f"Incomplete property data for '{name}' (CAS {cas})." + ) + + return _ChemicalComponentData( + name=name, + CAS=cas, + tc=float(tc), + pc=float(pc) / 1.0e5, # chemicals returns pressure in Pa + omega=float(omega), + ) + + +def _get_extended_provider(system): + provider = getattr(system, "_extended_database_provider", None) + if provider is None: + provider = _create_extended_database_provider() + system._extended_database_provider = provider # type: ignore[attr-defined] + return provider + + +@jpype.JImplementationFor("neqsim.thermo.system.SystemInterface") +class _SystemInterface: + def useExtendedDatabase(self, enable: bool = True): + """Enable or disable usage of the chemicals based component database.""" + + if enable: + provider = _create_extended_database_provider() + self._use_extended_database = True # type: ignore[attr-defined] + self._extended_database_provider = provider # type: ignore[attr-defined] + else: + self._use_extended_database = False # type: ignore[attr-defined] + if hasattr(self, "_extended_database_provider"): + delattr(self, "_extended_database_provider") + return self + + def fluid(name="srk", temperature=298.15, pressure=1.01325): """ Create a thermodynamic fluid system. @@ -1100,6 +1186,33 @@ def addComponent(thermoSystem, name, moles, unit="no", phase=-10): Returns: None """ + alias_name = name + try: + alias_name = jneqsim.thermo.component.Component.getComponentNameFromAlias(name) + except Exception: # pragma: no cover - defensive alias resolution + pass + + if getattr(thermoSystem, "_use_extended_database", False) and not jneqsim.util.database.NeqSimDataBase.hasComponent(alias_name): + try: + provider = _get_extended_provider(thermoSystem) + component_data = provider.get_component(name) + except (ExtendedDatabaseError, ModuleNotFoundError): + component_data = None + if component_data is not None: + if unit != "no" or phase != -10: + raise NotImplementedError( + "Extended database currently supports components specified in moles (unit='no') " + "without explicit phase targeting." + ) + thermoSystem.addComponent( + name, + moles, + component_data.tc, + component_data.pc, + component_data.omega, + ) + return + if phase == -10 and unit == "no": thermoSystem.addComponent(name, moles) elif phase == -10: diff --git a/tests/test_extended_database.py b/tests/test_extended_database.py new file mode 100644 index 00000000..e30f7475 --- /dev/null +++ b/tests/test_extended_database.py @@ -0,0 +1,26 @@ +import pytest + + +chemicals = pytest.importorskip("chemicals") + +from chemicals.critical import Pc, Tc, omega # type: ignore # noqa: E402 +from chemicals.identifiers import CAS_from_any # type: ignore # noqa: E402 + +from neqsim.thermo.thermoTools import addComponent, fluid + + +def test_use_extended_database_allows_missing_component(): + system = fluid("srk") + + with pytest.raises(Exception): + addComponent(system, "dimethylsulfoxide", 1.0) + + system.useExtendedDatabase(True) + addComponent(system, "dimethylsulfoxide", 1.0) + + component = system.getPhase(0).getComponent("dimethylsulfoxide") + cas = CAS_from_any("dimethylsulfoxide") + + assert pytest.approx(component.getTC(), rel=1e-6) == Tc(cas) + assert pytest.approx(component.getPC(), rel=1e-6) == Pc(cas) / 1.0e5 + assert pytest.approx(component.getAcentricFactor(), rel=1e-6) == omega(cas) From a94f6e7e22927ffba9c814c63c873654802dbdd1 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 26 Oct 2025 08:32:57 +0100 Subject: [PATCH 2/7] Load additional properties from chemicals extended DB --- pyproject.toml | 2 + src/neqsim/thermo/thermoTools.py | 277 ++++++++++++++++++++++++++++++- tests/test_extended_database.py | 71 ++++++++ 3 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 tests/test_extended_database.py diff --git a/pyproject.toml b/pyproject.toml index 50466d44..87d8ddc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ pandas = [ { version = "^1.3.5", markers = "python_version == '3.8'" }, { version = "^2.0.3", markers = "python_version > '3.8'" }, ] +chemicals = { version = "^1.1.5", optional = true } matplotlib = { version = "^3.7.0", optional = true } jupyter = { version = "^1.0.0", optional = true } tabulate = { version = "^0.9.0", optional = true } @@ -30,6 +31,7 @@ pre-commit = "^3.5.0" # Higher versions require python 3.9+ [tool.poetry.extras] interactive = ["matplotlib", "jupyter", "tabulate"] +extended-database = ["chemicals"] [build-system] requires = ["poetry-core"] diff --git a/src/neqsim/thermo/thermoTools.py b/src/neqsim/thermo/thermoTools.py index 73457423..c48f5a18 100644 --- a/src/neqsim/thermo/thermoTools.py +++ b/src/neqsim/thermo/thermoTools.py @@ -265,8 +265,10 @@ """ +import importlib import logging -from typing import List, Optional, Union +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union import jpype import pandas from jpype.types import * @@ -315,6 +317,256 @@ } +class ExtendedDatabaseError(Exception): + """Raised when a component cannot be resolved in the extended database.""" + + +@dataclass +class _ChemicalComponentData: + name: str + CAS: str + tc: float + pc: float + omega: float + molar_mass: Optional[float] = None + normal_boiling_point: Optional[float] = None + triple_point_temperature: Optional[float] = None + critical_volume: Optional[float] = None + critical_compressibility: Optional[float] = None + + +def _create_extended_database_provider(): + """Create a chemicals database provider.""" + + return _ChemicalsDatabaseProvider() + + +class _ChemicalsDatabaseProvider: + """Lookup component data from the `chemicals` package.""" + + def __init__(self): + try: + from chemicals.identifiers import CAS_from_any + except ImportError as exc: # pragma: no cover - import guard + raise ModuleNotFoundError( + "The 'chemicals' package is required to use the extended component database." + ) from exc + + self._cas_from_any = CAS_from_any + critical = importlib.import_module("chemicals.critical") + try: + phase_change = importlib.import_module("chemicals.phase_change") + except ImportError: # pragma: no cover - optional submodule + phase_change = None + try: + elements = importlib.import_module("chemicals.elements") + except ImportError: # pragma: no cover - optional submodule + elements = None + + self._tc = getattr(critical, "Tc") + self._pc = getattr(critical, "Pc") + self._omega = getattr(critical, "omega") + self._vc = getattr(critical, "Vc", None) + self._zc = getattr(critical, "Zc", None) + triple_point_candidates = [ + getattr(critical, "Ttriple", None), + getattr(critical, "Tt", None), + ] + if phase_change is not None: + triple_point_candidates.append(getattr(phase_change, "Tt", None)) + self._triple_point = next((func for func in triple_point_candidates if func), None) + self._tb = getattr(phase_change, "Tb", None) if phase_change is not None else None + self._molecular_weight = ( + getattr(elements, "molecular_weight", None) if elements is not None else None + ) + + def get_component(self, name: str) -> _ChemicalComponentData: + cas = self._cas_from_any(name) + if not cas: + raise ExtendedDatabaseError( + f"Component '{name}' was not found in the chemicals database." + ) + + tc = self._tc(cas) + pc = self._pc(cas) + omega = self._omega(cas) + + if None in (tc, pc, omega): + raise ExtendedDatabaseError( + f"Incomplete property data for '{name}' (CAS {cas})." + ) + + molar_mass = self._call_optional(self._molecular_weight, cas) + if molar_mass is not None: + molar_mass = float(molar_mass) / 1000.0 + + normal_boiling_point = self._call_optional(self._tb, cas) + triple_point_temperature = self._call_optional(self._triple_point, cas) + + critical_volume = self._call_optional(self._vc, cas) + if critical_volume is not None: + critical_volume = float(critical_volume) * 1.0e6 # m^3/mol -> cm^3/mol + + critical_compressibility = self._call_optional(self._zc, cas) + + return _ChemicalComponentData( + name=name, + CAS=cas, + tc=float(tc), + pc=float(pc) / 1.0e5, # chemicals returns pressure in Pa + omega=float(omega), + molar_mass=molar_mass, + normal_boiling_point= + float(normal_boiling_point) if normal_boiling_point is not None else None, + triple_point_temperature= + float(triple_point_temperature) + if triple_point_temperature is not None + else None, + critical_volume=critical_volume, + critical_compressibility= + float(critical_compressibility) + if critical_compressibility is not None + else None, + ) + + @staticmethod + def _call_optional(func, cas): + if func is None: + return None + for call in ( + lambda: func(cas), + lambda: func(CASRN=cas), + ): + try: + value = call() + except TypeError: + continue + except Exception: # pragma: no cover - defensive fallback + return None + else: + return value + return None + + +def _get_extended_provider(system): + provider = getattr(system, "_extended_database_provider", None) + if provider is None: + provider = _create_extended_database_provider() + system._extended_database_provider = provider # type: ignore[attr-defined] + return provider + + +def _apply_extended_properties( + system, component_names: Tuple[str, ...], data: _ChemicalComponentData +): + setter_map = { + "CAS": "setCASnumber", + "molar_mass": "setMolarMass", + "normal_boiling_point": "setNormalBoilingPoint", + "triple_point_temperature": "setTriplePointTemperature", + "critical_volume": "setCriticalVolume", + "critical_compressibility": "setCriticalCompressibilityFactor", + } + + for phase_index in range(system.getNumberOfPhases()): + try: + phase = system.getPhase(phase_index) + except Exception: # pragma: no cover - defensive fallback + continue + if not hasattr(phase, "hasComponent"): + continue + component = None + for name in component_names: + if phase.hasComponent(name): + component = phase.getComponent(name) + break + if component is None: + continue + for field, setter_name in setter_map.items(): + value = getattr(data, field, None) + if value is None: + continue + setter = getattr(component, setter_name, None) + if setter is None: + continue + setter(value) + +def _system_interface_class(): + """Return the JPype proxy for ``neqsim.thermo.system.SystemInterface``.""" + + if not hasattr(_system_interface_class, "_cached"): + _system_interface_class._cached = jpype.JClass( # type: ignore[attr-defined] + "neqsim.thermo.system.SystemInterface" + ) + return _system_interface_class._cached # type: ignore[attr-defined] + + +def _resolve_alias(name: str) -> str: + try: + return jneqsim.thermo.component.Component.getComponentNameFromAlias(name) + except Exception: # pragma: no cover - defensive alias resolution + return name + + +def _has_component_in_database(name: str) -> bool: + database = jneqsim.util.database.NeqSimDataBase + return database.hasComponent(name) or database.hasTempComponent(name) + + +def _args_look_like_component_properties(args: Tuple[object, ...]) -> bool: + return len(args) == 3 and all(isinstance(value, (int, float)) for value in args) + + +@jpype.JImplementationFor("neqsim.thermo.system.SystemInterface") +class _SystemInterface: + def useExtendedDatabase(self, enable: bool = True): + """Enable or disable usage of the chemicals based component database.""" + + if enable: + provider = _create_extended_database_provider() + self._use_extended_database = True # type: ignore[attr-defined] + self._extended_database_provider = provider # type: ignore[attr-defined] + else: + self._use_extended_database = False # type: ignore[attr-defined] + if hasattr(self, "_extended_database_provider"): + delattr(self, "_extended_database_provider") + return self + + def addComponent(self, name, amount, *args): # noqa: N802 - Java signature + alias_name = _resolve_alias(name) + component_data = None + + if getattr(self, "_use_extended_database", False) and not _has_component_in_database( + alias_name + ): + try: + provider = _get_extended_provider(self) + component_data = provider.get_component(name) + except (ExtendedDatabaseError, ModuleNotFoundError): + component_data = None + + if component_data is not None and not _args_look_like_component_properties(args): + if args: + raise NotImplementedError( + "Extended database currently supports components specified in moles (unit='no') " + "without explicit phase targeting or alternative units." + ) + result = _system_interface_class().addComponent( + self, + name, + float(amount), + component_data.tc, + component_data.pc, + component_data.omega, + ) + + _apply_extended_properties(self, (alias_name, name), component_data) + + return result + + return _system_interface_class().addComponent(self, name, amount, *args) + + def fluid(name="srk", temperature=298.15, pressure=1.01325): """ Create a thermodynamic fluid system. @@ -1100,6 +1352,29 @@ def addComponent(thermoSystem, name, moles, unit="no", phase=-10): Returns: None """ + alias_name = _resolve_alias(name) + + if getattr(thermoSystem, "_use_extended_database", False) and not _has_component_in_database(alias_name): + try: + provider = _get_extended_provider(thermoSystem) + component_data = provider.get_component(name) + except (ExtendedDatabaseError, ModuleNotFoundError): + component_data = None + if component_data is not None: + if unit != "no" or phase != -10: + raise NotImplementedError( + "Extended database currently supports components specified in moles (unit='no') " + "without explicit phase targeting." + ) + thermoSystem.addComponent( + name, + moles, + component_data.tc, + component_data.pc, + component_data.omega, + ) + return + if phase == -10 and unit == "no": thermoSystem.addComponent(name, moles) elif phase == -10: diff --git a/tests/test_extended_database.py b/tests/test_extended_database.py new file mode 100644 index 00000000..45d552f8 --- /dev/null +++ b/tests/test_extended_database.py @@ -0,0 +1,71 @@ +import pytest + + +chemicals = pytest.importorskip("chemicals") + +import chemicals.critical as critical_data # type: ignore # noqa: E402 +from chemicals.critical import Pc, Tc, Vc, Zc, omega # type: ignore # noqa: E402 +from chemicals.elements import molecular_weight # type: ignore # noqa: E402 +from chemicals.phase_change import Tb # type: ignore # noqa: E402 +from chemicals.identifiers import CAS_from_any # type: ignore # noqa: E402 + +from neqsim.thermo.thermoTools import addComponent, fluid + + +def test_use_extended_database_allows_missing_component(): + system = fluid("srk") + + with pytest.raises(Exception): + system.addComponent("dimethylsulfoxide", 1.0) + + system.useExtendedDatabase(True) + system.addComponent("dimethylsulfoxide", 1.0) + + component = system.getPhase(0).getComponent("dimethylsulfoxide") + cas = CAS_from_any("dimethylsulfoxide") + + assert pytest.approx(component.getTC(), rel=1e-6) == Tc(cas) + assert pytest.approx(component.getPC(), rel=1e-6) == Pc(cas) / 1.0e5 + assert pytest.approx(component.getAcentricFactor(), rel=1e-6) == omega(cas) + + molar_mass = molecular_weight(CASRN=cas) + assert molar_mass is not None + assert pytest.approx(component.getMolarMass(), rel=1e-6) == molar_mass / 1000.0 + + normal_boiling_point = Tb(cas) + if normal_boiling_point is not None: + assert pytest.approx(component.getNormalBoilingPoint(), rel=1e-6) == normal_boiling_point + + critical_volume = Vc(cas) + if critical_volume is not None: + assert pytest.approx(component.getCriticalVolume(), rel=1e-6) == critical_volume * 1.0e6 + + critical_compressibility = Zc(cas) + if critical_compressibility is not None: + assert ( + pytest.approx(component.getCriticalCompressibilityFactor(), rel=1e-6) + == critical_compressibility + ) + + triple_point_func = getattr(critical_data, "Ttriple", None) or getattr( + critical_data, "Tt", None + ) + if triple_point_func is not None: + triple_point_temperature = triple_point_func(cas) + if triple_point_temperature is not None: + assert ( + pytest.approx(component.getTriplePointTemperature(), rel=1e-6) + == triple_point_temperature + ) + + +def test_module_add_component_uses_extended_database(): + system = fluid("srk") + + with pytest.raises(Exception): + addComponent(system, "dimethylsulfoxide", 1.0) + + system.useExtendedDatabase(True) + addComponent(system, "dimethylsulfoxide", 1.0) + + assert system.getPhase(0).hasComponent("dimethylsulfoxide") From 23804bd8191a7c5ecce289483b76e6808851de36 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 26 Oct 2025 08:55:37 +0100 Subject: [PATCH 3/7] Add linting hooks to pre-commit configuration --- .pre-commit-config.yaml | 11 +++++++++++ README.md | 26 ++++++++++++++++++++------ pyproject.toml | 2 ++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6260e94b..89ecdf02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,3 +12,14 @@ repos: rev: 24.10.0 hooks: - id: black + language_version: python3 +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + args: ["--profile=black"] +- repo: https://github.com/pycqa/flake8 + rev: 7.1.1 + hooks: + - id: flake8 diff --git a/README.md b/README.md index 9caa1d05..36ce8d72 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,26 @@ See the [NeqSim Python Wiki](https://github.com/equinor/neqsimpython/wiki) for h Java version 8 or higher ([Java JDK](https://adoptium.net/)) needs to be installed. The Python package [JPype](https://github.com/jpype-project/jpype) is used to connect Python and Java. Read the [installation requirements for Jpype](https://jpype.readthedocs.io/en/latest/install.html). Be aware that mixing 64 bit Python with 32 bit Java and vice versa crashes on import of the jpype module. The needed Python packages are listed in the [NeqSim Python dependencies page](https://github.com/equinor/neqsimpython/network/dependencies). -## Contributing - -Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests. - - -## Discussion forum +## Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests. + +### Development tooling + +This repository uses [`pre-commit`](https://pre-commit.com/) to run automated formatting and linting before each commit. After installing the project dependencies (for example with `poetry install`), enable the hooks locally with: + +``` +poetry run pre-commit install +``` + +You can run all hooks against the codebase at any time with: + +``` +poetry run pre-commit run --all-files +``` + + +## Discussion forum Questions related to neqsim can be posted in the [github discussion pages](https://github.com/equinor/neqsim/discussions). diff --git a/pyproject.toml b/pyproject.toml index 87d8ddc8..9c0d43bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ tabulate = { version = "^0.9.0", optional = true } black = ">=23.12,<25.0" pytest = "^7.4.3" pre-commit = "^3.5.0" # Higher versions require python 3.9+ +flake8 = "^7.1.1" +isort = "^5.13.2" [tool.poetry.extras] interactive = ["matplotlib", "jupyter", "tabulate"] From c09d310b7594fb295a280035e880a3d069d1537e Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:00:44 +0100 Subject: [PATCH 4/7] Ensure pre-commit checks run automatically --- .githooks/pre-commit | 21 +++++++++++++++++++++ README.md | 8 +++++--- 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..cf2ae74e --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,21 @@ +#!/bin/sh +# Git pre-commit hook that ensures repository pre-commit checks run automatically. +# If Poetry is available we prefer it to ensure hooks execute in the managed virtualenv. + +set -eu + +run_pre_commit() { + if command -v poetry >/dev/null 2>&1; then + exec poetry run pre-commit run --hook-stage pre-commit "$@" + elif command -v pre-commit >/dev/null 2>&1; then + exec pre-commit run --hook-stage pre-commit "$@" + else + cat >&2 <<'MSG' +Error: pre-commit is not installed. +Install it with "pip install pre-commit" or "poetry add --group dev pre-commit". +MSG + exit 1 + fi +} + +run_pre_commit "$@" diff --git a/README.md b/README.md index 36ce8d72..599c9c94 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,15 @@ Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduc ### Development tooling -This repository uses [`pre-commit`](https://pre-commit.com/) to run automated formatting and linting before each commit. After installing the project dependencies (for example with `poetry install`), enable the hooks locally with: +This repository uses [`pre-commit`](https://pre-commit.com/) to run automated formatting and linting before each commit. After installing the project dependencies (for example with `poetry install`), point Git to the versioned hook scripts so the checks run automatically on every commit: ``` -poetry run pre-commit install +git config core.hooksPath .githooks ``` -You can run all hooks against the codebase at any time with: +The configured hook calls `pre-commit` via Poetry when available, so the same tooling is used regardless of how the command is invoked. You can still install the hooks with `poetry run pre-commit install` if you prefer the standard workflow. + +Run all hooks against the codebase at any time with: ``` poetry run pre-commit run --all-files From 4777b09589d5451639a6988ed8b184ec74404b69 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:07:05 +0100 Subject: [PATCH 5/7] Require Python 3.8.1 or newer --- .githooks/pre-commit | 21 +++++++++++++++++++++ .pre-commit-config.yaml | 11 +++++++++++ README.md | 28 ++++++++++++++++++++++------ pyproject.toml | 4 +++- 4 files changed, 57 insertions(+), 7 deletions(-) create mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..cf2ae74e --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,21 @@ +#!/bin/sh +# Git pre-commit hook that ensures repository pre-commit checks run automatically. +# If Poetry is available we prefer it to ensure hooks execute in the managed virtualenv. + +set -eu + +run_pre_commit() { + if command -v poetry >/dev/null 2>&1; then + exec poetry run pre-commit run --hook-stage pre-commit "$@" + elif command -v pre-commit >/dev/null 2>&1; then + exec pre-commit run --hook-stage pre-commit "$@" + else + cat >&2 <<'MSG' +Error: pre-commit is not installed. +Install it with "pip install pre-commit" or "poetry add --group dev pre-commit". +MSG + exit 1 + fi +} + +run_pre_commit "$@" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6260e94b..89ecdf02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,3 +12,14 @@ repos: rev: 24.10.0 hooks: - id: black + language_version: python3 +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + args: ["--profile=black"] +- repo: https://github.com/pycqa/flake8 + rev: 7.1.1 + hooks: + - id: flake8 diff --git a/README.md b/README.md index 9caa1d05..599c9c94 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,28 @@ See the [NeqSim Python Wiki](https://github.com/equinor/neqsimpython/wiki) for h Java version 8 or higher ([Java JDK](https://adoptium.net/)) needs to be installed. The Python package [JPype](https://github.com/jpype-project/jpype) is used to connect Python and Java. Read the [installation requirements for Jpype](https://jpype.readthedocs.io/en/latest/install.html). Be aware that mixing 64 bit Python with 32 bit Java and vice versa crashes on import of the jpype module. The needed Python packages are listed in the [NeqSim Python dependencies page](https://github.com/equinor/neqsimpython/network/dependencies). -## Contributing - -Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests. - - -## Discussion forum +## Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests. + +### Development tooling + +This repository uses [`pre-commit`](https://pre-commit.com/) to run automated formatting and linting before each commit. After installing the project dependencies (for example with `poetry install`), point Git to the versioned hook scripts so the checks run automatically on every commit: + +``` +git config core.hooksPath .githooks +``` + +The configured hook calls `pre-commit` via Poetry when available, so the same tooling is used regardless of how the command is invoked. You can still install the hooks with `poetry run pre-commit install` if you prefer the standard workflow. + +Run all hooks against the codebase at any time with: + +``` +poetry run pre-commit run --all-files +``` + + +## Discussion forum Questions related to neqsim can be posted in the [github discussion pages](https://github.com/equinor/neqsim/discussions). diff --git a/pyproject.toml b/pyproject.toml index 87d8ddc8..5b02cc0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ repository = "https://github.com/Equinor/neqsimpython" classifiers = ["Operating System :: OS Independent"] [tool.poetry.dependencies] -python = "^3.8" +python = ">=3.8.1,<4.0" JPype1 = "^1.5.0" numpy = [ { version = "^1.24.4", markers = "python_version == '3.8'" }, @@ -28,6 +28,8 @@ tabulate = { version = "^0.9.0", optional = true } black = ">=23.12,<25.0" pytest = "^7.4.3" pre-commit = "^3.5.0" # Higher versions require python 3.9+ +flake8 = "^7.1.1" +isort = "^5.13.2" [tool.poetry.extras] interactive = ["matplotlib", "jupyter", "tabulate"] From ba4e5dc1d7fef1d56ae5c6891e9f68a111850239 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:11:25 +0100 Subject: [PATCH 6/7] Delete .githooks/pre-commit --- .githooks/pre-commit | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index cf2ae74e..00000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh -# Git pre-commit hook that ensures repository pre-commit checks run automatically. -# If Poetry is available we prefer it to ensure hooks execute in the managed virtualenv. - -set -eu - -run_pre_commit() { - if command -v poetry >/dev/null 2>&1; then - exec poetry run pre-commit run --hook-stage pre-commit "$@" - elif command -v pre-commit >/dev/null 2>&1; then - exec pre-commit run --hook-stage pre-commit "$@" - else - cat >&2 <<'MSG' -Error: pre-commit is not installed. -Install it with "pip install pre-commit" or "poetry add --group dev pre-commit". -MSG - exit 1 - fi -} - -run_pre_commit "$@" From c4b38bf3c2ee32d267217a0ae35b88563fbceb29 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:12:00 +0100 Subject: [PATCH 7/7] Delete .pre-commit-config.yaml --- .pre-commit-config.yaml | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 89ecdf02..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files -- repo: https://github.com/psf/black - rev: 24.10.0 - hooks: - - id: black - language_version: python3 -- repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - name: isort (python) - args: ["--profile=black"] -- repo: https://github.com/pycqa/flake8 - rev: 7.1.1 - hooks: - - id: flake8