General tidy for module ready for release.

Refresh charm to drop release usage in ops-sunbeam.

Drop surplus template fragments.

Refresh unit tests.

Tidy requirements.txt.

Switch to black + other linters.

Tidy docstrings across operator.

Change-Id: I872da0c54dda857a4005b84905cb248d7a9782ae
This commit is contained in:
James Page 2022-11-03 15:17:03 +01:00
parent a0a7785924
commit 49173f55dd
10 changed files with 296 additions and 181 deletions

View File

@ -0,0 +1,39 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
# Testing tools configuration
[tool.coverage.run]
branch = true
[tool.coverage.report]
show_missing = true
[tool.pytest.ini_options]
minversion = "6.0"
log_cli_level = "INFO"
# Formatting tools configuration
[tool.black]
line-length = 79
[tool.isort]
profile = "black"
multi_line_output = 3
force_grid_wrap = true
# Linting tools configuration
[tool.flake8]
max-line-length = 79
max-doc-length = 99
max-complexity = 10
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
# Ignore W503, E501 because using black creates errors with this
# Ignore D107 Missing docstring in __init__
ignore = ["W503", "E501", "D107", "E402"]
per-file-ignores = []
docstring-convention = "google"
# Check for properly formatted copyright header in each file
copyright-check = "True"
copyright-author = "Canonical Ltd."
copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"

View File

@ -16,12 +16,4 @@ lightkube
lightkube-models
ops
git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
python-keystoneclient # keystone-k8s
git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates
# Note: Required for cinder-k8s, cinder-ceph-k8s, glance-k8s, nova-k8s
git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
# Charmhelpers is only present as interface_ceph_client uses it.
git+https://github.com/juju/charm-helpers.git#egg=charmhelpers

View File

