Skip to content

Commit 8b51ff9

Browse files
feat: add logic to check user modification in trame-geos (#96)
* feat: add properties checker * test: add properties checker test * feat: use simput to check values * refactor: type checking and split files * feat: properties checked using geometry data * refactor: remove src nesting + mypy checks working * chore: add geos-trame to type checked packages * chore: remove black/isort and add ruff * refactor: ruff checks * docs: remove duplicate CONTRIBUTING and update README * feat: load data upon check fields * refactor: add src folder and split geos_trame into geos/trame * chore: update path * Add type hints for YAML --------- Co-authored-by: Jean Fechter <jean.fechter@kitware.com>
1 parent 630e1ca commit 8b51ff9

61 files changed

Lines changed: 1739 additions & 1647 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/typing-check.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
max-parallel: 3
1717
matrix:
1818
# add packages to check typing
19-
package-name: ["geos-geomechanics", "geos-posp", "geos-timehistory", "geos-utils", "geos-xml-tools", "hdf5-wrapper"]
19+
package-name: ["geos-geomechanics", "geos-posp", "geos-timehistory", "geos-utils", "geos-trame", "geos-xml-tools", "hdf5-wrapper"]
2020

2121
steps:
2222
- uses: actions/checkout@v4
@@ -30,7 +30,7 @@ jobs:
3030
# working-directory: ./${{ matrix.package-name }}
3131
run: |
3232
python -m pip install --upgrade pip
33-
python -m pip install mypy ruff
33+
python -m pip install mypy ruff types-PyYAML
3434
3535
- name: Typing check with mypy
3636
# working-directory: ./${{ matrix.package-name }}

geos-trame/.pre-commit-config.yaml

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
repos:
2-
- repo: https://github.com/psf/black
3-
rev: 22.3.0
4-
hooks:
5-
- id: black
6-
exclude: ^.*\b(schema_generated)\b.*$
7-
entry: black --check --force-exclude
8-
92
- repo: https://github.com/codespell-project/codespell
103
rev: v2.1.0
114
hooks:
125
- id: codespell
136

14-
- repo: https://github.com/PyCQA/flake8
15-
rev: 4.0.1
7+
- repo: https://github.com/astral-sh/ruff-pre-commit
8+
rev: v0.11.12
9+
hooks:
10+
- id: ruff
11+
args: ["--config", "./.ruff.toml"]
12+
13+
- repo: https://github.com/google/yapf
14+
rev: v0.43.0
15+
hooks:
16+
- id: yapf
17+
args: ["-ir", "--style", "./.style.yapf"]
18+
19+
- repo: https://github.com/pre-commit/mirrors-mypy
20+
rev: v1.16.0
1621
hooks:
17-
- id: flake8
22+
- id: mypy
23+
additional_dependencies: [types-PyYAML]

geos-trame/CONTRIBUTING.rst

Lines changed: 0 additions & 19 deletions
This file was deleted.

geos-trame/README.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,23 @@ To be able to run the test suite, make sure to install the additionals dependenc
4747
4848
Then you can run the test with `pytest .`
4949

50+
Optional
51+
--------
52+
53+
To use pre-commit hooks (ruff, mypy, yapf,...), make sure to install the dev dependencies:
54+
55+
.. code-block:: console
56+
57+
pip install -e '.[dev]'
58+
5059
Regarding GEOS
5160
--------------
5261

5362
This application takes an XML file from the GEOS project to load dynamically all of its components.
5463
To be able to do that, we need first to generate the corresponding python class based on a
5564
xsd schema provided by GEOS.
5665

57-
`For more details <src/geos_trame/schema_generated/README.md>`_
66+
`For more details <src/geos/trame/schema_generated/README.md>`_
5867

5968
Features
6069
--------

geos-trame/pyproject.toml

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies = [
4747
"funcy==2.0",
4848
"typing_inspect==0.9.0",
4949
"typing_extensions>=4.12",
50+
"PyYAML",
5051
]
5152

5253
[project.optional-dependencies]
@@ -56,14 +57,16 @@ build = [
5657
dev = [
5758
"pylint",
5859
"mypy",
59-
"black",
60-
"isort"
60+
"types-PyYAML",
61+
"ruff",
62+
"pre-commit"
6163
]
6264
test = [
6365
"pytest==8.3.3",
6466
"pytest-seleniumbase==4.31.6",
6567
"pixelmatch==0.3.0",
6668
"Pillow==11.0.0",
69+
"pytest-mypy==0.10.3",
6770
"pytest-xprocess==1.0.2"
6871
]
6972

@@ -72,10 +75,10 @@ file = "README.md"
7275
content-type = "text/markdown"
7376

7477
[project.scripts]
75-
geos-trame = "geos_trame.app.__main__:main"
78+
geos-trame = "geos.trame.app.main:main"
7679

7780
[project.entry-points.jupyter_serverproxy_servers]
78-
geos-trame = "geos_trame.app.jupyter:jupyter_proxy_info"
81+
geos-trame = "geos.trame.app.jupyter:jupyter_proxy_info"
7982

8083
[tool.setuptools]
8184
license-files = ["LICENSE"]
@@ -116,18 +119,3 @@ disable = [
116119
"R0913", # (too-many-arguments)
117120
"W0105", # (pointless-string-statement)
118121
]
119-
120-
[tool.black]
121-
line-length = 88
122-
target-version = ['py310']
123-
include = '\.pyi?$'
124-
extend-exclude = '''
125-
src/geos_trame/schema_generated/*.py
126-
'''
127-
128-
[tool.isort]
129-
profile = "black"
130-
src_paths = ["src", "tests"]
131-
blackArgs = ["--preview"]
132-
py_version = 310
133-
File renamed without changes.

geos-trame/src/geos_trame/app/ui/alertHandler.py renamed to geos-trame/src/geos/trame/app/components/alertHandler.py

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77

88

99
class AlertHandler( vuetify3.VContainer ):
10-
"""
11-
Vuetify component used to display an alert status.
10+
"""Vuetify component used to display an alert status.
1211
1312
This alert will be displayed in the bottom right corner of the screen.
1413
It will be displayed until closed by the user or after 10 seconds if it is a success or warning.
1514
"""
1615

17-
def __init__( self ):
16+
def __init__( self ) -> None:
17+
"""Constructor."""
1818
super().__init__(
1919
fluid=True,
2020
classes="pa-0 ma-0",
@@ -31,31 +31,32 @@ def __init__( self ):
3131

3232
self.generate_alert_ui()
3333

34-
def generate_alert_ui( self ):
35-
"""
36-
Generate the alert UI.
34+
def generate_alert_ui( self ) -> None:
35+
"""Generate the alert UI.
3736
3837
The alert will be displayed in the bottom right corner of the screen.
3938
4039
Use an abritary z-index value to put the alert on top of the other components.
4140
"""
42-
with self:
43-
with vuetify3.VCol( style="width: 40%; position: fixed; right: 50px; bottom: 50px; z-index: 100;", ):
44-
vuetify3.VAlert(
45-
style="max-height: 20vh; overflow-y: auto",
46-
classes="ma-2",
47-
v_for=( "(status, index) in alerts", ),
48-
key="status",
49-
type=( "status.type", "info" ),
50-
text=( "status.message", "" ),
51-
title=( "status.title", "" ),
52-
closable=True,
53-
click_close=( self.on_close, f"[status.id]" ),
54-
)
55-
56-
def add_alert( self, type: str, title: str, message: str ):
57-
"""
58-
Add a status to the stack with a unique id.
41+
with (
42+
self,
43+
vuetify3.VCol( style="width: 40%; position: fixed; right: 50px; bottom: 50px; z-index: 100;", ),
44+
):
45+
vuetify3.VAlert(
46+
style="max-height: 20vh; overflow-y: auto",
47+
classes="ma-2",
48+
v_for=( "(status, index) in alerts", ),
49+
key="status",
50+
type=( "status.type", "info" ),
51+
text=( "status.message", "" ),
52+
title=( "status.title", "" ),
53+
closable=True,
54+
click_close=( self.on_close, "[status.id]" ),
55+
)
56+
57+
def add_alert( self, type: str, title: str, message: str ) -> None:
58+
"""Add a status to the stack with a unique id.
59+
5960
If there are more than 5 alerts displayed, remove the oldest.
6061
A warning will be automatically closed after 10 seconds.
6162
"""
@@ -77,21 +78,15 @@ def add_alert( self, type: str, title: str, message: str ):
7778
if type == "warning":
7879
asyncio.get_event_loop().call_later( self.__lifetime_of_alert, self.on_close, alert_id )
7980

80-
async def add_warning( self, title: str, message: str ):
81-
"""
82-
Add an alert of type "warning"
83-
"""
81+
async def add_warning( self, title: str, message: str ) -> None:
82+
"""Add an alert of type 'warning'."""
8483
self.add_alert( "warning", title, message )
8584

86-
async def add_error( self, title: str, message: str ):
87-
"""
88-
Add an alert of type "error"
89-
"""
85+
async def add_error( self, title: str, message: str ) -> None:
86+
"""Add an alert of type 'error'."""
9087
self.add_alert( "error", title, message )
9188

92-
def on_close( self, alert_id ):
93-
"""
94-
Remove in the state the alert associated to the given id.
95-
"""
89+
def on_close( self, alert_id: int ) -> None:
90+
"""Remove in the state the alert associated to the given id."""
9691
self.state.alerts = list( filter( lambda i: i[ "id" ] != alert_id, self.state.alerts ) )
9792
self.state.flush()
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3+
# SPDX-FileContributor: Kitware
4+
from typing import Any
5+
6+
from trame_client.widgets.core import AbstractElement
7+
from trame_simput import get_simput_manager
8+
9+
from geos.trame.app.data_types.field_status import FieldStatus
10+
from geos.trame.app.data_types.renderable import Renderable
11+
from geos.trame.app.deck.tree import DeckTree
12+
from geos.trame.app.ui.viewer.regionViewer import RegionViewer
13+
from geos.trame.app.utils.geos_utils import group_name_ref_array_to_list
14+
15+
# Doc reference: https://geosx-geosx.readthedocs-hosted.com/en/latest/docs/sphinx/datastructure/CompleteXMLSchema.html
16+
attributes_to_check = [ ( "region_attribute", str ), ( "fields_to_import", list ), ( "surfacicFieldsToImport", list ) ]
17+
18+
19+
class PropertiesChecker( AbstractElement ):
20+
"""Validity checker of properties within a deck tree."""
21+
22+
def __init__( self, tree: DeckTree, region_viewer: RegionViewer, **kwargs: Any ) -> None:
23+
"""Constructor."""
24+
super().__init__( "div", **kwargs )
25+
26+
self.tree = tree
27+
self.region_viewer = region_viewer
28+
self.simput_manager = get_simput_manager( id=self.state.sm_id )
29+
30+
def check_fields( self ) -> None:
31+
"""Check all the fields in the deck_tree.
32+
33+
Get the names of all the cell data arrays from the input of the region viewer, then check that
34+
all the attributes in `attributes_to_check` have a value corresponding to one of the array names.
35+
"""
36+
array_names = self._get_array_names()
37+
for field in self.state.deck_tree:
38+
self._check_field( field, array_names )
39+
self.state.dirty( "deck_tree" )
40+
self.state.flush()
41+
42+
def _check_field( self, field: dict, array_names: list[ str ] ) -> None:
43+
"""Check that all the attributes in `attributes_to_check` have a value corresponding to one of the array names.
44+
45+
Set the `valid` property to the result of this check, and if necessary, indicate which properties are invalid.
46+
"""
47+
if len( array_names ) == 0 and Renderable.VTKMESH.value in field[ "id" ]:
48+
self.ctrl.load_vtkmesh_from_id( field[ "id" ] )
49+
array_names = self._get_array_names()
50+
field[ "drawn" ] = True
51+
field[ "valid" ] = FieldStatus.VALID.value
52+
field[ "invalid_properties" ] = []
53+
54+
proxy = self.simput_manager.proxymanager.get( field[ "id" ] )
55+
if proxy is not None:
56+
for attr, expected_type in attributes_to_check:
57+
if attr in proxy.definition:
58+
if ( expected_type is str and proxy[ attr ] # value is not empty (valid)
59+
and proxy[ attr ] not in array_names # value is not in the expected names
60+
):
61+
field[ "invalid_properties" ].append( attr )
62+
elif expected_type is list:
63+
arrays: list[ str ] | None = group_name_ref_array_to_list( proxy[ attr ] )
64+
if arrays is None:
65+
field[ "invalid_properties" ].append( attr )
66+
continue
67+
for array_name in arrays:
68+
if array_name not in array_names:
69+
field[ "invalid_properties" ].append( attr )
70+
break
71+
72+
if len( field[ "invalid_properties" ] ) != 0:
73+
field[ "valid" ] = FieldStatus.INVALID.value
74+
else:
75+
field.pop( "invalid_properties", None )
76+
77+
if field[ "children" ] is not None:
78+
# Parents are only valid if all children are valid
79+
field[ "invalid_children" ] = []
80+
for child in field[ "children" ]:
81+
self._check_field( child, array_names )
82+
if child[ "valid" ] == FieldStatus.INVALID.value:
83+
field[ "valid" ] = FieldStatus.INVALID.value
84+
field[ "invalid_children" ].append( child[ "title" ] )
85+
if len( field[ "invalid_children" ] ) == 0:
86+
field.pop( "invalid_children", None )
87+
88+
def _get_array_names( self ) -> list[ str ]:
89+
cellData = self.region_viewer.input.GetCellData()
90+
return [ cellData.GetArrayName( i ) for i in range( cellData.GetNumberOfArrays() ) ]

0 commit comments

Comments
 (0)