AAP-85334: Normalize formatting with ruff format

Pure formatting commit — no logic changes. Run `ruff format` across
awx/ and awxkit/ (awx_collection excluded).
This commit is contained in:
Dirk Julich
2026-08-20 18:35:23 +02:00
parent 4b73e1264b
commit f506ccab45
98 changed files with 457 additions and 433 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ class LoggedBasicAuthentication(authentication.BasicAuthentication):
ret = super(LoggedBasicAuthentication, self).authenticate(request)
if ret:
username = ret[0].username if ret[0] else '<none>'
logger.info(smart_str(u"User {} performed a {} to {} through the API".format(username, request.method, request.path)))
logger.info(smart_str("User {} performed a {} to {} through the API".format(username, request.method, request.path)))
return ret
def authenticate_header(self, request):
+2 -3
View File
@@ -81,7 +81,6 @@ analytics_logger = logging.getLogger('awx.analytics.performance')
class LoggedLoginView(auth_views.LoginView):
def get(self, request, *args, **kwargs):
if is_proxied_request():
next = request.GET.get('next', "")
@@ -116,7 +115,7 @@ class LoggedLoginView(auth_views.LoginView):
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
ip = get_remote_host(request) # request.META.get('REMOTE_ADDR', None)
if request.user.is_authenticated:
logger.info(smart_str(u"User {} logged in from {}".format(self.request.user.username, ip)))
logger.info(smart_str("User {} logged in from {}".format(self.request.user.username, ip)))
ret.set_cookie(
'userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False), samesite=getattr(settings, 'USER_COOKIE_SAMESITE', 'Lax')
)
@@ -125,7 +124,7 @@ class LoggedLoginView(auth_views.LoginView):
return ret
else:
if 'username' in self.request.POST:
logger.warning(smart_str(u"Login failed for user {} from {}".format(self.request.POST.get('username'), ip)))
logger.warning(smart_str("Login failed for user {} from {}".format(self.request.POST.get('username'), ip)))
ret.status_code = 401
return ret
+1 -2
View File
@@ -171,8 +171,7 @@ class Metadata(metadata.SimpleMetadata):
):
field_info['type'] = 'json'
elif (
isinstance(field, ManyRelatedField)
and field.field_name == 'credentials'
isinstance(field, ManyRelatedField) and field.field_name == 'credentials'
# launch-time credentials
):
field_info['type'] = 'list_of_ids'
+3 -3
View File
@@ -99,9 +99,9 @@ class CustomAutoSchema(AutoSchema):
except Exception:
serializer = None
warnings.warn(
'{}.get_serializer() raised an exception during '
'schema generation. Serializer fields will not be '
'generated for this view.'.format(self.view.__class__.__name__)
'{}.get_serializer() raised an exception during schema generation. Serializer fields will not be generated for this view.'.format(
self.view.__class__.__name__
)
)
if hasattr(self.view, 'swagger_topic'):
+5 -5
View File
@@ -1292,9 +1292,9 @@ class ProjectOptionsSerializer(BaseSerializer):
# Don't allow assigning a local_path when scm_type is set.
valid_local_paths = Project.get_local_path_choices()
if self.instance:
scm_type = attrs.get('scm_type', self.instance.scm_type) or u''
scm_type = attrs.get('scm_type', self.instance.scm_type) or ''
else:
scm_type = attrs.get('scm_type', u'') or u''
scm_type = attrs.get('scm_type', '') or ''
if self.instance and not scm_type:
valid_local_paths.append(self.instance.local_path)
if self.instance and scm_type and "local_path" in attrs and self.instance.local_path != attrs['local_path']:
@@ -1914,7 +1914,7 @@ class HostSerializer(BaseSerializerWithVariables):
if port < 1 or port > 65535:
raise ValueError
except ValueError:
raise serializers.ValidationError(_(u'Invalid port specification: %s') % force_str(port))
raise serializers.ValidationError(_('Invalid port specification: %s') % force_str(port))
return name, port
def validate_name(self, value):
@@ -3595,7 +3595,7 @@ class JobRelaunchSerializer(BaseSerializer):
res = super(JobRelaunchSerializer, self).to_representation(obj)
view = self.context.get('view', None)
if hasattr(view, '_raw_data_form_marker'):
password_keys = dict([(p, u'') for p in self.get_passwords_needed_to_start(obj)])
password_keys = dict([(p, '') for p in self.get_passwords_needed_to_start(obj)])
res.update(password_keys)
return res
@@ -3765,7 +3765,7 @@ class AdHocCommandRelaunchSerializer(AdHocCommandSerializer):
def to_representation(self, obj):
if obj:
return dict([(p, u'') for p in obj.passwords_needed_to_start])
return dict([(p, '') for p in obj.passwords_needed_to_start])
else:
return {}
+11 -7
View File
@@ -1768,11 +1768,15 @@ class CredentialExternalTest(OIDCCredentialTestMixin, SubDetailAPIView):
obj_permission_type = 'use'
resource_purpose = 'test external credential'
@extend_schema_if_available(extensions={"x-ai-description": """Test update the input values and metadata of an external credential.
@extend_schema_if_available(
extensions={
"x-ai-description": """Test update the input values and metadata of an external credential.
This endpoint supports testing credentials that connect to external secret management systems
such as CyberArk AIM, CyberArk Conjur, HashiCorp Vault, AWS Secrets Manager, Azure Key Vault,
Centrify Vault, Thycotic DevOps Secrets Vault, and GitHub App Installation Access Token Lookup.
It does not support standard credential types such as Machine, SCM, and Cloud."""})
It does not support standard credential types such as Machine, SCM, and Cloud."""
}
)
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.credential_type.kind != 'external':
@@ -2692,11 +2696,11 @@ class JobTemplateLaunch(RetrieveAPIView):
if needed_passwords:
data['credential_passwords'] = {}
for p in needed_passwords:
data['credential_passwords'][p] = u''
data['credential_passwords'][p] = ''
else:
data.pop('credential_passwords')
for v in obj.variables_needed_to_start:
extra_vars.setdefault(v, u'')
extra_vars.setdefault(v, '')
if extra_vars:
data['extra_vars'] = extra_vars
modified_ask_mapping = models.JobTemplate.get_ask_mapping()
@@ -3347,7 +3351,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su
'''
relationships = ['success_nodes', 'failure_nodes', 'always_nodes']
relationships.remove(self.relationship)
qs = functools.reduce(lambda x, y: (x | y), (Q(**{'{}__in'.format(r): [sub.id]}) for r in relationships))
qs = functools.reduce(lambda x, y: x | y, (Q(**{'{}__in'.format(r): [sub.id]}) for r in relationships))
if models.WorkflowJobTemplateNode.objects.filter(Q(pk=parent.id) & qs).exists():
return {"Error": _("Relationship not allowed.")}
@@ -3548,7 +3552,7 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView):
extra_vars = data.pop('extra_vars', None) or {}
if obj:
for v in obj.variables_needed_to_start:
extra_vars.setdefault(v, u'')
extra_vars.setdefault(v, '')
if extra_vars:
data['extra_vars'] = extra_vars
modified_ask_mapping = models.WorkflowJobTemplate.get_ask_mapping()
@@ -3958,7 +3962,7 @@ class JobRelaunch(RetrieveAPIView):
if needed_passwords:
data['credential_passwords'] = {}
for p in needed_passwords:
data['credential_passwords'][p] = u''
data['credential_passwords'][p] = ''
else:
data.pop('credential_passwords', None)
return data
+5 -5
View File
@@ -246,7 +246,7 @@ class ApiV2SubscriptionView(APIView):
elif isinstance(exc, (ValueError, OSError)) and exc.args:
msg = exc.args[0]
else:
logger.exception(smart_str(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
logger.exception(smart_str("Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
return Response(validated)
@@ -298,7 +298,7 @@ class ApiV2AttachView(APIView):
elif isinstance(exc, (ValueError, OSError)) and exc.args:
msg = exc.args[0]
else:
logger.exception(smart_str(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
logger.exception(smart_str("Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
for sub in validated:
@@ -381,7 +381,7 @@ class ApiV2ConfigView(APIView):
try:
data_actual = json.dumps(request.data)
except Exception:
logger.info(smart_str(u"Invalid JSON submitted for license."), extra=dict(actor=request.user.username))
logger.info(smart_str("Invalid JSON submitted for license."), extra=dict(actor=request.user.username))
return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST)
license_data = json.loads(data_actual)
@@ -406,7 +406,7 @@ class ApiV2ConfigView(APIView):
license_data_validated = get_licenser().license_from_manifest(license_data)
connection.on_commit(lambda: clear_setting_cache.delay(['LICENSE']))
except Exception:
logger.warning(smart_str(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
logger.warning(smart_str("Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
else:
license_data_validated = get_licenser().validate()
@@ -417,7 +417,7 @@ class ApiV2ConfigView(APIView):
settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host())
return Response(license_data_validated)
logger.warning(smart_str(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
logger.warning(smart_str("Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": _("Invalid subscription")}, status=status.HTTP_400_BAD_REQUEST)
@extend_schema_if_available(
+3 -3
View File
@@ -27,11 +27,11 @@ class Setting(CreatedModifiedModel):
json_value = json.dumps(self.value)
except ValueError:
# In the rare case the DB value is invalid JSON.
json_value = u'<Invalid JSON>'
json_value = '<Invalid JSON>'
if self.user:
return u'{} ({}) = {}'.format(self.key, self.user, json_value)
return '{} ({}) = {}'.format(self.key, self.user, json_value)
else:
return u'{} = {}'.format(self.key, json_value)
return '{} = {}'.format(self.key, json_value)
def save(self, *args, **kwargs):
encrypted = settings_registry.is_setting_encrypted(self.key)
+20 -13
View File
@@ -53,8 +53,9 @@ def test_non_admin_user_does_not_see_categories(api_request, dummy_setting, norm
@pytest.mark.django_db
def test_setting_singleton_detail_retrieve(api_request, dummy_setting):
with dummy_setting('FOO_BAR_1', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'), dummy_setting(
'FOO_BAR_2', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'
with (
dummy_setting('FOO_BAR_1', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'),
dummy_setting('FOO_BAR_2', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'),
):
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
assert response.status_code == 200
@@ -64,8 +65,9 @@ def test_setting_singleton_detail_retrieve(api_request, dummy_setting):
@pytest.mark.django_db
def test_setting_singleton_detail_invalid_retrieve(api_request, dummy_setting, normal_user):
with dummy_setting('FOO_BAR_1', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'), dummy_setting(
'FOO_BAR_2', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'
with (
dummy_setting('FOO_BAR_1', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'),
dummy_setting('FOO_BAR_2', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'),
):
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'barfoo'}))
assert response.status_code == 404
@@ -104,8 +106,9 @@ def test_setting_singleton_update(api_request, dummy_setting):
@pytest.mark.django_db
def test_setting_singleton_update_dont_change_readonly_fields(api_request, dummy_setting):
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, read_only=True, default=4, category='FooBar', category_slug='foobar'), mock.patch(
'awx.conf.views.clear_setting_cache'
with (
dummy_setting('FOO_BAR', field_class=fields.IntegerField, read_only=True, default=4, category='FooBar', category_slug='foobar'),
mock.patch('awx.conf.views.clear_setting_cache'),
):
api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 5})
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
@@ -114,8 +117,9 @@ def test_setting_singleton_update_dont_change_readonly_fields(api_request, dummy
@pytest.mark.django_db
def test_setting_singleton_update_dont_change_encrypted_mark(api_request, dummy_setting):
with dummy_setting('FOO_BAR', field_class=fields.CharField, encrypted=True, category='FooBar', category_slug='foobar'), mock.patch(
'awx.conf.views.clear_setting_cache'
with (
dummy_setting('FOO_BAR', field_class=fields.CharField, encrypted=True, category='FooBar', category_slug='foobar'),
mock.patch('awx.conf.views.clear_setting_cache'),
):
api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 'password'})
assert Setting.objects.get(key='FOO_BAR').value.startswith('$encrypted$')
@@ -132,9 +136,11 @@ def test_setting_singleton_update_runs_custom_validate(api_request, dummy_settin
def func_raising_exception(serializer, attrs):
raise serializers.ValidationError('Error')
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'), dummy_validate(
'foobar', func_raising_exception
), mock.patch('awx.conf.views.clear_setting_cache'):
with (
dummy_setting('FOO_BAR', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'),
dummy_validate('foobar', func_raising_exception),
mock.patch('awx.conf.views.clear_setting_cache'),
):
response = api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 23})
assert response.status_code == 400
@@ -149,8 +155,9 @@ def test_setting_singleton_delete(api_request, dummy_setting):
@pytest.mark.django_db
def test_setting_singleton_delete_no_read_only_fields(api_request, dummy_setting):
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, read_only=True, default=23, category='FooBar', category_slug='foobar'), mock.patch(
'awx.conf.views.clear_setting_cache'
with (
dummy_setting('FOO_BAR', field_class=fields.IntegerField, read_only=True, default=23, category='FooBar', category_slug='foobar'),
mock.patch('awx.conf.views.clear_setting_cache'),
):
api_request('delete', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
+2 -2
View File
@@ -227,7 +227,7 @@ def counts(since, **kwargs):
counts['pending_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').filter(status__in=('pending',)).count()
if connection.vendor == 'postgresql':
with connection.cursor() as cursor:
cursor.execute(f"select count(*) from pg_stat_activity where datname=\'{connection.settings_dict['NAME']}\'")
cursor.execute(f"select count(*) from pg_stat_activity where datname='{connection.settings_dict['NAME']}'")
counts['database_connections'] = cursor.fetchone()[0]
else:
# We should be using postgresql, but if we do that change that ever we should change the below value
@@ -441,7 +441,7 @@ def _events_table(since, full_path, until, tbl, where_column, project_job_create
WHERE ({tbl}.{where_column} > '{since.isoformat()}' AND {tbl}.{where_column} <= '{until.isoformat()}')) TO STDOUT WITH CSV HEADER'''
return query
return _copy_table(table='events', query=query(fr"replace({tbl}.event_data, '\u', '\u005cu')::jsonb"), path=full_path)
return _copy_table(table='events', query=query(rf"replace({tbl}.event_data, '\u', '\u005cu')::jsonb"), path=full_path)
@register('events_table', '1.5', format='csv', description=_('Automation task records'), expensive=four_hour_slicing)
+3 -1
View File
@@ -181,7 +181,9 @@ class HistogramM(BaseM):
node_label = f'node="{instance}"'
subsystem_label = f',subsystem="{namespace}"' if namespace else ''
for i, b in enumerate(self.buckets):
output_text += f'{self.field}_bucket{{le="{b}",{node_label}{subsystem_label}}} {sum(instance_data[instance][self.field]["counts"][0:i+1])}\n'
output_text += (
f'{self.field}_bucket{{le="{b}",{node_label}{subsystem_label}}} {sum(instance_data[instance][self.field]["counts"][0 : i + 1])}\n'
)
output_text += f'{self.field}_bucket{{le="+Inf",{node_label}{subsystem_label}}} {instance_data[instance][self.field]["inf"]}\n'
output_text += f'{self.field}_count{{{node_label}{subsystem_label}}} {instance_data[instance][self.field]["inf"]}\n'
output_text += f'{self.field}_sum{{{node_label}{subsystem_label}}} {instance_data[instance][self.field]["sum"]}\n'
+1 -2
View File
@@ -39,8 +39,7 @@ class MainConfig(AppConfig):
read_only=True,
label=_('List of all named url graph nodes.'),
help_text=_(
'Read-only list of key-value pairs that exposes named URL graph topology.'
' Use this list to programmatically generate named URLs for resources'
'Read-only list of key-value pairs that exposes named URL graph topology. Use this list to programmatically generate named URLs for resources'
),
category=_('Named URL'),
category_slug='named-url',
+2 -4
View File
@@ -488,7 +488,7 @@ register(
min_value=0,
label=_('Job Event Standard Output Maximum Display Size'),
help_text=_(
u'Maximum Size of Standard Output in bytes to display for a single job or ad hoc command event. `stdout` will end with `\u2026` when truncated.'
'Maximum Size of Standard Output in bytes to display for a single job or ad hoc command event. `stdout` will end with `\u2026` when truncated.'
),
category=_('Jobs'),
category_slug='jobs',
@@ -722,9 +722,7 @@ register(
default='https',
label=_('Logging Aggregator Protocol'),
help_text=_(
'Protocol used to communicate with log aggregator. '
'HTTPS/HTTP assumes HTTPS unless http:// is explicitly used in '
'the Logging Aggregator hostname.'
'Protocol used to communicate with log aggregator. HTTPS/HTTP assumes HTTPS unless http:// is explicitly used in the Logging Aggregator hostname.'
),
category=_('Logging'),
category_slug='logging',
-1
View File
@@ -20,7 +20,6 @@ def signame(sig):
class AWXConsumerRedis(object):
def __init__(self, name, worker):
self.name = name
self.pool = WorkerPool()
-1
View File
@@ -4,7 +4,6 @@ from django.db import connection
class AWXTaskWorker(TaskWorker):
def on_start(self) -> None:
"""Get worker connected so that first task it gets will be worked quickly"""
connection.ensure_connection()
@@ -20,7 +20,7 @@ class Command(BaseCommand):
"""
with connection.cursor() as cursor:
cursor.execute(
"SELECT pg_index.indisvalid " "FROM pg_class JOIN pg_index ON pg_class.oid = pg_index.indexrelid " "WHERE pg_class.relname = %s",
"SELECT pg_index.indisvalid FROM pg_class JOIN pg_index ON pg_class.oid = pg_index.indexrelid WHERE pg_class.relname = %s",
[self.INDEX_NAME],
)
row = cursor.fetchone()
@@ -52,7 +52,7 @@ class Command(BaseCommand):
# have set ALTER ROLE ... SET statement_timeout which would kill
# the index build on large tables.
cursor.execute("SET statement_timeout = 0")
cursor.execute(f"CREATE INDEX CONCURRENTLY {self.INDEX_NAME} " f"ON {self.TABLE_NAME} (host_id, id DESC)")
cursor.execute(f"CREATE INDEX CONCURRENTLY {self.INDEX_NAME} ON {self.TABLE_NAME} (host_id, id DESC)")
finally:
connection.connection.autocommit = old_autocommit
@@ -22,8 +22,8 @@ class Command(BaseCommand):
with tarfile.open(tar_filename, "w") as tar:
for cis in CustomInventoryScript.objects.all():
# naming convention similar to project paths
slug_name = slugify(str(cis.name)).replace(u'-', u'_')
script_filename = u'_%d__%s' % (int(cis.pk), slug_name)
slug_name = slugify(str(cis.name)).replace('-', '_')
script_filename = '_%d__%s' % (int(cis.pk), slug_name)
script_path = os.path.join(tmpdirname, script_filename)
with open(script_path, 'w') as f:
+3 -5
View File
@@ -43,9 +43,7 @@ class Command(BaseCommand):
'hosts_added',
'hosts_deleted',
'indirectly_managed_hosts',
).order_by(
'date'
)[offset : offset + limit]
).order_by('date')[offset : offset + limit]
)
return list_of_queryset
@@ -90,8 +88,8 @@ class Command(BaseCommand):
def csv_for_tar(self, temp_dir, type, filter_kwargs, rows_per_file, always_header=True):
for index, list_of_queryset in enumerate(self.paginated_db_retrieval(type, filter_kwargs, rows_per_file)):
csv_file = f'{temp_dir}/{type}{index+1}.csv'
arcname_file = f'{type}{index+1}.csv'
csv_file = f'{temp_dir}/{type}{index + 1}.csv'
arcname_file = f'{type}{index + 1}.csv'
first_write = True if index == 0 else False
@@ -114,9 +114,15 @@ class Command(BaseCommand):
parser.add_argument(
'--hostnames', dest='hostnames', type=str, help='Comma-Delimited Hosts to add to the Queue (will not remove already assigned instances)'
)
parser.add_argument(
'--instance_percent', dest='instance_percent', type=int, default=0, help='The percentage of active instances that will be assigned to this group'
),
(
parser.add_argument(
'--instance_percent',
dest='instance_percent',
type=int,
default=0,
help='The percentage of active instances that will be assigned to this group',
),
)
parser.add_argument(
'--instance_minimum',
dest='instance_minimum',
+1 -3
View File
@@ -42,9 +42,7 @@ def migrate_event_data(apps, schema_editor):
# create parent table
cursor.execute(
f'CREATE TABLE {tblname} '
f'(LIKE tmp_{tblname} INCLUDING ALL, job_created TIMESTAMP WITH TIME ZONE NOT NULL) '
f'PARTITION BY RANGE(job_created);'
f'CREATE TABLE {tblname} (LIKE tmp_{tblname} INCLUDING ALL, job_created TIMESTAMP WITH TIME ZONE NOT NULL) PARTITION BY RANGE(job_created);'
)
cursor.execute(f'DROP TABLE tmp_{tblname}')
@@ -5,7 +5,6 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0195_EE_permissions'),
]
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0196_indirect_managed_node_audit'),
]
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [('main', '0197_add_opa_query_path')]
operations = [
@@ -5,7 +5,6 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0198_alter_inventorysource_source_and_more'),
]
@@ -31,7 +31,6 @@ def rename_wfjt(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('main', '0199_inventorygroupvariableswithhistory_and_more'),
]
@@ -8,7 +8,6 @@ logger = logging.getLogger('awx.main.migrations')
class Migration(migrations.Migration):
dependencies = [
('main', '0202_convert_controller_role_definitions'),
]
@@ -37,7 +37,7 @@ class Migration(migrations.Migration):
# delete all sso application content permissions
# Added reverse_sql=migrations.RunSQL.noop to make this reversible for tests
migrations.RunSQL(
"DELETE FROM auth_permission " "WHERE content_type_id IN (SELECT id FROM django_content_type WHERE app_label = 'sso');",
"DELETE FROM auth_permission WHERE content_type_id IN (SELECT id FROM django_content_type WHERE app_label = 'sso');",
reverse_sql=migrations.RunSQL.noop,
),
# delete sso application content type
@@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0204_squashed_deletions'),
]
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0206_jobhostsummary_host_id_idx'),
]
+1 -1
View File
@@ -93,7 +93,7 @@ class ActivityStream(models.Model):
timestamp = self.timestamp
else:
timestamp = '_delayed_'
return u'%s-%s-pk=%s' % (operation, timestamp, self.pk)
return '%s-%s-pk=%s' % (operation, timestamp, self.pk)
def get_absolute_url(self, request=None):
return reverse('api:activity_stream_detail', kwargs={'pk': self.pk}, request=request)
+1 -1
View File
@@ -221,7 +221,7 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
self.preferred_instance_groups_cache = self._get_preferred_instance_group_cache()
add_to_update_fields("preferred_instance_groups_cache")
if not self.name:
self.name = Truncator(u': '.join(filter(None, (self.module_name, self.module_args)))).chars(512)
self.name = Truncator(': '.join(filter(None, (self.module_name, self.module_args)))).chars(512)
add_to_update_fields("name")
if self.task_impact == 0:
self.task_impact = self._get_task_impact()
+2 -2
View File
@@ -102,9 +102,9 @@ class BaseModel(models.Model):
def __str__(self):
if 'name' in self.__dict__:
return u'%s-%s' % (self.name, self.pk)
return '%s-%s' % (self.name, self.pk)
else:
return u'%s-%s' % (self._meta.verbose_name, self.pk)
return '%s-%s' % (self._meta.verbose_name, self.pk)
def clean_fields(self, exclude=None):
"""
+2 -2
View File
@@ -525,7 +525,7 @@ class JobEvent(BasePlaybookEvent):
return reverse('api:job_event_detail', kwargs={'pk': self.pk}, request=request)
def __str__(self):
return u'%s @ %s' % (self.get_event_display2(), self.created.isoformat())
return '%s @ %s' % (self.get_event_display2(), self.created.isoformat())
def _hostnames(self):
hostnames = set()
@@ -717,7 +717,7 @@ class BaseCommandEvent(CreatedModifiedModel):
)
def __str__(self):
return u'%s @ %s' % (self.get_event_display(), self.created.isoformat())
return '%s @ %s' % (self.get_event_display(), self.created.isoformat())
@classmethod
def create_from_data(cls, **kwargs):
+1 -1
View File
@@ -701,7 +701,7 @@ class WebhookMixin(models.Model):
if response.status_code < 400:
logger.debug("Webhook status update sent.")
else:
logger.error("Posting webhook status failed, code: {}\n" "{}\nPayload sent: {}".format(response.status_code, response.text, json.dumps(data)))
logger.error("Posting webhook status failed, code: {}\n{}\nPayload sent: {}".format(response.status_code, response.text, json.dumps(data)))
class OpaQueryPathMixin(models.Model):
+2 -2
View File
@@ -371,8 +371,8 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
self._skip_update = bool(kwargs.pop('skip_update', False))
# Create auto-generated local path if project uses SCM.
if self.pk and self.scm_type and not self.local_path.startswith('_'):
slug_name = slugify(str(self.name)).replace(u'-', u'_')
self.local_path = u'_%d__%s' % (int(self.pk), slug_name)
slug_name = slugify(str(self.name)).replace('-', '_')
self.local_path = '_%d__%s' % (int(self.pk), slug_name)
if 'local_path' not in update_fields:
update_fields.append('local_path')
# Do the actual save.
+12 -6
View File
@@ -180,9 +180,9 @@ class Role(models.Model):
def __str__(self):
if 'role_field' in self.__dict__:
return u'%s-%s' % (self.name, self.pk)
return '%s-%s' % (self.name, self.pk)
else:
return u'%s-%s' % (self._meta.verbose_name, self.pk)
return '%s-%s' % (self._meta.verbose_name, self.pk)
def save(self, *args, **kwargs):
super(Role, self).save(*args, **kwargs)
@@ -365,7 +365,8 @@ class Role(models.Model):
if len(removals) > 0:
for ids in split_ids_for_sqlite(removals):
sql_params['ids'] = ','.join(str(x) for x in ids)
cursor.execute('''
cursor.execute(
'''
DELETE FROM %(ancestors_table)s
WHERE descendent_id IN (%(ids)s)
AND descendent_id != ancestor_id
@@ -377,7 +378,9 @@ class Role(models.Model):
WHERE parents.from_role_id = %(ancestors_table)s.descendent_id
AND %(ancestors_table)s.ancestor_id = inner_ancestors.ancestor_id
)
''' % sql_params)
'''
% sql_params
)
delete_ct += cursor.rowcount
@@ -385,7 +388,8 @@ class Role(models.Model):
if len(additions) > 0:
for ids in split_ids_for_sqlite(additions):
sql_params['ids'] = ','.join(str(x) for x in ids)
cursor.execute('''
cursor.execute(
'''
INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id)
SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM (
SELECT roles.id from_id,
@@ -415,7 +419,9 @@ class Role(models.Model):
AND %(ancestors_table)s.ancestor_id = new_ancestry_list.to_id
)
''' % sql_params)
'''
% sql_params
)
insert_ct += cursor.rowcount
if insert_ct == 0 and delete_ct == 0:
+1 -1
View File
@@ -276,7 +276,7 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
return rruleset
def __str__(self):
return u'%s_t%s_%s_%s' % (self.name, self.unified_job_template.id, self.id, self.next_run)
return '%s_t%s_%s_%s' % (self.name, self.unified_job_template.id, self.id, self.next_run)
def get_absolute_url(self, request=None):
return reverse('api:schedule_detail', kwargs={'pk': self.pk}, request=request)
+3 -3
View File
@@ -836,7 +836,7 @@ class UnifiedJob(
return True
def __str__(self):
return u'%s-%s-%s' % (self.created, self.id, self.status)
return '%s-%s-%s' % (self.created, self.id, self.status)
@property
def log_format(self):
@@ -1416,14 +1416,14 @@ class UnifiedJob(
if not all(opts.values()):
missing_fields = ', '.join([k for k, v in opts.items() if not v])
self.job_explanation = u'Missing needed fields: %s.' % missing_fields
self.job_explanation = 'Missing needed fields: %s.' % missing_fields
self.save(update_fields=['job_explanation'])
return opts
def pre_start(self):
if not self.can_start:
self.job_explanation = u'%s is not in a startable state: %s, expecting one of %s' % (self._meta.verbose_name, self.status, str(('new', 'waiting')))
self.job_explanation = '%s is not in a startable state: %s, expecting one of %s' % (self._meta.verbose_name, self.status, str(('new', 'waiting')))
self.save(update_fields=['job_explanation'])
return (False, None)
+1 -1
View File
@@ -93,7 +93,7 @@ class WebhookBackend(AWXBaseEmailBackend, CustomNotificationBase):
url_next_log_safe = base64.b64encode(url_next.encode('UTF-8')) if url_next else b'None'
# we've hit a redirect. extract the redirect URL out of the first response header and try again
logger.warning(f"Received a {resp.status_code} from {url_log_safe}, trying to reach redirect url {url_next_log_safe}; attempt #{retries+1}")
logger.warning(f"Received a {resp.status_code} from {url_log_safe}, trying to reach redirect url {url_next_log_safe}; attempt #{retries + 1}")
# take the first redirect URL in the response header and try that
url = url_next
+2 -3
View File
@@ -256,8 +256,7 @@ class WorkflowManager(TaskBase):
)
display_list = [spawn_node.unified_job_template] + workflow_ancestors
job.job_explanation = gettext_noop(
"Workflow Job spawned from workflow could not start because it "
"would result in recursion (spawn order, most recent first: {})"
"Workflow Job spawned from workflow could not start because it would result in recursion (spawn order, most recent first: {})"
).format(', '.join('<{}>'.format(tmp) for tmp in display_list))
else:
logger.debug(
@@ -344,7 +343,7 @@ class DependencyManager(TaskBase):
if update.status in ['waiting', 'pending', 'running']:
return False
return bool(((update.finished + timedelta(seconds=cache_timeout))) < tz_now())
return bool((update.finished + timedelta(seconds=cache_timeout)) < tz_now())
def get_or_create_project_update(self, project_id):
project = self.all_projects.get(project_id, None)
+1 -1
View File
@@ -563,7 +563,7 @@ class BaseTask(object):
project.save()
lock_path = project.get_lock_file()
if lock_path is None:
raise RuntimeError(u'Invalid lock file path')
raise RuntimeError('Invalid lock file path')
try:
self.lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT)
+2 -2
View File
@@ -180,7 +180,7 @@ def find_node_in_mesh(node_name, receptor_ctl):
if node.get('NodeID') == node_name:
return node
else:
logger.warning(f"Instance {node_name} is not in the receptor mesh. {attempts-attempt} attempts left.")
logger.warning(f"Instance {node_name} is not in the receptor mesh. {attempts - attempt} attempts left.")
time.sleep(backoff)
backoff += 1
else:
@@ -848,7 +848,7 @@ def reload_receptor():
receptor_ctl.simple_command("reload")
break
except ValueError:
logger.warning(f"Unable to reload Receptor configuration. {attempts-backoff} attempts left.")
logger.warning(f"Unable to reload Receptor configuration. {attempts - backoff} attempts left.")
time.sleep(backoff)
else:
raise RuntimeError("Receptor reload failed")
+5 -4
View File
@@ -79,7 +79,7 @@ from awx.main.utils.reload import stop_local_services
logger = logging.getLogger('awx.main.tasks.system')
OPENSSH_KEY_ERROR = u'''\
OPENSSH_KEY_ERROR = '''\
It looks like you're trying to use a private key in OpenSSH format, which \
isn't supported by the installed version of OpenSSH on this instance. \
Try upgrading OpenSSH or providing your private key in an different format. \
@@ -690,7 +690,6 @@ def _get_active_task_ids_from_dispatcherd(binder):
"""
active_task_ids = []
try:
logger.debug("Querying dispatcherd API for running tasks")
data = binder.control('running')
@@ -934,8 +933,10 @@ def awx_periodic_scheduler():
continue
if not can_start:
new_unified_job.status = 'failed'
new_unified_job.job_explanation = gettext_noop("Scheduled job could not start because it \
was not in the right state or required manual credentials")
new_unified_job.job_explanation = gettext_noop(
"Scheduled job could not start because it \
was not in the right state or required manual credentials"
)
new_unified_job.save(update_fields=['status', 'job_explanation'])
new_unified_job.websocket_emit_status("failed")
emit_channel_notification('schedules-changed', dict(id=schedule.id, group_name="schedules"))
+1 -3
View File
@@ -4,9 +4,7 @@ from django import template
register = template.Library()
CONSONANT_SOUND = re.compile(r'''one(![ir])''', re.IGNORECASE | re.VERBOSE) # noqa
VOWEL_SOUND = re.compile(
r'''[aeio]|u([aeiou]|[^n][^aeiou]|ni[^dmnl]|nil[^l])|h(ier|onest|onou?r|ors\b|our(!i))|[fhlmnrsx]\b''', re.IGNORECASE | re.VERBOSE
) # noqa
VOWEL_SOUND = re.compile(r'''[aeio]|u([aeiou]|[^n][^aeiou]|ni[^dmnl]|nil[^l])|h(ier|onest|onou?r|ors\b|our(!i))|[fhlmnrsx]\b''', re.IGNORECASE | re.VERBOSE) # noqa
@register.filter
@@ -166,6 +166,6 @@ def test_copy_tables_workflow_job_node_query(sqlite_copy, workflow_job):
for index, relationship in zip([7, 8, 9], ["success_nodes", "failure_nodes", "always_nodes"]):
for i, l in enumerate(lines):
related_nodes = [int(e) for e in l[index].split(",")] if l[index] else []
assert related_nodes == list(
getattr(workflow_job.workflow_nodes.all()[i], relationship).all().values_list("id", flat=True)
), f"(right side) workflow_nodes.all()[{i}].{relationship}.all()"
assert related_nodes == list(getattr(workflow_job.workflow_nodes.all()[i], relationship).all().values_list("id", flat=True)), (
f"(right side) workflow_nodes.all()[{i}].{relationship}.all()"
)
@@ -233,9 +233,10 @@ class TestAnalyticsGenericView:
request = RequestFactory().post('/some/path')
view = AnalyticsGenericView()
with mock.patch('awx.api.views.analytics.OIDCClient') as mock_oidc_client, mock.patch(
'awx.api.views.analytics.AnalyticsGenericView._base_auth_request'
) as mock_base_auth_request:
with (
mock.patch('awx.api.views.analytics.OIDCClient') as mock_oidc_client,
mock.patch('awx.api.views.analytics.AnalyticsGenericView._base_auth_request') as mock_base_auth_request,
):
# Configure the mock OIDCClient instance and its make_request method
mock_client_instance = mock.Mock()
mock_oidc_client.return_value = mock_client_instance
@@ -930,7 +930,7 @@ def test_field_removal(put, organization, admin, credentialtype_ssh):
},
}
cred = Credential(
credential_type=credentialtype_ssh, name='Best credential ever', organization=organization, inputs={'username': u'jim', 'password': u'secret'}
credential_type=credentialtype_ssh, name='Best credential ever', organization=organization, inputs={'username': 'jim', 'password': 'secret'}
)
cred.save()
@@ -957,7 +957,7 @@ def test_field_removal(put, organization, admin, credentialtype_ssh):
)
def test_credential_type_mutability(patch, organization, admin, credentialtype_ssh, credentialtype_aws, relation, related_obj):
cred = Credential(
credential_type=credentialtype_ssh, name='Best credential ever', organization=organization, inputs={'username': u'jim', 'password': u'pass'}
credential_type=credentialtype_ssh, name='Best credential ever', organization=organization, inputs={'username': 'jim', 'password': 'pass'}
)
cred.save()
@@ -967,7 +967,7 @@ def test_credential_type_mutability(patch, organization, admin, credentialtype_s
def _change_credential_type():
return patch(
reverse('api:credential_detail', kwargs={'pk': cred.pk}),
{'credential_type': credentialtype_aws.pk, 'inputs': {'username': u'jim', 'password': u'pass'}},
{'credential_type': credentialtype_aws.pk, 'inputs': {'username': 'jim', 'password': 'pass'}},
admin,
)
@@ -992,7 +992,7 @@ def test_vault_credential_type_mutability(patch, organization, admin, credential
name='Best credential ever',
organization=organization,
inputs={
'vault_password': u'some-vault',
'vault_password': 'some-vault',
},
)
cred.save()
@@ -1004,7 +1004,7 @@ def test_vault_credential_type_mutability(patch, organization, admin, credential
def _change_credential_type():
return patch(
reverse('api:credential_detail', kwargs={'pk': cred.pk}),
{'credential_type': credentialtype_ssh.pk, 'inputs': {'username': u'jim', 'password': u'pass'}},
{'credential_type': credentialtype_ssh.pk, 'inputs': {'username': 'jim', 'password': 'pass'}},
admin,
)
@@ -1025,7 +1025,7 @@ def test_vault_credential_type_mutability(patch, organization, admin, credential
@pytest.mark.django_db
def test_cloud_credential_type_mutability(patch, organization, admin, credentialtype_ssh, credentialtype_aws):
cred = Credential(
credential_type=credentialtype_aws, name='Best credential ever', organization=organization, inputs={'username': u'jim', 'password': u'pass'}
credential_type=credentialtype_aws, name='Best credential ever', organization=organization, inputs={'username': 'jim', 'password': 'pass'}
)
cred.save()
@@ -1036,7 +1036,7 @@ def test_cloud_credential_type_mutability(patch, organization, admin, credential
def _change_credential_type():
return patch(
reverse('api:credential_detail', kwargs={'pk': cred.pk}),
{'credential_type': credentialtype_ssh.pk, 'inputs': {'username': u'jim', 'password': u'pass'}},
{'credential_type': credentialtype_ssh.pk, 'inputs': {'username': 'jim', 'password': 'pass'}},
admin,
)
@@ -1084,7 +1084,7 @@ def test_ssh_unlock_needed(put, organization, admin, credentialtype_ssh):
credential_type=credentialtype_ssh,
name='Best credential ever',
organization=organization,
inputs={'username': u'joe', 'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY, 'ssh_key_unlock': 'unlock'},
inputs={'username': 'joe', 'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY, 'ssh_key_unlock': 'unlock'},
)
cred.save()
@@ -1110,7 +1110,7 @@ def test_ssh_unlock_not_needed(put, organization, admin, credentialtype_ssh):
name='Best credential ever',
organization=organization,
inputs={
'username': u'joe',
'username': 'joe',
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
},
)
@@ -1137,7 +1137,7 @@ def test_ssh_unlock_with_prior_value(put, organization, admin, credentialtype_ss
credential_type=credentialtype_ssh,
name='Best credential ever',
organization=organization,
inputs={'username': u'joe', 'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY, 'ssh_key_unlock': 'old-unlock'},
inputs={'username': 'joe', 'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY, 'ssh_key_unlock': 'old-unlock'},
)
cred.save()
@@ -1165,7 +1165,7 @@ def test_ssh_bad_key_unlock_not_checked(put, organization, admin, credentialtype
name='Best credential ever',
organization=organization,
inputs={
'username': u'oscar',
'username': 'oscar',
'ssh_key_data': 'invalid-key',
'ssh_key_unlock': 'unchecked-unlock',
},
@@ -1251,7 +1251,7 @@ def test_secret_encryption_previous_value(patch, organization, admin, credential
}
}
cred = Credential(
credential_type=credentialtype_ssh, name='Best credential ever', organization=organization, inputs={'username': u'jim', 'password': u'secret'}
credential_type=credentialtype_ssh, name='Best credential ever', organization=organization, inputs={'username': 'jim', 'password': 'secret'}
)
cred.save()
@@ -102,7 +102,7 @@ def test_delete_instance_group_jobs_running(delete, instance_group_jobs_running,
response_sorted = sorted(response.data['active_jobs'], key=sort_keys)
expect_sorted = sorted(expect_transformed, key=sort_keys)
assert response.data['error'] == u"Resource is being used by running jobs."
assert response.data['error'] == "Resource is being used by running jobs."
assert response_sorted == expect_sorted
@@ -285,12 +285,12 @@ def test_urlencode_host_filter(post, admin_user, organization):
def test_host_filter_unicode(post, admin_user, organization):
post(
reverse('api:inventory_list'),
data={'name': 'smart inventory', 'kind': 'smart', 'organization': organization.pk, 'host_filter': u'ansible_facts__ansible_distribution=レッドハット'},
data={'name': 'smart inventory', 'kind': 'smart', 'organization': organization.pk, 'host_filter': 'ansible_facts__ansible_distribution=レッドハット'},
user=admin_user,
expect=201,
)
si = Inventory.objects.get(name='smart inventory')
assert si.host_filter == u'ansible_facts__ansible_distribution=レッドハット'
assert si.host_filter == 'ansible_facts__ansible_distribution=レッドハット'
@pytest.mark.django_db
@@ -302,7 +302,7 @@ def test_host_filter_invalid_ansible_facts_lookup(post, admin_user, organization
'name': 'smart inventory',
'kind': 'smart',
'organization': organization.pk,
'host_filter': u'ansible_facts__ansible_distribution__{}=cent'.format(lookup),
'host_filter': 'ansible_facts__ansible_distribution__{}=cent'.format(lookup),
},
user=admin_user,
expect=400,
@@ -417,7 +417,7 @@ def test_inventory_source_vars_prohibition(post, inventory, admin_user):
mock_settings.INV_ENV_VARIABLE_BLOCKED = ('FOOBAR',)
r = post(
reverse('api:inventory_source_list'),
{'name': 'new inv src', 'source_vars': '{\"FOOBAR\": \"val\"}', 'inventory': inventory.pk},
{'name': 'new inv src', 'source_vars': '{"FOOBAR": "val"}', 'inventory': inventory.pk},
admin_user,
expect=400,
)
@@ -729,7 +729,6 @@ class TestConstructedInventory:
@pytest.mark.django_db
class TestInventoryAllVariables:
@staticmethod
def simulate_update_from_source(inv_src, variables_dict, overwrite_vars=True):
"""
@@ -278,9 +278,9 @@ def test_job_reject_invalid_prompted_vars(runtime_data, job_template_prompts, po
expect=400,
)
assert response.data['job_type'] == [u'"foobicate" is not a valid choice.']
assert response.data['inventory'] == [u'Invalid pk "87865" - object does not exist.']
assert response.data['credentials'] == [u'Invalid pk "48474" - object does not exist.']
assert response.data['job_type'] == ['"foobicate" is not a valid choice.']
assert response.data['inventory'] == ['Invalid pk "87865" - object does not exist.']
assert response.data['credentials'] == ['Invalid pk "48474" - object does not exist.']
@pytest.mark.django_db
@@ -314,7 +314,7 @@ def test_job_launch_fails_without_inventory_access(job_template_prompts, runtime
# Assure that giving an inventory without access to the inventory blocks the launch
response = post(reverse('api:job_template_launch', kwargs={'pk': job_template.pk}), dict(inventory=runtime_data['inventory']), rando, expect=403)
assert response.data['detail'] == u'You do not have permission to perform this action.'
assert response.data['detail'] == 'You do not have permission to perform this action.'
@pytest.mark.django_db
@@ -540,15 +540,15 @@ def test_job_launch_fails_with_missing_multivault_password(machine_credential, v
assert {
'credential_type': vault_cred_first.credential_type_id,
'passwords_needed': ['vault_password.abc'],
'vault_id': u'abc',
'name': u'Vault #1',
'vault_id': 'abc',
'name': 'Vault #1',
'id': vault_cred_first.id,
} in resp.data['defaults']['credentials']
assert {
'credential_type': vault_cred_second.credential_type_id,
'passwords_needed': ['vault_password.xyz'],
'vault_id': u'xyz',
'name': u'Vault #2',
'vault_id': 'xyz',
'name': 'Vault #2',
'id': vault_cred_second.id,
} in resp.data['defaults']['credentials']
@@ -13,10 +13,12 @@ class TestLicenseCacheClearing:
"""Test that posting a manifest to /api/v2/config/ clears the LICENSE cache"""
# Mock the licenser and clear_setting_cache
with patch('awx.api.views.root.get_licenser') as mock_get_licenser, patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate, patch(
'awx.api.views.root.clear_setting_cache'
) as mock_clear_cache, patch('django.db.connection.on_commit') as mock_on_commit:
with (
patch('awx.api.views.root.get_licenser') as mock_get_licenser,
patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate,
patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache,
patch('django.db.connection.on_commit') as mock_on_commit,
):
# Set up mock license data
mock_license_data = {'valid_key': True, 'license_type': 'enterprise', 'instance_count': 100, 'subscription_name': 'Test Enterprise License'}
@@ -54,7 +56,6 @@ class TestLicenseCacheClearing:
"""Test that DELETE /api/v2/config/ clears the LICENSE cache"""
with patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache, patch('django.db.connection.on_commit') as mock_on_commit:
# Make the DELETE request
url = reverse('api:api_v2_config_view')
delete(url, admin_user, expect=204)
@@ -72,10 +73,12 @@ class TestLicenseCacheClearing:
def test_attach_view_clears_cache(self, admin_user, post):
"""Test that posting to /api/v2/config/attach/ clears the LICENSE cache"""
with patch('awx.api.views.root.get_licenser') as mock_get_licenser, patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache, patch(
'django.db.connection.on_commit'
) as mock_on_commit, patch('awx.api.views.root.settings') as mock_settings:
with (
patch('awx.api.views.root.get_licenser') as mock_get_licenser,
patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache,
patch('django.db.connection.on_commit') as mock_on_commit,
patch('awx.api.views.root.settings') as mock_settings,
):
# Set up subscription credentials in settings
mock_settings.SUBSCRIPTIONS_CLIENT_ID = 'test-client-id'
mock_settings.SUBSCRIPTIONS_CLIENT_SECRET = 'test-client-secret'
@@ -113,10 +116,11 @@ class TestLicenseCacheClearing:
def test_attach_view_subscription_not_found_no_cache_clear(self, admin_user, post):
"""Test that attach view doesn't clear cache when subscription is not found"""
with patch('awx.api.views.root.get_licenser') as mock_get_licenser, patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache, patch(
'django.db.connection.on_commit'
) as mock_on_commit:
with (
patch('awx.api.views.root.get_licenser') as mock_get_licenser,
patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache,
patch('django.db.connection.on_commit') as mock_on_commit,
):
# Set up mock licenser with different subscription
mock_licenser = MagicMock()
subscription_data = {'subscription_id': 'different-subscription-456', 'valid_key': False, 'license_type': 'enterprise'} # Different ID
@@ -142,10 +146,11 @@ class TestLicenseCacheClearing:
def test_manifest_validation_error_no_cache_clear(self, admin_user, post):
"""Test that config view doesn't clear cache when manifest validation fails"""
with patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate, patch(
'awx.api.views.root.clear_setting_cache'
) as mock_clear_cache, patch('django.db.connection.on_commit') as mock_on_commit:
with (
patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate,
patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache,
patch('django.db.connection.on_commit') as mock_on_commit,
):
# Mock validation to raise ValueError
mock_validate.side_effect = ValueError("Invalid manifest")
@@ -166,10 +171,12 @@ class TestLicenseCacheClearing:
def test_license_processing_error_no_cache_clear(self, admin_user, post):
"""Test that config view doesn't clear cache when license processing fails"""
with patch('awx.api.views.root.get_licenser') as mock_get_licenser, patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate, patch(
'awx.api.views.root.clear_setting_cache'
) as mock_clear_cache, patch('django.db.connection.on_commit') as mock_on_commit:
with (
patch('awx.api.views.root.get_licenser') as mock_get_licenser,
patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate,
patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache,
patch('django.db.connection.on_commit') as mock_on_commit,
):
# Mock validation to succeed but license processing to fail
mock_validate.return_value = [{'some': 'manifest_data'}]
mock_licenser = MagicMock()
@@ -62,10 +62,11 @@ def oidc_credential(oidc_credentialtype):
@pytest.fixture
def mock_oidc_backend():
"""Fixture that mocks OIDC JWT generation and credential backend."""
with mock.patch('awx.api.views.retrieve_workload_identity_jwt_with_claims') as mock_jwt, mock.patch('awx.api.views._jwt_decode') as mock_decode, mock.patch(
'awx.main.models.credential.CredentialType.plugin', new_callable=mock.PropertyMock
) as mock_plugin:
with (
mock.patch('awx.api.views.retrieve_workload_identity_jwt_with_claims') as mock_jwt,
mock.patch('awx.api.views._jwt_decode') as mock_decode,
mock.patch('awx.main.models.credential.CredentialType.plugin', new_callable=mock.PropertyMock) as mock_plugin,
):
# Set default return values
mock_jwt.return_value = 'fake.jwt.token'
mock_decode.return_value = {'iss': 'http://gateway/o', 'aud': 'vault'}
@@ -272,7 +272,7 @@ def test_organization_delete_with_active_jobs(delete, admin, organization, organ
resp_sorted = sorted(resp.data['active_jobs'], key=sort_keys)
expect_sorted = sorted(expect_transformed, key=sort_keys)
assert resp.data['error'] == u"Resource is being used by running jobs."
assert resp.data['error'] == "Resource is being used by running jobs."
assert resp_sorted == expect_sorted
@@ -150,13 +150,13 @@ def test_host_filter_unicode(post, admin_user, smart_inv_org):
'name': 'unicode-smart',
'kind': 'smart',
'organization': smart_inv_org.pk,
'host_filter': u'ansible_facts__ansible_distribution=レッドハット',
'host_filter': 'ansible_facts__ansible_distribution=レッドハット',
},
user=admin_user,
expect=201,
)
si = Inventory.objects.get(name='unicode-smart')
assert si.host_filter == u'ansible_facts__ansible_distribution=レッドハット'
assert si.host_filter == 'ansible_facts__ansible_distribution=レッドハット'
@pytest.mark.django_db
@@ -168,7 +168,7 @@ def test_host_filter_invalid_ansible_facts_lookup(post, admin_user, smart_inv_or
'name': 'invalid-lookup-smart',
'kind': 'smart',
'organization': smart_inv_org.pk,
'host_filter': u'ansible_facts__ansible_distribution__{}=cent'.format(lookup),
'host_filter': 'ansible_facts__ansible_distribution__{}=cent'.format(lookup),
},
user=admin_user,
expect=400,
@@ -168,8 +168,9 @@ def test_text_stdout_with_max_stdout(sqlite_copy, get, admin):
url = reverse('api:system_job_detail', kwargs={'pk': job.pk})
response = get(url, user=admin, expect=200)
assert response.data['result_stdout'] == (
'Standard Output too large to display ({actual} bytes), only download '
'supported for sizes over {max} bytes.'.format(actual=total_bytes, max=settings.STDOUT_MAX_BYTES_DISPLAY)
'Standard Output too large to display ({actual} bytes), only download supported for sizes over {max} bytes.'.format(
actual=total_bytes, max=settings.STDOUT_MAX_BYTES_DISPLAY
)
)
@@ -196,8 +197,9 @@ def test_max_bytes_display(sqlite_copy, Parent, Child, relation, view, fmt, get,
response = get(url + '?format={}'.format(fmt), user=admin, expect=200)
assert smart_str(response.content) == (
'Standard Output too large to display ({actual} bytes), only download '
'supported for sizes over {max} bytes.'.format(actual=total_bytes, max=settings.STDOUT_MAX_BYTES_DISPLAY)
'Standard Output too large to display ({actual} bytes), only download supported for sizes over {max} bytes.'.format(
actual=total_bytes, max=settings.STDOUT_MAX_BYTES_DISPLAY
)
)
response = get(url + '?format={}_download'.format(fmt), user=admin, expect=200)
@@ -236,8 +238,9 @@ def test_legacy_result_stdout_with_max_bytes(Cls, view, fmt, get, admin):
response = get(url + '?format={}'.format(fmt), user=admin, expect=200)
assert smart_str(response.content) == (
'Standard Output too large to display ({actual} bytes), only download '
'supported for sizes over {max} bytes.'.format(actual=total_bytes, max=settings.STDOUT_MAX_BYTES_DISPLAY)
'Standard Output too large to display ({actual} bytes), only download supported for sizes over {max} bytes.'.format(
actual=total_bytes, max=settings.STDOUT_MAX_BYTES_DISPLAY
)
)
response = get(url + '?format={}'.format(fmt + '_download'), user=admin, expect=200)
@@ -259,7 +262,7 @@ def test_text_with_unicode_stdout(sqlite_copy, Parent, Child, relation, view, ge
job = Parent()
job.save()
for i in range(3):
Child(**{relation: job, 'stdout': u'オ{}\n'.format(i), 'start_line': i}).save()
Child(**{relation: job, 'stdout': 'オ{}\n'.format(i), 'start_line': i}).save()
url = reverse(view, kwargs={'pk': job.pk}) + '?format=' + fmt
response = get(url, user=admin, expect=200)
@@ -126,7 +126,7 @@ class TestKeyRegeneration:
name='Example Template',
survey_spec=survey_spec_factory([{'variable': 'secret_key', 'default': encrypt_value('donttell', pk=None), 'type': 'password'}]),
survey_enabled=True,
**params
**params,
)
job = jt.create_unified_job()
assert jt.survey_spec['spec'][0]['default'].startswith(PREFIX)
@@ -15,7 +15,6 @@ from awx.main.models import Organization, Team
@pytest.mark.django_db
class TestClaimsOldRbacSync:
def _build_claims(self, orgs, teams):
"""Build a claims dict from org/team model instances."""
objects = {"organization": [], "team": []}
@@ -85,12 +85,12 @@ def test_consolidate_indirect_user_roles_with_nested_teams(setup_managed_roles,
assert team_g_users_after == set(team_g.member_role.members.all().values_list('id', flat=True))
# Verify team-to-team relationships are removed after consolidation
assert not RoleTeamAssignment.objects.filter(
role_definition=team_member_role, team=team_f, object_id=team_e.id
).exists(), "Team-to-team relationship F→E should be removed"
assert not RoleTeamAssignment.objects.filter(
role_definition=team_member_role, team=team_g, object_id=team_f.id
).exists(), "Team-to-team relationship G→F should be removed"
assert not RoleTeamAssignment.objects.filter(role_definition=team_member_role, team=team_f, object_id=team_e.id).exists(), (
"Team-to-team relationship F→E should be removed"
)
assert not RoleTeamAssignment.objects.filter(role_definition=team_member_role, team=team_g, object_id=team_f.id).exists(), (
"Team-to-team relationship G→F should be removed"
)
@pytest.mark.django_db
@@ -28,7 +28,7 @@ class TestHostManagerDeferral:
"""ansible_facts should be in the deferred set for default Host queries."""
qs = Host.objects.all()
deferred = qs.query.deferred_loading[0]
assert 'ansible_facts' in deferred, f'ansible_facts should be deferred by the HostManager. ' f'Deferred fields: {deferred}'
assert 'ansible_facts' in deferred, f'ansible_facts should be deferred by the HostManager. Deferred fields: {deferred}'
def test_ansible_facts_accessible_when_needed(self, inventory):
"""Deferred fields are still accessible — Django fetches on access."""
@@ -62,9 +62,9 @@ def test_role_migration_matches(request, model, setup_managed_roles):
rd_data = {}
for rd in model_rds:
rd_data[rd.name] = list(rd.permissions.values_list('codename', flat=True))
assert (
'Compat' not in rd.name
), f'Permissions for old vs new roles did not match.\nold {field.name}: {old_codenames}\nnew:\n{json.dumps(rd_data, indent=2)}'
assert 'Compat' not in rd.name, (
f'Permissions for old vs new roles did not match.\nold {field.name}: {old_codenames}\nnew:\n{json.dumps(rd_data, indent=2)}'
)
assert new_codenames == set(old_codenames)
# In the old system these models did not have object-level roles, all others expect some model roles
@@ -135,7 +135,7 @@ def test_unified_job_list_team_member_sees_team_granted_jobs(user, setup_managed
response = get(reverse('api:unified_job_list'), team_user)
assert response.status_code == 200
result_ids = [r['id'] for r in response.data['results']]
assert job.pk in result_ids, f"Team member should see job {job.pk} via team-granted JT execute permission, " f"but got result IDs: {result_ids}"
assert job.pk in result_ids, f"Team member should see job {job.pk} via team-granted JT execute permission, but got result IDs: {result_ids}"
@pytest.mark.django_db
@@ -171,7 +171,7 @@ class TestFinishFactCacheConcurrentProtection:
)
host.refresh_from_db()
assert host.ansible_facts == {}, 'Stale facts should have been cleared when the fact file is missing ' 'and ansible_facts_modified predates job_created'
assert host.ansible_facts == {}, 'Stale facts should have been cleared when the fact file is missing and ansible_facts_modified predates job_created'
@pytest.mark.django_db
@@ -230,9 +230,9 @@ def test_inventory_update_injected_content(product_name, this_kind, inventory, f
# Assert inventory plugin inventory file is in private_data_dir
inventory_filename = InventorySource.injectors[inventory_update.source]().filename
assert (
len([True for k in content.keys() if k.endswith(inventory_filename)]) > 0
), f"'{inventory_filename}' file not found in inventory update runtime files {content.keys()}"
assert len([True for k in content.keys() if k.endswith(inventory_filename)]) > 0, (
f"'{inventory_filename}' file not found in inventory update runtime files {content.keys()}"
)
env.pop('ANSIBLE_COLLECTIONS_PATH', None)
base_dir = os.path.join(DATA, 'plugins')
+24 -24
View File
@@ -164,15 +164,15 @@ class TestMigrationSmoke:
CredentialType = new_state.apps.get_model('main', 'CredentialType')
for expected_name in validate_exists:
assert CredentialType.objects.filter(
name=expected_name
).exists(), f'Could not find {expected_name} credential type name, all names: {list(CredentialType.objects.values_list("name", flat=True))}'
assert CredentialType.objects.filter(name=expected_name).exists(), (
f'Could not find {expected_name} credential type name, all names: {list(CredentialType.objects.values_list("name", flat=True))}'
)
# Verify the system_administrator role exists
Role = new_state.apps.get_model('main', 'Role')
assert Role.objects.filter(
singleton_name='system_administrator', role_field='system_administrator'
).exists(), "expected to find a system_administrator singleton role"
assert Role.objects.filter(singleton_name='system_administrator', role_field='system_administrator').exists(), (
"expected to find a system_administrator singleton role"
)
@pytest.mark.django_db
@@ -215,15 +215,15 @@ class TestGithubAppBug:
CredentialType = new_state.apps.get_model('main', 'CredentialType') # Get CredentialType model from the new state
# Assertion 1: The CredentialType with the old 'github_app' kind should no longer exist.
assert not CredentialType.objects.filter(
namespace='github_app'
).exists(), "CredentialType with old 'github_app' kind should no longer exist after migration."
assert not CredentialType.objects.filter(namespace='github_app').exists(), (
"CredentialType with old 'github_app' kind should no longer exist after migration."
)
# Assertion 2: The CredentialType should now exist with the new 'github_app_lookup' kind
# and retain its original name.
assert CredentialType.objects.filter(
namespace='github_app_lookup', name='Legacy GitHub App Credential'
).exists(), "CredentialType should be updated to 'github_app_lookup' and retain its name."
assert CredentialType.objects.filter(namespace='github_app_lookup', name='Legacy GitHub App Credential').exists(), (
"CredentialType should be updated to 'github_app_lookup' and retain its name."
)
@pytest.mark.django_db
@@ -245,9 +245,9 @@ class TestSystemAuditorMigration:
new_state = migrator.apply_tested_migration(('main', '0192_custom_roles'))
RoleUserAssignment = new_state.apps.get_model('dab_rbac', 'RoleUserAssignment')
assert RoleUserAssignment.objects.filter(
user=user.id, role_definition__name='Platform Auditor'
).exists(), "system_auditor member should have been granted Platform Auditor"
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Platform Auditor').exists(), (
"system_auditor member should have been granted Platform Auditor"
)
def test_fix_system_auditor_migration_backfills_missing(self, migrator):
"""
@@ -269,16 +269,16 @@ class TestSystemAuditorMigration:
# Simulate the pre-fix state: remove the Platform Auditor assignment
platform_auditor = RoleDefinition.objects.get(name='Platform Auditor')
RoleUserAssignment.objects.filter(user=user.id, role_definition=platform_auditor).delete()
assert not RoleUserAssignment.objects.filter(
user=user.id, role_definition=platform_auditor
).exists(), "Setup: user should not have Platform Auditor before corrective migration"
assert not RoleUserAssignment.objects.filter(user=user.id, role_definition=platform_auditor).exists(), (
"Setup: user should not have Platform Auditor before corrective migration"
)
state = migrator.apply_tested_migration(('main', '0208_fix_system_auditor_migration'))
RoleUserAssignment = state.apps.get_model('dab_rbac', 'RoleUserAssignment')
assert RoleUserAssignment.objects.filter(
user=user.id, role_definition__name='Platform Auditor'
).exists(), "Corrective migration should have backfilled Platform Auditor"
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Platform Auditor').exists(), (
"Corrective migration should have backfilled Platform Auditor"
)
def test_fix_system_auditor_migration_no_duplicates(self, migrator):
"""
@@ -302,6 +302,6 @@ class TestSystemAuditorMigration:
state = migrator.apply_tested_migration(('main', '0208_fix_system_auditor_migration'))
RoleUserAssignment = state.apps.get_model('dab_rbac', 'RoleUserAssignment')
assert (
RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Platform Auditor').count() == 1
), "Should have exactly one Platform Auditor assignment, not duplicates"
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Platform Auditor').count() == 1, (
"Should have exactly one Platform Auditor assignment, not duplicates"
)
@@ -163,9 +163,11 @@ def test_custom_environment_injection(post, user, organization):
expect=201,
)
template = NotificationTemplate.objects.get(pk=response.data['id'])
with pytest.raises(ConnectionError), override_settings(AWX_TASK_ENV={'HTTPS_PROXY': '192.168.50.100:1234'}), mock.patch.object(
HTTPAdapter, 'send'
) as fake_send:
with (
pytest.raises(ConnectionError),
override_settings(AWX_TASK_ENV={'HTTPS_PROXY': '192.168.50.100:1234'}),
mock.patch.object(HTTPAdapter, 'send') as fake_send,
):
def _send_side_effect(request, **kw):
assert select_proxy(request.url, kw['proxies']) == '192.168.50.100:1234'
@@ -218,7 +218,7 @@ def test_embedded_query_takes_precedence(live_tmp_folder, run_job_from_playbook,
# Verify the embedded query was used (includes device_type in facts)
host_audit = IndirectManagedNodeAudit.objects.filter(job=job).first()
assert host_audit.facts == {'device_type': 'Fake Host'}, (
'Expected embedded query output (with device_type). ' 'If facts is {}, the external query was incorrectly used instead.'
'Expected embedded query output (with device_type). If facts is {}, the external query was incorrectly used instead.'
)
@@ -288,7 +288,7 @@ def test_fallback_does_not_overcount(live_tmp_folder, run_job_from_playbook, ena
# Critical safety check: fallback must never count MORE than exact
assert fallback_count <= exact_count, (
f'Overcounting detected! Fallback produced {fallback_count} records ' f'but exact match produced only {exact_count} records.'
f'Overcounting detected! Fallback produced {fallback_count} records but exact match produced only {exact_count} records.'
)
# Both use the same jq expression and same module, so counts should be equal
@@ -343,5 +343,5 @@ def test_no_counting_without_compatible_fallback(live_tmp_folder, run_job_from_p
# No audit records should exist for this job
assert IndirectManagedNodeAudit.objects.filter(job=job).count() == 0, (
'IndirectManagedNodeAudit records were created despite no compatible ' 'fallback existing for demo.external v3.0.0 (only v1.x queries available).'
'IndirectManagedNodeAudit records were created despite no compatible fallback existing for demo.external v3.0.0 (only v1.x queries available).'
)
@@ -120,7 +120,7 @@ def test_nested_workflow_set_stats_precedence(live_tmp_folder, demo_inv, project
reader_extra_vars = json.loads(reader_node.job.extra_vars)
# var1: only set by outer job_first, no conflict — should propagate through
assert reader_extra_vars.get('var1') == 'outer-only', f'var1: expected "outer-only" (uncontested outer artifact), ' f'got "{reader_extra_vars.get("var1")}"'
assert reader_extra_vars.get('var1') == 'outer-only', f'var1: expected "outer-only" (uncontested outer artifact), got "{reader_extra_vars.get("var1")}"'
# var2: set by outer as "should-be-overridden", then by inner as "from-inner"
# Inner workflow's own ancestor artifacts should take precedence
@@ -132,7 +132,7 @@ def test_nested_workflow_set_stats_precedence(live_tmp_folder, demo_inv, project
)
# var3: only set by inner job_second — should propagate normally
assert reader_extra_vars.get('var3') == 'inner-only', f'var3: expected "inner-only" (inner-only artifact), ' f'got "{reader_extra_vars.get("var3")}"'
assert reader_extra_vars.get('var3') == 'inner-only', f'var3: expected "inner-only" (inner-only artifact), got "{reader_extra_vars.get("var3")}"'
@pytest.mark.django_db(transaction=True)
@@ -76,7 +76,7 @@ class TestJobSerializerGetRelated:
class TestJobSerializerSubstitution:
def test_survey_password_hide(self, mocker):
job = mocker.MagicMock(
**{'display_extra_vars.return_value': '{\"secret_key\": \"$encrypted$\"}', 'extra_vars.return_value': '{\"secret_key\": \"my_password\"}'}
**{'display_extra_vars.return_value': '{"secret_key": "$encrypted$"}', 'extra_vars.return_value': '{"secret_key": "my_password"}'}
)
serializer = JobSerializer(job)
rep = serializer.to_representation(job)
@@ -29,7 +29,7 @@ class TestNotificationTemplateSerializer:
},
{'started': {'body': '{{ job_metadata }}'}},
{'started': {'body': '{{ job.summary_fields.inventory.total_hosts }}'}},
{'started': {'body': u'Iñtërnâtiônàlizætiøn'}},
{'started': {'body': 'Iñtërnâtiônàlizætiøn'}},
],
)
def test_valid_messages(self, valid_messages):
@@ -29,9 +29,9 @@ def test_unified_job_list_field_consistency():
for cls in UnifiedJob.__subclasses__():
list_serializer = getattr(serializers, '{}ListSerializer'.format(cls.__name__))
unified_serializer = serializers.UnifiedJobListSerializer().get_sub_serializer(cls())
assert set(list_serializer().fields.keys()) == set(
unified_serializer().fields.keys()
), 'Mismatch between {} list serializer & unified list serializer'.format(cls)
assert set(list_serializer().fields.keys()) == set(unified_serializer().fields.keys()), (
'Mismatch between {} list serializer & unified list serializer'.format(cls)
)
def test_unified_job_detail_exclusive_fields():
+2 -2
View File
@@ -278,9 +278,9 @@ class TestSurveySpecValidation:
({'type': 'password', 'default': ['some', 'invalid', 'list']}, 'expected to be string'),
({'type': 'password', 'default': False}, 'expected to be string'),
({'type': 'integer', 'default': 'foo'}, 'expected to be int'),
({'type': 'integer', 'default': u'🐉'}, 'expected to be int'),
({'type': 'integer', 'default': '🐉'}, 'expected to be int'),
({'type': 'foo'}, 'allowed question types'),
({'type': u'🐉'}, 'allowed question types'),
({'type': '🐉'}, 'allowed question types'),
({'type': 'multiplechoice'}, 'multiplechoice must specify choices'),
({'type': 'integer', 'min': 'foo'}, 'min limit in survey question 0 expected to be integer'),
({'question_name': 42}, "'question_name' in survey question 0 expected to be string."),
+1 -1
View File
@@ -29,7 +29,7 @@ def hosts(ref_time):
Host(name='host1', ansible_facts={"a": 1, "b": 2}, ansible_facts_modified=ref_time, inventory=inventory),
Host(name='host2', ansible_facts={"a": 1, "b": 2}, ansible_facts_modified=ref_time, inventory=inventory),
Host(name='host3', ansible_facts={"a": 1, "b": 2}, ansible_facts_modified=ref_time, inventory=inventory),
Host(name=u'Iñtërnâtiônàlizætiøn', ansible_facts={"a": 1, "b": 2}, ansible_facts_modified=ref_time, inventory=inventory),
Host(name='Iñtërnâtiônàlizætiøn', ansible_facts={"a": 1, "b": 2}, ansible_facts_modified=ref_time, inventory=inventory),
]
+1 -1
View File
@@ -11,4 +11,4 @@ def test_clean_credential_insights():
with pytest.raises(ValidationError) as e:
proj.clean_credential()
assert json.dumps(str(e.value)) == json.dumps(str([u'Insights Credential is required for an Insights Project.']))
assert json.dumps(str(e.value)) == json.dumps(str(['Insights Credential is required for an Insights Project.']))
@@ -96,8 +96,8 @@ class TestSurveyVariableValidation:
def job(mocker):
ret = mocker.MagicMock(
**{
'decrypted_extra_vars.return_value': '{\"secret_key\": \"my_password\"}',
'display_extra_vars.return_value': '{\"secret_key\": \"$encrypted$\"}',
'decrypted_extra_vars.return_value': '{"secret_key": "my_password"}',
'display_extra_vars.return_value': '{"secret_key": "$encrypted$"}',
'extra_vars_dict': {"secret_key": "my_password"},
'pk': 1,
'job_template.pk': 1,
@@ -313,7 +313,7 @@ class TestWorkflowSurveys:
({'tmpl_var': 'bar2'}, False), # different value from template, not okay
({'tmpl_var': 'bar', 'a': 2}, False), # extra key, not okay
({'tmpl_var': 'bar', False: 2}, False), # Falsy key
({'tmpl_var': 'bar', u'🐉': u'🐉'}, False), # dragons
({'tmpl_var': 'bar', '🐉': '🐉'}, False), # dragons
],
)
class TestExtraVarsNoPrompt:
@@ -39,7 +39,7 @@ def test_valid__clean_extra_data_system_jobs(extra_data):
)
def test_invalid__extra_data_system_jobs(extra_data):
accepted, rejected, errors = SystemJobTemplate(job_type='cleanup_jobs').accept_or_ignore_variables(extra_data)
assert str(errors['extra_vars'][0]) == u'days must be a positive integer.'
assert str(errors['extra_vars'][0]) == 'days must be a positive integer.'
def test_unallowed_system_job_data():
@@ -7,9 +7,10 @@ import awx.main.notifications.rocketchat_backend as rocketchat_backend
def test_send_messages():
with mock.patch('awx.main.notifications.rocketchat_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.rocketchat_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.rocketchat_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.rocketchat_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.post.return_value.status_code = 201
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = rocketchat_backend.RocketChatBackend()
@@ -92,9 +93,10 @@ def test_send_messages_with_icon_url():
def test_send_messages_with_no_verify_ssl():
with mock.patch('awx.main.notifications.rocketchat_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.rocketchat_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.rocketchat_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.rocketchat_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.post.return_value.status_code = 201
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = rocketchat_backend.RocketChatBackend(rocketchat_no_verify_ssl=True)
@@ -7,9 +7,10 @@ import awx.main.notifications.webhook_backend as webhook_backend
def test_send_messages_as_POST():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None)
@@ -38,9 +39,10 @@ def test_send_messages_as_POST():
def test_send_messages_as_PUT():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.put.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('PUT', None)
@@ -69,9 +71,10 @@ def test_send_messages_as_PUT():
def test_send_messages_with_username():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None, username='userstring')
@@ -100,9 +103,10 @@ def test_send_messages_with_username():
def test_send_messages_with_password():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None, password='passwordstring')
@@ -131,9 +135,10 @@ def test_send_messages_with_password():
def test_send_messages_with_username_and_password():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None, username='userstring', password='passwordstring')
@@ -162,9 +167,10 @@ def test_send_messages_with_username_and_password():
def test_send_messages_with_no_verify_ssl():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None, disable_ssl_verification=True)
@@ -193,9 +199,10 @@ def test_send_messages_with_no_verify_ssl():
def test_send_messages_with_additional_headers():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
):
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', {'X-Test-Header1': 'test-content-1', 'X-Test-Header2': 'test-content-2'})
@@ -229,9 +236,10 @@ def test_send_messages_with_additional_headers():
def test_send_messages_with_redirects_ok():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
):
# First two calls return redirects, third call returns 200
requests_mock.post.side_effect = [
mock.Mock(status_code=301, headers={"Location": "http://redirect1.com"}),
@@ -266,9 +274,11 @@ def test_send_messages_with_redirects_ok():
def test_send_messages_with_redirects_blank():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock, mock.patch('awx.main.notifications.webhook_backend.logger') as logger_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
mock.patch('awx.main.notifications.webhook_backend.logger') as logger_mock,
):
# First call returns a redirect with Location header, second call returns 301 but NO Location header
requests_mock.post.side_effect = [
mock.Mock(status_code=301, headers={"Location": "http://redirect1.com"}),
@@ -299,9 +309,11 @@ def test_send_messages_with_redirects_blank():
def test_send_messages_with_redirects_max_retries_exceeded():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock, mock.patch('awx.main.notifications.webhook_backend.logger') as logger_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
mock.patch('awx.main.notifications.webhook_backend.logger') as logger_mock,
):
# Return MAX_RETRIES (5) redirect responses to exceed the retry limit
requests_mock.post.side_effect = [
mock.Mock(status_code=301, headers={"Location": "http://redirect1.com"}),
@@ -336,9 +348,11 @@ def test_send_messages_with_redirects_max_retries_exceeded():
def test_send_messages_with_error_status_code():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock, mock.patch('awx.main.notifications.webhook_backend.logger') as logger_mock:
with (
mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock,
mock.patch('awx.main.notifications.webhook_backend.get_awx_http_client_headers') as version_mock,
mock.patch('awx.main.notifications.webhook_backend.logger') as logger_mock,
):
# Return a 404 error status code
requests_mock.post.return_value = mock.Mock(status_code=404)
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
@@ -470,8 +470,7 @@ class TestIsWorkflowDone:
True,
smart_str(
_(
"No error handling path for workflow job node(s) [({},{})]. Workflow job node(s)"
" missing unified job template and error handling path []."
"No error handling path for workflow job node(s) [({},{})]. Workflow job node(s) missing unified job template and error handling path []."
).format(nodes[2].id, nodes[2].job.status)
),
)
@@ -485,9 +484,9 @@ class TestIsWorkflowDone:
assert g.has_workflow_failed() == (
True,
smart_str(
_(
"No error handling path for workflow job node(s) []. Workflow job node(s) missing" " unified job template and error handling path [{}]."
).format(nodes[2].id)
_("No error handling path for workflow job node(s) []. Workflow job node(s) missing unified job template and error handling path [{}].").format(
nodes[2].id
)
),
)
@@ -501,9 +500,9 @@ class TestIsWorkflowDone:
assert g.has_workflow_failed() == (
True,
smart_str(
_(
"No error handling path for workflow job node(s) []. Workflow job node(s) missing" " unified job template and error handling path [{}]."
).format(nodes[0].id)
_("No error handling path for workflow job node(s) []. Workflow job node(s) missing unified job template and error handling path [{}].").format(
nodes[0].id
)
),
)
@@ -514,8 +513,7 @@ class TestIsWorkflowDone:
True,
smart_str(
_(
"No error handling path for workflow job node(s) [({},{})]. Workflow job node(s)"
" missing unified job template and error handling path []."
"No error handling path for workflow job node(s) [({},{})]. Workflow job node(s) missing unified job template and error handling path []."
).format(nodes[0].id, nodes[0].job.status)
),
)
@@ -527,8 +525,7 @@ class TestIsWorkflowDone:
True,
smart_str(
_(
"No error handling path for workflow job node(s) [({},{})]. Workflow job node(s)"
" missing unified job template and error handling path []."
"No error handling path for workflow job node(s) [({},{})]. Workflow job node(s) missing unified job template and error handling path []."
).format(nodes[0].id, nodes[0].job.status)
),
)
+4 -3
View File
@@ -141,8 +141,8 @@ def test_cred_type_input_schema_validity(input_, valid):
({'env': {'99': '{{awx_secret}}'}}, False),
({'env': {'AWX_SECRET=': '{{awx_secret}}'}}, False),
({'env': {'ANSIBLE_SETTING': '{{awx_secret}}'}}, False),
({'env': {'DRAGON': u'🐉'}}, False),
({'env': {u'🐉': 'DRAGON'}}, False),
({'env': {'DRAGON': '🐉'}}, False),
({'env': {'🐉': 'DRAGON'}}, False),
({'extra_vars': 123}, False),
({'extra_vars': {}}, True),
({'extra_vars': {'hostname': '{{host}}'}}, True),
@@ -306,7 +306,8 @@ def test_implicit_role_field_parents():
second_field_descriptor = getattr(cls, field_name)
# all supported linkage types
assert isinstance(
second_field_descriptor, (ReverseManyToOneDescriptor, ImplicitRoleDescriptor, ForwardManyToOneDescriptor) # not currently used
second_field_descriptor,
(ReverseManyToOneDescriptor, ImplicitRoleDescriptor, ForwardManyToOneDescriptor), # not currently used
)
# only these links are supported
if field_attr:
+10 -4
View File
@@ -36,7 +36,8 @@ uri = URI(scheme="https", username="myusername", password="mypasswordwith%40", h
TEST_CLEARTEXT.append(
{
'uri': uri,
'text': textwrap.dedent("""\
'text': textwrap.dedent(
"""\
PLAY [all] ********************************************************************
TASK: [delete project directory before update] ********************************
@@ -58,7 +59,9 @@ TEST_CLEARTEXT.append(
localhost : ok=0 changed=0 unreachable=0 failed=1
""" % (uri.username, uri.password, str(uri), str(uri))),
"""
% (uri.username, uri.password, str(uri), str(uri))
),
'host_occurrences': 2,
}
)
@@ -67,7 +70,8 @@ uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYr
TEST_CLEARTEXT.append(
{
'uri': uri,
'text': textwrap.dedent("""\
'text': textwrap.dedent(
"""\
TASK: [update project using git] **
failed: [localhost] => {"cmd": "/usr/bin/git ls-remote https://REDACTED:********", "failed": true, "rc": 128}
stderr: error: Couldn't resolve host '@%s' while accessing %s
@@ -77,7 +81,9 @@ TEST_CLEARTEXT.append(
msg: error: Couldn't resolve host '@%s' while accessing %s
fatal: HTTP request failed
""" % (uri.host, str(uri), uri.host, str(uri))),
"""
% (uri.host, str(uri), uri.host, str(uri))
),
'host_occurrences': 4,
}
)
+4 -4
View File
@@ -1303,10 +1303,10 @@ class TestInventoryUpdateCredentials(TestJobExecution):
if credential:
credential.credential_type.inject_credential(credential, env, safe_env, [], private_data_dir)
env["VMWARE_USER"] == "bob",
env["VMWARE_PASSWORD"] == "secret",
env["VMWARE_HOST"] == "https://example.org",
env["VMWARE_VALIDATE_CERTS"] == "False",
(env["VMWARE_USER"] == "bob",)
(env["VMWARE_PASSWORD"] == "secret",)
(env["VMWARE_HOST"] == "https://example.org",)
(env["VMWARE_VALIDATE_CERTS"] == "False",)
def test_azure_rm_source_with_tenant(self, private_data_dir, inventory_update, mocker, mock_me):
task = jobs.RunInventoryUpdate()
+3 -3
View File
@@ -163,13 +163,13 @@ def memoized_function(mocker, mock_cache):
def test_memoize_track_function(memoized_function, mock_cache):
assert memoized_function('scott', 'scotterson') == 'scotterson'
assert mock_cache.get('myfunction') == {u'scott-scotterson': 'scotterson'}
assert mock_cache.get('myfunction') == {'scott-scotterson': 'scotterson'}
assert memoized_function('scott', 'scotterson') == 'scotterson'
assert memoized_function.calls['scott'] == 1
assert memoized_function('john', 'smith') == 'smith'
assert mock_cache.get('myfunction') == {u'scott-scotterson': 'scotterson', u'john-smith': 'smith'}
assert mock_cache.get('myfunction') == {'scott-scotterson': 'scotterson', 'john-smith': 'smith'}
assert memoized_function('john', 'smith') == 'smith'
assert memoized_function.calls['john'] == 1
@@ -180,7 +180,7 @@ def test_memoize_delete(memoized_function, mock_cache):
assert memoized_function('john', 'smith') == 'smith'
assert memoized_function.calls['john'] == 1
assert mock_cache.get('myfunction') == {u'john-smith': 'smith'}
assert mock_cache.get('myfunction') == {'john-smith': 'smith'}
with mock.patch('awx.main.utils.common.memoize_delete', side_effect=mock_cache.delete):
common.memoize_delete('myfunction')
+1 -1
View File
@@ -23,7 +23,7 @@ def test_encrypt_field_without_pk():
def test_encrypt_field_with_unicode_string():
value = u'Iñtërnâtiônàlizætiøn'
value = 'Iñtërnâtiônàlizætiøn'
field = Setting(value=value)
encrypted = field.value = encryption.encrypt_field(field, 'value')
assert encryption.decrypt_field(field, 'value') == value
+39 -42
View File
@@ -74,16 +74,16 @@ class TestSmartFilterQueryFromString:
@pytest.mark.parametrize(
"filter_string,q_expected",
[
('facts__facts__blank=""', Q(**{u"facts__facts__blank": u""})),
('"facts__facts__ space "="f"', Q(**{u"facts__facts__ space ": u"f"})),
('"facts__facts__ e "=no_quotes_here', Q(**{u"facts__facts__ e ": u"no_quotes_here"})),
('a__b__c=3', Q(**{u"a__b__c": 3})),
('a__b__c=3.14', Q(**{u"a__b__c": 3.14})),
('a__b__c=true', Q(**{u"a__b__c": True})),
('a__b__c=false', Q(**{u"a__b__c": False})),
('a__b__c=null', Q(**{u"a__b__c": None})),
('ansible_facts__a="true"', Q(**{u"ansible_facts__contains": {u"a": u"true"}})),
('ansible_facts__a__exact="true"', Q(**{u"ansible_facts__contains": {u"a": u"true"}})),
('facts__facts__blank=""', Q(**{"facts__facts__blank": ""})),
('"facts__facts__ space "="f"', Q(**{"facts__facts__ space ": "f"})),
('"facts__facts__ e "=no_quotes_here', Q(**{"facts__facts__ e ": "no_quotes_here"})),
('a__b__c=3', Q(**{"a__b__c": 3})),
('a__b__c=3.14', Q(**{"a__b__c": 3.14})),
('a__b__c=true', Q(**{"a__b__c": True})),
('a__b__c=false', Q(**{"a__b__c": False})),
('a__b__c=null', Q(**{"a__b__c": None})),
('ansible_facts__a="true"', Q(**{"ansible_facts__contains": {"a": "true"}})),
('ansible_facts__a__exact="true"', Q(**{"ansible_facts__contains": {"a": "true"}})),
# ('"a__b\"__c"="true"', Q(**{u"a__b\"__c": "true"})),
# ('a__b\"__c="true"', Q(**{u"a__b\"__c": "true"})),
],
@@ -101,7 +101,7 @@ class TestSmartFilterQueryFromString:
def test_invalid_filter_strings(self, mock_get_host_model, filter_string):
with pytest.raises(RuntimeError) as e:
SmartFilter.query_from_string(filter_string)
assert str(e.value) == u"Invalid query " + filter_string
assert str(e.value) == "Invalid query " + filter_string
@pytest.mark.parametrize(
"filter_string",
@@ -118,8 +118,8 @@ class TestSmartFilterQueryFromString:
@pytest.mark.parametrize(
"filter_string,q_expected",
[
(u'(a=abc\u1f5e3def)', Q(**{u"a": u"abc\u1f5e3def"})),
(u'(ansible_facts__a=abc\u1f5e3def)', Q(**{u"ansible_facts__contains": {u"a": u"abc\u1f5e3def"}})),
('(a=abc\u1f5e3def)', Q(**{"a": "abc\u1f5e3def"})),
('(ansible_facts__a=abc\u1f5e3def)', Q(**{"ansible_facts__contains": {"a": "abc\u1f5e3def"}})),
],
)
def test_unicode(self, mock_get_host_model, filter_string, q_expected):
@@ -129,17 +129,14 @@ class TestSmartFilterQueryFromString:
@pytest.mark.parametrize(
"filter_string,q_expected",
[
('(a=b)', Q(**{u"a": u"b"})),
('a=b and c=d', Q(**{u"a": u"b"}) & Q(**{u"c": u"d"})),
('(a=b and c=d)', Q(**{u"a": u"b"}) & Q(**{u"c": u"d"})),
('a=b or c=d', Q(**{u"a": u"b"}) | Q(**{u"c": u"d"})),
('(a=b and c=d) or (e=f)', (Q(**{u"a": u"b"}) & Q(**{u"c": u"d"})) | (Q(**{u"e": u"f"}))),
('(a=b)', Q(**{"a": "b"})),
('a=b and c=d', Q(**{"a": "b"}) & Q(**{"c": "d"})),
('(a=b and c=d)', Q(**{"a": "b"}) & Q(**{"c": "d"})),
('a=b or c=d', Q(**{"a": "b"}) | Q(**{"c": "d"})),
('(a=b and c=d) or (e=f)', (Q(**{"a": "b"}) & Q(**{"c": "d"})) | (Q(**{"e": "f"}))),
(
'a=b or a=d or a=e or a=z and b=h and b=i and b=j and b=k',
Q(**{u"a": u"b"})
| Q(**{u"a": u"d"})
| Q(**{u"a": u"e"})
| Q(**{u"a": u"z"}) & Q(**{u"b": u"h"}) & Q(**{u"b": u"i"}) & Q(**{u"b": u"j"}) & Q(**{u"b": u"k"}),
Q(**{"a": "b"}) | Q(**{"a": "d"}) | Q(**{"a": "e"}) | Q(**{"a": "z"}) & Q(**{"b": "h"}) & Q(**{"b": "i"}) & Q(**{"b": "j"}) & Q(**{"b": "k"}),
),
],
)
@@ -150,21 +147,21 @@ class TestSmartFilterQueryFromString:
@pytest.mark.parametrize(
"filter_string,q_expected",
[
('ansible_facts__a__b__c[]=3', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [3]}}}})),
('ansible_facts__a__b__c[]=3.14', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [3.14]}}}})),
('ansible_facts__a__b__c[]=true', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [True]}}}})),
('ansible_facts__a__b__c[]=false', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [False]}}}})),
('ansible_facts__a__b__c[]="true"', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [u"true"]}}}})),
('ansible_facts__a__b__c[]="hello world"', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [u"hello world"]}}}})),
('ansible_facts__a__b__c[]__d[]="foobar"', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [{u"d": [u"foobar"]}]}}}})),
('ansible_facts__a__b__c[]__d="foobar"', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [{u"d": u"foobar"}]}}}})),
('ansible_facts__a__b__c[]__d__e="foobar"', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [{u"d": {u"e": u"foobar"}}]}}}})),
('ansible_facts__a__b__c[]__d__e[]="foobar"', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [{u"d": {u"e": [u"foobar"]}}]}}}})),
('ansible_facts__a__b__c[]__d__e__f[]="foobar"', Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [{u"d": {u"e": {u"f": [u"foobar"]}}}]}}}})),
('ansible_facts__a__b__c[]=3', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [3]}}}})),
('ansible_facts__a__b__c[]=3.14', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [3.14]}}}})),
('ansible_facts__a__b__c[]=true', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [True]}}}})),
('ansible_facts__a__b__c[]=false', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [False]}}}})),
('ansible_facts__a__b__c[]="true"', Q(**{"ansible_facts__contains": {"a": {"b": {"c": ["true"]}}}})),
('ansible_facts__a__b__c[]="hello world"', Q(**{"ansible_facts__contains": {"a": {"b": {"c": ["hello world"]}}}})),
('ansible_facts__a__b__c[]__d[]="foobar"', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [{"d": ["foobar"]}]}}}})),
('ansible_facts__a__b__c[]__d="foobar"', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [{"d": "foobar"}]}}}})),
('ansible_facts__a__b__c[]__d__e="foobar"', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [{"d": {"e": "foobar"}}]}}}})),
('ansible_facts__a__b__c[]__d__e[]="foobar"', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [{"d": {"e": ["foobar"]}}]}}}})),
('ansible_facts__a__b__c[]__d__e__f[]="foobar"', Q(**{"ansible_facts__contains": {"a": {"b": {"c": [{"d": {"e": {"f": ["foobar"]}}}]}}}})),
(
'(ansible_facts__a__b__c[]__d__e__f[]="foobar") and (ansible_facts__a__b__c[]__d__e[]="foobar")',
Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [{u"d": {u"e": {u"f": [u"foobar"]}}}]}}}})
& Q(**{u"ansible_facts__contains": {u"a": {u"b": {u"c": [{u"d": {u"e": [u"foobar"]}}]}}}}),
Q(**{"ansible_facts__contains": {"a": {"b": {"c": [{"d": {"e": {"f": ["foobar"]}}}]}}}})
& Q(**{"ansible_facts__contains": {"a": {"b": {"c": [{"d": {"e": ["foobar"]}}]}}}}),
),
# ('"a__b\"__c"="true"', Q(**{u"a__b\"__c": "true"})),
# ('a__b\"__c="true"', Q(**{u"a__b\"__c": "true"})),
@@ -178,7 +175,7 @@ class TestSmartFilterQueryFromString:
"filter_string,q_expected",
[
# ('a__b__c[]="true"', Q(**{u"a__b__c__contains": u"\"true\""})),
('ansible_facts__a="true"', Q(**{u"ansible_facts__contains": {u"a": u"true"}})),
('ansible_facts__a="true"', Q(**{"ansible_facts__contains": {"a": "true"}})),
# ('"a__b\"__c"="true"', Q(**{u"a__b\"__c": "true"})),
# ('a__b\"__c="true"', Q(**{u"a__b\"__c": "true"})),
],
@@ -190,8 +187,8 @@ class TestSmartFilterQueryFromString:
@pytest.mark.parametrize(
"filter_string,q_expected",
[
('ansible_facts__a=null', Q(**{u"ansible_facts__contains": {u"a": None}})),
('ansible_facts__c="null"', Q(**{u"ansible_facts__contains": {u"c": u"\"null\""}})),
('ansible_facts__a=null', Q(**{"ansible_facts__contains": {"a": None}})),
('ansible_facts__c="null"', Q(**{"ansible_facts__contains": {"c": "\"null\""}})),
],
)
def test_contains_query_generated_null(self, mock_get_host_model, filter_string, q_expected):
@@ -201,17 +198,17 @@ class TestSmartFilterQueryFromString:
@pytest.mark.parametrize(
"filter_string,q_expected",
[
('group__search=foo', Q(Q(**{u"group__name__icontains": u"foo"}) | Q(**{u"group__description__icontains": u"foo"}))),
('group__search=foo', Q(Q(**{"group__name__icontains": "foo"}) | Q(**{"group__description__icontains": "foo"}))),
(
'search=foo and group__search=foo',
Q(
Q(**{u"name__icontains": u"foo"}) | Q(**{u"description__icontains": u"foo"}),
Q(**{u"group__name__icontains": u"foo"}) | Q(**{u"group__description__icontains": u"foo"}),
Q(**{"name__icontains": "foo"}) | Q(**{"description__icontains": "foo"}),
Q(**{"group__name__icontains": "foo"}) | Q(**{"group__description__icontains": "foo"}),
),
),
(
'search=foo or ansible_facts__a=null',
Q(Q(**{u"name__icontains": u"foo"}) | Q(**{u"description__icontains": u"foo"})) | Q(**{u"ansible_facts__contains": {u"a": None}}),
Q(Q(**{"name__icontains": "foo"}) | Q(**{"description__icontains": "foo"})) | Q(**{"ansible_facts__contains": {"a": None}}),
),
],
)
+1 -1
View File
@@ -45,7 +45,7 @@ def test_kv_unsafe():
def test_kv_unsafe_unicode():
assert safe_dump({'a': u'🐉'}) == '!unsafe \'a\': !unsafe "\\U0001F409"\n'
assert safe_dump({'a': '🐉'}) == '!unsafe \'a\': !unsafe "\\U0001F409"\n'
def test_kv_unsafe_in_list():
+45 -47
View File
@@ -14,16 +14,14 @@ def test_validate_rh_basic_auth_rhsm():
"""
licenser = Licenser()
with patch('awx.main.utils.licensing.settings') as mock_settings, patch.object(
licenser, 'get_host_from_rhsm_config', return_value='https://subscription.rhsm.redhat.com'
) as mock_get_host, patch.object(licenser, 'get_rhsm_subs', return_value=[]) as mock_get_rhsm, patch.object(
licenser, 'get_satellite_subs'
) as mock_get_satellite, patch.object(
licenser, 'get_crc_subs'
) as mock_get_crc, patch.object(
licenser, 'generate_license_options_from_entitlements'
) as mock_generate:
with (
patch('awx.main.utils.licensing.settings') as mock_settings,
patch.object(licenser, 'get_host_from_rhsm_config', return_value='https://subscription.rhsm.redhat.com') as mock_get_host,
patch.object(licenser, 'get_rhsm_subs', return_value=[]) as mock_get_rhsm,
patch.object(licenser, 'get_satellite_subs') as mock_get_satellite,
patch.object(licenser, 'get_crc_subs') as mock_get_crc,
patch.object(licenser, 'generate_license_options_from_entitlements') as mock_generate,
):
mock_settings.REDHAT_CANDLEPIN_HOST = None
licenser.validate_rh('testuser', 'testpass', basic_auth=True)
@@ -44,16 +42,14 @@ def test_validate_rh_basic_auth_satellite():
"""
licenser = Licenser()
with patch('awx.main.utils.licensing.settings') as mock_settings, patch.object(
licenser, 'get_host_from_rhsm_config', return_value='https://satellite.example.com'
) as mock_get_host, patch.object(licenser, 'get_rhsm_subs') as mock_get_rhsm, patch.object(
licenser, 'get_satellite_subs', return_value=[]
) as mock_get_satellite, patch.object(
licenser, 'get_crc_subs'
) as mock_get_crc, patch.object(
licenser, 'generate_license_options_from_entitlements'
) as mock_generate:
with (
patch('awx.main.utils.licensing.settings') as mock_settings,
patch.object(licenser, 'get_host_from_rhsm_config', return_value='https://satellite.example.com') as mock_get_host,
patch.object(licenser, 'get_rhsm_subs') as mock_get_rhsm,
patch.object(licenser, 'get_satellite_subs', return_value=[]) as mock_get_satellite,
patch.object(licenser, 'get_crc_subs') as mock_get_crc,
patch.object(licenser, 'generate_license_options_from_entitlements') as mock_generate,
):
mock_settings.REDHAT_CANDLEPIN_HOST = None
licenser.validate_rh('testuser', 'testpass', basic_auth=True)
@@ -72,14 +68,14 @@ def test_validate_rh_service_account_crc():
"""
licenser = Licenser()
with patch('awx.main.utils.licensing.settings') as mock_settings, patch.object(licenser, 'get_host_from_rhsm_config') as mock_get_host, patch.object(
licenser, 'get_rhsm_subs'
) as mock_get_rhsm, patch.object(licenser, 'get_satellite_subs') as mock_get_satellite, patch.object(
licenser, 'get_crc_subs', return_value=[]
) as mock_get_crc, patch.object(
licenser, 'generate_license_options_from_entitlements'
) as mock_generate:
with (
patch('awx.main.utils.licensing.settings') as mock_settings,
patch.object(licenser, 'get_host_from_rhsm_config') as mock_get_host,
patch.object(licenser, 'get_rhsm_subs') as mock_get_rhsm,
patch.object(licenser, 'get_satellite_subs') as mock_get_satellite,
patch.object(licenser, 'get_crc_subs', return_value=[]) as mock_get_crc,
patch.object(licenser, 'generate_license_options_from_entitlements') as mock_generate,
):
mock_settings.SUBSCRIPTIONS_RHSM_URL = 'https://console.redhat.com/api/rhsm/v1/subscriptions'
licenser.validate_rh('client_id', 'client_secret', basic_auth=False)
@@ -99,14 +95,14 @@ def test_validate_rh_candlepin_host_prioritized_over_rhsm_config():
"""
licenser = Licenser()
with patch('awx.main.utils.licensing.settings') as mock_settings, patch.object(licenser, 'get_host_from_rhsm_config') as mock_get_host, patch.object(
licenser, 'get_rhsm_subs'
) as mock_get_rhsm, patch.object(licenser, 'get_satellite_subs', return_value=[]) as mock_get_satellite, patch.object(
licenser, 'get_crc_subs'
) as mock_get_crc, patch.object(
licenser, 'generate_license_options_from_entitlements'
) as mock_generate:
with (
patch('awx.main.utils.licensing.settings') as mock_settings,
patch.object(licenser, 'get_host_from_rhsm_config') as mock_get_host,
patch.object(licenser, 'get_rhsm_subs') as mock_get_rhsm,
patch.object(licenser, 'get_satellite_subs', return_value=[]) as mock_get_satellite,
patch.object(licenser, 'get_crc_subs') as mock_get_crc,
patch.object(licenser, 'generate_license_options_from_entitlements') as mock_generate,
):
mock_settings.REDHAT_CANDLEPIN_HOST = 'https://satellite.example.com'
licenser.validate_rh('testuser', 'testpass', basic_auth=True)
@@ -121,14 +117,14 @@ def test_validate_rh_prepends_scheme_when_missing():
"""REDHAT_CANDLEPIN_HOST without a scheme gets https:// prepended"""
licenser = Licenser()
with patch('awx.main.utils.licensing.settings') as mock_settings, patch.object(licenser, 'get_host_from_rhsm_config'), patch.object(
licenser, 'get_rhsm_subs'
) as mock_get_rhsm, patch.object(licenser, 'get_satellite_subs', return_value=[]) as mock_get_satellite, patch.object(
licenser, 'get_crc_subs'
), patch.object(
licenser, 'generate_license_options_from_entitlements'
with (
patch('awx.main.utils.licensing.settings') as mock_settings,
patch.object(licenser, 'get_host_from_rhsm_config'),
patch.object(licenser, 'get_rhsm_subs') as mock_get_rhsm,
patch.object(licenser, 'get_satellite_subs', return_value=[]) as mock_get_satellite,
patch.object(licenser, 'get_crc_subs'),
patch.object(licenser, 'generate_license_options_from_entitlements'),
):
mock_settings.REDHAT_CANDLEPIN_HOST = 'satellite.example.com'
licenser.validate_rh('testuser', 'testpass', basic_auth=True)
@@ -140,8 +136,9 @@ def test_validate_rh_missing_user_raises_error():
"""Test validate_rh raises ValueError when user is missing"""
licenser = Licenser()
with patch('awx.main.utils.licensing.settings') as mock_settings, patch.object(
licenser, 'get_host_from_rhsm_config', return_value='https://subscription.rhsm.redhat.com'
with (
patch('awx.main.utils.licensing.settings') as mock_settings,
patch.object(licenser, 'get_host_from_rhsm_config', return_value='https://subscription.rhsm.redhat.com'),
):
mock_settings.REDHAT_CANDLEPIN_HOST = None
with pytest.raises(ValueError, match='subscriptions_client_id or subscriptions_username is required'):
@@ -152,8 +149,9 @@ def test_validate_rh_missing_password_raises_error():
"""Test validate_rh raises ValueError when password is missing"""
licenser = Licenser()
with patch('awx.main.utils.licensing.settings') as mock_settings, patch.object(
licenser, 'get_host_from_rhsm_config', return_value='https://subscription.rhsm.redhat.com'
with (
patch('awx.main.utils.licensing.settings') as mock_settings,
patch.object(licenser, 'get_host_from_rhsm_config', return_value='https://subscription.rhsm.redhat.com'),
):
mock_settings.REDHAT_CANDLEPIN_HOST = None
with pytest.raises(ValueError, match='subscriptions_client_secret or subscriptions_password is required'):
+8 -8
View File
@@ -333,16 +333,16 @@ def update_scm_url(scm_type, url, username=True, password=True, check_special_ca
netloc_password = ''
if netloc_username and parts.scheme != 'file' and scm_type not in ("insights", "archive"):
netloc = u':'.join([urllib.parse.quote(x, safe='') for x in (netloc_username, netloc_password) if x])
netloc = ':'.join([urllib.parse.quote(x, safe='') for x in (netloc_username, netloc_password) if x])
else:
netloc = u''
netloc = ''
# urllib.parse strips brackets from IPv6 addresses, so we need to add them back in
hostname = parts.hostname
if hostname and ':' in hostname and '[' in url and ']' in url:
hostname = f'[{hostname}]'
netloc = u'@'.join(filter(None, [netloc, hostname]))
netloc = '@'.join(filter(None, [netloc, hostname]))
if parts.port:
netloc = u':'.join([netloc, str(parts.port)])
netloc = ':'.join([netloc, str(parts.port)])
new_url = urllib.parse.urlunsplit([parts.scheme, netloc, parts.path, parts.query, parts.fragment])
if scp_format and parts.scheme == 'git+ssh':
new_url = new_url.replace('git+ssh://', '', 1).replace('/', ':', 1)
@@ -378,7 +378,7 @@ def _convert_model_field_for_display(obj, field_name, password_fields=None):
if password_fields is None:
password_fields = set(getattr(type(obj), 'PASSWORD_FIELDS', [])) | set(['password'])
if field_name in password_fields or (isinstance(field_val, str) and field_val.startswith('$encrypted$')):
return u'hidden'
return 'hidden'
if hasattr(obj, 'display_%s' % field_name):
field_val = getattr(obj, 'display_%s' % field_name)()
if isinstance(field_val, (list, dict)):
@@ -1126,15 +1126,15 @@ def truncate_stdout(stdout, size):
if size <= 0 or len(stdout) <= size:
return stdout
stdout = stdout[: (size - 1)] + u'\u2026'
stdout = stdout[: (size - 1)] + '\u2026'
set_count, reset_count = 0, 0
for m in ANSI_SGR_PATTERN.finditer(stdout):
if m.group() == u'\u001b[0m':
if m.group() == '\u001b[0m':
reset_count += 1
else:
set_count += 1
return stdout + u'\u001b[0m' * (set_count - reset_count)
return stdout + '\u001b[0m' * (set_count - reset_count)
def deepmerge(a, b):
-1
View File
@@ -46,7 +46,6 @@ def db_requirement_violations() -> Optional[str]:
if os.getenv('SKIP_PG_VERSION_CHECK', False):
return None
if connection.vendor == 'postgresql':
# enforce the postgres version is a minimum of 12 (we need this for partitioning); if not, then terminate program with exit code of 1
# In the future if we require a feature of a version of postgres > 12 this should be updated to reflect that.
# The return of connection.pg_version is something like 12013
+10 -10
View File
@@ -118,11 +118,11 @@ class DynamicLevelFilter(Filter):
def string_to_type(t):
if t == u'null':
if t == 'null':
return None
if t == u'true':
if t == 'true':
return True
elif t == u'false':
elif t == 'false':
return False
if re.search(r'^[-+]?[0-9]+$', t):
@@ -151,7 +151,7 @@ class SmartFilter(object):
search_kwargs = self._expand_search(k, v)
if search_kwargs:
kwargs.update(search_kwargs)
q = reduce(lambda x, y: x | y, [models.Q(**{u'%s__icontains' % _k: _v}) for _k, _v in kwargs.items()])
q = reduce(lambda x, y: x | y, [models.Q(**{'%s__icontains' % _k: _v}) for _k, _v in kwargs.items()])
self.result = Host.objects.filter(q)
else:
# detect loops and restrict access to sensitive fields
@@ -168,7 +168,7 @@ class SmartFilter(object):
return v
def strip_quotes_json_logic(self, v):
if type(v) is str and v.startswith('"') and v.endswith('"') and v != u'"null"':
if type(v) is str and v.startswith('"') and v.endswith('"') and v != '"null"':
return v[1:-1]
return v
@@ -203,9 +203,9 @@ class SmartFilter(object):
strip_len = len(SmartFilter.SEARCHABLE_RELATIONSHIP)
k = k[strip_len:]
pieces = k.split(u'__')
pieces = k.split('__')
assembled_k = u'%s__contains' % (SmartFilter.SEARCHABLE_RELATIONSHIP)
assembled_k = '%s__contains' % (SmartFilter.SEARCHABLE_RELATIONSHIP)
assembled_v = None
last_v = None
@@ -213,7 +213,7 @@ class SmartFilter(object):
for i, piece in enumerate(pieces):
new_kv = dict()
if piece.endswith(u'[]'):
if piece.endswith('[]'):
new_v = []
new_kv[piece[0:-2]] = new_v
else:
@@ -241,7 +241,7 @@ class SmartFilter(object):
def _extract_key_value(self, t):
k = t[0]
v = t[1] if len(t) > 1 else u""
v = t[1] if len(t) > 1 else ""
# Strip quotes from key
if isinstance(k, str) and k.startswith('"') and k.endswith('"'):
@@ -333,7 +333,7 @@ class SmartFilter(object):
try:
res = bool_expr.parse_string(filter_string, parse_all=True)
except (pp.ParseException, FieldDoesNotExist):
raise RuntimeError(u"Invalid query %s" % filter_string_raw)
raise RuntimeError("Invalid query %s" % filter_string_raw)
if len(res) > 0:
return res[0].result
+2 -6
View File
@@ -265,9 +265,7 @@ class Licenser(object):
except requests.exceptions.ConnectionError as error:
raise error
except OSError as error:
raise OSError(
'Unable to open certificate bundle {}. Check that the service is running on Red Hat Enterprise Linux.'.format(verify)
) from error # noqa
raise OSError('Unable to open certificate bundle {}. Check that the service is running on Red Hat Enterprise Linux.'.format(verify)) from error # noqa
subs.raise_for_status()
for sub in subs.json():
@@ -328,9 +326,7 @@ class Licenser(object):
except requests.exceptions.ConnectionError as error:
raise error
except OSError as error:
raise OSError(
'Unable to open certificate bundle {}. Check that the service is running on Red Hat Enterprise Linux.'.format(verify)
) from error # noqa
raise OSError('Unable to open certificate bundle {}. Check that the service is running on Red Hat Enterprise Linux.'.format(verify)) from error # noqa
orgs.raise_for_status()
for org in orgs.json()['results']:
+1 -1
View File
@@ -134,7 +134,7 @@ class MemInventory(object):
try:
port = int(name.split(':')[1])
except (ValueError, UnicodeDecodeError):
logger.warning(u'Invalid port "%s" for host "%s"', name.split(':')[1], host_name)
logger.warning('Invalid port "%s" for host "%s"', name.split(':')[1], host_name)
port = None
else:
host_name = name
+1 -1
View File
@@ -20,7 +20,7 @@ class SafeLoader(yaml.Loader):
return node
SafeLoader.add_constructor(u'!unsafe', SafeLoader.construct_yaml_unsafe)
SafeLoader.add_constructor('!unsafe', SafeLoader.construct_yaml_unsafe)
def safe_dump(x, safe_dict=None):
+2 -2
View File
@@ -39,7 +39,7 @@ class AdHocCommand(HasCreate, UnifiedJob):
verbosity=verbosity,
inventory=self.ds.inventory,
credential=self.ds.credential,
**kwargs
**kwargs,
)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
@@ -53,7 +53,7 @@ class AdHocCommand(HasCreate, UnifiedJob):
verbosity=verbosity,
inventory=inventory,
credential=credential,
**kwargs
**kwargs,
)
return self.update_identity(AdHocCommands(self.connection).post(payload))
@@ -42,7 +42,7 @@ class ExecutionEnvironment(HasCreate, HasCopy, base.Base):
organization=organization.id if organization else None,
credential=credential.id if credential else None,
pull=pull,
**kwargs
**kwargs,
)
return payload
+2 -2
View File
@@ -70,7 +70,7 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
scm_url=scm_url,
scm_branch=scm_branch,
credential=credential,
**kwargs
**kwargs,
)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
@@ -84,7 +84,7 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
scm_branch=scm_branch,
organization=organization,
credential=credential,
**kwargs
**kwargs,
)
self.update_identity(Projects(self.connection).post(payload))
+1 -1
View File
@@ -156,7 +156,7 @@ class CLI(object):
self.root.connection.login(username, password)
self.root.get()
except Exception as e:
raise RuntimeError(f"Basic authentication failed: {str(e)}. " "Verify credentials and network connectivity.") from e
raise RuntimeError(f"Basic authentication failed: {str(e)}. Verify credentials and network connectivity.") from e
return
# Use session-based authentication (default)
+1 -1
View File
@@ -174,7 +174,7 @@ class ProjectCreate(CustomAction):
resource = 'projects'
def add_arguments(self, parser, resource_options_parser):
parser.choices[self.action].add_argument('--monitor', action='store_true', help=('If set, prints stdout of the project update until ' 'it finishes.'))
parser.choices[self.action].add_argument('--monitor', action='store_true', help=('If set, prints stdout of the project update until it finishes.'))
parser.choices[self.action].add_argument('--wait', action='store_true', help='If set, waits until the new project has updated.')
def post(self, kwargs):
+1 -3
View File
@@ -42,9 +42,7 @@ class CircularDependencyError(ValueError):
def __init__(self, data):
# Sort the data just to make the output consistent, for use in
# error messages. That's convenient for doctests.
s = 'Circular dependencies exist among these items: {{{}}}'.format(
', '.join('{!r}:{!r}'.format(key, value) for key, value in sorted(data.items()))
) # noqa
s = 'Circular dependencies exist among these items: {{{}}}'.format(', '.join('{!r}:{!r}'.format(key, value) for key, value in sorted(data.items()))) # noqa
super(CircularDependencyError, self).__init__(s)
self.data = data