Skip to content

Commit

Permalink
feat: expose the Juju version via Model objects (canonical#1563)
Browse files Browse the repository at this point in the history
Charm code currently needs to do `JujuVersion.from_environ()` to get the
Juju version. This isn't very 'ops-y' - we expose all the other Juju
state through the `Model` object, and it also means that charms
instantiate `JujuVersion` objects multiple times - even though the
framework has actually done this already when parsing the Juju
environment variables.

This PR exposes the Juju version via `Model.juju_version`, and
deprecates the `JujuVersion.from_environ()` method (actual removal will
presumably be in 3.0, a long time off).

This also centralises all the Juju environment variable processing in
the `JujuContext` object.
  • Loading branch information
tonyandrewmeyer authored Feb 13, 2025
1 parent e6f435e commit 204667c
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 4 deletions.
11 changes: 10 additions & 1 deletion ops/jujuversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import os
import re
import warnings
from functools import total_ordering
from typing import Union

Expand Down Expand Up @@ -100,7 +101,15 @@ def __lt__(self, other: Union[str, 'JujuVersion']) -> bool:

@classmethod
def from_environ(cls) -> 'JujuVersion':
"""Build a version from the ``JUJU_VERSION`` environment variable."""
"""Build a version from the ``JUJU_VERSION`` environment variable.
.. deprecated:: 2.19.0 Use :meth:`Model.juju_version` instead.
"""
warnings.warn(
'JujuVersion.from_environ() is deprecated, use self.model.juju_version instead',
DeprecationWarning,
stacklevel=2,
)
v = os.environ.get('JUJU_VERSION')
if v is None:
v = '0.0.0'
Expand Down
5 changes: 5 additions & 0 deletions ops/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ def uuid(self) -> str:
"""
return self._backend.model_uuid

@property
def juju_version(self) -> 'ops.JujuVersion':
"""Return the version of Juju that is running the model."""
return self._backend._juju_context.version

def get_unit(self, unit_name: str) -> 'Unit':
"""Get an arbitrary unit by name.
Expand Down
9 changes: 6 additions & 3 deletions test/test_jujuversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,18 @@ def test_parsing(vs: str, major: int, minor: int, tag: str, patch: int, build: i
@unittest.mock.patch('os.environ', new={})
def test_from_environ():
# JUJU_VERSION is not set
v = ops.JujuVersion.from_environ()
with pytest.deprecated_call():
v = ops.JujuVersion.from_environ()
assert v == ops.JujuVersion('0.0.0')

os.environ['JUJU_VERSION'] = 'no'
with pytest.raises(RuntimeError, match='not a valid Juju version'):
ops.JujuVersion.from_environ()
with pytest.deprecated_call():
ops.JujuVersion.from_environ()

os.environ['JUJU_VERSION'] = '2.8.0'
v = ops.JujuVersion.from_environ()
with pytest.deprecated_call():
v = ops.JujuVersion.from_environ()
assert v == ops.JujuVersion('2.8.0')


Expand Down
10 changes: 10 additions & 0 deletions test/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,16 @@ def test_push_path_unnamed(self, getpwuid: mock.MagicMock, getgrgid: mock.MagicM
container.push_path(push_path, '/')
assert container.exists('/src.txt'), 'push_path failed: file "src.txt" missing'

def test_juju_version_from_model(self):
version = '3.6.2'
context = _JujuContext.from_dict({'JUJU_VERSION': version})
backend = _ModelBackend('myapp/0', juju_context=context)
model = ops.Model(ops.CharmMeta(), backend)
assert model.juju_version == version
assert isinstance(model.juju_version, ops.JujuVersion)
# Make sure it's not being loaded from the environment.
assert JujuVersion.from_environ() == '0.0.0'


class PushPullCase:
"""Test case for table-driven tests."""
Expand Down

0 comments on commit 204667c

Please sign in to comment.