@ -1,4 +1,18 @@
#!/usr/bin/env python3
# Copyright 2022 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.
"""Nova Operator Charm.
This charm provide Nova services as part of an OpenStack deployment
@ -6,19 +20,25 @@ This charm provide Nova services as part of an OpenStack deployment
import logging
import uuid
from typing import Callable, List, Mapping
import ops.framework
from ops.main import main
from ops.pebble import ExecError
import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.container_handlers as sunbeam_chandlers
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
import ops_sunbeam.config_contexts as sunbeam_ctxts
from typing import (
Callable,
List,
Mapping,
)
import charms.sunbeam_nova_compute_operator.v0.cloud_compute as cloud_compute
import ops.framework
import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.config_contexts as sunbeam_ctxts
import ops_sunbeam.container_handlers as sunbeam_chandlers
import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
from ops.main import (
main,
)
from ops.pebble import (
ExecError,
)
logger = logging.getLogger(__name__)
@ -31,23 +51,24 @@ class WSGINovaMetadataConfigContext(sunbeam_ctxts.ConfigContext):
def context(self) -> dict:
"""WSGI configuration options."""
log_svc_name = self.charm.service_name.replace('-', '_')
log_svc_name = self.charm.service_name.replace("-", "_")
return {
"name": self.charm.service_name,
"public_port": 8775,
"user": self.charm.service_user,
"group": self.charm.service_group,
"wsgi_admin_script": '/usr/bin/nova-metadata-wsgi',
"wsgi_public_script": '/usr/bin/nova-metadata-wsgi',
"wsgi_admin_script": "/usr/bin/nova-metadata-wsgi",
"wsgi_public_script": "/usr/bin/nova-metadata-wsgi",
"error_log": f"/var/log/apache2/{log_svc_name}_error.log",
"custom_log": f"/var/log/apache2/{log_svc_name}_access.log",
}
class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"""Pebble handler for Nova scheduler."""
def get_layer(self) -> dict:
"""Nova Scheduler service
"""Nova Scheduler service layer.
:returns: pebble layer configuration for scheduler service
:rtype: dict
@ -60,9 +81,9 @@ class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"override": "replace",
"summary": "Nova Scheduler",
"command": "nova-scheduler",
"startup": "enabled"
"startup": "enabled",
}
}
},
}
def get_healthcheck_layer(self) -> dict:
@ -76,25 +97,27 @@ class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"online": {
"override": "replace",
"level": "ready",
"exec": {
"command": "service nova-scheduler status"
}
"exec": {"command": "service nova-scheduler status"},
},
}
}
def default_container_configs(self):
def default_container_configs(
self,
) -> List[sunbeam_core.ContainerConfigFile]:
"""Container configurations for handler."""
return [
sunbeam_core.ContainerConfigFile(
'/etc/nova/nova.conf',
'nova',
'nova')]
"/etc/nova/nova.conf", "nova", "nova"
)
]
class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"""Pebble handler for Nova Conductor container."""
def get_layer(self):
"""Nova Conductor service
"""Nova Conductor service.
:returns: pebble service layer configuration for conductor service
:rtype: dict
@ -107,9 +130,9 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"override": "replace",
"summary": "Nova Conductor",
"command": "nova-conductor",
"startup": "enabled"
"startup": "enabled",
}
}
},
}
def get_healthcheck_layer(self) -> dict:
@ -122,19 +145,20 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"online": {
"override": "replace",
"level": "ready",
"exec": {
"command": "service nova-conductor status"
}
"exec": {"command": "service nova-conductor status"},
},
}
}
def default_container_configs(self):
def default_container_configs(
self,
) -> List[sunbeam_core.ContainerConfigFile]:
"""Container configurations for handler."""
return [
sunbeam_core.ContainerConfigFile(
'/etc/nova/nova.conf',
'nova',
'nova')]
"/etc/nova/nova.conf", "nova", "nova"
)
]
class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
@ -148,7 +172,9 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
callback_f: Callable,
mandatory: bool = False,
):
"""Creates a new CloudComputeRequiresHandler that handles initial
"""Constructor for CloudComputeRequiresHandler.
Creates a new CloudComputeRequiresHandler that handles initial
events from the relation and invokes the provided callbacks based on
the event raised.
@ -175,11 +201,11 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
)
self.framework.observe(
compute_service.on.compute_nodes_connected,
self._compute_nodes_connected
self._compute_nodes_connected,
)
self.framework.observe(
compute_service.on.compute_nodes_ready,
self._compute_nodes_connected
self._compute_nodes_connected,
)
return compute_service
@ -191,6 +217,7 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
@property
def ready(self) -> bool:
"""Interface ready for use."""
return True
@ -199,20 +226,21 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
_state = ops.framework.StoredState()
service_name = "nova-api"
wsgi_admin_script = '/usr/bin/nova-api-wsgi'
wsgi_public_script = '/usr/bin/nova-api-wsgi'
shared_metadata_secret_key = 'shared-metadata-secret'
wsgi_admin_script = "/usr/bin/nova-api-wsgi"
wsgi_public_script = "/usr/bin/nova-api-wsgi"
shared_metadata_secret_key = "shared-metadata-secret"
mandatory_relations = {
'database',
'api-database',
'cell-database',
'amqp',
'identity-service',
'ingress-public',
"database",
"api-database",
"cell-database",
"amqp",
"identity-service",
"ingress-public",
}
@property
def db_sync_cmds(self) -> List[List[str]]:
"""DB sync commands for Nova operator."""
# we must provide the database connection for the cell database,
# because the database credentials are different to the main database.
# If we don't provide them:
@ -222,14 +250,29 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
# https://docs.openstack.org/nova/yoga/admin/cells.html#configuring-a-new-deployment
cell_database = self.dbs["cell-database"].context()["connection"]
return [
['sudo', '-u', 'nova', 'nova-manage', 'api_db', 'sync'],
["sudo", "-u", "nova", "nova-manage", "api_db", "sync"],
[
'sudo', '-u', 'nova', 'nova-manage', 'cell_v2', 'map_cell0',
'--database_connection', cell_database
"sudo",
"-u",
"nova",
"nova-manage",
"cell_v2",
"map_cell0",
"--database_connection",
cell_database,
],
["sudo", "-u", "nova", "nova-manage", "db", "sync"],
[
"sudo",
"-u",
"nova",
"nova-manage",
"cell_v2",
"create_cell",
"--name",
"cell1",
"--verbose",
],
['sudo', '-u', 'nova', 'nova-manage', 'db', 'sync'],
['sudo', '-u', 'nova', 'nova-manage', 'cell_v2', 'create_cell',
'--name', 'cell1', '--verbose'],
]
@property
@ -240,26 +283,30 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
@property
def service_user(self) -> str:
"""Service user file and directory ownership."""
return 'nova'
return "nova"
@property
def service_group(self) -> str:
"""Service group file and directory ownership."""
return 'nova'
return "nova"
@property
def service_endpoints(self):
"""Service endpoints for Nova."""
return [
{
'service_name': 'nova',
'type': 'compute',
'description': "OpenStack Compute",
'internal_url': f'{self.internal_url}/v2.1',
'public_url': f'{self.public_url}/v2.1',
'admin_url': f'{self.admin_url}/v2.1'}]
"service_name": "nova",
"type": "compute",
"description": "OpenStack Compute",
"internal_url": f"{self.internal_url}/v2.1",
"public_url": f"{self.public_url}/v2.1",
"admin_url": f"{self.admin_url}/v2.1",
}
]
@property
def default_public_ingress_port(self):
"""Default port for service ingress."""
return 8774
@property
@ -275,41 +322,43 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"cell-database": "nova_cell0",
}
def get_pebble_handlers(self):
def get_pebble_handlers(
self,
) -> List[sunbeam_chandlers.ServicePebbleHandler]:
"""Pebble handlers for operator."""
pebble_handlers = super().get_pebble_handlers()
pebble_handlers.extend([
NovaSchedulerPebbleHandler(
self,
NOVA_SCHEDULER_CONTAINER,
'nova-scheduler',
[],
self.template_dir,
self.openstack_release,
self.configure_charm),
NovaConductorPebbleHandler(
self,
NOVA_CONDUCTOR_CONTAINER,
'nova-conductor',
[],
self.template_dir,
self.openstack_release,
self.configure_charm)])
pebble_handlers.extend(
[
NovaSchedulerPebbleHandler(
self,
NOVA_SCHEDULER_CONTAINER,
"nova-scheduler",
[],
self.template_dir,
self.configure_charm,
),
NovaConductorPebbleHandler(
self,
NOVA_CONDUCTOR_CONTAINER,
"nova-conductor",
[],
self.template_dir,
self.configure_charm,
),
]
)
return pebble_handlers
def get_relation_handlers(
self, handlers: List[sunbeam_rhandlers.RelationHandler] = None
) -> List[sunbeam_rhandlers.RelationHandler]:
"""
:param handlers:
:return:
"""
"""Relation handlers for operator."""
handlers = super().get_relation_handlers(handlers or [])
if self.can_add_handler("cloud-compute", handlers):
self.compute_nodes = CloudComputeRequiresHandler(
self,
'cloud-compute',
self.model.config['region'],
"cloud-compute",
self.model.config["region"],
self.register_compute_nodes,
)
handlers.append(self.compute_nodes)
@ -322,7 +371,8 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
_cadapters.extend(
[
WSGINovaMetadataConfigContext(
self, 'wsgi_nova_metadata',
self,
"wsgi_nova_metadata",
)
]
)
@ -334,8 +384,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
def set_shared_metadatasecret(self):
"""Store the shared metadata secret."""
self.leader_set(
{self.shared_metadata_secret_key: str(uuid.uuid1())})
self.leader_set({self.shared_metadata_secret_key: str(uuid.uuid1())})
def register_compute_nodes(self, event: ops.framework.EventBase) -> None:
"""Register compute nodes when the event is received.
@ -363,27 +412,33 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
# return
self.compute_nodes.interface.set_controller_info(
region=self.model.config['region'],
region=self.model.config["region"],
cross_az_attach=False,
)
try:
logger.debug('Discovering hosts for cell1')
cell1_uuid = self.get_cell_uuid('cell1')
cmd = ['nova-manage', 'cell_v2', 'discover_hosts', '--cell_uuid',
cell1_uuid, '--verbose']
logger.debug("Discovering hosts for cell1")
cell1_uuid = self.get_cell_uuid("cell1")
cmd = [
"nova-manage",
"cell_v2",
"discover_hosts",
"--cell_uuid",
cell1_uuid,
"--verbose",
]
handler.execute(cmd, exception_on_error=True)
except ExecError:
logger.exception('Failed to discover hosts for cell1')
logger.exception("Failed to discover hosts for cell1")
raise
def get_cell_uuid(self, cell, fatal=True):
"""Returns the cell UUID from the name
"""Returns the cell UUID from the name.
:param cell: string cell name i.e. 'cell1'
:returns: string cell uuid
"""
logger.debug(f'listing cells for {cell}')
logger.debug(f"listing cells for {cell}")
cells = self.get_cells()
cell_info = cells.get(cell)
if not cell_info:
@ -391,7 +446,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
raise Exception(f"Cell {cell} not found")
return None
return cell_info['uuid']
return cell_info["uuid"]
def get_cells(self):
"""Returns the cells configured in the environment.
@ -401,31 +456,33 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""
logger.info("Getting details of cells")
cells = {}
cmd = ['sudo', 'nova-manage', 'cell_v2', 'list_cells', '--verbose']
cmd = ["sudo", "nova-manage", "cell_v2", "list_cells", "--verbose"]
handler = self.get_named_pebble_handler(NOVA_CONDUCTOR_CONTAINER)
try:
out = handler.execute(cmd, exception_on_error=True)
except ExecError:
logger.exception('list_cells failed')
logger.exception("list_cells failed")
raise
for line in out.split('\n'):
columns = line.split('|')
for line in out.split("\n"):
columns = line.split("|")
if len(columns) < 2:
continue
columns = [c.strip() for c in columns]
try:
uuid.UUID(columns[2].strip())
cells[columns[1]] = {
'uuid': columns[2],
'amqp': columns[3],
'db': columns[4]}
"uuid": columns[2],
"amqp": columns[3],
"db": columns[4],
}
except ValueError:
pass
return cells
def configure_charm(self, event: ops.framework.EventBase) -> None:
"""Callback handler for nova operator configuration."""
if not self.peers.ready:
return
metadata_secret = self.get_shared_metadatasecret()
@ -441,12 +498,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
super().configure_charm(event)
class NovaXenaOperatorCharm(NovaOperatorCharm):
openstack_release = 'xena'
if __name__ == "__main__":
# Note: use_juju_for_storage=True required per
# https://github.com/canonical/operator/issues/506
main(NovaXenaOperatorCharm, use_juju_for_storage=True)
main(NovaOperatorCharm, use_juju_for_storage=True)

