ops 2.23.0.dev0__tar.gz → 2.23.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. {ops-2.23.0.dev0 → ops-2.23.2}/CHANGES.md +83 -0
  2. {ops-2.23.0.dev0 → ops-2.23.2}/PKG-INFO +8 -8
  3. {ops-2.23.0.dev0 → ops-2.23.2}/README.md +3 -3
  4. {ops-2.23.0.dev0/test/charms/test_main/lib → ops-2.23.2}/ops/_main.py +16 -10
  5. {ops-2.23.0.dev0 → ops-2.23.2}/ops/_private/harness.py +1 -1
  6. {ops-2.23.0.dev0 → ops-2.23.2}/ops/charm.py +2 -2
  7. {ops-2.23.0.dev0/test/charms/test_main/lib → ops-2.23.2}/ops/model.py +37 -17
  8. {ops-2.23.0.dev0/test/charms/test_main/lib → ops-2.23.2}/ops/version.py +1 -1
  9. {ops-2.23.0.dev0 → ops-2.23.2}/ops.egg-info/PKG-INFO +8 -8
  10. {ops-2.23.0.dev0 → ops-2.23.2}/ops.egg-info/requires.txt +2 -2
  11. {ops-2.23.0.dev0 → ops-2.23.2}/pyproject.toml +4 -4
  12. {ops-2.23.0.dev0 → ops-2.23.2/test/charms/test_main/lib}/ops/_main.py +16 -10
  13. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/_private/harness.py +1 -1
  14. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/charm.py +2 -2
  15. {ops-2.23.0.dev0 → ops-2.23.2/test/charms/test_main/lib}/ops/model.py +37 -17
  16. {ops-2.23.0.dev0 → ops-2.23.2/test/charms/test_main/lib}/ops/version.py +1 -1
  17. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_model.py +21 -78
  18. {ops-2.23.0.dev0 → ops-2.23.2}/tox.ini +5 -2
  19. {ops-2.23.0.dev0 → ops-2.23.2}/CODE_OF_CONDUCT.md +0 -0
  20. {ops-2.23.0.dev0 → ops-2.23.2}/CONTRIBUTING.md +0 -0
  21. {ops-2.23.0.dev0 → ops-2.23.2}/HACKING.md +0 -0
  22. {ops-2.23.0.dev0 → ops-2.23.2}/LICENSE.txt +0 -0
  23. {ops-2.23.0.dev0 → ops-2.23.2}/MANIFEST.in +0 -0
  24. {ops-2.23.0.dev0 → ops-2.23.2}/SECURITY.md +0 -0
  25. {ops-2.23.0.dev0 → ops-2.23.2}/STYLE.md +0 -0
  26. {ops-2.23.0.dev0 → ops-2.23.2}/ops/__init__.py +0 -0
  27. {ops-2.23.0.dev0 → ops-2.23.2}/ops/_private/__init__.py +0 -0
  28. {ops-2.23.0.dev0 → ops-2.23.2}/ops/_private/timeconv.py +0 -0
  29. {ops-2.23.0.dev0 → ops-2.23.2}/ops/_private/yaml.py +0 -0
  30. {ops-2.23.0.dev0 → ops-2.23.2}/ops/framework.py +0 -0
  31. {ops-2.23.0.dev0 → ops-2.23.2}/ops/jujucontext.py +0 -0
  32. {ops-2.23.0.dev0 → ops-2.23.2}/ops/jujuversion.py +0 -0
  33. {ops-2.23.0.dev0 → ops-2.23.2}/ops/lib/__init__.py +0 -0
  34. {ops-2.23.0.dev0 → ops-2.23.2}/ops/log.py +0 -0
  35. {ops-2.23.0.dev0 → ops-2.23.2}/ops/main.py +0 -0
  36. {ops-2.23.0.dev0 → ops-2.23.2}/ops/pebble.py +0 -0
  37. {ops-2.23.0.dev0 → ops-2.23.2}/ops/py.typed +0 -0
  38. {ops-2.23.0.dev0 → ops-2.23.2}/ops/storage.py +0 -0
  39. {ops-2.23.0.dev0 → ops-2.23.2}/ops/testing.py +0 -0
  40. {ops-2.23.0.dev0 → ops-2.23.2}/ops.egg-info/SOURCES.txt +0 -0
  41. {ops-2.23.0.dev0 → ops-2.23.2}/ops.egg-info/dependency_links.txt +0 -0
  42. {ops-2.23.0.dev0 → ops-2.23.2}/ops.egg-info/top_level.txt +0 -0
  43. {ops-2.23.0.dev0 → ops-2.23.2}/setup.cfg +0 -0
  44. {ops-2.23.0.dev0 → ops-2.23.2}/test/__init__.py +0 -0
  45. {ops-2.23.0.dev0 → ops-2.23.2}/test/benchmark/__init__.py +0 -0
  46. {ops-2.23.0.dev0 → ops-2.23.2}/test/bin/relation-ids +0 -0
  47. {ops-2.23.0.dev0 → ops-2.23.2}/test/bin/relation-ids.bat +0 -0
  48. {ops-2.23.0.dev0 → ops-2.23.2}/test/bin/relation-list +0 -0
  49. {ops-2.23.0.dev0 → ops-2.23.2}/test/bin/relation-list.bat +0 -0
  50. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/actions.yaml +0 -0
  51. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/config.yaml +0 -0
  52. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/__init__.py +0 -0
  53. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/__init__.py +0 -0
  54. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/_private/__init__.py +0 -0
  55. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/_private/timeconv.py +0 -0
  56. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/_private/yaml.py +0 -0
  57. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/framework.py +0 -0
  58. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/jujucontext.py +0 -0
  59. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/jujuversion.py +0 -0
  60. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/lib/__init__.py +0 -0
  61. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/log.py +0 -0
  62. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/main.py +0 -0
  63. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/pebble.py +0 -0
  64. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/py.typed +0 -0
  65. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/storage.py +0 -0
  66. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/lib/ops/testing.py +0 -0
  67. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/metadata.yaml +0 -0
  68. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_main/src/charm.py +0 -0
  69. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_relation/.gitignore +0 -0
  70. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_relation/charmcraft.yaml +0 -0
  71. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_relation/src/charm.py +0 -0
  72. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_smoke/README.md +0 -0
  73. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_smoke/metadata.yaml +0 -0
  74. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_smoke/src/charm.py +0 -0
  75. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_tracing/.gitignore +0 -0
  76. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_tracing/charmcraft.yaml +0 -0
  77. {ops-2.23.0.dev0 → ops-2.23.2}/test/charms/test_tracing/src/charm.py +0 -0
  78. {ops-2.23.0.dev0 → ops-2.23.2}/test/conftest.py +0 -0
  79. {ops-2.23.0.dev0 → ops-2.23.2}/test/fake_pebble.py +0 -0
  80. {ops-2.23.0.dev0 → ops-2.23.2}/test/integration/conftest.py +0 -0
  81. {ops-2.23.0.dev0 → ops-2.23.2}/test/integration/test_relation.py +0 -0
  82. {ops-2.23.0.dev0 → ops-2.23.2}/test/integration/test_tracing.py +0 -0
  83. {ops-2.23.0.dev0 → ops-2.23.2}/test/pebble_cli.py +0 -0
  84. {ops-2.23.0.dev0 → ops-2.23.2}/test/smoke/test_smoke.py +0 -0
  85. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_charm.py +0 -0
  86. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_framework.py +0 -0
  87. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_helpers.py +0 -0
  88. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_infra.py +0 -0
  89. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_jujucontext.py +0 -0
  90. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_jujuversion.py +0 -0
  91. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_lib.py +0 -0
  92. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_log.py +0 -0
  93. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_main.py +0 -0
  94. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_main_invocation.py +0 -0
  95. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_main_type_hint.py +0 -0
  96. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_model_relation_data_class.py +0 -0
  97. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_pebble.py +0 -0
  98. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_real_pebble.py +0 -0
  99. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_storage.py +0 -0
  100. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_testing.py +0 -0
  101. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_timeconv.py +0 -0
  102. {ops-2.23.0.dev0 → ops-2.23.2}/test/test_yaml.py +0 -0
