ops 2.19.0__tar.gz → 2.20.0.dev0__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 (90) hide show
  1. {ops-2.19.0/ops.egg-info → ops-2.20.0.dev0}/PKG-INFO +1 -1
  2. {ops-2.19.0 → ops-2.20.0.dev0}/STYLE.md +22 -0
  3. {ops-2.19.0 → ops-2.20.0.dev0}/ops/_main.py +41 -41
  4. {ops-2.19.0 → ops-2.20.0.dev0}/ops/_private/harness.py +54 -27
  5. {ops-2.19.0 → ops-2.20.0.dev0}/ops/charm.py +15 -3
  6. {ops-2.19.0 → ops-2.20.0.dev0}/ops/framework.py +3 -3
  7. {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/jujucontext.py +1 -1
  8. {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/log.py +1 -1
  9. {ops-2.19.0 → ops-2.20.0.dev0}/ops/main.py +2 -3
  10. {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/model.py +17 -17
  11. {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/pebble.py +5 -5
  12. {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/testing.py +4 -11
  13. {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/version.py +1 -1
  14. {ops-2.19.0 → ops-2.20.0.dev0/ops.egg-info}/PKG-INFO +1 -1
  15. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_main.py +41 -41
  16. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_private/harness.py +54 -27
  17. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/charm.py +15 -3
  18. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/framework.py +3 -3
  19. {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/jujucontext.py +1 -1
  20. {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/log.py +1 -1
  21. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/main.py +2 -3
  22. {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/model.py +17 -17
  23. {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/pebble.py +5 -5
  24. {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/testing.py +4 -11
  25. {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/version.py +1 -1
  26. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_charm.py +16 -0
  27. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_testing.py +33 -8
  28. {ops-2.19.0 → ops-2.20.0.dev0}/CHANGES.md +0 -0
  29. {ops-2.19.0 → ops-2.20.0.dev0}/CODE_OF_CONDUCT.md +0 -0
  30. {ops-2.19.0 → ops-2.20.0.dev0}/HACKING.md +0 -0
  31. {ops-2.19.0 → ops-2.20.0.dev0}/LICENSE.txt +0 -0
  32. {ops-2.19.0 → ops-2.20.0.dev0}/MANIFEST.in +0 -0
  33. {ops-2.19.0 → ops-2.20.0.dev0}/README.md +0 -0
  34. {ops-2.19.0 → ops-2.20.0.dev0}/SECURITY.md +0 -0
  35. {ops-2.19.0 → ops-2.20.0.dev0}/ops/__init__.py +0 -0
  36. {ops-2.19.0 → ops-2.20.0.dev0}/ops/_private/__init__.py +0 -0
  37. {ops-2.19.0 → ops-2.20.0.dev0}/ops/_private/timeconv.py +0 -0
  38. {ops-2.19.0 → ops-2.20.0.dev0}/ops/_private/yaml.py +0 -0
  39. {ops-2.19.0 → ops-2.20.0.dev0}/ops/jujuversion.py +0 -0
  40. {ops-2.19.0 → ops-2.20.0.dev0}/ops/lib/__init__.py +0 -0
  41. {ops-2.19.0 → ops-2.20.0.dev0}/ops/py.typed +0 -0
  42. {ops-2.19.0 → ops-2.20.0.dev0}/ops/storage.py +0 -0
  43. {ops-2.19.0 → ops-2.20.0.dev0}/ops.egg-info/SOURCES.txt +0 -0
  44. {ops-2.19.0 → ops-2.20.0.dev0}/ops.egg-info/dependency_links.txt +0 -0
  45. {ops-2.19.0 → ops-2.20.0.dev0}/ops.egg-info/requires.txt +0 -0
  46. {ops-2.19.0 → ops-2.20.0.dev0}/ops.egg-info/top_level.txt +0 -0
  47. {ops-2.19.0 → ops-2.20.0.dev0}/pyproject.toml +0 -0
  48. {ops-2.19.0 → ops-2.20.0.dev0}/setup.cfg +0 -0
  49. {ops-2.19.0 → ops-2.20.0.dev0}/test/__init__.py +0 -0
  50. {ops-2.19.0 → ops-2.20.0.dev0}/test/benchmark/__init__.py +0 -0
  51. {ops-2.19.0 → ops-2.20.0.dev0}/test/bin/relation-ids +0 -0
  52. {ops-2.19.0 → ops-2.20.0.dev0}/test/bin/relation-ids.bat +0 -0
  53. {ops-2.19.0 → ops-2.20.0.dev0}/test/bin/relation-list +0 -0
  54. {ops-2.19.0 → ops-2.20.0.dev0}/test/bin/relation-list.bat +0 -0
  55. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/actions.yaml +0 -0
  56. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/config.yaml +0 -0
  57. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/__init__.py +0 -0
  58. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/__init__.py +0 -0
  59. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_private/__init__.py +0 -0
  60. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_private/timeconv.py +0 -0
  61. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_private/yaml.py +0 -0
  62. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/jujuversion.py +0 -0
  63. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/lib/__init__.py +0 -0
  64. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/py.typed +0 -0
  65. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/storage.py +0 -0
  66. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/metadata.yaml +0 -0
  67. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/src/charm.py +0 -0
  68. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_smoke/README.md +0 -0
  69. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_smoke/metadata.yaml +0 -0
  70. {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_smoke/src/charm.py +0 -0
  71. {ops-2.19.0 → ops-2.20.0.dev0}/test/fake_pebble.py +0 -0
  72. {ops-2.19.0 → ops-2.20.0.dev0}/test/pebble_cli.py +0 -0
  73. {ops-2.19.0 → ops-2.20.0.dev0}/test/smoke/test_smoke.py +0 -0
  74. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_framework.py +0 -0
  75. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_helpers.py +0 -0
  76. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_infra.py +0 -0
  77. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_jujucontext.py +0 -0
  78. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_jujuversion.py +0 -0
  79. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_lib.py +0 -0
  80. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_log.py +0 -0
  81. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_main.py +0 -0
  82. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_main_invocation.py +0 -0
  83. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_main_type_hint.py +0 -0
  84. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_model.py +0 -0
  85. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_pebble.py +0 -0
  86. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_real_pebble.py +0 -0
  87. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_storage.py +0 -0
  88. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_timeconv.py +0 -0
  89. {ops-2.19.0 → ops-2.20.0.dev0}/test/test_yaml.py +0 -0
  90. {ops-2.19.0 → ops-2.20.0.dev0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ops
3
- Version: 2.19.0
3
+ Version: 2.20.0.dev0
4
4
  Summary: The Python library behind great charms
5
5
  Author-email: "The Charm Tech team at Canonical Ltd." <charm-tech@lists.launchpad.net>
6
6
  Project-URL: Homepage, https://juju.is/docs/sdk
@@ -52,6 +52,28 @@ counts: Optional[Tuple[str, int]]
52
52
  ```
53
53
 
54
54
 
55
+ ### Use relative imports inside a package
56
+
57
+ When writing code inside a package (a directory containing an `__init__.py` file), use relative imports with a `.` instead of absolute imports.
58
+ For example, within the `ops` package:
59
+
60
+ **Don't:**
61
+
62
+ ```python
63
+ from ops import charm
64
+ ```
65
+
66
+ **Do:**
67
+
68
+ ```python
69
+ from . import charm
70
+
71
+ # Or, if you need to avoid adding the public name "charm" to the namespace:
72
+
73
+ from . import charm as _charm
74
+ ```
75
+
76
+
55
77
  ### Avoid nested comprehensions and generator expressions
56
78
 
57
79
  "Flat is better than nested."
@@ -23,13 +23,13 @@ import warnings
23
23
  from pathlib import Path
24
24
  from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast
25
25
 
26
- import ops.charm
27
- import ops.framework
28
- import ops.model
29
- import ops.storage
30
- from ops.charm import CharmMeta
31
- from ops.jujucontext import _JujuContext
32
- from ops.log import setup_root_logging
26
+ from . import charm as _charm
27
+ from . import framework as _framework
28
+ from . import model as _model
29
+ from . import storage as _storage
30
+ from . import version as _version
31
+ from .jujucontext import _JujuContext
32
+ from .log import setup_root_logging
33
33
 
34
34
  CHARM_STATE_FILE = '.unit-state.db'
35
35
 
@@ -49,8 +49,8 @@ def _exe_path(path: Path) -> Optional[Path]:
49
49
 
50
50
 
51
51
  def _create_event_link(
52
- charm: 'ops.charm.CharmBase',
53
- bound_event: 'ops.framework.BoundEvent',
52
+ charm: '_charm.CharmBase',
53
+ bound_event: '_framework.BoundEvent',
54
54
  link_to: Union[str, Path],
55
55
  ):
56
56
  """Create a symlink for a particular event.
@@ -63,10 +63,10 @@ def _create_event_link(
63
63
  # type guard
64
64
  assert bound_event.event_kind, f'unbound BoundEvent {bound_event}'
65
65
 
66
- if issubclass(bound_event.event_type, ops.charm.HookEvent):
66
+ if issubclass(bound_event.event_type, _charm.HookEvent):
67
67
  event_dir = charm.framework.charm_dir / 'hooks'
68
68
  event_path = event_dir / bound_event.event_kind.replace('_', '-')
69
- elif issubclass(bound_event.event_type, ops.charm.ActionEvent):
69
+ elif issubclass(bound_event.event_type, _charm.ActionEvent):
70
70
  if not bound_event.event_kind.endswith('_action'):
71
71
  raise RuntimeError(f'action event name {bound_event.event_kind} needs _action suffix')
72
72
  event_dir = charm.framework.charm_dir / 'actions'
@@ -89,7 +89,7 @@ def _create_event_link(
89
89
  event_path.symlink_to(target_path)
90
90
 
91
91
 
92
- def _setup_event_links(charm_dir: Path, charm: 'ops.charm.CharmBase', juju_context: _JujuContext):
92
+ def _setup_event_links(charm_dir: Path, charm: '_charm.CharmBase', juju_context: _JujuContext):
93
93
  """Set up links for supported events that originate from Juju.
94
94
 
95
95
  Whether a charm can handle an event or not can be determined by
@@ -108,42 +108,42 @@ def _setup_event_links(charm_dir: Path, charm: 'ops.charm.CharmBase', juju_conte
108
108
  link_to = os.path.realpath(juju_context.dispatch_path or sys.argv[0])
109
109
  for bound_event in charm.on.events().values():
110
110
  # Only events that originate from Juju need symlinks.
111
- if issubclass(bound_event.event_type, (ops.charm.HookEvent, ops.charm.ActionEvent)):
111
+ if issubclass(bound_event.event_type, (_charm.HookEvent, _charm.ActionEvent)):
112
112
  _create_event_link(charm, bound_event, link_to)
113
113
 
114
114
 
115
115
  def _get_event_args(
116
- charm: 'ops.charm.CharmBase',
117
- bound_event: 'ops.framework.BoundEvent',
116
+ charm: '_charm.CharmBase',
117
+ bound_event: '_framework.BoundEvent',
118
118
  juju_context: _JujuContext,
119
119
  ) -> Tuple[List[Any], Dict[str, Any]]:
120
120
  event_type = bound_event.event_type
121
121
  model = charm.framework.model
122
122
 
123
123
  relation = None
124
- if issubclass(event_type, ops.charm.WorkloadEvent):
124
+ if issubclass(event_type, _charm.WorkloadEvent):
125
125
  workload_name = juju_context.workload_name
126
126
  assert workload_name is not None
127
127
  container = model.unit.get_container(workload_name)
128
128
  args: List[Any] = [container]
129
- if issubclass(event_type, ops.charm.PebbleNoticeEvent):
129
+ if issubclass(event_type, _charm.PebbleNoticeEvent):
130
130
  notice_id = juju_context.notice_id
131
131
  notice_type = juju_context.notice_type
132
132
  notice_key = juju_context.notice_key
133
133
  args.extend([notice_id, notice_type, notice_key])
134
- elif issubclass(event_type, ops.charm.PebbleCheckEvent):
134
+ elif issubclass(event_type, _charm.PebbleCheckEvent):
135
135
  check_name = juju_context.pebble_check_name
136
136
  args.append(check_name)
137
137
  return args, {}
138
- elif issubclass(event_type, ops.charm.SecretEvent):
138
+ elif issubclass(event_type, _charm.SecretEvent):
139
139
  args: List[Any] = [
140
140
  juju_context.secret_id,
141
141
  juju_context.secret_label,
142
142
  ]
143
- if issubclass(event_type, (ops.charm.SecretRemoveEvent, ops.charm.SecretExpiredEvent)):
143
+ if issubclass(event_type, (_charm.SecretRemoveEvent, _charm.SecretExpiredEvent)):
144
144
  args.append(juju_context.secret_revision)
145
145
  return args, {}
146
- elif issubclass(event_type, ops.charm.StorageEvent):
146
+ elif issubclass(event_type, _charm.StorageEvent):
147
147
  # Before JUJU_STORAGE_ID exists, take the event name as
148
148
  # <storage_name>_storage_<attached|detached> and replace it with <storage_name>
149
149
  storage_name = juju_context.storage_name or '-'.join(
@@ -157,17 +157,17 @@ def _get_event_args(
157
157
  else:
158
158
  # If there's more than one value, pick the right one. We'll realize the key on lookup
159
159
  storage = next((s for s in storages if s.index == index), None)
160
- storage = cast(Union[ops.storage.JujuStorage, ops.storage.SQLiteStorage], storage)
160
+ storage = cast(Union[_storage.JujuStorage, _storage.SQLiteStorage], storage)
161
161
  storage.location = storage_location # type: ignore
162
162
  return [storage], {}
163
- elif issubclass(event_type, ops.charm.ActionEvent):
163
+ elif issubclass(event_type, _charm.ActionEvent):
164
164
  args: List[Any] = [juju_context.action_uuid]
165
165
  return args, {}
166
- elif issubclass(event_type, ops.charm.RelationEvent):
166
+ elif issubclass(event_type, _charm.RelationEvent):
167
167
  relation_name = juju_context.relation_name
168
168
  assert relation_name is not None
169
169
  relation_id = juju_context.relation_id
170
- relation: Optional[ops.model.Relation] = model.get_relation(relation_name, relation_id)
170
+ relation: Optional[_model.Relation] = model.get_relation(relation_name, relation_id)
171
171
 
172
172
  remote_app_name = juju_context.remote_app_name
173
173
  remote_unit_name = juju_context.remote_unit_name
@@ -223,7 +223,7 @@ class _Dispatcher:
223
223
  else:
224
224
  self._init_legacy()
225
225
 
226
- def ensure_event_links(self, charm: 'ops.charm.CharmBase'):
226
+ def ensure_event_links(self, charm: '_charm.CharmBase'):
227
227
  """Make sure necessary symlinks are present on disk."""
228
228
  if self.is_dispatch_aware:
229
229
  # links aren't needed
@@ -324,7 +324,7 @@ class _Dispatcher:
324
324
 
325
325
 
326
326
  def _should_use_controller_storage(
327
- db_path: Path, meta: CharmMeta, juju_context: _JujuContext
327
+ db_path: Path, meta: _charm.CharmMeta, juju_context: _JujuContext
328
328
  ) -> bool:
329
329
  """Figure out whether we want to use controller storage or not."""
330
330
  # if local state has been used previously, carry on using that
@@ -375,8 +375,8 @@ class _Manager:
375
375
 
376
376
  def __init__(
377
377
  self,
378
- charm_class: Type['ops.charm.CharmBase'],
379
- model_backend: Optional[ops.model._ModelBackend] = None,
378
+ charm_class: Type['_charm.CharmBase'],
379
+ model_backend: Optional[_model._ModelBackend] = None,
380
380
  use_juju_for_storage: Optional[bool] = None,
381
381
  charm_state_path: str = CHARM_STATE_FILE,
382
382
  juju_context: Optional[_JujuContext] = None,
@@ -387,7 +387,7 @@ class _Manager:
387
387
  self._charm_state_path = charm_state_path
388
388
  self._charm_class = charm_class
389
389
  if model_backend is None:
390
- model_backend = ops.model._ModelBackend(juju_context=self._juju_context)
390
+ model_backend = _model._ModelBackend(juju_context=self._juju_context)
391
391
  self._model_backend = model_backend
392
392
 
393
393
  # Do this as early as possible to be sure to catch the most logs.
@@ -405,9 +405,9 @@ class _Manager:
405
405
  self.charm = self._make_charm(self.framework, self.dispatcher)
406
406
 
407
407
  def _load_charm_meta(self):
408
- return CharmMeta.from_charm_root(self._charm_root)
408
+ return _charm.CharmMeta.from_charm_root(self._charm_root)
409
409
 
410
- def _make_charm(self, framework: 'ops.framework.Framework', dispatcher: _Dispatcher):
410
+ def _make_charm(self, framework: '_framework.Framework', dispatcher: _Dispatcher):
411
411
  charm = self._charm_class(framework)
412
412
  dispatcher.ensure_event_links(charm)
413
413
  return charm
@@ -421,13 +421,13 @@ class _Manager:
421
421
  self._model_backend, debug=self._juju_context.debug, exc_stderr=handling_action
422
422
  )
423
423
 
424
- logger.debug('ops %s up and running.', ops.__version__)
424
+ logger.debug('ops %s up and running.', _version.version)
425
425
 
426
426
  def _make_storage(self, dispatcher: _Dispatcher):
427
427
  charm_state_path = self._charm_root / self._charm_state_path
428
428
 
429
429
  use_juju_for_storage = self._use_juju_for_storage
430
- if use_juju_for_storage and not ops.storage.juju_backend_available():
430
+ if use_juju_for_storage and not _storage.juju_backend_available():
431
431
  # raise an exception; the charm is broken and needs fixing.
432
432
  msg = 'charm set use_juju_for_storage=True, but Juju version {} does not support it'
433
433
  raise RuntimeError(msg.format(self._juju_context.version))
@@ -457,9 +457,9 @@ class _Manager:
457
457
  raise _Abort(0)
458
458
 
459
459
  if self._use_juju_for_storage:
460
- store = ops.storage.JujuStorage()
460
+ store = _storage.JujuStorage()
461
461
  else:
462
- store = ops.storage.SQLiteStorage(charm_state_path)
462
+ store = _storage.SQLiteStorage(charm_state_path)
463
463
  return store
464
464
 
465
465
  def _make_framework(self, dispatcher: _Dispatcher):
@@ -471,11 +471,11 @@ class _Manager:
471
471
  else:
472
472
  broken_relation_id = None
473
473
 
474
- model = ops.model.Model(
474
+ model = _model.Model(
475
475
  self._charm_meta, self._model_backend, broken_relation_id=broken_relation_id
476
476
  )
477
477
  store = self._make_storage(dispatcher)
478
- framework = ops.framework.Framework(
478
+ framework = _framework.Framework(
479
479
  store,
480
480
  self._charm_root,
481
481
  self._charm_meta,
@@ -501,9 +501,9 @@ class _Manager:
501
501
  # Emit the Juju event.
502
502
  self._emit_charm_event(self.dispatcher.event_name)
503
503
  # Emit collect-status events.
504
- ops.charm._evaluate_status(self.charm)
504
+ _charm._evaluate_status(self.charm)
505
505
 
506
- def _get_event_to_emit(self, event_name: str) -> Optional[ops.framework.BoundEvent]:
506
+ def _get_event_to_emit(self, event_name: str) -> Optional[_framework.BoundEvent]:
507
507
  try:
508
508
  return getattr(self.charm.on, event_name)
509
509
  except AttributeError:
@@ -547,7 +547,7 @@ class _Manager:
547
547
  self.framework.close()
548
548
 
549
549
 
550
- def main(charm_class: Type[ops.charm.CharmBase], use_juju_for_storage: Optional[bool] = None):
550
+ def main(charm_class: Type[_charm.CharmBase], use_juju_for_storage: Optional[bool] = None):
551
551
  """Set up the charm and dispatch the observed event.
552
552
 
553
553
  See `ops.main() <#ops-main-entry-point>`_ for details.
@@ -58,16 +58,16 @@ from typing import (
58
58
  cast,
59
59
  )
60
60
 
61
- from ops import charm, framework, model, pebble, storage
62
- from ops._private import yaml
63
- from ops.charm import CharmBase, CharmMeta, RelationRole
64
- from ops.jujucontext import _JujuContext
65
- from ops.model import Container, RelationNotFoundError, StatusName, _NetworkDict
66
- from ops.pebble import ExecProcess
61
+ from .. import charm, framework, model, pebble, storage
62
+ from ..charm import CharmBase, CharmMeta, RelationRole
63
+ from ..jujucontext import _JujuContext
64
+ from ..model import Container, RelationNotFoundError, StatusName, _NetworkDict
65
+ from ..pebble import ExecProcess
66
+ from . import yaml
67
67
 
68
68
  if typing.TYPE_CHECKING:
69
69
  try:
70
- from ops.testing import State # type: ignore
70
+ from ..testing import State # type: ignore
71
71
  except ImportError:
72
72
  # This is used in the ActionFailed type annotations: it will never be
73
73
  # used in this case, because it's only relevant when ops.testing has
@@ -3213,75 +3213,102 @@ class _TestingPebbleClient:
3213
3213
  self._new_perform_check(info)
3214
3214
  return self.autostart_services(timeout, delay)
3215
3215
 
3216
+ def _new_service_change(self, kind: pebble.ChangeKind, services: List[str]) -> pebble.Change:
3217
+ now = datetime.datetime.now()
3218
+ if kind == pebble.ChangeKind.START:
3219
+ action = 'Start'
3220
+ elif kind == pebble.ChangeKind.STOP:
3221
+ action = 'Stop'
3222
+ elif kind == pebble.ChangeKind.RESTART:
3223
+ action = 'Restart'
3224
+ else:
3225
+ raise ValueError(f'unknown kind {kind}')
3226
+ summary = f'{action} service {services[0]}'
3227
+ if len(services) > 1:
3228
+ summary += f' and {len(services) - 1} more'
3229
+ change = pebble.Change(
3230
+ pebble.ChangeID(str(uuid.uuid4())),
3231
+ kind.value,
3232
+ summary=summary,
3233
+ status=pebble.ChangeStatus.DONE.value,
3234
+ tasks=[],
3235
+ ready=True,
3236
+ err=None,
3237
+ spawn_time=now,
3238
+ ready_time=now,
3239
+ )
3240
+ self._changes[change.id] = change
3241
+ return change
3242
+
3216
3243
  def start_services(
3217
3244
  self,
3218
3245
  services: List[str],
3219
3246
  timeout: float = 30.0,
3220
3247
  delay: float = 0.1,
3221
- ):
3248
+ ) -> pebble.ChangeID:
3222
3249
  # A common mistake is to pass just the name of a service, rather than a list of services,
3223
3250
  # so trap that so it is caught quickly.
3224
3251
  if isinstance(services, str):
3225
3252
  raise TypeError(f'start_services should take a list of names, not just "{services}"')
3226
-
3253
+ if not services:
3254
+ raise self._api_error(400, 'must specify services for start action')
3227
3255
  self._check_connection()
3228
3256
 
3229
- # Note: jam 2021-04-20 We don't implement ChangeID, but the default caller of this is
3230
- # Container.start() which currently ignores the return value
3231
3257
  known_services = self._render_services()
3232
3258
  # Names appear to be validated before any are activated, so do two passes
3233
3259
  for name in services:
3234
3260
  if name not in known_services:
3235
- # TODO: jam 2021-04-20 This needs a better error type
3236
- raise RuntimeError(f'400 Bad Request: service "{name}" does not exist')
3261
+ raise self._api_error(400, f'cannot start services: service {name} does not exist')
3237
3262
  for name in services:
3238
3263
  self._service_status[name] = pebble.ServiceStatus.ACTIVE
3264
+ return self._new_service_change(pebble.ChangeKind.START, services).id
3239
3265
 
3240
3266
  def stop_services(
3241
3267
  self,
3242
3268
  services: List[str],
3243
3269
  timeout: float = 30.0,
3244
3270
  delay: float = 0.1,
3245
- ):
3246
- # handle a common mistake of passing just a name rather than a list of names
3271
+ ) -> pebble.ChangeID:
3272
+ # Handle a common mistake of passing just a name rather than a list of names.
3247
3273
  if isinstance(services, str):
3248
3274
  raise TypeError(f'stop_services should take a list of names, not just "{services}"')
3275
+ if not services:
3276
+ raise self._api_error(400, 'must specify services for start action')
3249
3277
 
3250
3278
  self._check_connection()
3251
3279
 
3252
- # Note: jam 2021-04-20 We don't implement ChangeID, but the default caller of this is
3253
- # Container.stop() which currently ignores the return value
3254
3280
  known_services = self._render_services()
3255
3281
  for name in services:
3256
3282
  if name not in known_services:
3257
- # TODO: jam 2021-04-20 This needs a better error type
3258
- # 400 Bad Request: service "bal" does not exist
3259
- raise RuntimeError(f'400 Bad Request: service "{name}" does not exist')
3283
+ raise self._api_error(400, f'cannot stop services: service {name} does not exist')
3260
3284
  for name in services:
3261
3285
  self._service_status[name] = pebble.ServiceStatus.INACTIVE
3286
+ return self._new_service_change(pebble.ChangeKind.STOP, services).id
3262
3287
 
3263
3288
  def restart_services(
3264
3289
  self,
3265
3290
  services: List[str],
3266
3291
  timeout: float = 30.0,
3267
3292
  delay: float = 0.1,
3268
- ):
3269
- # handle a common mistake of passing just a name rather than a list of names
3293
+ ) -> pebble.ChangeID:
3294
+ # Handle a common mistake of passing just a name rather than a list of names.
3270
3295
  if isinstance(services, str):
3271
3296
  raise TypeError(f'restart_services should take a list of names, not just "{services}"')
3297
+ if not services:
3298
+ raise self._api_error(400, 'must specify services for start action')
3272
3299
 
3273
3300
  self._check_connection()
3274
3301
 
3275
- # Note: jam 2021-04-20 We don't implement ChangeID, but the default caller of this is
3276
- # Container.restart() which currently ignores the return value
3277
3302
  known_services = self._render_services()
3278
3303
  for name in services:
3279
3304
  if name not in known_services:
3280
- # TODO: jam 2021-04-20 This needs a better error type
3281
- # 400 Bad Request: service "bal" does not exist
3282
- raise RuntimeError(f'400 Bad Request: service "{name}" does not exist')
3305
+ raise self._api_error(
3306
+ 400,
3307
+ f'cannot restart services: service {name} does not exist',
3308
+ )
3283
3309
  for name in services:
3284
3310
  self._service_status[name] = pebble.ServiceStatus.ACTIVE
3311
+ return self._new_service_change(pebble.ChangeKind.RESTART, services).id
3285
3312
 
3286
3313
  def wait_change(
3287
3314
  self,
@@ -36,9 +36,9 @@ from typing import (
36
36
  cast,
37
37
  )
38
38
 
39
- from ops import model
40
- from ops._private import yaml
41
- from ops.framework import (
39
+ from . import model
40
+ from ._private import yaml
41
+ from .framework import (
42
42
  EventBase,
43
43
  EventSource,
44
44
  Framework,
@@ -1451,6 +1451,17 @@ class CharmMeta:
1451
1451
  assumes: 'JujuAssumes'
1452
1452
  """Juju features this charm requires."""
1453
1453
 
1454
+ charm_user: Literal['root', 'sudoer', 'non-root']
1455
+ """Type of user used to run the charm hook code.
1456
+
1457
+ The value of ``root`` ensures the charm runs as root. The value of
1458
+ ``sudoer`` runs the charm as a user other than root with access to sudo to
1459
+ elevate its privileges. ``non-root`` ensures the charm does not run as root
1460
+ and also does not have ``sudo`` privileges.
1461
+
1462
+ .. jujuadded 3.6.0
1463
+ """
1464
+
1454
1465
  containers: Dict[str, 'ContainerMeta']
1455
1466
  """Container metadata for each defined container."""
1456
1467
 
@@ -1524,6 +1535,7 @@ class CharmMeta:
1524
1535
  # Note that metadata v2 does not define min-juju-version ('assumes'
1525
1536
  # should be used instead).
1526
1537
  self.min_juju_version = raw_.get('min-juju-version')
1538
+ self.charm_user = raw_.get('charm-user', 'root')
1527
1539
  self.requires = {
1528
1540
  name: RelationMeta(RelationRole.requires, name, rel)
1529
1541
  for name, rel in raw_.get('requires', {}).items()
@@ -47,9 +47,9 @@ from typing import (
47
47
  Union,
48
48
  )
49
49
 
50
- from ops import charm
51
- from ops.model import Model, _ModelBackend
52
- from ops.storage import JujuStorage, NoSnapshotError, SQLiteStorage
50
+ from . import charm
51
+ from .model import Model, _ModelBackend
52
+ from .storage import JujuStorage, NoSnapshotError, SQLiteStorage
53
53
 
54
54
 
55
55
  class Serializable(typing.Protocol):
@@ -18,7 +18,7 @@ import dataclasses
18
18
  from pathlib import Path
19
19
  from typing import Any, Mapping, Optional, Set
20
20
 
21
- from ops.jujuversion import JujuVersion
21
+ from .jujuversion import JujuVersion
22
22
 
23
23
 
24
24
  @dataclasses.dataclass(frozen=True)
@@ -20,7 +20,7 @@ import types
20
20
  import typing
21
21
  import warnings
22
22
 
23
- from ops.model import _ModelBackend
23
+ from .model import _ModelBackend
24
24
 
25
25
 
26
26
  class JujuLogHandler(logging.Handler):
@@ -16,9 +16,8 @@
16
16
 
17
17
  from typing import Optional, Type
18
18
 
19
- import ops.charm
20
-
21
19
  from . import _main
20
+ from . import charm as _charm
22
21
 
23
22
  # Re-export specific set of symbols that Scenario 6 imports from ops.main
24
23
  from ._main import ( # noqa: F401
@@ -29,7 +28,7 @@ from ._main import ( # noqa: F401
29
28
  )
30
29
 
31
30
 
32
- def main(charm_class: Type[ops.charm.CharmBase], use_juju_for_storage: Optional[bool] = None):
31
+ def main(charm_class: Type[_charm.CharmBase], use_juju_for_storage: Optional[bool] = None):
33
32
  """Legacy entrypoint to set up the charm and dispatch the observed event.
34
33
 
35
34
  .. deprecated:: 2.16.0
@@ -57,13 +57,13 @@ from typing import (
57
57
  get_args,
58
58
  )
59
59
 
60
- import ops
61
- import ops.pebble as pebble
62
- from ops._private import timeconv, yaml
63
- from ops.jujucontext import _JujuContext
64
- from ops.jujuversion import JujuVersion
60
+ from . import charm as _charm
61
+ from . import pebble
62
+ from ._private import timeconv, yaml
63
+ from .jujucontext import _JujuContext
64
+ from .jujuversion import JujuVersion
65
65
 
66
- # JujuVersion is not used in ops.model, but there are charms that are importing JujuVersion
66
+ # JujuVersion is not used in this file, but there are charms that are importing JujuVersion
67
67
  # from ops.model, so we keep it here.
68
68
  _ = JujuVersion
69
69
 
@@ -82,7 +82,7 @@ _SETTABLE_STATUS_NAMES: Tuple[_SettableStatusName, ...] = get_args(_SettableStat
82
82
  # mapping from relation name to a list of relation objects
83
83
  _RelationMapping_Raw = Dict[str, Optional[List['Relation']]]
84
84
  # mapping from container name to container metadata
85
- _ContainerMeta_Raw = Dict[str, 'ops.charm.ContainerMeta']
85
+ _ContainerMeta_Raw = Dict[str, '_charm.ContainerMeta']
86
86
 
87
87
  # relation data is a string key: string value mapping so far as the
88
88
  # controller is concerned
@@ -124,14 +124,14 @@ class Model:
124
124
 
125
125
  def __init__(
126
126
  self,
127
- meta: 'ops.charm.CharmMeta',
127
+ meta: '_charm.CharmMeta',
128
128
  backend: '_ModelBackend',
129
129
  broken_relation_id: Optional[int] = None,
130
130
  ):
131
131
  self._cache = _ModelCache(meta, backend)
132
132
  self._backend = backend
133
133
  self._unit = self.get_unit(self._backend.unit_name)
134
- relations: Dict[str, ops.RelationMeta] = meta.relations
134
+ relations: Dict[str, _charm.RelationMeta] = meta.relations
135
135
  self._relations = RelationMapping(
136
136
  relations, self.unit, self._backend, self._cache, broken_relation_id=broken_relation_id
137
137
  )
@@ -219,7 +219,7 @@ class Model:
219
219
  return self._backend.model_uuid
220
220
 
221
221
  @property
222
- def juju_version(self) -> 'ops.JujuVersion':
222
+ def juju_version(self) -> JujuVersion:
223
223
  """Return the version of Juju that is running the model."""
224
224
  return self._backend._juju_context.version
225
225
 
@@ -343,7 +343,7 @@ if typing.TYPE_CHECKING:
343
343
 
344
344
 
345
345
  class _ModelCache:
346
- def __init__(self, meta: 'ops.charm.CharmMeta', backend: '_ModelBackend'):
346
+ def __init__(self, meta: '_charm.CharmMeta', backend: '_ModelBackend'):
347
347
  self._meta = meta
348
348
  self._backend = backend
349
349
  self._secret_set_cache: collections.defaultdict[str, Dict[str, Any]] = (
@@ -383,7 +383,7 @@ class Application:
383
383
  """
384
384
 
385
385
  def __init__(
386
- self, name: str, meta: 'ops.charm.CharmMeta', backend: '_ModelBackend', cache: _ModelCache
386
+ self, name: str, meta: '_charm.CharmMeta', backend: '_ModelBackend', cache: _ModelCache
387
387
  ):
388
388
  self.name = name
389
389
  self._backend = backend
@@ -559,7 +559,7 @@ class Unit:
559
559
  def __init__(
560
560
  self,
561
561
  name: str,
562
- meta: 'ops.charm.CharmMeta',
562
+ meta: '_charm.CharmMeta',
563
563
  backend: '_ModelBackend',
564
564
  cache: '_ModelCache',
565
565
  ):
@@ -908,7 +908,7 @@ class RelationMapping(Mapping[str, List['Relation']]):
908
908
 
909
909
  def __init__(
910
910
  self,
911
- relations_meta: Dict[str, 'ops.RelationMeta'],
911
+ relations_meta: Dict[str, '_charm.RelationMeta'],
912
912
  our_unit: 'Unit',
913
913
  backend: '_ModelBackend',
914
914
  cache: '_ModelCache',
@@ -1670,7 +1670,7 @@ class Relation:
1670
1670
 
1671
1671
  This class should not be instantiated directly, instead use :meth:`Model.get_relation`,
1672
1672
  :attr:`Model.relations`, or :attr:`ops.RelationEvent.relation`. This is principally used by
1673
- :class:`ops.RelationMeta` to represent the relationships between charms.
1673
+ :class:`ops.charm.RelationMeta` to represent the relationships between charms.
1674
1674
  """
1675
1675
 
1676
1676
  name: str
@@ -3962,7 +3962,7 @@ class LazyNotice:
3962
3962
  self.type = type
3963
3963
  self.key = key
3964
3964
 
3965
- self._notice: Optional[ops.pebble.Notice] = None
3965
+ self._notice: Optional[pebble.Notice] = None
3966
3966
 
3967
3967
  def __repr__(self):
3968
3968
  type_repr = self.type if isinstance(self.type, pebble.NoticeType) else repr(self.type)
@@ -3999,7 +3999,7 @@ class LazyCheckInfo:
3999
3999
  def __init__(self, container: Container, name: str):
4000
4000
  self._container = container
4001
4001
  self.name = name
4002
- self._info: Optional[ops.pebble.CheckInfo] = None
4002
+ self._info: Optional[pebble.CheckInfo] = None
4003
4003
 
4004
4004
  def __repr__(self):
4005
4005
  return f'LazyCheckInfo(name={self.name!r})'