View File

@ -1,7 +0,0 @@
[database]
{% if database.connection -%}
connection = {{ database.connection }}
{% else -%}
connection = sqlite:////var/lib/cinder/cinder.db
{% endif -%}
connection_recycle_time = 200

View File

@ -1,10 +0,0 @@
{% if trusted_dashboards %}
[federation]
{% for dashboard_url in trusted_dashboards -%}
trusted_dashboard = {{ dashboard_url }}
{% endfor -%}
{% endif %}
{% for sp in fid_sps -%}
[{{ sp['protocol-name'] }}]
remote_id_attribute = {{ sp['remote-id-attribute'] }}
{% endfor -%}

View File

@ -1,6 +0,0 @@
{% for section in sections -%}
[{{section}}]
{% for key, value in sections[section].items() -%}
{{ key }} = {{ value }}
{% endfor %}
{%- endfor %}

View File

@ -1,15 +0,0 @@
{% if enable_signing -%}
[signing]
{% if certfile -%}
certfile = {{ certfile }}
{% endif -%}
{% if keyfile -%}
keyfile = {{ keyfile }}
{% endif -%}
{% if ca_certs -%}
ca_certs = {{ ca_certs }}
{% endif -%}
{% if ca_key -%}
ca_key = {{ ca_key }}
{% endif -%}
{% endif -%}