@@ -1,3 +1,86 @@
1
+ # 2.23.2 - 11 February 2026
2
+
3
+ ## Fixes
4
+
5
+ * Drop unused `setuptools_scm` build dependency (#2318)
6
+
7
+ ## Documentation
8
+
9
+ * For 2.23, update links and config for switch to documentation.ubuntu.com/ops (#1942)
10
+ * For 2.x, fix site title and unstyled error pages (#1945)
11
+ * For 2.x, remove .html extensions (#1954)
12
+ * For 2.x, fix unstyled error pages (#1973)
13
+
14
+ # 2.23.1 - 30 July 2025
15
+
16
+ ## Fixes
17
+
18
+ * Add the remote unit to `Relation.data` but not `Relation.units` (#1928)
19
+
20
+ ## Documentation
21
+
22
+ * Be consistent with recommending `self.app` and `self.unit` (#1856)
23
+ * Add notice about ops 2 and ops 3 (#1867)
24
+ * Update title and edit links for ops 2.23 docs (#1885)
25
+
26
+ ## CI
27
+
28
+ * Hotfix, publish job for ops-tracing (#1865)
29
+
30
+ # 2.23.0 - 30 June 2025
31
+
32
+ ## Features
33
+
34
+ * Support for config schema as Python classes (#1741)
35
+ * Support for action parameter schema as Python classes (#1756)
36
+ * Ops[tracing] compatibility with jhack (#1806)
37
+ * Support for relation data schema as Python classes (#1701)
38
+ * Add CheckInfo.successes field and .has_run property (#1819)
39
+ * Provide a method to create a testing.State from a testing.Context (#1797)
40
+ * Expose trace data in testing (#1842)
41
+ * Add a helper to generate a Layer from rockcraft.yaml (#1831)
42
+
43
+ ## Fixes
44
+
45
+ * Correctly load an empty Juju config options map (#1778)
46
+ * Fix type annotation of container check_infos in ops.testing (#1784)
47
+ * Restrict the version of a dependency, opentelemetry-sdk (#1794)
48
+ * Remote unit data is available in relation-departed (#1364)
49
+ * Juju allows access to the remote app databag in relation-broken, so Harness should too (#1787)
50
+ * Don't use private OpenTelemetry API (#1798)
51
+ * Do not return this unit in a mocked peer relation (#1828)
52
+ * Testing.PeerRelation properly defaults to no peers (#1832)
53
+ * In meter-status-changed JUJU_VERSION is not set (#1840)
54
+ * Only provide the units belonging to the app in Relation.units (#1837)
55
+
56
+ ## Documentation
57
+
58
+ * Remove two best practices, and drop two to tips (#1758)
59
+ * Update link to Charmcraft for managing app config (#1763)
60
+ * Update link to Juju documentation for setting up deployment (#1781)
61
+ * Fix external OTLP link (#1786)
62
+ * Distribute the ops-scenario README content across the ops docs (#1773)
63
+ * Improve testing.errors.UncaughtCharmError message (#1795)
64
+ * In the "manage the charm version" how-to, give an example of using override-build (#1802)
65
+ * Small adjustments to the 'how to trace charm code' doc (#1792)
66
+ * Replace Harness example and fix links in README (#1820)
67
+ * Add httpbin charm from Charmcraft as an example charm (#1743)
68
+ * Fix on_collect mistake in sample code (#1829)
69
+ * Update code in K8s tutorial, with source in repo (part 2) (#1734)
70
+ * Update Loki section on charming zero-to-hero tutorial (#1847)
71
+ * Remove expandable boxes of text (#1844)
72
+ * Improve httpbin charm by removing defer() and adding collect_status (#1833)
73
+ * Move {posargs} to the end of pytest command lines in tox.ini (#1854)
74
+
75
+ ## CI
76
+
77
+ * Install the ops[tracing] dependencies for the TIOBE action (#1761)
78
+ * Add ops-scenario and ops-tracing as explicit installs for TIOBE (#1764)
79
+ * Persist credentials for update-charm-pins workflow (#1766)
80
+ * Stop smoke testing Charmcraft 2 (#1782)
81
+ * Use Charmcraft 3.x for smoke testing 20.04 and 22.04 (#1821)
82
+ * Enable xdist for the 'unit' tox environments (#1830)
83
+
1
84
  # 2.22.0 - 29 May 2025
2
85
 
3
86
  ## Features
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ops
3
- Version: 2.23.0.dev0
3
+ Version: 2.23.2
4
4
  Summary: The Python library behind great charms
5
5
  Author: The Charm Tech team at Canonical Ltd.
6
- Project-URL: Homepage, https://ops.readthedocs.io/en/latest/
6
+ Project-URL: Homepage, https://documentation.ubuntu.com/ops/2.x/
7
7
  Project-URL: Repository, https://github.com/canonical/operator
8
8
  Project-URL: Issues, https://github.com/canonical/operator/issues
9
- Project-URL: Documentation, https://ops.readthedocs.io
9
+ Project-URL: Documentation, https://documentation.ubuntu.com/ops/2.x/
10
10
  Project-URL: Changelog, https://github.com/canonical/operator/blob/main/CHANGES.md
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: License :: OSI Approved :: Apache Software License
@@ -23,9 +23,9 @@ Requires-Dist: websocket-client==1.*
23
23
  Requires-Dist: opentelemetry-api~=1.0
24
24
  Requires-Dist: importlib-metadata
25
25
  Provides-Extra: testing
26
- Requires-Dist: ops-scenario==7.23.0.dev0; extra == "testing"
26
+ Requires-Dist: ops-scenario==7.23.2; extra == "testing"
27
27
  Provides-Extra: tracing
28
- Requires-Dist: ops-tracing==2.23.0.dev0; extra == "tracing"
28
+ Requires-Dist: ops-tracing==2.23.2; extra == "tracing"
29
29
  Provides-Extra: harness
30
30
  Dynamic: license-file
31
31
 
@@ -36,8 +36,8 @@ Dynamic: license-file
36
36
  The `ops` library is a Python framework for developing and testing Kubernetes and machine [charms](https://charmhub.io/). While charms can be written in any language, `ops` defines the latest standard, and charmers are encouraged to use Python with `ops` for all charms. The library is an official component of the Charm SDK, itself a part of [the Juju universe](https://juju.is/).
37
37
 
38
38
  > - `ops` is [available on PyPI](https://pypi.org/project/ops/).
39
- > - The latest version of `ops` requires Python 3.8 or above.
40
- > - Read our [docs](https://ops.readthedocs.io/en/latest/) for tutorials, how-to guides, the library reference, and more.
39
+ > - Version 2.x of `ops` requires Python 3.8 or above.
40
+ > - Read our [docs](https://documentation.ubuntu.com/ops/2.x/) for tutorials, how-to guides, the library reference, and more.
41
41
 
42
42
  ## Give it a try
43
43
 
@@ -181,6 +181,6 @@ Congratulations, you’ve just built your first Kubernetes charm using `ops`!
181
181
 
182
182
  ## Next steps
183
183
 
184
- - Read the [docs](https://ops.readthedocs.io/en/latest/).
184
+ - Read the [docs](https://documentation.ubuntu.com/ops/2.x/).
185
185
  - Read our [Code of conduct](https://ubuntu.com/community/code-of-conduct) and join our [chat](https://matrix.to/#/#charmhub-ops:ubuntu.com) and [forum](https://discourse.charmhub.io/) or [open an issue](https://github.com/canonical/operator/issues).
186
186
  - Read our [CONTRIBUTING guide](https://github.com/canonical/operator/blob/main/HACKING.md) and contribute!
@@ -5,8 +5,8 @@
5
5
  The `ops` library is a Python framework for developing and testing Kubernetes and machine [charms](https://charmhub.io/). While charms can be written in any language, `ops` defines the latest standard, and charmers are encouraged to use Python with `ops` for all charms. The library is an official component of the Charm SDK, itself a part of [the Juju universe](https://juju.is/).
6
6
 
7
7
  > - `ops` is [available on PyPI](https://pypi.org/project/ops/).
8
- > - The latest version of `ops` requires Python 3.8 or above.
9
- > - Read our [docs](https://ops.readthedocs.io/en/latest/) for tutorials, how-to guides, the library reference, and more.
8
+ > - Version 2.x of `ops` requires Python 3.8 or above.
9
+ > - Read our [docs](https://documentation.ubuntu.com/ops/2.x/) for tutorials, how-to guides, the library reference, and more.
10
10
 
11
11
  ## Give it a try
12
12
 
@@ -150,6 +150,6 @@ Congratulations, you’ve just built your first Kubernetes charm using `ops`!
150
150
 
151
151
  ## Next steps
152
152
 
153
- - Read the [docs](https://ops.readthedocs.io/en/latest/).
153
+ - Read the [docs](https://documentation.ubuntu.com/ops/2.x/).
154
154
  - Read our [Code of conduct](https://ubuntu.com/community/code-of-conduct) and join our [chat](https://matrix.to/#/#charmhub-ops:ubuntu.com) and [forum](https://discourse.charmhub.io/) or [open an issue](https://github.com/canonical/operator/issues).
155
155
  - Read our [CONTRIBUTING guide](https://github.com/canonical/operator/blob/main/HACKING.md) and contribute!
@@ -368,24 +368,30 @@ class _Manager:
368
368
  def _make_framework(self, dispatcher: _Dispatcher):
369
369
  # If we are in a RelationBroken event, we want to know which relation is
370
370
  # broken within the model, not only in the event's `.relation` attribute.
371
- if self._juju_context.dispatch_path.endswith(('-relation-broken', '_relation_broken')):
371
+ if dispatcher.event_name.endswith('_relation_broken'):
372
372
  broken_relation_id = self._juju_context.relation_id
373
373
  else:
374
374
  broken_relation_id = None
375
375
 
376
+ # In a RelationDeparted event, the unit is not included in the Juju
377
+ # `relation-list` output, but the charm still has access to the remote
378
+ # relation data. To provide the charm with a mechanism for getting
379
+ # access to that data, we include the remote unit in Relation.units.
380
+ # We also expect it to be available in RelationBroken events, so ensure
381
+ # that it's available then as well. For other relation events, the unit
382
+ # will either already be in the set via `relation-list` (such as in a
383
+ # RelationChanged event) or correctly not in the list yet because the
384
+ # relation has not been fully set up (such as in a RelationJoined event).
385
+ if dispatcher.event_name.endswith(('_relation_departed', '_relation_broken')):
386
+ remote_unit_name = self._juju_context.remote_unit_name
387
+ else:
388
+ remote_unit_name = None
389
+
376
390
  model = _model.Model(
377
391
  self._charm_meta,
378
392
  self._model_backend,
379
393
  broken_relation_id=broken_relation_id,
380
- # In a RelationDeparted event, the unit is not included in the Juju
381
- # `relation-list` output, but the charm still has access to the remote
382
- # relation data. To provide the charm with a mechanism for getting
383
- # access to that data, we include the remote unit in Relation.units.
384
- # In other relation events (such as RelationChanged) the unit will
385
- # already be in the set via `relation-list` - adding it via this extra
386
- # mechanism will not change the final set, and is simpler than only
387
- # adding it in specific events.
388
- remote_unit_name=self._juju_context.remote_unit_name,
394
+ remote_unit_name=remote_unit_name,
389
395
  )
390
396
  store = self._make_storage(dispatcher)
391
397
  framework = _framework.Framework(
@@ -323,7 +323,7 @@ class Harness(Generic[CharmType]):
323
323
 
324
324
  warnings.warn(
325
325
  'Harness is deprecated. For the recommended approach, see: '
326
- 'https://ops.readthedocs.io/en/latest/howto/write-unit-tests-for-a-charm.html',
326
+ 'https://documentation.ubuntu.com/ops/2.x/howto/write-unit-tests-for-a-charm.html',
327
327
  PendingDeprecationWarning,
328
328
  stacklevel=2,
329
329
  )
@@ -1467,12 +1467,12 @@ class CharmBase(Object):
1467
1467
 
1468
1468
  @property
1469
1469
  def app(self) -> model.Application:
1470
- """Application that this unit is part of."""
1470
+ """The application that this unit is part of."""
1471
1471
  return self.framework.model.app
1472
1472
 
1473
1473
  @property
1474
1474
  def unit(self) -> model.Unit:
1475
- """Unit that this execution is responsible for."""
1475
+ """The current unit."""
1476
1476
  return self.framework.model.unit
1477
1477
 
1478
1478
  @property
@@ -159,17 +159,17 @@ class Model:
159
159
 
160
160
  @property
161
161
  def unit(self) -> Unit:
162
- """The unit that is running this code.
162
+ """The current unit. Equivalent to :attr:`CharmBase.unit`.
163
163
 
164
- Use :meth:`get_unit` to get an arbitrary unit by name.
164
+ To get a unit by name, use :meth:`get_unit`.
165
165
  """
166
166
  return self._unit
167
167
 
168
168
  @property
169
169
  def app(self) -> Application:
170
- """The application this unit is a part of.
170
+ """The application that this unit is part of. Equivalent to :attr:`CharmBase.app`.
171
171
 
172
- Use :meth:`get_app` to get an arbitrary application by name.
172
+ To get an application by name, use :meth:`get_app`.
173
173
  """
174
174
  return self._unit.app
175
175
 
@@ -238,22 +238,22 @@ class Model:
238
238
  return self._backend._juju_context.version
239
239
 
240
240
  def get_unit(self, unit_name: str) -> Unit:
241
- """Get an arbitrary unit by name.
242
-
243
- Use :attr:`unit` to get the current unit.
241
+ """Get a unit by name.
244
242
 
245
243
  Internally this uses a cache, so asking for the same unit two times will
246
244
  return the same object.
245
+
246
+ To get the current unit, use :attr:`CharmBase.unit` or :attr:`unit`.
247
247
  """
248
248
  return self._cache.get(Unit, unit_name)
249
249
 
250
250
  def get_app(self, app_name: str) -> Application:
251
251
  """Get an application by name.
252
252
 
253
- Use :attr:`app` to get this charm's application.
254
-
255
253
  Internally this uses a cache, so asking for the same application two times will
256
254
  return the same object.
255
+
256
+ To get the application that this unit is part of, use :attr:`CharmBase.app` or :attr:`app`.
257
257
  """
258
258
  return self._cache.get(Application, app_name)
259
259
 
@@ -380,9 +380,10 @@ class Application:
380
380
  """Represents a named application in the model.
381
381
 
382
382
  This might be this charm's application, or might be an application this charm is integrated
383
- with. Charmers should not instantiate Application objects directly, but should use
384
- :attr:`Model.app` to get the application this unit is part of, or
385
- :meth:`Model.get_app` if they need a reference to a given application.
383
+ with.
384
+
385
+ Don't instantiate Application objects directly. To get the application that this unit is
386
+ part of, use :attr:`CharmBase.app`. To get an application by name, use :meth:`Model.get_app`.
386
387
  """
387
388
 
388
389
  name: str
@@ -426,7 +427,7 @@ class Application:
426
427
 
427
428
  Example::
428
429
 
429
- self.model.app.status = ops.BlockedStatus('I need a human to come help me')
430
+ self.app.status = ops.BlockedStatus('I need a human to come help me')
430
431
  """
431
432
  if not self._is_our_app:
432
433
  return UnknownStatus()
@@ -556,6 +557,9 @@ class Unit:
556
557
 
557
558
  This might be the current unit, another unit of the charm's application, or a unit of
558
559
  another application that the charm is integrated with.
560
+
561
+ Don't instantiate Unit objects directly. To get the current unit, use :attr:`CharmBase.unit`.
562
+ To get a unit by name, use :meth:`Model.get_unit`.
559
563
  """
560
564
 
561
565
  name: str
@@ -609,7 +613,7 @@ class Unit:
609
613
 
610
614
  Example::
611
615
 
612
- self.model.unit.status = ops.MaintenanceStatus('reconfiguring the frobnicators')
616
+ self.unit.status = ops.MaintenanceStatus('reconfiguring the frobnicators')
613
617
  """
614
618
  if not self._is_our_unit:
615
619
  return UnknownStatus()
@@ -1770,16 +1774,23 @@ class Relation:
1770
1774
 
1771
1775
  # self.app will not be None and always be set because of the fallback mechanism above.
1772
1776
  self.app = typing.cast('Application', app)
1773
- self.data = RelationData(self, our_unit, backend)
1774
1777
 
1775
1778
  # In relation-departed `relation-list` doesn't include the remote unit,
1776
1779
  # but the data should still be available.
1777
1780
  if (
1778
1781
  _remote_unit is not None
1779
1782
  and not is_peer
1783
+ # In practice, the "self.app will not be None" statement above is not
1784
+ # necessarily true. Once https://bugs.launchpad.net/juju/+bug/1960934
1785
+ # is resolved, we should be able to remove the next line.
1786
+ and self.app is not None
1780
1787
  and _remote_unit.name.startswith(f'{self.app.name}/')
1781
1788
  ):
1782
- self.units.add(_remote_unit)
1789
+ remote_unit = _remote_unit
1790
+ else:
1791
+ remote_unit = None
1792
+
1793
+ self.data = RelationData(self, our_unit, backend, remote_unit)
1783
1794
 
1784
1795
  self._remote_model: RemoteModel | None = None
1785
1796
 
@@ -1975,7 +1986,13 @@ class RelationData(Mapping[Union[Unit, Application], 'RelationDataContent']):
1975
1986
  :attr:`Relation.data`
1976
1987
  """
1977
1988
 
1978
- def __init__(self, relation: Relation, our_unit: Unit, backend: _ModelBackend):
1989
+ def __init__(
1990
+ self,
1991
+ relation: Relation,
1992
+ our_unit: Unit,
1993
+ backend: _ModelBackend,
1994
+ remote_unit: Unit | None = None,
1995
+ ):
1979
1996
  self.relation = weakref.proxy(relation)
1980
1997
  self._data: dict[Unit | Application, RelationDataContent] = {
1981
1998
  our_unit: RelationDataContent(self.relation, our_unit, backend),
@@ -1989,6 +2006,9 @@ class RelationData(Mapping[Union[Unit, Application], 'RelationDataContent']):
1989
2006
  self._data.update({
1990
2007
  self.relation.app: RelationDataContent(self.relation, self.relation.app, backend),
1991
2008
  })
2009
+ # The unit might be departing or broken, so not in relation-list, but accessible.
2010
+ if remote_unit is not None and remote_unit not in self._data:
2011
+ self._data[remote_unit] = RelationDataContent(self.relation, remote_unit, backend)
1992
2012
 
1993
2013
  def __contains__(self, key: Unit | Application):
1994
2014
  return key in self._data
@@ -19,4 +19,4 @@ This module is NOT to be used when developing charms using ops.
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
- version: str = '2.23.0.dev0'
22
+ version: str = '2.23.2'
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ops
3
- Version: 2.23.0.dev0
3
+ Version: 2.23.2
4
4
  Summary: The Python library behind great charms
5
5
  Author: The Charm Tech team at Canonical Ltd.
6
- Project-URL: Homepage, https://ops.readthedocs.io/en/latest/
6
+ Project-URL: Homepage, https://documentation.ubuntu.com/ops/2.x/
7
7
  Project-URL: Repository, https://github.com/canonical/operator
8
8
  Project-URL: Issues, https://github.com/canonical/operator/issues
9
- Project-URL: Documentation, https://ops.readthedocs.io
9
+ Project-URL: Documentation, https://documentation.ubuntu.com/ops/2.x/
10
10
  Project-URL: Changelog, https://github.com/canonical/operator/blob/main/CHANGES.md
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: License :: OSI Approved :: Apache Software License
@@ -23,9 +23,9 @@ Requires-Dist: websocket-client==1.*
23
23
  Requires-Dist: opentelemetry-api~=1.0
24
24
  Requires-Dist: importlib-metadata
25
25
  Provides-Extra: testing
26
- Requires-Dist: ops-scenario==7.23.0.dev0; extra == "testing"
26
+ Requires-Dist: ops-scenario==7.23.2; extra == "testing"
27
27
  Provides-Extra: tracing
28
- Requires-Dist: ops-tracing==2.23.0.dev0; extra == "tracing"
28
+ Requires-Dist: ops-tracing==2.23.2; extra == "tracing"
29
29
  Provides-Extra: harness
30
30
  Dynamic: license-file
31
31
 
@@ -36,8 +36,8 @@ Dynamic: license-file
36
36
  The `ops` library is a Python framework for developing and testing Kubernetes and machine [charms](https://charmhub.io/). While charms can be written in any language, `ops` defines the latest standard, and charmers are encouraged to use Python with `ops` for all charms. The library is an official component of the Charm SDK, itself a part of [the Juju universe](https://juju.is/).
37
37
 
38
38
  > - `ops` is [available on PyPI](https://pypi.org/project/ops/).
39
- > - The latest version of `ops` requires Python 3.8 or above.
40
- > - Read our [docs](https://ops.readthedocs.io/en/latest/) for tutorials, how-to guides, the library reference, and more.
39
+ > - Version 2.x of `ops` requires Python 3.8 or above.
40
+ > - Read our [docs](https://documentation.ubuntu.com/ops/2.x/) for tutorials, how-to guides, the library reference, and more.
41
41
 
42
42
  ## Give it a try
43
43
 
@@ -181,6 +181,6 @@ Congratulations, you’ve just built your first Kubernetes charm using `ops`!
181
181
 
182
182
  ## Next steps
183
183
 
184
- - Read the [docs](https://ops.readthedocs.io/en/latest/).
184
+ - Read the [docs](https://documentation.ubuntu.com/ops/2.x/).
185
185
  - Read our [Code of conduct](https://ubuntu.com/community/code-of-conduct) and join our [chat](https://matrix.to/#/#charmhub-ops:ubuntu.com) and [forum](https://discourse.charmhub.io/) or [open an issue](https://github.com/canonical/operator/issues).
186
186
  - Read our [CONTRIBUTING guide](https://github.com/canonical/operator/blob/main/HACKING.md) and contribute!
@@ -6,7 +6,7 @@ importlib-metadata
6
6
  [harness]
7
7
 
8
8
  [testing]
9
- ops-scenario==7.23.0.dev0
9
+ ops-scenario==7.23.2
10
10
 
11
11
  [tracing]
12
- ops-tracing==2.23.0.dev0
12
+ ops-tracing==2.23.2
@@ -26,10 +26,10 @@ dynamic = ["version"]
26
26
 
27
27
  [project.optional-dependencies]
28
28
  testing = [
29
- "ops-scenario==7.23.0.dev0",
29
+ "ops-scenario==7.23.2",
30
30
  ]
31
31
  tracing = [
32
- "ops-tracing==2.23.0.dev0",
32
+ "ops-tracing==2.23.2",
33
33
  ]
34
34
  # Empty for now, because Harness is bundled with the base install, but allow
35
35
  # specifying the extra to ease transition later.
@@ -89,10 +89,10 @@ integration = [
89
89
  ]
90
90
 
91
91
  [project.urls]
92
- "Homepage" = "https://ops.readthedocs.io/en/latest/"
92
+ "Homepage" = "https://documentation.ubuntu.com/ops/2.x/"
93
93
  "Repository" = "https://github.com/canonical/operator"
94
94
  "Issues" = "https://github.com/canonical/operator/issues"
95
- "Documentation" = "https://ops.readthedocs.io"
95
+ "Documentation" = "https://documentation.ubuntu.com/ops/2.x/"
96
96
  "Changelog" = "https://github.com/canonical/operator/blob/main/CHANGES.md"
97
97
 
98
98
  [build-system]
@@ -368,24 +368,30 @@ class _Manager:
368
368
  def _make_framework(self, dispatcher: _Dispatcher):
369
369
  # If we are in a RelationBroken event, we want to know which relation is
370
370
  # broken within the model, not only in the event's `.relation` attribute.
371
- if self._juju_context.dispatch_path.endswith(('-relation-broken', '_relation_broken')):
371
+ if dispatcher.event_name.endswith('_relation_broken'):
372
372
  broken_relation_id = self._juju_context.relation_id
373
373
  else:
374
374
  broken_relation_id = None
375
375
 
376
+ # In a RelationDeparted event, the unit is not included in the Juju
377
+ # `relation-list` output, but the charm still has access to the remote
378
+ # relation data. To provide the charm with a mechanism for getting
379
+ # access to that data, we include the remote unit in Relation.units.
380
+ # We also expect it to be available in RelationBroken events, so ensure
381
+ # that it's available then as well. For other relation events, the unit
382
+ # will either already be in the set via `relation-list` (such as in a
383
+ # RelationChanged event) or correctly not in the list yet because the
384
+ # relation has not been fully set up (such as in a RelationJoined event).
385
+ if dispatcher.event_name.endswith(('_relation_departed', '_relation_broken')):
386
+ remote_unit_name = self._juju_context.remote_unit_name
387
+ else:
388
+ remote_unit_name = None
389
+
376
390
  model = _model.Model(
377
391
  self._charm_meta,
378
392
  self._model_backend,
379
393
  broken_relation_id=broken_relation_id,
380
- # In a RelationDeparted event, the unit is not included in the Juju
381
- # `relation-list` output, but the charm still has access to the remote
382
- # relation data. To provide the charm with a mechanism for getting
383
- # access to that data, we include the remote unit in Relation.units.
384
- # In other relation events (such as RelationChanged) the unit will
385
- # already be in the set via `relation-list` - adding it via this extra
386
- # mechanism will not change the final set, and is simpler than only
387
- # adding it in specific events.
388
- remote_unit_name=self._juju_context.remote_unit_name,
394
+ remote_unit_name=remote_unit_name,
389
395
  )
390
396
  store = self._make_storage(dispatcher)
391
397
  framework = _framework.Framework(
@@ -323,7 +323,7 @@ class Harness(Generic[CharmType]):
323
323
 
324
324
  warnings.warn(
325
325
  'Harness is deprecated. For the recommended approach, see: '
326
- 'https://ops.readthedocs.io/en/latest/howto/write-unit-tests-for-a-charm.html',
326
+ 'https://documentation.ubuntu.com/ops/2.x/howto/write-unit-tests-for-a-charm.html',
327
327
  PendingDeprecationWarning,
328
328
  stacklevel=2,
329
329
  )
@@ -1467,12 +1467,12 @@ class CharmBase(Object):
1467
1467
 
1468
1468
  @property
1469
1469
  def app(self) -> model.Application:
1470
- """Application that this unit is part of."""
1470
+ """The application that this unit is part of."""
1471
1471
  return self.framework.model.app
1472
1472
 
1473
1473
  @property
1474
1474
  def unit(self) -> model.Unit:
1475
- """Unit that this execution is responsible for."""
1475
+ """The current unit."""
1476
1476
  return self.framework.model.unit
1477
1477
 
1478
1478
  @property
@@ -159,17 +159,17 @@ class Model:
159
159
 
160
160
  @property
161
161
  def unit(self) -> Unit:
162
- """The unit that is running this code.
162
+ """The current unit. Equivalent to :attr:`CharmBase.unit`.
163
163
 
164
- Use :meth:`get_unit` to get an arbitrary unit by name.
164
+ To get a unit by name, use :meth:`get_unit`.
165
165
  """
166
166
  return self._unit
167
167
 
168
168
  @property
169
169
  def app(self) -> Application:
170
- """The application this unit is a part of.
170
+ """The application that this unit is part of. Equivalent to :attr:`CharmBase.app`.
171
171
 
172
- Use :meth:`get_app` to get an arbitrary application by name.
172
+ To get an application by name, use :meth:`get_app`.
173
173
  """
174
174
  return self._unit.app
175
175
 
@@ -238,22 +238,22 @@ class Model:
238
238
  return self._backend._juju_context.version
239
239
 
240
240
  def get_unit(self, unit_name: str) -> Unit:
241
- """Get an arbitrary unit by name.
242
-
243
- Use :attr:`unit` to get the current unit.
241
+ """Get a unit by name.
244
242
 
245
243
  Internally this uses a cache, so asking for the same unit two times will
246
244
  return the same object.
245
+
246
+ To get the current unit, use :attr:`CharmBase.unit` or :attr:`unit`.
247
247
  """
248
248
  return self._cache.get(Unit, unit_name)
249
249
 
250
250
  def get_app(self, app_name: str) -> Application:
251
251
  """Get an application by name.
252
252
 
253
- Use :attr:`app` to get this charm's application.
254
-
255
253
  Internally this uses a cache, so asking for the same application two times will
256
254
  return the same object.
255
+
256
+ To get the application that this unit is part of, use :attr:`CharmBase.app` or :attr:`app`.
257
257
  """
258
258
  return self._cache.get(Application, app_name)
259
259
 
@@ -380,9 +380,10 @@ class Application:
380
380
  """Represents a named application in the model.
381
381
 
382
382
  This might be this charm's application, or might be an application this charm is integrated
383
- with. Charmers should not instantiate Application objects directly, but should use
384
- :attr:`Model.app` to get the application this unit is part of, or
385
- :meth:`Model.get_app` if they need a reference to a given application.
383
+ with.
384
+
385
+ Don't instantiate Application objects directly. To get the application that this unit is
386
+ part of, use :attr:`CharmBase.app`. To get an application by name, use :meth:`Model.get_app`.
386
387
  """
387
388
 
388
389
  name: str
@@ -426,7 +427,7 @@ class Application:
426
427
 
427
428
  Example::
428
429
 
429
- self.model.app.status = ops.BlockedStatus('I need a human to come help me')
430
+ self.app.status = ops.BlockedStatus('I need a human to come help me')
430
431
  """
431
432
  if not self._is_our_app:
432
433
  return UnknownStatus()
@@ -556,6 +557,9 @@ class Unit:
556
557
 
557
558
  This might be the current unit, another unit of the charm's application, or a unit of
558
559
  another application that the charm is integrated with.
560
+
561
+ Don't instantiate Unit objects directly. To get the current unit, use :attr:`CharmBase.unit`.
562
+ To get a unit by name, use :meth:`Model.get_unit`.
559
563
  """
560
564
 
561
565
  name: str
@@ -609,7 +613,7 @@ class Unit:
609
613
 
610
614
  Example::
611
615
 
612
- self.model.unit.status = ops.MaintenanceStatus('reconfiguring the frobnicators')
616
+ self.unit.status = ops.MaintenanceStatus('reconfiguring the frobnicators')
613
617
  """
614
618
  if not self._is_our_unit:
615
619
  return UnknownStatus()
@@ -1770,16 +1774,23 @@ class Relation:
1770
1774
 
1771
1775
  # self.app will not be None and always be set because of the fallback mechanism above.
1772
1776
  self.app = typing.cast('Application', app)
1773
- self.data = RelationData(self, our_unit, backend)
1774
1777
 
1775
1778
  # In relation-departed `relation-list` doesn't include the remote unit,
1776
1779
  # but the data should still be available.
1777
1780
  if (
1778
1781
  _remote_unit is not None
1779
1782
  and not is_peer
1783
+ # In practice, the "self.app will not be None" statement above is not
1784
+ # necessarily true. Once https://bugs.launchpad.net/juju/+bug/1960934
1785
+ # is resolved, we should be able to remove the next line.
1786
+ and self.app is not None
1780
1787
  and _remote_unit.name.startswith(f'{self.app.name}/')
1781
1788
  ):
1782
- self.units.add(_remote_unit)
1789
+ remote_unit = _remote_unit
1790
+ else:
1791
+ remote_unit = None
1792
+
1793
+ self.data = RelationData(self, our_unit, backend, remote_unit)
1783
1794
 
1784
1795
  self._remote_model: RemoteModel | None = None
1785
1796
 
@@ -1975,7 +1986,13 @@ class RelationData(Mapping[Union[Unit, Application], 'RelationDataContent']):
1975
1986
  :attr:`Relation.data`
1976
1987
  """
1977
1988
 
1978
- def __init__(self, relation: Relation, our_unit: Unit, backend: _ModelBackend):
1989
+ def __init__(
1990
+ self,
1991
+ relation: Relation,
1992
+ our_unit: Unit,
1993
+ backend: _ModelBackend,
1994
+ remote_unit: Unit | None = None,
1995
+ ):
1979
1996
  self.relation = weakref.proxy(relation)
1980
1997
  self._data: dict[Unit | Application, RelationDataContent] = {
1981
1998
  our_unit: RelationDataContent(self.relation, our_unit, backend),
@@ -1989,6 +2006,9 @@ class RelationData(Mapping[Union[Unit, Application], 'RelationDataContent']):
1989
2006
  self._data.update({
1990
2007
  self.relation.app: RelationDataContent(self.relation, self.relation.app, backend),
1991
2008
  })
2009
+ # The unit might be departing or broken, so not in relation-list, but accessible.
2010
+ if remote_unit is not None and remote_unit not in self._data:
2011
+ self._data[remote_unit] = RelationDataContent(self.relation, remote_unit, backend)
1992
2012
 
1993
2013
  def __contains__(self, key: Unit | Application):
1994
2014
  return key in self._data
@@ -19,4 +19,4 @@ This module is NOT to be used when developing charms using ops.
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
- version: str = '2.23.0.dev0'
22
+ version: str = '2.23.2'
@@ -4421,84 +4421,27 @@ class TestGetCloudSpec:
4421
4421
  assert str(excinfo.value) == 'ERROR cannot access cloud credentials\n'
4422
4422
 
4423
4423
 
4424
- @pytest.mark.skipif(
4425
- not hasattr(ops.testing, 'Context'), reason='requires optional ops[testing] install'
4426
- )
4427
- def test_departing_unit_in_relations():
4428
- ctx = ops.testing.Context(
4429
- ops.CharmBase, meta={'name': 'mycharm', 'requires': {'db': {'interface': 'db'}}}
4430
- )
4431
- # In this mocked Juju data, only unit/0 is included.
4432
- rel = ops.testing.Relation('db', remote_units_data={0: {}}, remote_app_name='db')
4433
- state_in = ops.testing.State(relations={rel})
4434
- # We simulate a relation-departed event where the departing unit is unit/1.
4435
- with ctx(ctx.on.relation_departed(rel, remote_unit=1), state_in) as mgr:
4436
- mgr.run()
4437
- # The departing unit, unit/1, should be in the .units set for the relation
4438
- # even though it was not in the mocked Juju data.
4439
- assert {unit.name for unit in mgr.charm.model.relations['db'][0].units} == {'db/0', 'db/1'}
4440
-
4441
-
4442
- @pytest.mark.skipif(
4443
- not hasattr(ops.testing, 'Context'), reason='requires optional ops[testing] install'
4444
- )
4445
- def test_relation_has_correct_units():
4446
- class Charm(ops.CharmBase):
4447
- def __init__(self, framework: ops.Framework):
4448
- super().__init__(framework)
4449
- framework.observe(self.on['db'].relation_changed, self._on_peer_relation_changed)
4450
- framework.observe(self.on['peer'].relation_changed, self._on_peer_relation_changed)
4451
-
4452
- def _on_peer_relation_changed(self, event: ops.RelationChangedEvent):
4453
- self.event = event
4454
-
4455
- ctx = ops.testing.Context(
4456
- Charm,
4457
- meta={
4458
- 'name': 'mycharm',
4459
- 'requires': {'db': {'interface': 'db'}, 'ingress': {'interface': 'ingress'}},
4460
- 'peers': {'peer': {'interface': 'gossip'}},
4461
- },
4462
- )
4463
- rel1 = ops.testing.Relation(
4464
- 'db', remote_units_data={1: {}, 2: {}, 3: {}}, remote_app_name='test-db'
4465
- )
4466
- rel2 = ops.testing.Relation(
4467
- 'ingress', remote_units_data={4: {}, 6: {}}, remote_app_name='test-ingress'
4468
- )
4469
- peer = ops.testing.PeerRelation('peer', peers_data={1: {}, 2: {}})
4470
- state_in = ops.testing.State(relations={rel1, rel2, peer})
4471
-
4472
- def unit_names(relation: ops.Relation):
4473
- return {unit.name for unit in relation.units}
4474
-
4475
- with ctx(ctx.on.relation_changed(peer, remote_unit=1), state_in) as mgr:
4476
- mgr.run()
4477
- assert unit_names(mgr.charm.event.relation) == {'mycharm/1', 'mycharm/2'}
4478
- assert unit_names(mgr.charm.model.relations['peer'][0]) == {'mycharm/1', 'mycharm/2'}
4479
- assert unit_names(mgr.charm.model.relations['db'][0]) == {
4480
- 'test-db/1',
4481
- 'test-db/2',
4482
- 'test-db/3',
4483
- }
4484
- assert unit_names(mgr.charm.model.relations['ingress'][0]) == {
4485
- 'test-ingress/4',
4486
- 'test-ingress/6',
4487
- }
4488
-
4489
- with ctx(ctx.on.relation_changed(rel1, remote_unit=1), state_in) as mgr:
4490
- mgr.run()
4491
- assert unit_names(mgr.charm.event.relation) == {'test-db/1', 'test-db/2', 'test-db/3'}
4492
- assert unit_names(mgr.charm.model.relations['peer'][0]) == {'mycharm/1', 'mycharm/2'}
4493
- assert unit_names(mgr.charm.model.relations['db'][0]) == {
4494
- 'test-db/1',
4495
- 'test-db/2',
4496
- 'test-db/3',
4497
- }
4498
- assert unit_names(mgr.charm.model.relations['ingress'][0]) == {
4499
- 'test-ingress/4',
4500
- 'test-ingress/6',
4501
- }
4424
+ def test_departing_unit_data_available(fake_script: FakeScript):
4425
+ fake_script.write('relation-ids', """echo '["db0:1"]'""")
4426
+ fake_script.write('relation-list', """echo '["db/0"]'""")
4427
+ fake_script.write('relation-get', """echo '{"db": "data"}'""")
4428
+
4429
+ meta = ops.charm.CharmMeta({'name': 'mycharm', 'requires': {'db': {'interface': 'db'}}})
4430
+ backend = ops.model._ModelBackend('myapp/0')
4431
+ model = ops.model.Model(meta, backend, remote_unit_name='db/1')
4432
+ relation = model.get_relation('db')
4433
+ assert relation is not None
4434
+ for unit in relation.units:
4435
+ assert relation.data[unit] == {'db': 'data'}
4436
+ unit = model.get_unit('db/1')
4437
+ assert relation.data[unit] == {'db': 'data'}
4438
+ calls = fake_script.calls(clear=True)
4439
+ assert calls[:2] == [
4440
+ ['relation-ids', 'db', '--format=json'],
4441
+ ['relation-list', '-r', '1', '--format=json'],
4442
+ ]
4443
+ assert ['relation-get', '-r', '1', '-', 'db/0', '--format=json'] in calls
4444
+ assert ['relation-get', '-r', '1', '-', 'db/1', '--format=json'] in calls
4502
4445
 
4503
4446
 
4504
4447
  if __name__ == '__main__':
@@ -47,14 +47,17 @@ passenv =
47
47
  # ReadTheDocs builder wants the output in a place of its choosing.
48
48
  # https://docs.readthedocs.com/platform/stable/build-customization.html#where-to-put-files
49
49
  READTHEDOCS_OUTPUT
50
+ # docs/conf.py uses these variables to build URLs of static assets on error pages.
51
+ READTHEDOCS_CANONICAL_URL
52
+ READTHEDOCS_VERSION
50
53
  commands =
51
- sphinx-build -W --keep-going docs/ {env:READTHEDOCS_OUTPUT:docs/_build}/html
54
+ sphinx-build -W --keep-going -b dirhtml docs/ {env:READTHEDOCS_OUTPUT:docs/_build}/html
52
55
 
53
56
  [testenv:docs-live]
54
57
  description = Live development: build the Sphinx docs with autoreloading enabled
55
58
  dependency_groups = docs
56
59
  commands =
57
- sphinx-autobuild docs/ docs/_build/html --watch ops/ --port 8000 {posargs}
60
+ sphinx-autobuild -b dirhtml docs/ docs/_build/html --watch ops/ --port 8000 {posargs}
58
61
 
59
62
  [testenv:format]
60
63
  description = Apply coding style standards to code
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes