Skip to content

Commit 483d105

Browse files
authored
feat(framework): expose operator changelog (#2324)
1 parent 51af2c7 commit 483d105

File tree

10 files changed

+471
-0
lines changed

10 files changed

+471
-0
lines changed

src/ansys/dpf/core/changelog.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Copyright (C) 2020 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Provides classes for changelogs."""
24+
25+
from __future__ import annotations
26+
27+
from packaging.version import Version
28+
29+
import ansys.dpf.core as dpf
30+
from ansys.dpf.core.server_types import AnyServerType
31+
32+
33+
class Changelog:
34+
"""Changelog of an operator.
35+
36+
Requires DPF 11.0 (2026 R1) or above.
37+
38+
Parameters
39+
----------
40+
gdc:
41+
An optional GenericDataContainer to initialize the changelog with.
42+
server:
43+
The server to create the changelog on. Defaults to the current global server.
44+
"""
45+
46+
def __init__(self, gdc: dpf.GenericDataContainer = None, server: AnyServerType = None):
47+
if gdc is None:
48+
gdc = dpf.GenericDataContainer(server=server)
49+
versions_sf = dpf.StringField(server=server)
50+
versions_sf.append(data=["0.0.0"], scopingid=1)
51+
changes_sf = dpf.StringField(server=server)
52+
changes_sf.append(data=["Initial version."], scopingid=1)
53+
gdc.set_property(property_name="versions", prop=versions_sf)
54+
gdc.set_property(property_name="changes", prop=changes_sf)
55+
gdc.set_property(property_name="class", prop="Changelog")
56+
self.gdc = gdc
57+
self._server = server
58+
59+
def append(self, version: Version, changes: str):
60+
"""Append a version and associated changes description to the changelog."""
61+
versions_sf: dpf.StringField = self.gdc.get_property(
62+
property_name="versions", output_type=dpf.StringField
63+
)
64+
new_id = versions_sf.scoping.size + 1
65+
versions_sf.append(data=[str(version)], scopingid=new_id)
66+
changes_sf: dpf.StringField = self.gdc.get_property(
67+
property_name="changes", output_type=dpf.StringField
68+
)
69+
changes_sf.append(data=[changes], scopingid=new_id)
70+
71+
def patch_bump(self, changes: str) -> Changelog:
72+
"""Bump the patch of the current version with associated changes description.
73+
74+
Parameters
75+
----------
76+
changes:
77+
Description of the changes associated to the patch bump.
78+
79+
Returns
80+
-------
81+
changelog:
82+
Returns the current changelog to allow for chaining calls to bumps.
83+
"""
84+
current_version = self.last_version
85+
new_version = Version(
86+
f"{current_version.major}.{current_version.minor}.{current_version.micro+1}"
87+
)
88+
self.append(version=new_version, changes=changes)
89+
return self
90+
91+
def minor_bump(self, changes: str) -> Changelog:
92+
"""Bump the minor of the current version with associated changes description.
93+
94+
Parameters
95+
----------
96+
changes:
97+
Description of the changes associated to the minor bump.
98+
99+
Returns
100+
-------
101+
changelog:
102+
Returns the current changelog to allow for chaining calls to bumps.
103+
"""
104+
current_version = self.last_version
105+
new_version = Version(f"{current_version.major}.{current_version.minor+1}.0")
106+
self.append(version=new_version, changes=changes)
107+
return self
108+
109+
def major_bump(self, changes: str) -> Changelog:
110+
"""Bump the major of the current version with associated changes description.
111+
112+
Parameters
113+
----------
114+
changes:
115+
Description of the changes associated to the major bump.
116+
117+
Returns
118+
-------
119+
changelog:
120+
Returns the current changelog to allow for chaining calls to bumps.
121+
"""
122+
current_version = self.last_version
123+
new_version = Version(f"{current_version.major+1}.0.0")
124+
self.append(version=new_version, changes=changes)
125+
return self
126+
127+
def expect_version(self, version: Version | str) -> Changelog:
128+
"""Check the current latest version of the changelog.
129+
130+
Useful when chaining version bumps to check the resulting version is as expected.
131+
Adds readability to the specification of the operator.
132+
133+
Parameters
134+
----------
135+
version:
136+
Expected current latest version of the changelog.
137+
138+
Returns
139+
-------
140+
changelog:
141+
Returns the current changelog to allow for chaining calls to bumps.
142+
"""
143+
if isinstance(version, str):
144+
version = Version(version)
145+
if self.last_version != version:
146+
raise ValueError(
147+
f"Last version in the changelog ({self.last_version}) does not match expected version ({version})."
148+
)
149+
return self
150+
151+
@property
152+
def last_version(self) -> Version:
153+
"""Highest version in the changelog.
154+
155+
Returns
156+
-------
157+
version:
158+
Highest version in the changelog.
159+
"""
160+
return self.versions[-1]
161+
162+
@property
163+
def versions(self) -> [Version]:
164+
"""List of all versions for which the changelog stores descriptions."""
165+
versions_sf: dpf.StringField = self.gdc.get_property(
166+
property_name="versions", output_type=dpf.StringField
167+
)
168+
return [Version(version) for version in versions_sf.data_as_list]
169+
170+
def __getitem__(self, item: Version | int) -> str | [Version, str]:
171+
"""Return item at the given index or changes description for the given version."""
172+
if isinstance(item, int):
173+
if item > len(self) - 1:
174+
raise IndexError(f"Index {item} out of range for changelog of size {len(self)}.")
175+
return self.versions[item], self.changes_for_version(self.versions[item])
176+
return self.changes_for_version(item)
177+
178+
def __len__(self):
179+
"""Return the number of items in the changelog."""
180+
return len(self.versions)
181+
182+
def __contains__(self, item: Version):
183+
"""Check if version is in the changelog."""
184+
return item in self.versions
185+
186+
def changes_for_version(self, version: Version) -> str:
187+
"""Return changes description for a specific version in the changelog."""
188+
versions_sf: dpf.StringField = self.gdc.get_property(
189+
property_name="versions", output_type=dpf.StringField
190+
)
191+
changes_sf: dpf.StringField = self.gdc.get_property(
192+
property_name="changes", output_type=dpf.StringField
193+
)
194+
versions_list = versions_sf.data_as_list
195+
for i, x in enumerate(versions_sf.scoping.ids):
196+
if Version(versions_list[i]) == version:
197+
return changes_sf.get_entity_data_by_id(x)[0]
198+
raise ValueError(f"Changelog has no version '{version}'.")
199+
200+
def __str__(self):
201+
"""Create string representation of the changelog."""
202+
string = "Changelog:\n"
203+
string += "Version Changes\n"
204+
string += "------- -------\n"
205+
for version in self.versions:
206+
string += f"{str(version): <15}" + self[version].replace("\n", f"\n{'': >15}") + "\n"
207+
return string

src/ansys/dpf/core/custom_operator.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import zipfile
3838

3939
import numpy
40+
from packaging.version import Version
4041

4142
from ansys.dpf import core as dpf
4243
from ansys.dpf.core import (
@@ -55,6 +56,8 @@
5556
external_operator_api,
5657
functions_registry,
5758
)
59+
from ansys.dpf.core.changelog import Changelog
60+
from ansys.dpf.core.check_version import version_requires
5861
from ansys.dpf.gate import capi, dpf_vector, integral_types, object_handler
5962

6063

@@ -400,3 +403,39 @@ def name(self) -> str:
400403
This name can then be used to instantiate the Operator.
401404
"""
402405
pass
406+
407+
@property
408+
@version_requires("11.0")
409+
def changelog(self) -> Changelog:
410+
"""Return the changelog of this operator.
411+
412+
Requires DPF 11.0 (2026 R1) or above.
413+
414+
Returns
415+
-------
416+
changelog:
417+
Changelog of the operator.
418+
"""
419+
from ansys.dpf.core.operators.utility.operator_changelog import operator_changelog
420+
421+
return Changelog(operator_changelog(operator_name=self.name).eval())
422+
423+
@changelog.setter
424+
@version_requires("11.0")
425+
def changelog(self, changelog: Changelog):
426+
"""Set the changelog of this operator.
427+
428+
Requires DPF 11.0 (2026 R1) or above.
429+
430+
"""
431+
self.specification.set_changelog(changelog)
432+
433+
@property
434+
@version_requires("11.0")
435+
def version(self) -> Version:
436+
"""Return the current version of the operator based on its changelog.
437+
438+
Requires DPF 11.0 (2026 R1) or above.
439+
440+
"""
441+
return self.changelog.last_version

src/ansys/dpf/core/dpf_operator.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
import warnings
3030

3131
import numpy
32+
from packaging.version import Version
3233

3334
from ansys.dpf.core import server as server_module
35+
from ansys.dpf.core.changelog import Changelog
3436
from ansys.dpf.core.check_version import (
3537
server_meet_version,
3638
server_meet_version_and_raise,
@@ -931,6 +933,35 @@ def specification(self):
931933
else:
932934
return Specification(operator_name=self.name, server=self._server)
933935

936+
@property
937+
@version_requires("11.0")
938+
def changelog(self) -> Changelog:
939+
"""Return the changelog of this operator.
940+
941+
Requires DPF 11.0 (2026 R1) or above.
942+
943+
Returns
944+
-------
945+
changelog:
946+
Changelog of the operator.
947+
"""
948+
from ansys.dpf.core.operators.utility.operator_changelog import operator_changelog
949+
950+
return Changelog(
951+
gdc=operator_changelog(operator_name=self.name, server=self._server).eval(),
952+
server=self._server,
953+
)
954+
955+
@property
956+
@version_requires("11.0")
957+
def version(self) -> Version:
958+
"""Return the current version of the operator.
959+
960+
Requires DPF 11.0 (2026 R1) or above.
961+
962+
"""
963+
return self.changelog.last_version
964+
934965
def __truediv__(self, inpt):
935966
"""
936967
Perform division with another operator or a scalar.

src/ansys/dpf/core/operator_specification.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from typing import Union
3333

3434
from ansys.dpf.core import common, mapping_types, server as server_module
35+
from ansys.dpf.core.changelog import Changelog
3536
from ansys.dpf.core.check_version import server_meet_version, version_requires
3637
from ansys.dpf.gate import (
3738
integral_types,
@@ -497,6 +498,15 @@ def config_specification(self) -> ConfigSpecification:
497498
)
498499
return self._config_specification
499500

501+
@version_requires("11.0")
502+
def set_changelog(self, changelog: Changelog):
503+
"""Set the changelog for this operator specification.
504+
505+
Requires DPF 11.0 (2026 R1) or above.
506+
507+
"""
508+
self._api.operator_specification_set_changelog(self, changelog.gdc)
509+
500510

501511
class CustomConfigOptionSpec(ConfigOptionSpec):
502512
"""Custom documentation of a configuration option available for a given operator."""

src/ansys/dpf/gate/operator_specification_grpcapi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,7 @@ def operator_specification_get_config_printable_default_value(specification, num
169169
def operator_specification_get_config_description(specification, numOption):
170170
option = specification._internal_obj.config_spec.config_options_spec[numOption]
171171
return option.document
172+
173+
@staticmethod
174+
def operator_specification_set_changelog(specification, changelog):
175+
specification._internal_obj.changelog = changelog

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ def return_ds(server=None):
329329
return return_ds
330330

331331

332+
SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_11_0 = meets_version(
333+
get_server_version(core._global_server()), "11.0"
334+
)
332335
SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0 = meets_version(
333336
get_server_version(core._global_server()), "10.0"
334337
)

0 commit comments

Comments
 (0)