View File

@ -0,0 +1,15 @@
# Copyright 2022 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.
"""Unit tests for Nova operator."""

View File

@ -14,13 +14,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
"""Unit tests for Nova operator."""
import charm
import mock
import ops_sunbeam.test_utils as test_utils
import charm
class _NovaXenaOperatorCharm(charm.NovaXenaOperatorCharm):
class _NovaTestOperatorCharm(charm.NovaOperatorCharm):
"""Test Operator Charm for Nova Operator."""
def __init__(self, framework):
self.seen_events = []
@ -39,24 +42,28 @@ class _NovaXenaOperatorCharm(charm.NovaXenaOperatorCharm):
class TestNovaOperatorCharm(test_utils.CharmTestCase):
"""Unit tests for Nova Operator."""
PATCHES = []
@mock.patch(
'charms.observability_libs.v0.kubernetes_service_patch.'
'KubernetesServicePatch')
"charms.observability_libs.v0.kubernetes_service_patch."
"KubernetesServicePatch"
)
def setUp(self, mock_patch):
"""Setup environment for unit test."""
super().setUp(charm, self.PATCHES)
self.harness = test_utils.get_harness(
_NovaXenaOperatorCharm,
container_calls=self.container_calls)
_NovaTestOperatorCharm, container_calls=self.container_calls
)
# clean up events that were dynamically defined,
# otherwise we get issues because they'll be redefined,
# which is not allowed.
from charms.data_platform_libs.v0.database_requires import (
DatabaseEvents
DatabaseEvents,
)
for attr in (
"database_database_created",
"database_endpoints_changed",
@ -77,11 +84,13 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
self.harness.begin()
def test_pebble_ready_handler(self):
"""Test pebble ready handler."""
self.assertEqual(self.harness.charm.seen_events, [])
test_utils.set_all_pebbles_ready(self.harness)
self.assertEqual(len(self.harness.charm.seen_events), 3)
def test_all_relations(self):
"""Test all integrations for operator."""
self.harness.set_leader()
test_utils.set_all_pebbles_ready(self.harness)
# this adds all the default/common relations
@ -95,25 +104,40 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
test_utils.add_db_relation_credentials(self.harness, rel_id)
setup_cmds = [
['a2ensite', 'wsgi-nova-api'],
['sudo', '-u', 'nova', 'nova-manage', 'api_db', 'sync'],
["a2ensite", "wsgi-nova-api"],
["sudo", "-u", "nova", "nova-manage", "api_db", "sync"],
[
'sudo', '-u', 'nova', 'nova-manage', 'cell_v2', 'map_cell0',
'--database_connection',
"sudo",
"-u",
"nova",
"nova-manage",
"cell_v2",
"map_cell0",
"--database_connection",
# values originate in test_utils.add_db_relation_credentials()
'mysql+pymysql://foo:hardpassword@10.0.0.10/nova_cell0'
"mysql+pymysql://foo:hardpassword@10.0.0.10/nova_cell0",
],
["sudo", "-u", "nova", "nova-manage", "db", "sync"],
[
"sudo",
"-u",
"nova",
"nova-manage",
"cell_v2",
"create_cell",
"--name",
"cell1",
"--verbose",
],
['sudo', '-u', 'nova', 'nova-manage', 'db', 'sync'],
['sudo', '-u', 'nova', 'nova-manage', 'cell_v2', 'create_cell',
'--name', 'cell1', '--verbose'],
]
for cmd in setup_cmds:
self.assertIn(cmd, self.container_calls.execute['nova-api'])
self.assertIn(cmd, self.container_calls.execute["nova-api"])
config_files = [
'/etc/apache2/sites-available/wsgi-nova-api.conf',
'/etc/nova/nova.conf']
"/etc/apache2/sites-available/wsgi-nova-api.conf",
"/etc/nova/nova.conf",
]
for f in config_files:
self.check_file('nova-api', f)
self.check_file("nova-api", f)
def add_db_relation(harness, name) -> str:

View File

@ -15,6 +15,8 @@ minversion = 3.18.0
src_path = {toxinidir}/src/
tst_path = {toxinidir}/tests/
lib_path = {toxinidir}/lib/
pyproject_toml = {toxinidir}/pyproject.toml
all_path = {[vars]src_path} {[vars]tst_path}
[testenv]
basepython = python3
@ -33,6 +35,15 @@ allowlist_externals =
deps =
-r{toxinidir}/test-requirements.txt
[testenv:fmt]
description = Apply coding style standards to code
deps =
black
isort
commands =
isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
[testenv:build]
basepython = python3
deps =
@ -64,11 +75,6 @@ deps = {[testenv:py3]deps}
basepython = python3.10
deps = {[testenv:py3]deps}
[testenv:pep8]
basepython = python3
deps = {[testenv]deps}
commands = flake8 {posargs} {[vars]src_path} {[vars]tst_path}
[testenv:cover]
basepython = python3
deps = {[testenv:py3]deps}
@ -83,6 +89,31 @@ commands =
coverage xml -o cover/coverage.xml
coverage report
[testenv:pep8]
description = Alias for lint
deps = {[testenv:lint]deps}
commands = {[testenv:lint]commands}
[testenv:lint]
description = Check code against coding style standards
deps =
black
# flake8==4.0.1 # Pin version until https://github.com/csachs/pyproject-flake8/pull/14 is merged
flake8
flake8-docstrings
flake8-copyright
flake8-builtins
pyproject-flake8
pep8-naming
isort
codespell
commands =
codespell {[vars]all_path}
# pflake8 wrapper supports config from pyproject.toml
pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
[testenv:func-noop]
basepython = python3
commands =