From 16a0e48a8f7329791937045d8b58b74972d7b831 Mon Sep 17 00:00:00 2001 From: Weii Wang Date: Thu, 14 Sep 2023 16:52:14 +0800 Subject: [PATCH 1/7] Add the flask extension --- charmcraft/extensions/__init__.py | 3 + charmcraft/extensions/flask.py | 164 +++++++++++++++++++++++ charmcraft/extensions/registry.py | 8 +- tests/extensions/test_flask_extension.py | 104 ++++++++++++++ 4 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 charmcraft/extensions/flask.py create mode 100644 tests/extensions/test_flask_extension.py diff --git a/charmcraft/extensions/__init__.py b/charmcraft/extensions/__init__.py index 161ca91d4..09767ed9d 100644 --- a/charmcraft/extensions/__init__.py +++ b/charmcraft/extensions/__init__.py @@ -17,6 +17,7 @@ """Extension processor and related utilities.""" from charmcraft.extensions._utils import apply_extensions +from charmcraft.extensions.flask import Flask from charmcraft.extensions.registry import ( get_extension_class, get_extension_names, @@ -31,3 +32,5 @@ "register", "unregister", ] + +register("flask", Flask) diff --git a/charmcraft/extensions/flask.py b/charmcraft/extensions/flask.py new file mode 100644 index 000000000..0b294a3d6 --- /dev/null +++ b/charmcraft/extensions/flask.py @@ -0,0 +1,164 @@ +# Copyright 2023 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# For further info, check https://github.com/canonical/charmcraft + +"""The flask extension.""" + +from typing import Any, Dict, List, Optional, Tuple + +from overrides import override + +from ..errors import ExtensionError +from .extension import Extension + +ACTIONS = { + "rotate-secret-key": { + "description": "Rotate the flask secret key. Users will be forced to log in again. This might be useful if a security breach occurs.\n" + } +} +OPTIONS = { + "database_migration_script": { + "type": "string", + "description": "Specifies the relative path from /srv/flask/app that points to a shell script executing database migrations for the Flask application. This script is designed to run once for each Flask container unit. However, users must ensure: 1. The script can be executed multiple times without issues; 2. Concurrent migrations from different units are safe. In case of migration failure, the charm will re-attempt during the update-status event. Successful database migration in a container ensures that any configuration updates won't trigger another migration unless the Flask container is upgraded or restarted.", + }, + "flask_application_root": { + "type": "string", + "description": "Path in which the application / web server is mounted. This configuration will set the FLASK_APPLICATION_ROOT environment variable. Run `app.config.from_prefixed_env()` in your Flask application in order to receive this configuration.", + }, + "flask_debug": { + "type": "boolean", + "description": "Whether Flask debug mode is enabled.", + }, + "flask_env": { + "type": "string", + "description": "What environment the Flask app is running in, by default it's 'production'.", + }, + "flask_permanent_session_lifetime": { + "type": "int", + "description": "Time in seconds for the cookie to expire in the Flask application permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME environment variable. Run `app.config.from_prefixed_env()` in your Flask application in order to receive this configuration.", + }, + "flask_preferred_url_scheme": { + "type": "string", + "default": "HTTPS", + "description": 'Scheme for generating external URLs when not in a request context in the Flask application. By default, it\'s "HTTPS". This configuration will set the FLASK_PREFERRED_URL_SCHEME environment variable. Run `app.config.from_prefixed_env()` in your Flask application in order to receive this configuration.', + }, + "flask_secret_key": { + "type": "string", + "description": "The secret key used for securely signing the session cookie and for any other security related needs by your Flask application. This configuration will set the FLASK_SECRET_KEY environment variable. Run `app.config.from_prefixed_env()` in your Flask application in order to receive this configuration.", + }, + "flask_session_cookie_secure": { + "type": "boolean", + "description": "Set the secure attribute in the Flask application cookies. This configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. Run `app.config.from_prefixed_env()` in your Flask application in order to receive this configuration.", + }, + "webserver_keepalive": { + "type": "int", + "description": "Time in seconds for webserver to wait for requests on a Keep-Alive connection.", + }, + "webserver_threads": { + "type": "int", + "description": "Run each webserver worker with the specified number of threads.", + }, + "webserver_timeout": { + "type": "int", + "description": "Time in seconds to kill and restart silent webserver workers.", + }, + "webserver_workers": { + "type": "int", + "description": "The number of webserver worker processes for handling requests.", + }, + "webserver_wsgi_path": { + "type": "string", + "default": "app:app", + "description": 'The WSGI application path. By default, it\'s set to "app:app".', + }, +} + + +class Flask(Extension): + @staticmethod + @override + def get_supported_bases() -> List[Tuple[str, ...]]: + """Return supported bases.""" + return [("ubuntu", "22.04")] + + @staticmethod + @override + def is_experimental(base: Optional[Tuple[str, ...]]) -> bool: + """Check if the extension is in an experimental state.""" + return True + + @override + def get_root_snippet(self) -> Dict[str, Any]: + """Fill in some required root components for Flask.""" + protected_fields = { + "type": "charm", + "assumes": ["k8s-api"], + "containers": { + "flask-app": {"resource": "flask-app-image"}, + "statsd-prometheus-exporter": {"resource": "statsd-prometheus-exporter-image"}, + }, + "resources": { + "flask-app-image": { + "type": "oci-image", + "description": "Flask application image.", + }, + "statsd-prometheus-exporter-image": { + "type": "oci-image", + "description": "Prometheus exporter for statsd data", + "upstream-source": "prom/statsd-exporter:v0.24.0", + }, + }, + "peers": {"secret-storage": {"interface": "secret-storage"}}, + } + merging_fields = { + "actions": ACTIONS, + "options": OPTIONS, + } + incompatible_fields = ("devices", "extra-bindings", "storage") + for incompatible_field in incompatible_fields: + if incompatible_field in self.yaml_data: + raise ExtensionError( + f"the flask extension is incompatible with the field {incompatible_field!r}" + ) + snippet = protected_fields + for protected, protected_value in protected_fields.items(): + if protected in self.yaml_data and self.yaml_data[protected] != protected_value: + raise ExtensionError( + f"{protected!r} in charmcraft.yaml conflicts with a reserved field " + f"in the flask extension, please remove it." + ) + for merging_field, merging_field_value in merging_fields.items(): + if merging_field not in self.yaml_data: + snippet[merging_field] = merging_field_value + continue + user_provided = self.yaml_data[merging_field] + overlap = user_provided.keys() & merging_field_value.keys() + if overlap: + raise ExtensionError( + f"overlapping keys {overlap} in {merging_field} of charmcraft.yaml " + f"which conflict with the flask extension, please rename or remove it" + ) + snippet[merging_field] = {**merging_field_value, **user_provided} + return snippet + + @override + def get_part_snippet(self) -> Dict[str, Any]: + """Return the part snippet to apply to existing parts.""" + return {} + + @override + def get_parts_snippet(self) -> Dict[str, Any]: + """Return the parts to add to parts.""" + return {} diff --git a/charmcraft/extensions/registry.py b/charmcraft/extensions/registry.py index c1c2744db..9bb457e8e 100644 --- a/charmcraft/extensions/registry.py +++ b/charmcraft/extensions/registry.py @@ -16,12 +16,12 @@ """Extension registry.""" -from typing import Dict, List +from typing import Dict, List, Type from charmcraft import errors from charmcraft.extensions.extension import Extension -_EXTENSIONS: Dict[str, Extension] = {} +_EXTENSIONS: Dict[str, Type[Extension]] = {} def get_extension_names() -> List[str]: @@ -34,7 +34,7 @@ def get_extension_names() -> List[str]: return list(_EXTENSIONS.keys()) -def get_extension_class(extension_name: str) -> Extension: +def get_extension_class(extension_name: str) -> Type[Extension]: """Obtain a extension class given the name. :param name: The extension name. @@ -50,7 +50,7 @@ def get_extension_class(extension_name: str) -> Extension: ) from None -def register(extension_name: str, extension_class: Extension) -> None: +def register(extension_name: str, extension_class: Type[Extension]) -> None: """Register extension. :param extension_name: the name to register. diff --git a/tests/extensions/test_flask_extension.py b/tests/extensions/test_flask_extension.py new file mode 100644 index 000000000..7d3413d5d --- /dev/null +++ b/tests/extensions/test_flask_extension.py @@ -0,0 +1,104 @@ +# Copyright 2023 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# For further info, check https://github.com/canonical/charmcraft +import pytest + +from charmcraft.errors import ExtensionError +from charmcraft.extensions import apply_extensions +from charmcraft.extensions.flask import ACTIONS, OPTIONS + + +@pytest.fixture(name="input_yaml") +def input_yaml_fixture(monkeypatch, tmp_path): + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + return { + "type": "charm", + "name": "test-flask", + "summary": "test summary", + "description": "test description", + "bases": [{"name": "ubuntu", "channel": "22.04"}], + "extensions": ["flask"], + } + + +def test_flask_extension(input_yaml, tmp_path): + applied = apply_extensions(tmp_path, input_yaml) + assert applied == { + "actions": ACTIONS, + "assumes": ["k8s-api"], + "bases": [{"channel": "22.04", "name": "ubuntu"}], + "containers": { + "flask-app": {"resource": "flask-app-image"}, + "statsd-prometheus-exporter": {"resource": "statsd-prometheus-exporter-image"}, + }, + "description": "test description", + "name": "test-flask", + "options": OPTIONS, + "parts": {}, + "peers": {"secret-storage": {"interface": "secret-storage"}}, + "resources": { + "flask-app-image": {"description": "Flask application image.", "type": "oci-image"}, + "statsd-prometheus-exporter-image": { + "description": "Prometheus exporter for statsd data", + "type": "oci-image", + "upstream-source": "prom/statsd-exporter:v0.24.0", + }, + }, + "summary": "test summary", + "type": "charm", + } + + +PROTECTED_FIELDS_TEST_PARAMETERS = [ + pytest.param({"type": "bundle"}, id="type"), + pytest.param({"containers": {"foobar": {"resource": "foobar"}}}, id="containers"), + pytest.param({"peers": {"foobar": {"interface": "foobar"}}}, id="peers"), + pytest.param({"resources": {"foobar": {"type": "oci-image"}}}, id="resources"), +] + + +@pytest.mark.parametrize("modification", PROTECTED_FIELDS_TEST_PARAMETERS) +def test_flask_protected_fields(modification, input_yaml, tmp_path): + input_yaml.update(modification) + with pytest.raises(ExtensionError): + apply_extensions(tmp_path, input_yaml) + + +def test_flask_merge_options(input_yaml, tmp_path): + added_options = {"api_secret": {"type": "string"}} + input_yaml["options"] = added_options + applied = apply_extensions(tmp_path, input_yaml) + assert applied["options"] == {**OPTIONS, **added_options} + + +def test_flask_merge_action(input_yaml, tmp_path): + added_actions = {"foobar": {}} + input_yaml["actions"] = added_actions + applied = apply_extensions(tmp_path, input_yaml) + assert applied["actions"] == {**ACTIONS, **added_actions} + + +INCOMPATIBLE_FIELDS_TEST_PARAMETERS = [ + pytest.param({"devices": {"gpu": {"type": "gpu"}}}, id="devices"), + pytest.param({"extra-bindings": {"foobar": {}}}, id="extra-bindings"), + pytest.param({"storage": {"foobar": {"type": "filesystem"}}}, id="storage"), +] + + +@pytest.mark.parametrize("modification", INCOMPATIBLE_FIELDS_TEST_PARAMETERS) +def test_flask_incompatible_fields(modification, input_yaml, tmp_path): + input_yaml.update(modification) + with pytest.raises(ExtensionError): + apply_extensions(tmp_path, input_yaml) From de9c4bd0df52061ae06854d842a578d2178e579f Mon Sep 17 00:00:00 2001 From: Weii Wang Date: Thu, 12 Oct 2023 17:17:36 +0800 Subject: [PATCH 2/7] Update charmcraft/extensions/flask.py Co-authored-by: Alex Lowe --- charmcraft/extensions/flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charmcraft/extensions/flask.py b/charmcraft/extensions/flask.py index 0b294a3d6..ec478328e 100644 --- a/charmcraft/extensions/flask.py +++ b/charmcraft/extensions/flask.py @@ -124,7 +124,7 @@ def get_root_snippet(self) -> Dict[str, Any]: } merging_fields = { "actions": ACTIONS, - "options": OPTIONS, + "config": {"options": OPTIONS}, } incompatible_fields = ("devices", "extra-bindings", "storage") for incompatible_field in incompatible_fields: From 250e18425a49be9bdd09719d123f18a1fd990ac5 Mon Sep 17 00:00:00 2001 From: Weii Wang Date: Fri, 10 Nov 2023 09:19:38 +0200 Subject: [PATCH 3/7] Add more default component to the extension --- charmcraft/extensions/__init__.py | 2 +- charmcraft/extensions/flask.py | 24 +++++++++++++++++++++++- charmcraft/metafiles/actions.py | 1 + charmcraft/models/charmcraft.py | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/charmcraft/extensions/__init__.py b/charmcraft/extensions/__init__.py index 09767ed9d..142692b38 100644 --- a/charmcraft/extensions/__init__.py +++ b/charmcraft/extensions/__init__.py @@ -33,4 +33,4 @@ "unregister", ] -register("flask", Flask) +register("flask-framework", Flask) diff --git a/charmcraft/extensions/flask.py b/charmcraft/extensions/flask.py index ec478328e..623ddec90 100644 --- a/charmcraft/extensions/flask.py +++ b/charmcraft/extensions/flask.py @@ -124,7 +124,14 @@ def get_root_snippet(self) -> Dict[str, Any]: } merging_fields = { "actions": ACTIONS, - "config": {"options": OPTIONS}, + "requires": { + "logging": {"interface": "loki_push_api"}, + "ingress": {"interface": "ingress", "limit": 1}, + }, + "provides": { + "metrics-endpoint": {"interface": "prometheus_scrape"}, + "grafana-dashboard": {"interface": "grafana_dashboard"}, + }, } incompatible_fields = ("devices", "extra-bindings", "storage") for incompatible_field in incompatible_fields: @@ -151,6 +158,21 @@ def get_root_snippet(self) -> Dict[str, Any]: f"which conflict with the flask extension, please rename or remove it" ) snippet[merging_field] = {**merging_field_value, **user_provided} + if "config" not in self.yaml_data or "options" not in self.yaml_data["config"]: + snippet["config"] = self.yaml_data.get("config", {}) + snippet["config"]["options"] = OPTIONS + else: + user_provided = self.yaml_data["config"]["options"] + overlap = user_provided.keys() & OPTIONS.keys() + if overlap: + raise ExtensionError( + f"overlapping keys {overlap} in config.options of charmcraft.yaml " + f"which conflict with the flask extension, please rename or remove it" + ) + snippet_config = snippet.get("config", {}) + snippet_config["options"] = {**OPTIONS, **user_provided} + snippet["config"] = snippet_config + return snippet @override diff --git a/charmcraft/metafiles/actions.py b/charmcraft/metafiles/actions.py index 4e1c43a0c..f644dc945 100644 --- a/charmcraft/metafiles/actions.py +++ b/charmcraft/metafiles/actions.py @@ -88,6 +88,7 @@ def create_actions_yaml( shutil.copyfile(original_file_path, target_file_path) else: if charmcraft_config.actions: + basedir.mkdir(exist_ok=True) target_file_path.write_text( yaml.dump( charmcraft_config.actions.dict( diff --git a/charmcraft/models/charmcraft.py b/charmcraft/models/charmcraft.py index c2f027477..d39a43f57 100644 --- a/charmcraft/models/charmcraft.py +++ b/charmcraft/models/charmcraft.py @@ -177,7 +177,7 @@ def validate_special_parts(cls, parts, values): # extra error here, it gets confusing to the user) return None - if parts is None: + if not parts: # no parts indicated, default to the type of package parts = {values["type"]: {}} From 5fa7c850e11dd3c6cc26c994f07158b505d380d5 Mon Sep 17 00:00:00 2001 From: Weii Wang Date: Fri, 10 Nov 2023 10:00:46 +0200 Subject: [PATCH 4/7] Add more unit tests for the flask extension --- tests/extensions/test_flask_extension.py | 46 +++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/tests/extensions/test_flask_extension.py b/tests/extensions/test_flask_extension.py index 7d3413d5d..673ac749f 100644 --- a/tests/extensions/test_flask_extension.py +++ b/tests/extensions/test_flask_extension.py @@ -29,7 +29,7 @@ def input_yaml_fixture(monkeypatch, tmp_path): "summary": "test summary", "description": "test description", "bases": [{"name": "ubuntu", "channel": "22.04"}], - "extensions": ["flask"], + "extensions": ["flask-framework"], } @@ -45,9 +45,17 @@ def test_flask_extension(input_yaml, tmp_path): }, "description": "test description", "name": "test-flask", - "options": OPTIONS, + "config": {"options": OPTIONS}, "parts": {}, "peers": {"secret-storage": {"interface": "secret-storage"}}, + "provides": { + "metrics-endpoint": {"interface": "prometheus_scrape"}, + "grafana-dashboard": {"interface": "grafana_dashboard"}, + }, + "requires": { + "logging": {"interface": "loki_push_api"}, + "ingress": {"interface": "ingress", "limit": 1}, + }, "resources": { "flask-app-image": {"description": "Flask application image.", "type": "oci-image"}, "statsd-prometheus-exporter-image": { @@ -78,9 +86,9 @@ def test_flask_protected_fields(modification, input_yaml, tmp_path): def test_flask_merge_options(input_yaml, tmp_path): added_options = {"api_secret": {"type": "string"}} - input_yaml["options"] = added_options + input_yaml["config"] = {"options": added_options} applied = apply_extensions(tmp_path, input_yaml) - assert applied["options"] == {**OPTIONS, **added_options} + assert applied["config"] == {"options": {**OPTIONS, **added_options}} def test_flask_merge_action(input_yaml, tmp_path): @@ -90,10 +98,40 @@ def test_flask_merge_action(input_yaml, tmp_path): assert applied["actions"] == {**ACTIONS, **added_actions} +def test_flask_merge_relation(input_yaml, tmp_path): + new_provides = {"provides-foobar": {"interface": "foobar"}} + new_requires = {"requires-foobar": {"interface": "foobar"}} + input_yaml["provides"] = new_provides + input_yaml["requires"] = new_requires + applied = apply_extensions(tmp_path, input_yaml) + assert applied["provides"] == { + "metrics-endpoint": {"interface": "prometheus_scrape"}, + "grafana-dashboard": {"interface": "grafana_dashboard"}, + **new_provides, + } + assert applied["requires"] == { + "logging": {"interface": "loki_push_api"}, + "ingress": {"interface": "ingress", "limit": 1}, + **new_requires, + } + + INCOMPATIBLE_FIELDS_TEST_PARAMETERS = [ pytest.param({"devices": {"gpu": {"type": "gpu"}}}, id="devices"), pytest.param({"extra-bindings": {"foobar": {}}}, id="extra-bindings"), pytest.param({"storage": {"foobar": {"type": "filesystem"}}}, id="storage"), + pytest.param( + {"config": {"options": {"webserver_wsgi_path": {"type": "string"}}}}, + id="duplicate-options", + ), + pytest.param( + {"requires": {"ingress": {"interface": "ingress"}}}, + id="duplicate-requires", + ), + pytest.param( + {"provides": {"metrics-endpoint": {"interface": "prometheus_scrape"}}}, + id="duplicate-provides", + ), ] From 1be5a2ccc9ab28db3da6cba94fb535e9495d7d0f Mon Sep 17 00:00:00 2001 From: Weii Wang Date: Fri, 10 Nov 2023 10:21:24 +0200 Subject: [PATCH 5/7] Fix some typing issues --- charmcraft/extensions/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charmcraft/extensions/registry.py b/charmcraft/extensions/registry.py index e405130bd..8635a921f 100644 --- a/charmcraft/extensions/registry.py +++ b/charmcraft/extensions/registry.py @@ -33,7 +33,7 @@ def get_extension_names() -> list[str]: return list(_EXTENSIONS.keys()) -def get_extension_class(extension_name: str) -> Type[Extension]: +def get_extension_class(extension_name: str) -> Extension: """Obtain a extension class given the name. :param name: The extension name. @@ -49,7 +49,7 @@ def get_extension_class(extension_name: str) -> Type[Extension]: ) from None -def register(extension_name: str, extension_class: Type[Extension]) -> None: +def register(extension_name: str, extension_class: Extension) -> None: """Register extension. :param extension_name: the name to register. From b56b6580716f7d51647b12f2027abe3828ebc7ca Mon Sep 17 00:00:00 2001 From: Weii Wang Date: Fri, 10 Nov 2023 11:00:44 +0200 Subject: [PATCH 6/7] Fix some linting checks --- charmcraft/extensions/flask.py | 20 +++++++++++--------- charmcraft/extensions/registry.py | 6 +++--- tests/commands/test_expand_extensions.py | 12 ++++++++++-- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/charmcraft/extensions/flask.py b/charmcraft/extensions/flask.py index 623ddec90..32a3d5ad8 100644 --- a/charmcraft/extensions/flask.py +++ b/charmcraft/extensions/flask.py @@ -16,7 +16,7 @@ """The flask extension.""" -from typing import Any, Dict, List, Optional, Tuple +from typing import Any from overrides import override @@ -87,20 +87,22 @@ class Flask(Extension): + """Extension for 12-factor Flask applications.""" + @staticmethod @override - def get_supported_bases() -> List[Tuple[str, ...]]: + def get_supported_bases() -> list[tuple[str, ...]]: """Return supported bases.""" return [("ubuntu", "22.04")] @staticmethod @override - def is_experimental(base: Optional[Tuple[str, ...]]) -> bool: + def is_experimental(_base: tuple[str, ...] | None) -> bool: """Check if the extension is in an experimental state.""" return True @override - def get_root_snippet(self) -> Dict[str, Any]: + def get_root_snippet(self) -> dict[str, Any]: """Fill in some required root components for Flask.""" protected_fields = { "type": "charm", @@ -122,7 +124,7 @@ def get_root_snippet(self) -> Dict[str, Any]: }, "peers": {"secret-storage": {"interface": "secret-storage"}}, } - merging_fields = { + merging_fields: dict[str, dict[str, Any]] = { "actions": ACTIONS, "requires": { "logging": {"interface": "loki_push_api"}, @@ -139,7 +141,7 @@ def get_root_snippet(self) -> Dict[str, Any]: raise ExtensionError( f"the flask extension is incompatible with the field {incompatible_field!r}" ) - snippet = protected_fields + snippet: dict[str, Any] = protected_fields for protected, protected_value in protected_fields.items(): if protected in self.yaml_data and self.yaml_data[protected] != protected_value: raise ExtensionError( @@ -150,7 +152,7 @@ def get_root_snippet(self) -> Dict[str, Any]: if merging_field not in self.yaml_data: snippet[merging_field] = merging_field_value continue - user_provided = self.yaml_data[merging_field] + user_provided: dict[str, Any] = self.yaml_data[merging_field] overlap = user_provided.keys() & merging_field_value.keys() if overlap: raise ExtensionError( @@ -176,11 +178,11 @@ def get_root_snippet(self) -> Dict[str, Any]: return snippet @override - def get_part_snippet(self) -> Dict[str, Any]: + def get_part_snippet(self) -> dict[str, Any]: """Return the part snippet to apply to existing parts.""" return {} @override - def get_parts_snippet(self) -> Dict[str, Any]: + def get_parts_snippet(self) -> dict[str, Any]: """Return the parts to add to parts.""" return {} diff --git a/charmcraft/extensions/registry.py b/charmcraft/extensions/registry.py index 8635a921f..419253f34 100644 --- a/charmcraft/extensions/registry.py +++ b/charmcraft/extensions/registry.py @@ -20,7 +20,7 @@ from charmcraft import errors from charmcraft.extensions.extension import Extension -_EXTENSIONS: dict[str, Extension] = {} +_EXTENSIONS: dict[str, type[Extension]] = {} def get_extension_names() -> list[str]: @@ -33,7 +33,7 @@ def get_extension_names() -> list[str]: return list(_EXTENSIONS.keys()) -def get_extension_class(extension_name: str) -> Extension: +def get_extension_class(extension_name: str) -> type[Extension]: """Obtain a extension class given the name. :param name: The extension name. @@ -49,7 +49,7 @@ def get_extension_class(extension_name: str) -> Extension: ) from None -def register(extension_name: str, extension_class: Extension) -> None: +def register(extension_name: str, extension_class: type[Extension]) -> None: """Register extension. :param extension_name: the name to register. diff --git a/tests/commands/test_expand_extensions.py b/tests/commands/test_expand_extensions.py index f6c90b5b0..263c77a15 100644 --- a/tests/commands/test_expand_extensions.py +++ b/tests/commands/test_expand_extensions.py @@ -63,7 +63,7 @@ def test_expand_extensions_simple(tmp_path, prepare_charmcraft_yaml, fake_extens cmd.run([]) emitter.assert_message( dedent( - """\ + f"""\ analysis: ignore: attributes: [] @@ -74,7 +74,15 @@ def test_expand_extensions_simple(tmp_path, prepare_charmcraft_yaml, fake_extens storage-url: https://storage.snapcraftcontent.com description: test-description name: test-charm-name - parts: {} + parts: + charm: + charm-binary-python-packages: [] + charm-entrypoint: src/charm.py + charm-python-packages: [] + charm-requirements: [] + charm-strict-dependencies: false + plugin: charm + source: {tmp_path} summary: test-summary terms: - https://example.com/test From 12e58530a0d3fdd46d3ea3129f124fe8082717a9 Mon Sep 17 00:00:00 2001 From: Weii Wang Date: Fri, 10 Nov 2023 11:08:37 +0200 Subject: [PATCH 7/7] Fix some linting issues --- charmcraft/extensions/flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charmcraft/extensions/flask.py b/charmcraft/extensions/flask.py index 32a3d5ad8..5b07aa47c 100644 --- a/charmcraft/extensions/flask.py +++ b/charmcraft/extensions/flask.py @@ -97,7 +97,7 @@ def get_supported_bases() -> list[tuple[str, ...]]: @staticmethod @override - def is_experimental(_base: tuple[str, ...] | None) -> bool: + def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 """Check if the extension is in an experimental state.""" return True