Use Python 3.12 for python3-django job

With migration from ubuntu jammy to noble, python3.11 is not available
anymore. This makes the job to fail on pre-install step.

So let's use Python 3.12 which is available out of the box on Noble
after switch.

This also bumps pylint version, as older one does not work
anymore with Python 3.12. New pylint brings quite some new
rules with it. Some were disabled, some were fixed within this
patch.

Change-Id: I4ba288966c582910e8a822d4531e29c9c005e48f
This commit is contained in:
Dmitriy Rabotyagov 2024-12-01 16:08:51 +01:00
parent 1b15f51bab
commit 2ec0177edc
41 changed files with 117 additions and 96 deletions

View File

@ -15,12 +15,14 @@ disable=
not-callable,
# "W" Warnings for stylistic problems or minor programming issues
arguments-differ,
arguments-renamed,
attribute-defined-outside-init,
bad-indentation,
broad-except,
fixme,
# python3 way: pylint suggests to follow PEP 3102
keyword-arg-before-vararg, # TODO
missing-timeout,
pointless-string-statement,
protected-access,
# We should do it carefully considering PEP3134
@ -37,9 +39,8 @@ disable=
# "C" Coding convention violations
abstract-method,
anomalous-backslash-in-string,
bad-builtin,
bad-continuation,
deprecated-lambda,
consider-using-dict-items,
consider-using-f-string,
global-statement,
# Not a good idea to disable this globally
# Check one by one and add pylint disabled comment if needed
@ -52,19 +53,22 @@ disable=
# import order is checked by flake8 (and pylint rule is incompatible with it)
wrong-import-order,
# "R" Refactor recommendations
consider-using-generator,
duplicate-code,
inconsistent-return-statements, # TODO
interface-not-implemented,
no-self-use,
too-many-ancestors,
too-many-arguments,
too-many-branches,
too-many-function-args,
too-many-instance-attributes,
too-many-locals,
too-many-positional-arguments,
too-many-return-statements,
too-many-statements,
useless-object-inheritance
use-a-generator,
use-dict-literal,
use-yield-from,
useless-object-inheritance,
[Basic]
# Variable names can be 1 to 31 characters long, with lowercase and underscores

View File

@ -17,10 +17,10 @@
pre-run: playbooks/horizon-tox-django/pre.yaml
run: playbooks/horizon-tox-django/run.yaml
vars:
tox_envlist: py311
tox_envlist: py312
# The following should match the base openstack-tox-pyNN job.
bindep_profile: test py311
python_version: "3.11"
bindep_profile: test py312
python_version: "3.12"
required-projects:
- name: openstack/horizon
@ -42,7 +42,7 @@
# sentinel to make this project template valid even when we support
# only one version of Django used as the default. Zuul project
# template configuration requires at least one job included.
- openstack-tox-py311
- openstack-tox-py312
# Let's keep at least one job as a template even when we support
# only one version of Django covered by the default job.
# Just comment it out for such case.
@ -50,5 +50,5 @@
gate:
jobs:
# Default python job in openstack-python3-antelope-jobs(-horizon)
- openstack-tox-py311
- openstack-tox-py312
- horizon-tox-python3-django42

View File

@ -595,6 +595,7 @@ class Dashboard(Registry, HorizonComponent):
panel_group = PanelGroup(self, panels=panel_set)
# Put our results into their appropriate places
# pylint: disable-next=possibly-used-before-assignment
panels_to_discover.extend(panel_group.panels)
panel_groups.append((panel_group.slug, panel_group))
if panel_group.slug == DEFAULT_PANEL_GROUP:
@ -615,6 +616,7 @@ class Dashboard(Registry, HorizonComponent):
before_import_registry = copy.copy(self._registry)
import_module('.%s.panel' % panel, package)
except Exception:
# pylint: disable-next=used-before-assignment
self._registry = before_import_registry
if module_has_submodule(mod, panel):
raise
@ -866,6 +868,7 @@ class Site(Registry, HorizonComponent):
before_import_registry = copy.copy(self._registry)
import_module('%s.%s' % (package, mod_name))
except Exception:
# pylint: disable-next=used-before-assignment
self._registry = before_import_registry
if module_has_submodule(mod, mod_name):
raise
@ -897,6 +900,7 @@ class Site(Registry, HorizonComponent):
before_import_registry = copy.copy(self._registry)
import_module('%s.%s' % (app, mod_name))
except Exception:
# pylint: disable-next=used-before-assignment
self._registry = before_import_registry
if module_has_submodule(mod, mod_name):
raise

