diff --git a/.github/workflows/framework-tests.yaml b/.github/workflows/framework-tests.yaml index 20a7695be..b04ce64a0 100644 --- a/.github/workflows/framework-tests.yaml +++ b/.github/workflows/framework-tests.yaml @@ -67,10 +67,10 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Set up Go 1.17 + - name: Set up Go 1.20 uses: actions/setup-go@v1 with: - go-version: "1.17" + go-version: "1.20" - name: Install tox run: pip install tox diff --git a/ops/charm.py b/ops/charm.py index cb599863b..92f9f74d2 100755 --- a/ops/charm.py +++ b/ops/charm.py @@ -241,7 +241,7 @@ class ConfigChangedEvent(HookEvent): This event notifies the charm of its initial configuration. Typically, this event will fire between an :class:`install ` and a :class:`start ` during the startup sequence - (when you first deploy a unit), but in general it will fire whenever + (when a unit is first deployed), but in general it will fire whenever the unit is (re)started, for example after pod churn on Kubernetes, on unit rescheduling, on unit upgrade or refresh, and so on. - As a specific instance of the above point: when networking changes @@ -519,8 +519,8 @@ def snapshot(self) -> Dict[str, Any]: def departing_unit(self) -> Optional[model.Unit]: """The :class:`ops.Unit` that is departing, if any. - You can use this to determine (for example) whether *you* are the - departing unit. + Use this method to determine (for example) whether this unit is the + departing one. """ # doing this on init would fail because `framework` gets patched in # post-init @@ -729,7 +729,7 @@ class SecretChangedEvent(SecretEvent): a new secret revision, and all applications or units that are tracking this secret will be notified via this event that a new revision is available. - Typically, you will want to fetch the new content by calling + Typically, the charm will fetch the new content by calling :meth:`event.secret.get_content() ` with ``refresh=True`` to tell Juju to start tracking the new revision. """ @@ -757,7 +757,7 @@ class SecretRemoveEvent(SecretEvent): observers have updated to that new revision, this event will be fired to inform the secret owner that the old revision can be removed. - Typically, you will want to call + Typically, the charm will call :meth:`event.secret.remove_revision() ` to remove the now-unused revision. """ @@ -1005,7 +1005,7 @@ class CharmBase(Object): :code:`CharmBase` is used to create a charm. This is done by inheriting from :code:`CharmBase` and customising the subclass as required. So to - create your own charm, say ``MyCharm``, define a charm class and set up the + create a charm called ``MyCharm``, define a charm class and set up the required event handlers (“hooks”) in its constructor:: import logging diff --git a/ops/framework.py b/ops/framework.py index df9aa1bb2..757911dcf 100755 --- a/ops/framework.py +++ b/ops/framework.py @@ -202,7 +202,7 @@ def defer(self) -> None: the result of an action, or any event other than metric events. The queue of events will be dispatched before the new event is processed. - From the above you may deduce, but it's important to point out: + Important points that follow from the above: * ``defer()`` does not interrupt the execution of the current event handler. In almost all cases, a call to ``defer()`` should be followed @@ -220,19 +220,18 @@ def defer(self) -> None: The general desire to call ``defer()`` happens when some precondition isn't yet met. However, care should be exercised as to whether it is - better to defer this event so that you see it again, or whether it is + better to defer this event so that it is seen again, or whether it is better to just wait for the event that indicates the precondition has been met. - For example, if ``config-changed`` is fired, and you are waiting for - different config, there is no reason to defer the event because there - will be a *different* ``config-changed`` event when the config actually - changes, rather than checking to see if maybe config has changed prior - to every other event that occurs. + For example, if handling a config change requires that two config + values are changed, there's no reason to defer the first + ``config-changed`` because there will be a *second* ``config-changed`` + event fired when the other config value changes. - Similarly, if you need 2 events to occur before you are ready to - proceed (say event A and B). When you see event A, you could chose to - ``defer()`` it because you haven't seen B yet. However, that leads to: + Similarly, if two events need to occur before execution can proceed + (say event A and B), the event A handler could ``defer()`` because B + has not been seen yet. However, that leads to: 1. event A fires, calls defer() @@ -242,7 +241,6 @@ def defer(self) -> None: 3. At some future time, event C happens, which also checks if A can proceed. - """ logger.debug("Deferring %s.", self) self.deferred = True @@ -1360,8 +1358,8 @@ def _from_iterable(cls, it: Iterable[_T]) -> Set[_T]: Per https://docs.python.org/3/library/collections.abc.html if the Set mixin is being used in a class with a different constructor signature, - you will need to override _from_iterable() with a classmethod that can construct - new instances from an iterable argument. + override _from_iterable() with a classmethod that can construct new instances + from an iterable argument. """ return set(it) diff --git a/ops/lib/__init__.py b/ops/lib/__init__.py index a716c244a..f67fbd831 100644 --- a/ops/lib/__init__.py +++ b/ops/lib/__init__.py @@ -96,9 +96,9 @@ def use(name: str, api: int, author: str) -> ModuleType: def autoimport(): """Find all libs in the path and enable use of them. - You only need to call this if you've installed a package or - otherwise changed sys.path in the current run, and need to see the - changes. Otherwise libraries are found on first call of `use`. + Call this function only when a package has been installed or sys.path has been + otherwise changed in the current run, and the changes need to be seen. + Otherwise libraries are found on first call of `use`. DEPRECATED: This function is deprecated. Prefer charm libraries instead (https://juju.is/docs/sdk/library). diff --git a/ops/main.py b/ops/main.py index d7a186f62..137965363 100755 --- a/ops/main.py +++ b/ops/main.py @@ -340,7 +340,7 @@ def is_restricted_context(self): def _should_use_controller_storage(db_path: Path, meta: CharmMeta) -> bool: """Figure out whether we want to use controller storage or not.""" - # if you've previously used local state, carry on using that + # if local state has been used previously, carry on using that if db_path.exists(): return False @@ -368,7 +368,7 @@ def main(charm_class: Type[ops.charm.CharmBase], The event name is based on the way this executable was called (argv[0]). Args: - charm_class: your charm class. + charm_class: the charm class to instantiate and receive the event. use_juju_for_storage: whether to use controller-side storage. If not specified then kubernetes charms that haven't previously used local storage and that are running on a new enough Juju default to controller-side storage, diff --git a/ops/model.py b/ops/model.py index 304af8fe9..5155a96db 100644 --- a/ops/model.py +++ b/ops/model.py @@ -123,7 +123,7 @@ def __init__(self, meta: 'ops.charm.CharmMeta', backend: '_ModelBackend'): @property def unit(self) -> 'Unit': - """The unit that is running this code (that is, yourself). + """The unit that is running this code. Use :meth:`get_unit` to get an arbitrary unit by name. """ @@ -195,7 +195,7 @@ def uuid(self) -> str: def get_unit(self, unit_name: str) -> 'Unit': """Get an arbitrary unit by name. - Use :attr:`unit` to get your own unit. + Use :attr:`unit` to get the current unit. Internally this uses a cache, so asking for the same unit two times will return the same object. @@ -205,7 +205,7 @@ def get_unit(self, unit_name: str) -> 'Unit': def get_app(self, app_name: str) -> 'Application': """Get an application by name. - Use :attr:`app` to get your own application. + Use :attr:`app` to get this charm's application. Internally this uses a cache, so asking for the same application two times will return the same object. @@ -305,8 +305,8 @@ def get(self, entity_type: 'UnitOrApplicationType', name: str): class Application: """Represents a named application in the model. - This might be your application, or might be an application that you are related to. - Charmers should not instantiate Application objects directly, but should use + This might be this charm's application, or might be an application this charm is related + to. Charmers should not instantiate Application objects directly, but should use :attr:`Model.app` to get the application this unit is part of, or :meth:`Model.get_app` if they need a reference to a given application. """ @@ -336,14 +336,13 @@ def status(self) -> 'StatusBase': The status of remote units is always Unknown. - You can also use the :attr:`collect_app_status ` - event if you want to evaluate and set application status consistently - at the end of every hook. + Alternatively, use the :attr:`collect_app_status ` + event to evaluate and set application status consistently at the end of every hook. Raises: - RuntimeError: if you try to set the status of another application, or if you try to - set the status of this application as a unit that is not the leader. - InvalidStatusError: if you try to set the status to something that is not a + RuntimeError: if setting the status of another application, or if setting the + status of this application as a unit that is not the leader. + InvalidStatusError: if setting the status to something that is not a :class:`StatusBase` Example:: @@ -453,8 +452,8 @@ def _calculate_expiry(expire: Optional[Union[datetime.datetime, datetime.timedel class Unit: """Represents a named unit in the model. - This might be your unit, another unit of your application, or a unit of another application - that you are related to. + This might be the current unit, another unit of the charm's application, or a unit of + another application that the charm is related to. """ name: str @@ -487,15 +486,14 @@ def _invalidate(self): def status(self) -> 'StatusBase': """Used to report or read the status of a specific unit. - The status of any unit other than yourself is always Unknown. + The status of any unit other than the current unit is always Unknown. - You can also use the :attr:`collect_unit_status ` - event if you want to evaluate and set unit status consistently at the - end of every hook. + Alternatively, use the :attr:`collect_unit_status ` + event to evaluate and set unit status consistently at the end of every hook. Raises: - RuntimeError: if you try to set the status of a unit other than yourself. - InvalidStatusError: if you try to set the status to something other than + RuntimeError: if setting the status of a unit other than the current unit + InvalidStatusError: if setting the status to something other than a :class:`StatusBase` Example:: @@ -531,10 +529,10 @@ def __repr__(self): def is_leader(self) -> bool: """Return whether this unit is the leader of its application. - This can only be called for your own unit. + This can only be called for the current unit. Raises: - RuntimeError: if called for a unit that is not yourself + RuntimeError: if called for another unit """ if self._is_our_unit: # This value is not cached as it is not guaranteed to persist for the whole duration @@ -597,14 +595,11 @@ def open_port(self, protocol: typing.Literal['tcp', 'udp', 'icmp'], port: Optional[int] = None): """Open a port with the given protocol for this unit. - Calling this registers intent with Juju that the application should be - accessed on the given port, but the port isn't actually opened - externally until the admin runs "juju expose". - - On Kubernetes sidecar charms, the ports opened are not strictly - per-unit: Juju will open the union of ports from all units. - However, normally charms should make the same open_port() call from - every unit. + Some behaviour, such as whether the port is opened externally without + using "juju expose" and whether the opened ports are per-unit, differs + between Kubernetes and machine charms. See the + `Juju documentation `__ + for more detail. Args: protocol: String representing the protocol; must be one of @@ -619,10 +614,11 @@ def close_port(self, protocol: typing.Literal['tcp', 'udp', 'icmp'], port: Optional[int] = None): """Close a port with the given protocol for this unit. - On Kubernetes sidecar charms, Juju will only close the port once the - last unit that opened that port has closed it. However, this is - usually not an issue; normally charms should make the same - close_port() call from every unit. + Some behaviour, such as whether the port is closed externally without + using "juju unexpose", differs between Kubernetes and machine charms. + See the + `Juju documentation `__ + for more detail. Args: protocol: String representing the protocol; must be one of @@ -852,8 +848,8 @@ class Network: interfaces: List['NetworkInterface'] """A list of network interface details. This includes the information - about how your application should be configured (for example, what IP - addresses you should bind to). + about how the application should be configured (for example, what IP + addresses should be bound to). Multiple addresses for a single interface are represented as multiple interfaces, for example:: @@ -862,11 +858,11 @@ class Network: """ ingress_addresses: List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address, str]] - """A list of IP addresses that other units should use to get in touch with you.""" + """A list of IP addresses that other units should use to get in touch with the charm.""" egress_subnets: List[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]] """A list of networks representing the subnets that other units will see - you connecting from. Due to things like NAT it isn't always possible to + the charm connecting from. Due to things like NAT it isn't always possible to narrow it down to a single address, but when it is clear, the CIDRs will be constrained to a single address (for example, 10.0.0.1/32). """ @@ -897,10 +893,10 @@ def __init__(self, network_info: '_NetworkDict'): @property def bind_address(self) -> Optional[Union[ipaddress.IPv4Address, ipaddress.IPv6Address, str]]: - """A single address that your application should bind() to. + """A single address that the charm's application should bind() to. For the common case where there is a single answer. This represents a single - address from :attr:`.interfaces` that can be used to configure where your + address from :attr:`.interfaces` that can be used to configure where the charm's application should bind() and listen(). """ if self.interfaces: @@ -911,11 +907,11 @@ def bind_address(self) -> Optional[Union[ipaddress.IPv4Address, ipaddress.IPv6Ad @property def ingress_address(self) -> Optional[ Union[ipaddress.IPv4Address, ipaddress.IPv6Address, str]]: - """The address other applications should use to connect to your unit. + """The address other applications should use to connect to the current unit. - Due to things like public/private addresses, NAT and tunneling, the address you bind() - to is not always the address other people can use to connect() to you. - This is just the first address from :attr:`.ingress_addresses`. + Due to things like public/private addresses, NAT and tunneling, the address the charm + will bind() to is not always the address other people can use to connect() to the + charm. This is just the first address from :attr:`.ingress_addresses`. """ if self.ingress_addresses: return self.ingress_addresses[0] @@ -1105,12 +1101,12 @@ def id(self) -> Optional[str]: may not include the model UUID (for cross-model secrets). Charms should treat this as an opaque string for looking up secrets - and sharing them via relation data. If you need a charm-local "name" + and sharing them via relation data. If a charm-local "name" is needed for a secret, use a :attr:`label`. (If a charm needs a truly unique identifier for identifying one secret in a set of secrets of arbitrary size, use :attr:`unique_identifier` -- this should be rare.) - This will be None if you obtained the secret using + This will be None if the secret was obtained using :meth:`Model.get_secret` with a label but no ID. """ return self._id @@ -1129,7 +1125,7 @@ def unique_identifier(self) -> Optional[str]: cases where the charm has a set of secrets of arbitrary size, for example, a group of 10 or 20 TLS certificates. - This will be None if you obtained the secret using + This will be None if the secret was obtained using :meth:`Model.get_secret` with a label but no ID. """ if self._id is None: @@ -1173,7 +1169,7 @@ def _on_secret_changed(self, event): Juju will ensure that the entity (the owner or observer) only has one secret with this label at once. - This will be None if you obtained the secret using + This will be None if the secret was obtained using :meth:`Model.get_secret` with an ID but no label. """ return self._label @@ -2179,7 +2175,7 @@ def push_path(self, * /foo/foobar.txt * /quux.txt - You could push the following ways:: + These are various push examples:: # copy one file container.push_path('/foo/foobar.txt', '/dst') @@ -2260,7 +2256,7 @@ def pull_path(self, * /foo/foobar.txt * /quux.txt - You could pull the following ways:: + These are various pull examples:: # copy one file container.pull_path('/foo/foobar.txt', '/dst') @@ -2651,9 +2647,9 @@ def __init__(self, relation_name: str, num_related: int, max_supported: int): class RelationDataError(ModelError): """Raised when a relation data read/write is invalid. - This is raised if you're either trying to set a value to something that isn't a string, - or if you are trying to set a value in a bucket that you don't have access to. (For example, - another application/unit, or setting your application data without being the leader.) + This is raised either when trying to set a value to something that isn't a string, + or when trying to set a value in a bucket without the required access. (For example, + another application/unit, or setting application data without being the leader.) """ @@ -2662,9 +2658,9 @@ class RelationDataTypeError(RelationDataError): class RelationDataAccessError(RelationDataError): - """Raised by ``Relation.data[entity][key] = value`` if you don't have access. + """Raised by ``Relation.data[entity][key] = value`` if unable to access. - This typically means that you don't have permission to write read/write the databag, + This typically means that permission to write read/write the databag is missing, but in some cases it is raised when attempting to read/write from a deceased remote entity. """ diff --git a/ops/testing.py b/ops/testing.py index 970d86099..8b6ebb8db 100755 --- a/ops/testing.py +++ b/ops/testing.py @@ -149,7 +149,7 @@ class ExecResult: class Harness(Generic[CharmType]): """This class represents a way to build up the model that will drive a test suite. - The model created is from the viewpoint of the charm that you are testing. + The model created is from the viewpoint of the charm that is being tested. Below is an example test using :meth:`begin_with_initial_hooks` that ensures the charm responds correctly to config changes:: @@ -194,7 +194,7 @@ def test_bar(self): assert (root / 'etc' / 'app.conf').exists() Args: - charm_cls: The Charm class that you'll be testing. + charm_cls: The Charm class to test. meta: A string or file-like object containing the contents of ``metadata.yaml``. If not supplied, we will look for a ``metadata.yaml`` file in the parent directory of the Charm, and if not found fall back to a trivial @@ -260,8 +260,8 @@ def _event_context(self, event_name: str): If event_name == '', conversely, the Harness will believe that no hook - is running, allowing you to temporarily have unrestricted access to read/write - a relation's databags even if you're inside an event handler. + is running, allowing temporary unrestricted access to read/write a relation's + databags even from inside an event handler. >>> def test_foo(): >>> class MyCharm: >>> ... @@ -292,9 +292,8 @@ def set_can_connect(self, container: Union[str, model.Container], val: bool): def charm(self) -> CharmType: """Return the instance of the charm class that was passed to ``__init__``. - Note that the Charm is not instantiated until you have called - :meth:`.begin()`. Until then, attempting to access this property will raise - an exception. + Note that the Charm is not instantiated until :meth:`.begin()` is called. + Until then, attempting to access this property will raise an exception. """ if self._charm is None: raise RuntimeError('The charm instance is not available yet. ' @@ -315,7 +314,7 @@ def begin(self) -> None: """Instantiate the Charm and start handling events. Before calling :meth:`begin`, there is no Charm instance, so changes to the Model won't - emit events. You must call :meth:`.begin` before :attr:`.charm` is valid. + emit events. Call :meth:`.begin` for :attr:`.charm` to be valid. """ if self._charm is not None: raise RuntimeError('cannot call the begin method on the harness more than once') @@ -339,18 +338,17 @@ class TestCharm(self._charm_cls): # type: ignore self._charm = TestCharm(self._framework) # type: ignore def begin_with_initial_hooks(self) -> None: - """Called when you want the Harness to fire the same hooks that Juju would fire at startup. + """Fire the same hooks that Juju would fire at startup. This triggers install, relation-created, config-changed, start, pebble-ready (for any containers), and any relation-joined hooks based on what relations have been added before - you called begin. Note that all of these are fired before returning control - to the test suite, so if you want to introspect what happens at each step, you need to fire - them directly (for example, ``Charm.on.install.emit()``). + begin was called. Note that all of these are fired before returning control + to the test suite, so to introspect what happens at each step, fire them directly + (for example, ``Charm.on.install.emit()``). - To use this with all the normal hooks, you should instantiate the harness, setup any - relations that you want active when the charm starts, and then call this method. This - method will automatically create and add peer relations that are specified in - metadata.yaml. + To use this with all the normal hooks, instantiate the harness, setup any relations that + should be active when the charm starts, and then call this method. This method will + automatically create and add peer relations that are specified in metadata.yaml. If the charm metadata specifies containers, this sets can_connect to True for all containers (in addition to triggering pebble-ready for each). @@ -445,10 +443,9 @@ def begin_with_initial_hooks(self) -> None: relation, remote_unit.app, remote_unit) def cleanup(self) -> None: - """Called by your test infrastructure to clean up any temporary directories/files/etc. + """Called by the test infrastructure to clean up any temporary directories/files/etc. - You should always call ``self.addCleanup(harness.cleanup)`` after creating a - :class:`Harness`. + Always call ``self.addCleanup(harness.cleanup)`` after creating a :class:`Harness`. """ self._backend._cleanup() @@ -619,9 +616,9 @@ def disable_hooks(self) -> None: def enable_hooks(self) -> None: """Re-enable hook events from charm.on when the model is changed. - By default hook events are enabled once you call :meth:`.begin`, - but if you have used :meth:`.disable_hooks`, this can be used to - enable them again. + By default, hook events are enabled once :meth:`.begin` is called, + but if :meth:`.disable_hooks` is used, this method will enable + them again. """ self._hooks_enabled = True @@ -889,7 +886,7 @@ def add_relation_unit(self, relation_id: int, remote_unit_name: str) -> None: """Add a new unit to a relation. This will trigger a `relation_joined` event. This would naturally be - followed by a `relation_changed` event, which you can trigger with + followed by a `relation_changed` event, which can be triggered with :meth:`.update_relation_data`. This separation is artificial in the sense that Juju will always fire the two, but is intended to make testing relations and their data bags slightly more natural. @@ -1046,7 +1043,7 @@ def get_container_pebble_plan( container_name: The simple name of the associated container Return: - The Pebble plan for this container. You can use + The Pebble plan for this container. Use :meth:`Plan.to_yaml ` to get a string form for the content. Will raise ``KeyError`` if no Pebble client exists for that container name (should only happen if container is @@ -1078,9 +1075,9 @@ def get_workload_version(self) -> str: def set_model_info(self, name: Optional[str] = None, uuid: Optional[str] = None) -> None: """Set the name and UUID of the model that this is representing. - This cannot be called once :meth:`begin` has been called. But it lets - you set the value that will be returned by :attr:`Model.name ` - and :attr:`Model.uuid `. + Cannot be called once :meth:`begin` has been called. Use it to set the + value that will be returned by :attr:`Model.name ` and + :attr:`Model.uuid `. This is a convenience method to invoke both :meth:`set_model_name` and :meth:`set_model_uuid` at once. @@ -1093,8 +1090,8 @@ def set_model_info(self, name: Optional[str] = None, uuid: Optional[str] = None) def set_model_name(self, name: str) -> None: """Set the name of the Model that this is representing. - This cannot be called once :meth:`begin` has been called. But it lets - you set the value that will be returned by :attr:`Model.name `. + Cannot be called once :meth:`begin` has been called. Use it to set the + value that will be returned by :attr:`Model.name `. """ if self._charm is not None: raise RuntimeError('cannot set the Model name after begin()') @@ -1103,8 +1100,8 @@ def set_model_name(self, name: str) -> None: def set_model_uuid(self, uuid: str) -> None: """Set the uuid of the Model that this is representing. - This cannot be called once :meth:`begin` has been called. But it lets - you set the value that will be returned by :attr:`Model.uuid `. + Cannot be called once :meth:`begin` has been called. Use it to set the + value that will be returned by :attr:`Model.uuid `. """ if self._charm is not None: raise RuntimeError('cannot set the Model uuid after begin()') @@ -1464,8 +1461,8 @@ def grant_secret(self, secret_id: str, observer: AppUnitOrName): secret_id: The ID of the secret to grant access to. This should normally be the return value of :meth:`add_model_secret`. observer: The name of the application (or specific unit) to grant - access to. You must already have created a relation between - this application and the charm under test. + access to. A relation between this application and the charm + under test must already have been created. """ secret = self._ensure_secret(secret_id) if secret.owner_name in [self.model.app.name, self.model.unit.name]: @@ -1487,8 +1484,8 @@ def revoke_secret(self, secret_id: str, observer: AppUnitOrName): secret_id: The ID of the secret to revoke access for. This should normally be the return value of :meth:`add_model_secret`. observer: The name of the application (or specific unit) to revoke - access to. You must already have created a relation between - this application and the charm under test. + access to. A relation between this application and the charm under + test must have already been created. """ secret = self._ensure_secret(secret_id) if secret.owner_name in [self.model.app.name, self.model.unit.name]: @@ -1551,7 +1548,7 @@ def trigger_secret_removal(self, secret_id: str, revision: int, *, This event is fired by Juju for a specific revision when all the secret's observers have refreshed to a later revision, however, in the - harness you call this method to fire the event manually. + harness call this method to fire the event manually. Args: secret_id: The ID of the secret associated with the event. @@ -1661,7 +1658,7 @@ def handle_exec(self, When :meth:`ops.Container.exec` is triggered, the registered handler is used to generate stdout and stderr for the simulated execution. - You can provide either a ``handler`` or a ``result``, but not both: + A ``handler`` or a ``result`` may be provided, but not both: - A ``handler`` is a function accepting :class:`ops.testing.ExecArgs` and returning :class:`ops.testing.ExecResult` as the simulated process outcome. For cases that