Skip to content

Commit 466918a

Browse files
bobbyxnglkstrp
andauthored
Bugfix: Handle alphanumeric versions (like "1.0.0rc1") (PyPSA#1338)
* Bugfix version.py for alphanumeric versions like 1.0.0rc1 * Extended unit tests by alphanumeric versions and parse_version_tuple_function * Using packaging.version to do version comparison * Fixed conversion back to str. * refactor: refactor version attributes and remove tuple versions * fix: simplify regex version match --------- Co-authored-by: lkstrp <lkstrp@pm.me>
1 parent 35aafc0 commit 466918a

File tree

10 files changed

+124
-55
lines changed

10 files changed

+124
-55
lines changed

doc/references/release-notes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Features
3737

3838
* Add additional standard line types from pandapower.
3939

40+
* Refactored version attributes: ``__version_semver__`` → ``__version_base__``,
41+
``__version_short__`` → ``__version_major_minor__``. Removed tuple versions.
42+
Old names raise ``DeprecationWarning``.
43+
4044
Bug Fixes
4145
---------
4246

pypsa/__init__.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
)
1313

1414

15+
from typing import NoReturn
16+
1517
from pypsa import (
1618
clustering,
1719
common,
@@ -32,10 +34,8 @@
3234
from pypsa.networks import Network, SubNetwork
3335
from pypsa.version import (
3436
__version__,
35-
__version_semver__,
36-
__version_semver_tuple__,
37-
__version_short__,
38-
__version_short_tuple__,
37+
__version_base__,
38+
__version_major_minor__,
3939
)
4040

4141
version = __version__ # Alias for legacy access
@@ -48,10 +48,8 @@
4848

4949
__all__ = [
5050
"__version__",
51-
"__version_semver__",
52-
"__version_short__",
53-
"__version_semver_tuple__",
54-
"__version_short_tuple__",
51+
"__version_base__",
52+
"__version_major_minor__",
5553
"version",
5654
"options",
5755
"set_option",
@@ -75,3 +73,35 @@
7573
"SubNetwork",
7674
"Components",
7775
]
76+
77+
78+
def __getattr__(name: str) -> NoReturn:
79+
"""Handle deprecated version attributes."""
80+
# Deprecated tuple versions (removed)
81+
if name == "__version_short_tuple__":
82+
msg = (
83+
"pypsa.__version_short_tuple__ has been removed. "
84+
"Use pypsa.__version_major_minor__ with packaging.version.parse() for version comparisons."
85+
)
86+
raise DeprecationWarning(msg)
87+
88+
if name == "__version_semver_tuple__":
89+
msg = (
90+
"pypsa.__version_semver_tuple__ has been removed. "
91+
"Use pypsa.__version_base__ with packaging.version.parse() for version comparisons."
92+
)
93+
raise DeprecationWarning(msg)
94+
95+
# Deprecated version names (renamed)
96+
if name == "__version_semver__":
97+
msg = "pypsa.__version_semver__ is deprecated. Use pypsa.__version_base__ instead."
98+
raise DeprecationWarning(msg)
99+
100+
if name == "__version_short__":
101+
msg = "pypsa.__version_short__ is deprecated. Use pypsa.__version_major_minor__ instead."
102+
raise DeprecationWarning(msg)
103+
104+
# Raise AttributeError for all other attributes
105+
# __getattr__ is only called if the attribute is not found through normal lookup
106+
msg = f"module '{__name__}' has no attribute '{name}'"
107+
raise AttributeError(msg)

pypsa/common.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from pypsa._options import options
2121
from pypsa.definitions.structures import Dict
22-
from pypsa.version import __version_semver__
22+
from pypsa.version import __version_base__
2323

2424
if TYPE_CHECKING:
2525
from collections.abc import Callable, Sequence
@@ -154,7 +154,7 @@ def __get__(self, obj: Any, objtype: Any = None) -> Any:
154154

155155

156156
@lru_cache(maxsize=1)
157-
def _check_for_update(current_version: tuple, repo_owner: str, repo_name: str) -> str:
157+
def _check_for_update(current_version: str, repo_owner: str, repo_name: str) -> str:
158158
"""Log a message if a newer version is available.
159159
160160
Checks the latest release on GitHub and compares it to the current version. Does
@@ -163,8 +163,8 @@ def _check_for_update(current_version: tuple, repo_owner: str, repo_name: str) -
163163
164164
Parameters
165165
----------
166-
current_version : tuple
167-
The current version of the package as a tuple (major, minor, patch).
166+
current_version : str
167+
The current version of the package as a semantic version string.
168168
repo_owner : str
169169
The owner of the repository.
170170
repo_name : str
@@ -193,12 +193,14 @@ def _check_for_update(current_version: tuple, repo_owner: str, repo_name: str) -
193193
response = request.urlopen(req) # noqa: S310
194194
latest_version = json.loads(response.read())["tag_name"].replace("v", "")
195195

196-
# Simple version comparison
197-
latest = tuple(map(int, latest_version.split(".")))
196+
# Version comparison using packaging.version
197+
latest_parsed = version.parse(latest_version)
198+
current_parsed = version.parse(current_version)
198199

199-
if latest > current_version:
200-
current_version_str = ".".join(map(str, current_version))
201-
return f"New version {latest_version} available! (Current: {current_version_str})"
200+
if latest_parsed > current_parsed:
201+
return (
202+
f"New version {latest_version} available! (Current: {current_version})"
203+
)
202204

203205
except Exception: # noqa: S110
204206
pass
@@ -565,7 +567,7 @@ def decorator(func: Callable) -> Callable:
565567
return deprecated(
566568
deprecated_in="1.0",
567569
removed_in="2.0",
568-
current_version=__version_semver__,
570+
current_version=__version_base__,
569571
details=details,
570572
)(func)
571573

@@ -597,11 +599,11 @@ def deprecated_namespace(
597599
A wrapper function that warns about the deprecated namespace.
598600
599601
"""
600-
current_version = version.parse(__version_semver__)
601-
if version.parse(deprecated_in) > current_version and __version_semver__ != "0.0":
602+
current_version = version.parse(__version_base__)
603+
if version.parse(deprecated_in) > current_version and __version_base__ != "0.0":
602604
msg = (
603605
"'deprecated_namespace' can only be used in a version >= deprecated_in "
604-
f"(current version: {__version_semver__}, deprecated_in: {deprecated_in})."
606+
f"(current version: {__version_base__}, deprecated_in: {deprecated_in})."
605607
)
606608
raise ValueError(msg)
607609

pypsa/examples.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@
77
from urllib.error import HTTPError, URLError
88
from urllib.request import urlopen
99

10+
from packaging.version import parse as parse_version
11+
1012
from pypsa.networks import Network
11-
from pypsa.version import __version_semver__, __version_semver_tuple__
13+
from pypsa.version import __version_base__
1214

1315
logger = logging.getLogger(__name__)
1416

1517

1618
def _repo_url(
1719
master: bool = False, url: str = "https://github.com/PyPSA/PyPSA/raw/"
1820
) -> str:
19-
if master or __version_semver_tuple__ < (0, 35): # Feature was added in 0.35.0
21+
if master or parse_version(__version_base__) < parse_version(
22+
"0.35.0"
23+
): # Feature was added in 0.35.0
2024
return f"{url}master/"
21-
return f"{url}v{__version_semver__}/"
25+
return f"{url}v{__version_base__}/"
2226

2327

2428
def _check_url_availability(url: str) -> bool:

pypsa/network/io.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
import pandas as pd
1919
import validators
2020
import xarray as xr
21+
from packaging.version import parse as parse_version
2122
from pandas.errors import ParserError
2223
from pyproj import CRS
2324

2425
from pypsa._options import options
2526
from pypsa.common import _check_for_update, check_optional_dependency
2627
from pypsa.descriptors import _update_linkports_component_attrs
2728
from pypsa.network.abstract import _NetworkABC
28-
from pypsa.version import __version_semver__, __version_semver_tuple__
29+
from pypsa.version import __version_base__
2930

3031
try:
3132
from cloudpathlib import AnyPath as Path
@@ -1267,11 +1268,9 @@ def _import_from_importer(
12671268
self.name = name
12681269

12691270
if "pypsa_version" in attrs:
1270-
pypsa_version_tuple = tuple(
1271-
int(v) for v in attrs.pop("pypsa_version", "0.0.0").split(".")
1272-
)
1271+
pypsa_version = parse_version(attrs.pop("pypsa_version", "0.0.0"))
12731272
else:
1274-
pypsa_version_tuple = (0, 0, 0)
1273+
pypsa_version = parse_version("0.0.0")
12751274

12761275
for attr, val in attrs.items():
12771276
if attr in ["model", "objective", "objective_constant"]:
@@ -1280,22 +1279,22 @@ def _import_from_importer(
12801279
setattr(self, attr, val)
12811280

12821281
## https://docs.python.org/3/tutorial/datastructures.html#comparing-sequences-and-other-types
1283-
if pypsa_version_tuple < __version_semver_tuple__:
1284-
pypsa_version_str = ".".join(map(str, pypsa_version_tuple))
1282+
if pypsa_version < parse_version(__version_base__):
1283+
pypsa_version_str = str(pypsa_version)
12851284
logger.warning(
12861285
"Importing network from PyPSA version v%s while current version is v%s. Read the "
12871286
"release notes at https://pypsa.readthedocs.io/en/latest/release_notes.html "
12881287
"to prepare your network for import.",
12891288
pypsa_version_str,
1290-
__version_semver__,
1289+
__version_base__,
12911290
)
12921291

12931292
# Check for newer PyPSA version available
1294-
update_msg = _check_for_update(__version_semver_tuple__, "PyPSA", "pypsa")
1293+
update_msg = _check_for_update(__version_base__, "PyPSA", "pypsa")
12951294
if update_msg:
12961295
logger.info(update_msg)
12971296

1298-
if pypsa_version_tuple < (0, 18, 0):
1297+
if pypsa_version < parse_version("0.18.0"):
12991298
self._multi_invest = 0
13001299

13011300
# if there is snapshots.csv, read in snapshot data

pypsa/networks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from pypsa.plot.accessor import PlotAccessor
5151
from pypsa.plot.maps import explore
5252
from pypsa.statistics.expressions import StatisticsAccessor
53-
from pypsa.version import __version_semver__
53+
from pypsa.version import __version_base__
5454

5555
if TYPE_CHECKING:
5656
from collections.abc import Collection, Iterator, Sequence
@@ -127,7 +127,7 @@ def __init__(
127127
logging.basicConfig(level=logging.INFO)
128128

129129
# Store PyPSA version
130-
self._pypsa_version: str = __version_semver__
130+
self._pypsa_version: str = __version_base__
131131

132132
# Set attributes
133133
self._name = name

pypsa/version.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,10 @@
44
--------
55
>>> pypsa.__version__ # doctest: +SKIP
66
'0.34.0.post1.dev44+gf5e415b6'
7-
>>> pypsa.__version_semver__ # doctest: +SKIP
7+
>>> pypsa.__version_base__ # doctest: +SKIP
88
'0.34.0'
9-
>>> pypsa.__version_short__ # doctest: +SKIP
9+
>>> pypsa.__version_major_minor__ # doctest: +SKIP
1010
'0.34'
11-
>>> pypsa.__version_semver_tuple__ # doctest: +SKIP
12-
(0, 34, 0)
13-
>>> pypsa.__version_short_tuple__ # doctest: +SKIP
14-
(0, 34)
1511
1612
"""
1713

@@ -40,17 +36,15 @@ def check_pypsa_version(version_string: str) -> None:
4036
msg = f"Could not determine release_version of pypsa: {__version__}"
4137
raise ValueError(msg)
4238

43-
__version_semver__ = match.group(0)
44-
__version_semver_tuple__ = tuple(map(int, __version_semver__.split(".")))
39+
__version_base__ = match.group(0)
4540
# e.g. "0.17"
4641
match = re.match(r"(\d+\.\d+)", __version__)
4742

4843
if not match:
49-
msg = f"Could not determine release_version_short of pypsa: {__version__}"
44+
msg = f"Could not determine major_minor version of pypsa: {__version__}"
5045
raise ValueError(msg)
5146

52-
__version_short__ = match.group(1)
53-
__version_short_tuple__ = tuple(map(int, __version_short__.split(".")))
47+
__version_major_minor__ = match.group(1)
5448

5549
# Check pypsa version
5650
check_pypsa_version(__version__)

test/test_common.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,14 @@ def read(self):
308308
[
309309
# Test case format: (current_version, latest_version, expected_message)
310310
(
311-
(1, 0, 0),
311+
"1.0.0",
312312
"2.0.0",
313313
"New version 2.0.0 available! (Current: 1.0.0)",
314314
), # newer version
315-
((1, 0, 0), "1.0.0", ""), # same version
316-
((2, 0, 0), "1.0.0", ""), # current is newer
315+
("1.0.0", "1.0.0", ""), # same version
316+
("2.0.0", "1.0.0", ""), # current is newer
317317
(
318-
(1, 2, 2),
318+
"1.2.2",
319319
"1.2.3",
320320
"New version 1.2.3 available! (Current: 1.2.2)",
321321
), # minor update
@@ -336,7 +336,7 @@ def test_check_for_update_error_handling():
336336
mock_urlopen.side_effect = Exception("Connection failed")
337337

338338
_check_for_update.cache_clear()
339-
result = _check_for_update((1, 0, 0), "test_owner", "test_repo")
339+
result = _check_for_update("1.0.0", "test_owner", "test_repo")
340340
assert result == ""
341341

342342

@@ -346,7 +346,7 @@ def test_check_for_update_respects_network_option(mock_response):
346346

347347
# Test that version check is skipped when network requests are disabled
348348
with pypsa.option_context("general.allow_network_requests", False):
349-
result = _check_for_update((1, 0, 0), "test_owner", "test_repo")
349+
result = _check_for_update("1.0.0", "test_owner", "test_repo")
350350
assert result == ""
351351

352352
# Test that version check works when network requests are allowed
@@ -355,7 +355,7 @@ def test_check_for_update_respects_network_option(mock_response):
355355

356356
_check_for_update.cache_clear()
357357
with pypsa.option_context("general.allow_network_requests", True):
358-
result = _check_for_update((1, 0, 0), "test_owner", "test_repo")
358+
result = _check_for_update("1.0.0", "test_owner", "test_repo")
359359
assert "New version 2.0.0 available!" in result
360360

361361

test/test_common_deprecations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
@pytest.fixture
1616
def mock_version_semver():
17-
with patch("pypsa.common.__version_semver__", "1.0.0"):
17+
with patch("pypsa.common.__version_base__", "1.0.0"):
1818
yield
1919

2020

test/test_version.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,45 @@
1+
import pytest
2+
3+
import pypsa
14
from pypsa.version import check_pypsa_version
25

36

47
def test_version_check(caplog):
8+
caplog.clear()
59
check_pypsa_version("0.20.0")
610
assert caplog.text == ""
711

12+
caplog.clear()
813
check_pypsa_version("0.0")
914
assert "The correct version of PyPSA could not be resolved" in caplog.text
15+
16+
17+
def test_version_deprecations():
18+
"""Test that deprecated version attributes raise DeprecationWarning."""
19+
# Test deprecated renamed attributes
20+
with pytest.raises(
21+
DeprecationWarning,
22+
match="pypsa.__version_semver__ is deprecated. Use pypsa.__version_base__ instead.",
23+
):
24+
_ = pypsa.__version_semver__
25+
26+
with pytest.raises(
27+
DeprecationWarning,
28+
match="pypsa.__version_short__ is deprecated. Use pypsa.__version_major_minor__ instead.",
29+
):
30+
_ = pypsa.__version_short__
31+
32+
# Test removed tuple attributes
33+
with pytest.raises(
34+
DeprecationWarning, match="pypsa.__version_semver_tuple__ has been removed"
35+
):
36+
_ = pypsa.__version_semver_tuple__
37+
38+
with pytest.raises(
39+
DeprecationWarning, match="pypsa.__version_short_tuple__ has been removed"
40+
):
41+
_ = pypsa.__version_short_tuple__
42+
43+
# Test that new attributes work
44+
assert isinstance(pypsa.__version_base__, str)
45+
assert isinstance(pypsa.__version_major_minor__, str)

0 commit comments

Comments
 (0)