View File

@ -628,14 +628,14 @@ class ExternalUploadMeta(forms.DeclarativeFieldsMetaclass):
"""
@classmethod
def __prepare__(cls, name, bases):
def __prepare__(mcs, name, bases):
# Required in python 3 to keep the form fields order.
# Without this method, the __new__(cls, name, bases, attrs) method
# receives a dict as attrs instead of OrderedDict.
# This method will be ignored by Python 2.
return collections.OrderedDict()
def __new__(cls, name, bases, attrs):
def __new__(mcs, name, bases, attrs):
def get_double_name(name):
suffix = '__hidden'
slen = len(suffix)
@ -660,4 +660,4 @@ class ExternalUploadMeta(forms.DeclarativeFieldsMetaclass):
new_attrs[new_attr_name] = hidden_field
meth_name = 'clean_' + new_attr_name
new_attrs[meth_name] = make_clean_method(new_attr_name)
return super().__new__(cls, name, bases, new_attrs)
return super().__new__(mcs, name, bases, new_attrs)

View File

@ -100,5 +100,5 @@ class Command(BaseCommand):
# Ensure to use UTF-8 encoding
new_po.encoding = 'utf-8'
with open(pofile, 'w+') as f:
with open(pofile, 'w+', encoding="utf-8") as f:
f.write(new_po.text)

View File

@ -69,6 +69,7 @@ class Command(TemplateCommand):
target = options.pop("target", None)
if target == "auto":
# pylint: disable-next=possibly-used-before-assignment
target = os.path.join(os.path.dirname(dashboard_mod.__file__),
panel_name)
if not os.path.exists(target):

View File

@ -110,7 +110,7 @@ def _is_path(path):
def _get_processed_messages(messages_path):
msgs = list()
msgs = list() # pylint: disable=use-list-literal
if not _is_path(messages_path):
LOG.error('%s is not a valid messages path.', messages_path)

View File

@ -48,7 +48,7 @@ class BaseActionMetaClass(type):
parameters for the initializer of the object. The object is then
initialized clean way. Similar principle is used in DataTableMetaclass.
"""
def __new__(cls, name, bases, attrs):
def __new__(mcs, name, bases, attrs):
# Options of action are set as class attributes, loading them.
options = {}
if attrs:
@ -74,7 +74,7 @@ class BaseActionMetaClass(type):
# instantiating of the specific Action.
attrs['base_options'] = options
return type.__new__(cls, name, bases, attrs)
return type.__new__(mcs, name, bases, attrs)
def __call__(cls, *args, **kwargs):
cls.base_options.update(kwargs)

View File

@ -1175,7 +1175,7 @@ class DataTableOptions(object):
class DataTableMetaclass(type):
"""Metaclass to add options to DataTable class and collect columns."""
def __new__(cls, name, bases, attrs):
def __new__(mcs, name, bases, attrs):
# Process options from Meta
class_name = name
dt_attrs = {}
@ -1247,7 +1247,7 @@ class DataTableMetaclass(type):
opts._filter_action = actions_dict[opts._filter_action.name]
# Create our new class!
return type.__new__(cls, name, bases, dt_attrs)
return type.__new__(mcs, name, bases, dt_attrs)
class DataTable(object, metaclass=DataTableMetaclass):

View File

@ -48,7 +48,7 @@ def read_from_file(key_file='.secret_key'):
raise FilePermissionError(
"Insecure permissions on key file %s, should be 0600." %
os.path.abspath(key_file))
with open(key_file, 'r') as f:
with open(key_file, 'r', encoding="utf-8") as f:
key = f.readline()
return key
@ -76,7 +76,7 @@ def generate_or_read_from_file(key_file='.secret_key', key_length=64):
if not os.path.exists(key_file):
key = generate_key(key_length)
old_umask = os.umask(0o177) # Use '0600' file permissions
with open(key_file, 'w') as f:
with open(key_file, 'w', encoding="utf-8") as f:
f.write(key)
os.umask(old_umask)
else:

View File

@ -54,28 +54,30 @@ class WorkflowContext(dict):
return self.__setitem__(key, None)
def set(self, key, val):
# pylint: disable-next=unnecessary-dunder-call
return self.__setitem__(key, val)
def unset(self, key):
# pylint: disable-next=unnecessary-dunder-call
return self.__delitem__(key)
class ActionMetaclass(forms.forms.DeclarativeFieldsMetaclass):
def __new__(cls, name, bases, attrs):
def __new__(mcs, name, bases, attrs):
# Pop Meta for later processing
opts = attrs.pop("Meta", None)
# Create our new class
cls_ = super().__new__(cls, name, bases, attrs)
mcs_ = super().__new__(mcs, name, bases, attrs)
# Process options from Meta
cls_.name = getattr(opts, "name", name)
cls_.slug = getattr(opts, "slug", slugify(name))
cls_.permissions = getattr(opts, "permissions", ())
cls_.policy_rules = getattr(opts, "policy_rules", ())
cls_.progress_message = getattr(opts, "progress_message",
mcs_.name = getattr(opts, "name", name)
mcs_.slug = getattr(opts, "slug", slugify(name))
mcs_.permissions = getattr(opts, "permissions", ())
mcs_.policy_rules = getattr(opts, "policy_rules", ())
mcs_.progress_message = getattr(opts, "progress_message",
_("Processing..."))
cls_.help_text = getattr(opts, "help_text", "")
cls_.help_text_template = getattr(opts, "help_text_template", None)
return cls_
mcs_.help_text = getattr(opts, "help_text", "")
mcs_.help_text_template = getattr(opts, "help_text_template", None)
return mcs_
class Action(forms.Form, metaclass=ActionMetaclass):
@ -478,10 +480,10 @@ class Step(object):
class WorkflowMetaclass(type):
def __new__(cls, name, bases, attrs):
super().__new__(cls, name, bases, attrs)
def __new__(mcs, name, bases, attrs):
super().__new__(mcs, name, bases, attrs)
attrs["_cls_registry"] = []
return type.__new__(cls, name, bases, attrs)
return type.__new__(mcs, name, bases, attrs)
class UpdateMembersStep(Step):

View File

@ -90,7 +90,7 @@ def _load_default_rules(service, enforcer):
return
try:
with open(policy_file) as f:
with open(policy_file, encoding="utf-8") as f:
policies = yaml.safe_load(f)
except IOError as e:
LOG.error('Failed to open the policy file for %(service)s %(path)s: '

View File

@ -280,6 +280,7 @@ class QuotaSet(collections.abc.Sequence):
return match.pop() if match else Quota(key, default)
def add(self, other):
# pylint: disable-next=unnecessary-dunder-call
return self.__add__(other)

View File

@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import json.encoder as encoder
from json import encoder
from django.utils.translation import gettext_lazy as _

View File

@ -76,8 +76,7 @@ def get_trace(trace_id):
# finishes before the dependent requests do so, to we need to
# normalize the duration of all requests by the finishing time of
# the one which took longest
if child_finished > finished:
finished = child_finished
finished = max(finished, child_finished)
return _data, finished
engine = _get_engine()

View File

@ -137,9 +137,8 @@ class SubnetsTab(project_tabs_subnets_tab):
subnet_id = subnet_usage.get("subnet_id")
subnet_used_ips = subnet_usage.get("used_ips")
subnet_total_ips = subnet_usage.get("total_ips")
subnet_free_ips = subnet_total_ips - subnet_used_ips
if subnet_free_ips < 0:
subnet_free_ips = 0
subnet_free_ips = max(subnet_total_ips - subnet_used_ips, 0)
for item in subnets_dict:
id = item.get("id")
if id == subnet_id:

View File

@ -12,7 +12,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import horizon.views as views
from horizon import views
class IndexView(views.HorizonTemplateView):

View File

@ -42,6 +42,7 @@ class QosSpecMixin(object):
qos_spec_id = context['qos_spec_id']
try:
# pylint: disable-next=possibly-used-before-assignment
qos_list = api.cinder.qos_spec_get(self.request, qos_spec_id)
context['qos_spec_name'] = qos_list.name
except Exception:

View File

@ -100,6 +100,7 @@ def download_ec2_bundle(request):
# Create our file bundle
template = 'project/api_access/ec2rc.sh.template'
try:
# pylint: disable-next=consider-using-with
temp_zip = tempfile.NamedTemporaryFile(delete=True)
with closing(zipfile.ZipFile(temp_zip.name, mode='w')) as archive:
archive.writestr('ec2rc.sh', render_to_string(template, context))

View File

@ -95,16 +95,16 @@ class DeleteRule(tables.DeleteAction):
@staticmethod
def action_present(count):
return ngettext_lazy(
u"Delete Rule",
u"Delete Rules",
"Delete Rule",
"Delete Rules",
count
)
@staticmethod
def action_past(count):
return ngettext_lazy(
u"Deleted Rule",
u"Deleted Rules",
"Deleted Rule",
"Deleted Rules",
count
)

View File

@ -121,8 +121,8 @@ class ReleaseIPsPortForwarding(ReleaseIPs):
@staticmethod
def action_past(count):
return ngettext_lazy(
u"Successfully redirected",
u"Successfully redirected",
"Successfully redirected",
"Successfully redirected",
count
)

View File

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
# pylint: disable=no-name-in-module,import-error
# pylint: disable=no-name-in-module,import-error,deprecated-module
from distutils.command import install

View File

@ -52,7 +52,9 @@ def _format_default_policy(default):
def _write_yaml_file(policies, output_file):
stream = open(output_file, 'w') if output_file else sys.stdout
# pylint: disable-next=consider-using-with
stream = open(output_file, 'w',
encoding="utf-8") if output_file else sys.stdout
yaml.dump(policies, stream=stream)
if output_file:
stream.close()

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=import-error
# pylint: disable=import-error,deprecated-module
from distutils.dist import Distribution
import os
from subprocess import call

View File

@ -26,7 +26,7 @@ from django import template
# rendering it unreadable.
warnings.simplefilter('ignore')
cmd_name = __name__.split('.')[-1]
cmd_name = __name__.rsplit('.', maxsplit=1)[-1]
CURDIR = os.path.realpath(os.path.dirname(__file__))
PROJECT_PATH = os.path.realpath(os.path.join(CURDIR, '../..'))
@ -301,11 +301,12 @@ location you desire, e.g.::
# Generate the WSGI.
if options.get('wsgi'):
with open(
os.path.join(CURDIR, 'horizon.wsgi.template'), 'r'
os.path.join(CURDIR, 'horizon.wsgi.template'), 'r',
encoding="utf-8"
) as fp:
wsgi_template = template.Template(fp.read())
if not os.path.exists(context['WSGI_FILE']) or force:
with open(context['WSGI_FILE'], 'w') as fp:
with open(context['WSGI_FILE'], 'w', encoding="utf-8") as fp:
fp.write(wsgi_template.render(context))
print('Generated "%s"' % context['WSGI_FILE'])
else:
@ -319,7 +320,8 @@ location you desire, e.g.::
context['WSGI_FILE'] = context['DEFAULT_WSGI_FILE']
with open(
os.path.join(CURDIR, 'apache_vhost.conf.template'), 'r'
os.path.join(CURDIR, 'apache_vhost.conf.template'), 'r',
encoding="utf-8"
) as fp:
wsgi_template = template.Template(fp.read())
sys.stdout.write(wsgi_template.render(context))

View File

@ -114,10 +114,13 @@ class Command(BaseCommand):
"""
with DirContext(self.local_settings_dir) as dircontext:
if not os.path.exists(self.local_settings_diff) or force:
with open(self.local_settings_example, 'r') as fp:
if not os.path.exists(self.local_settings_diff,
encoding="utf-8") or force:
with open(self.local_settings_example, 'r',
encoding="utf-8") as fp:
example_lines = fp.readlines()
with open(self.local_settings_file, 'r') as fp:
with open(self.local_settings_file, 'r',
encoding="utf-8") as fp:
local_settings_lines = fp.readlines()
local_settings_example_mtime = time.strftime(
self.time_fmt,
@ -134,7 +137,8 @@ class Command(BaseCommand):
dircontext.curdir,
self.local_settings_diff)
)
with open(self.local_settings_diff, 'w') as fp:
with open(self.local_settings_diff, 'w',
encoding="utf-8") as fp:
for line in difflib.unified_diff(
example_lines, local_settings_lines,
fromfile=self.local_settings_example,

View File

@ -18,8 +18,8 @@
import os
from subprocess import call
import babel.messages.catalog as catalog
import babel.messages.pofile as babel_pofile
from babel.messages import catalog
from babel.messages import pofile as babel_pofile
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils import translation
@ -110,7 +110,7 @@ class Command(BaseCommand):
continue
# Pseudo translation logic
with open(potfile, 'r') as f:
with open(potfile, 'r', encoding="utf-8") as f:
pot_cat = babel_pofile.read_po(f, ignore_obsolete=True)
new_cat = catalog.Catalog(locale=locale,

View File

@ -272,7 +272,9 @@ if os.path.exists(LOCAL_SETTINGS_DIR_PATH):
for filename in sorted(filenames):
if filename.endswith(".py"):
try:
with open(os.path.join(dirpath, filename)) as f:
with open(
os.path.join(dirpath, filename), encoding="utf-8"
) as f:
# pylint: disable=exec-used
exec(f.read())
except Exception:

View File

@ -127,7 +127,7 @@ def record_video(request, report_dir, xdisplay):
'-video_size', f'{width}x{height}',
'-framerate', str(frame_rate),
'-f', 'x11grab',
'-i', f':{display}',
'-i', f':{display}', # noqa: E231
filepath,
]
fnull = open(os.devnull, 'w')

View File

@ -224,7 +224,7 @@ def new_instance_demo(complete_default_test_network, request, instance_name,
yield instance
if count > 1:
for instance in range(0, count):
openstack_demo.delete_server(f"{instance_name}-{instance+1}")
openstack_demo.delete_server(f"{instance_name}-{instance + 1}")
else:
openstack_demo.delete_server(instance_name)

View File

@ -438,7 +438,7 @@ def test_edit_image_description_admin(login, driver, image_names,
".//button[@class='btn btn-primary finish']").click()
messages = widgets.get_and_dismiss_messages(driver)
assert f"Success: Image {image_name} " \
f"was successfully updated." in messages
f"was successfully updated." in messages
image_id = new_image_admin.id
assert (openstack_admin.compute.get(f"/images/{image_id}").json(
)['image']['metadata']['description'] == new_description)
@ -471,7 +471,7 @@ def test_update_image_metadata_admin(login, driver,
image_form.find_element_by_css_selector(
"button.btn span[class='fa fa-plus']").click()
image_form.find_element_by_xpath(
f"//span[@title='{name}']/parent::div/input").send_keys(value)
f"//span[@title='{name}']/parent::div/input").send_keys(value) # noqa: E231,E501
image_form.find_element_by_xpath(
"//button[@ng-click='modal.save()']").click()
messages = widgets.get_and_dismiss_messages(driver)
@ -548,7 +548,7 @@ def test_create_volume_from_image_admin(login, driver, volume_name,
name_field.send_keys(volume_name)
create_vol_btn = WebDriverWait(driver, config.selenium.page_timeout).until(
EC.element_to_be_clickable((By.XPATH, f"//button[@class='btn "
f"btn-primary finish']")))
f"btn-primary finish']")))
create_vol_btn.click()
messages = widgets.get_and_dismiss_messages(driver)
assert f"Info: Creating volume {volume_name}" in messages

View File

@ -42,7 +42,7 @@ def new_instance_admin(complete_default_test_network, request, instance_name,
yield instance
if count > 1:
for instance in range(0, count):
openstack_admin.delete_server(f"{instance_name}-{instance+1}")
openstack_admin.delete_server(f"{instance_name}-{instance + 1}")
else:
openstack_admin.delete_server(instance_name)

View File

@ -77,7 +77,7 @@ def ensure_checkbox(required_state, element):
current_state = element.is_selected()
if required_state != current_state:
element.find_element_by_xpath(
f".//following-sibling::label").click()
f".//following-sibling::label").click() # noqa: E231
def test_create_network_without_subnet_demo(login, openstack_demo, driver,

View File

@ -120,8 +120,8 @@ def test_add_member_to_project(login, driver, project_name, openstack_admin,
rows[0].find_element_by_css_selector(".data-table-action").click()
project_form = driver.find_element_by_css_selector("form .modal-content")
project_form.find_element_by_xpath(
f".//*[text()='{admin_name}']//ancestor::li"
f"/following-sibling::li/a[@href='#add_remove']").click()
f".//*[text()='{admin_name}']//ancestor::li" # noqa: E231
f"/following-sibling::li/a[@href='#add_remove']").click() # noqa: E231
project_form.find_element_by_css_selector(
".btn-primary[value='Save']").click()
messages = widgets.get_and_dismiss_messages(driver)
@ -151,8 +151,8 @@ def test_add_role_to_project_member(login, driver, openstack_admin, config,
rows[0].find_element_by_css_selector(".data-table-action").click()
project_form = driver.find_element_by_css_selector("form .modal-content")
select_roles_dropdown = project_form.find_element_by_xpath(
f".//*[text()='{admin_name}']//ancestor::li"
f"/following-sibling::li[@class='dropdown role_options']")
f".//*[text()='{admin_name}']//ancestor::li" # noqa: E231
f"/following-sibling::li[@class='dropdown role_options']") # noqa: E231
widgets.select_from_dropdown(select_roles_dropdown, admin_role_name)
project_form.find_element_by_css_selector(
".btn-primary[value='Save']").click()
@ -188,8 +188,8 @@ def test_add_group_to_project(login, driver, openstack_admin,
widgets.select_from_dropdown(actions_column, "Modify Groups")
project_form = driver.find_element_by_css_selector("form .modal-content")
project_form.find_element_by_xpath(
f".//*[text()='{group_name}']//ancestor::li"
f"/following-sibling::li/a[@href='#add_remove']").click()
f".//*[text()='{group_name}']//ancestor::li" # noqa: E231
f"/following-sibling::li/a[@href='#add_remove']").click() # noqa: E231
project_form.find_element_by_css_selector(
".btn-primary[value='Save']").click()
messages = widgets.get_and_dismiss_messages(driver)
@ -220,8 +220,8 @@ def test_add_role_to_project_group(login, driver, openstack_admin, config,
widgets.select_from_dropdown(actions_column, "Modify Groups")
project_form = driver.find_element_by_css_selector("form .modal-content")
select_roles_dropdown = project_form.find_element_by_xpath(
f".//*[text()='{group_name}']//ancestor::li"
f"/following-sibling::li[@class='dropdown role_options']")
f".//*[text()='{group_name}']//ancestor::li" # noqa: E231
f"/following-sibling::li[@class='dropdown role_options']") # noqa: E231
widgets.select_from_dropdown(select_roles_dropdown, admin_role_name)
project_form.find_element_by_css_selector(
".btn-primary[value='Save']").click()

View File

@ -157,7 +157,7 @@ def test_router_add_interface_demo(login, driver, router_name, openstack_demo,
".modal-dialog form")
widgets.select_from_dropdown(
add_interface_form, f"{new_network_demo.name}: {new_subnet_demo.cidr} "
f"({new_subnet_demo.name})")
f"({new_subnet_demo.name})")
add_interface_form.find_element_by_id(
"id_ip_address").send_keys(fixed_ip_test)
add_interface_form.find_element_by_css_selector(".btn-primary").click()

View File

@ -145,7 +145,7 @@ def test_browse_left_panel(live_server, driver, user, dashboard_data,
WebDriverWait(driver, config.selenium.implicit_wait).until(
EC.element_to_be_clickable(
(By.CSS_SELECTOR, f"a[data-target='#sidebar-accordion"
f"-{main_panel}-{sec_panel}']"))).click()
f"-{main_panel}-{sec_panel}']"))).click()
sidebar = driver.find_element_by_id(
f"sidebar-accordion-{main_panel}-{sec_panel}")
else:

View File

@ -53,24 +53,24 @@ def test_vcpu_pcpu_data_display(live_server, driver, user, dashboard_data):
driver.get(live_server.url + '/admin/hypervisors')
assert (driver.find_element_by_xpath(
f"//*[normalize-space()='VCPU Usage']/"
f"ancestor::div[contains(@class,'d3_quota_bar')]"
f"/div[contains(@class,'h6')]/"
f"ancestor::div[contains(@class,'d3_quota_bar')]" # noqa: E231
f"/div[contains(@class,'h6')]/" # noqa: E231
f"span[1]").text == str(p['vcpus_used']))
assert (driver.find_element_by_xpath(
f"//*[normalize-space()='VCPU Usage']/"
f"ancestor::div[contains(@class,'d3_quota_bar')]"
f"/div[contains(@class,'h6')]/"
f"ancestor::div[contains(@class,'d3_quota_bar')]" # noqa: E231
f"/div[contains(@class,'h6')]/" # noqa: E231
f"span[2]").text == str(p['vcpus_capacity']))
assert (driver.find_element_by_xpath(
f"//*[normalize-space()='PCPU Usage']/"
f"ancestor::div[contains(@class,'d3_quota_bar')]"
f"/div[contains(@class,'h6')]/"
f"ancestor::div[contains(@class,'d3_quota_bar')]" # noqa: E231
f"/div[contains(@class,'h6')]/" # noqa: E231
f"span[1]").text == str(p['pcpus_used']))
assert (driver.find_element_by_xpath(
f"//*[normalize-space()='PCPU Usage']/"
f"ancestor::div[contains(@class,'d3_quota_bar')]"
f"/div[contains(@class,'h6')]/"
f"ancestor::div[contains(@class,'d3_quota_bar')]" # noqa: E231
f"/div[contains(@class,'h6')]/" # noqa: E231
f"span[2]").text == str(p['pcpus_capacity']))
driver.find_element_by_link_text("Resource Provider").click()

View File

@ -55,7 +55,7 @@ def select_from_dropdown(element, label):
def select_from_specific_dropdown_in_form(driver, dropdown_id, label):
dropdown = driver.find_element_by_xpath(
f".//*[@for='{dropdown_id}']/following-sibling::div")
f".//*[@for='{dropdown_id}']/following-sibling::div") # noqa: E231
dropdown.click()
dropdown_options = dropdown.find_element_by_css_selector(
"ul.dropdown-menu")
@ -128,12 +128,12 @@ def select_from_transfer_table(element, label):
try:
element.find_element_by_xpath(
f".//*[text()='{label}']//ancestor::tr/td//*"
f".//*[text()='{label}']//ancestor::tr/td//*" # noqa: E231
f"[@class='btn btn-default fa fa-arrow-up']").click()
except exceptions.NoSuchElementException:
try:
element.find_element_by_xpath(
f".//*[text()='{label}']//ancestor::tr/td//*"
f".//*[text()='{label}']//ancestor::tr/td//*" # noqa: E231
f"[@class='btn btn-default fa fa-arrow-down']")
except exceptions.NoSuchElementException:
raise

View File

@ -163,9 +163,7 @@ class QuotaUsage(dict):
def update_available(self, name):
"""Updates the "available" metric for the given quota."""
quota = self.usages.get(name, {}).get('quota', float('inf'))
available = quota - self.usages[name]['used']
if available < 0:
available = 0
available = max(quota - self.usages[name]['used'], 0)
self.usages[name]['available'] = available

View File

@ -312,6 +312,7 @@ def get_xstatic_dirs(XSTATIC_MODULES, HORIZON_CONFIG):
file = 'horizon/lib/' + module.NAME + '/' + file
HORIZON_CONFIG['xstatic_lib_files'].append(file)
except TypeError:
# pylint: disable-next=broad-exception-raised
raise Exception(
'%s: Nothing to include because files to include are not '
'defined (i.e., None) in BASE_XSTATIC_MODULES list and '

View File

@ -37,7 +37,7 @@ commands = {posargs}
deps =
{[testenv]deps}
flake8-import-order==0.12 # LGPLv3
pylint==2.6.0 # GPLv2
pylint==3.3.1 # GPLv2
bandit[baseline]>=1.7.7 # Apache-2.0
setenv =
DJANGO_SETTINGS_MODULE=openstack_dashboard.test.settings