pre-commit: Add ruff

This handles formatting and allows us to replace both the pyupgrade and
bandit hooks.

We also bump the versions of the other hooks and migrate to the native
hacking extension.

Change-Id: I1aca8ef6c782252293a0b71aba8afe5409366d70
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-04-04 12:51:34 +01:00
parent f2a5f994eb
commit 7750bda6a6
11 changed files with 180 additions and 143 deletions

View File

@@ -18,20 +18,17 @@ repos:
- id: debug-statements
- id: check-yaml
files: .*\.(yaml|yml)$
- repo: https://github.com/PyCQA/bandit
rev: 1.7.10
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.3
hooks:
- id: bandit
args: ['-x', 'tests']
- repo: local
- id: ruff
args: ['--fix', '--unsafe-fixes']
- id: ruff-format
- repo: https://opendev.org/openstack/hacking
rev: 7.0.0
hooks:
- id: flake8
name: flake8
additional_dependencies:
- hacking>=6.1.0,<6.2.0
language: python
entry: flake8
files: '^.*\.py$'
- id: hacking
additional_dependencies: []
exclude: '^(doc|releasenotes)/.*$'
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
@@ -43,8 +40,3 @@ repos:
| doc/.*
| releasenotes/.*
)
- repo: https://github.com/asottile/pyupgrade
rev: v3.18.0
hooks:
- id: pyupgrade
args: [--py3-only]

View File

@@ -59,16 +59,19 @@ pygments_style = 'native'
html_theme = 'openstackdocs'
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
htmlhelp_basename = f'{project}doc'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'%s.tex' % project,
'%s Documentation' % project,
'OpenStack Foundation', 'manual'),
(
'index',
f'{project}.tex',
f'{project} Documentation',
'OpenStack Foundation',
'manual',
),
]
# openstackdocstheme options

View File

@@ -40,12 +40,12 @@ LOG = logging.getLogger(__name__)
LOG.info("Message without context")
# ids in Openstack are 32 characters long
# For readability a shorter id value is used
context.RequestContext(user_id='6ce90b4d',
project_id='d6134462',
project_domain_id='a6b9360e')
context.RequestContext(
user_id='6ce90b4d', project_id='d6134462', project_domain_id='a6b9360e'
)
LOG.info("Message with context")
context = context.RequestContext(user_id='ace90b4d',
project_id='b6134462',
project_domain_id='c6b9360e')
context = context.RequestContext(
user_id='ace90b4d', project_id='b6134462', project_domain_id='c6b9360e'
)
LOG.info("Message with passed context", context=context)

View File

@@ -33,7 +33,9 @@ CONF = cfg.CONF
DOMAIN = "demo"
logging.register_options(CONF)
CONF.logging_user_identity_format = "%(user_id)s/%(project_id)s@%(project_domain)s"
CONF.logging_user_identity_format = (
"%(user_id)s/%(project_id)s@%(project_domain)s"
)
logging.setup(CONF, DOMAIN)
LOG = logging.getLogger(__name__)
@@ -41,8 +43,10 @@ LOG = logging.getLogger(__name__)
LOG.info("Message without context")
# ids in Openstack are 32 characters long
# For readability a shorter id value is used
context.RequestContext(request_id='req-abc',
user_id='6ce90b4d',
project_id='d6134462',
project_domain_id='a6b9360e')
context.RequestContext(
request_id='req-abc',
user_id='6ce90b4d',
project_id='d6134462',
project_domain_id='a6b9360e',
)
LOG.info("Message with context")

View File

@@ -44,7 +44,7 @@ _request_store = threading.local()
# These arguments will be passed to a new context from the first available
# header to support backwards compatibility.
_ENVIRON_HEADERS: ty.Dict[str, ty.List[str]] = {
_ENVIRON_HEADERS: dict[str, list[str]] = {
'auth_token': ['HTTP_X_AUTH_TOKEN', 'HTTP_X_STORAGE_TOKEN'],
'user_id': ['HTTP_X_USER_ID', 'HTTP_X_USER'],
'project_id': ['HTTP_X_PROJECT_ID', 'HTTP_X_TENANT_ID', 'HTTP_X_TENANT'],
@@ -58,7 +58,6 @@ _ENVIRON_HEADERS: ty.Dict[str, ty.List[str]] = {
'project_domain_name': ['HTTP_X_PROJECT_DOMAIN_NAME'],
'request_id': ['openstack.request_id'],
'global_request_id': ['openstack.global_request_id'],
'service_token': ['HTTP_X_SERVICE_TOKEN'],
'service_user_id': ['HTTP_X_SERVICE_USER_ID'],
'service_user_name': ['HTTP_X_SERVICE_USER_NAME'],
@@ -73,7 +72,7 @@ _ENVIRON_HEADERS: ty.Dict[str, ty.List[str]] = {
def generate_request_id() -> str:
"""Generate a unique request id."""
return 'req-%s' % uuid.uuid4()
return f'req-{uuid.uuid4()}'
class _DeprecatedPolicyValues(_MutableMapping):
@@ -84,9 +83,9 @@ class _DeprecatedPolicyValues(_MutableMapping):
these values as oslo.policy will do will trigger a DeprecationWarning.
"""
def __init__(self, data: ty.Dict[str, ty.Any]):
def __init__(self, data: dict[str, ty.Any]):
self._data = data
self._deprecated: ty.Dict[str, ty.Any] = {}
self._deprecated: dict[str, ty.Any] = {}
def __getitem__(self, k: str) -> ty.Any:
try:
@@ -99,10 +98,12 @@ class _DeprecatedPolicyValues(_MutableMapping):
except KeyError:
pass
else:
warnings.warn('Policy enforcement is depending on the value of '
'%s. This key is deprecated. Please update your '
'policy file to use the standard policy values.' % k,
DeprecationWarning)
warnings.warn(
'Policy enforcement is depending on the value of '
f'{k}. This key is deprecated. Please update your '
'policy file to use the standard policy values.',
DeprecationWarning,
)
return val
raise KeyError(k)
@@ -126,14 +127,13 @@ class _DeprecatedPolicyValues(_MutableMapping):
return self._dict.__repr__()
@property
def _dict(self) -> ty.Dict[str, ty.Any]:
def _dict(self) -> dict[str, ty.Any]:
d = self._deprecated.copy()
d.update(self._data)
return d
class RequestContext:
"""Helper class to represent useful information about a request context.
Stores information about the security context under which the user
@@ -143,7 +143,7 @@ class RequestContext:
user_idt_format = '{user} {project_id} {domain} {user_domain} {p_domain}'
# Can be overridden in subclasses to specify extra keys that should be
# read when constructing a context using from_dict.
FROM_DICT_EXTRA_KEYS: ty.List[str] = []
FROM_DICT_EXTRA_KEYS: list[str] = []
def __init__(
self,
@@ -159,7 +159,7 @@ class RequestContext:
request_id: ty.Optional[str] = None,
resource_uuid: ty.Optional[str] = None,
overwrite: bool = True,
roles: ty.Optional[ty.List[str]] = None,
roles: ty.Optional[list[str]] = None,
user_name: ty.Optional[str] = None,
project_name: ty.Optional[str] = None,
domain_name: ty.Optional[str] = None,
@@ -175,7 +175,7 @@ class RequestContext:
service_project_name: ty.Optional[str] = None,
service_project_domain_id: ty.Optional[str] = None,
service_project_domain_name: ty.Optional[str] = None,
service_roles: ty.Optional[ty.List[str]] = None,
service_roles: ty.Optional[list[str]] = None,
global_request_id: ty.Optional[str] = None,
system_scope: ty.Optional[str] = None,
):
@@ -254,23 +254,25 @@ class RequestContext:
# deprecated policy values that trigger a warning when used in favour
# of our standard ones. This object acts like a dict but only values
# from oslo.policy don't show a warning.
return _DeprecatedPolicyValues({
'user_id': self.user_id,
'user_domain_id': self.user_domain_id,
'system_scope': self.system_scope,
'domain_id': self.domain_id,
'project_id': self.project_id,
'project_domain_id': self.project_domain_id,
'roles': self.roles,
'is_admin_project': self.is_admin_project,
'service_user_id': self.service_user_id,
'service_user_domain_id': self.service_user_domain_id,
'service_project_id': self.service_project_id,
'service_project_domain_id': self.service_project_domain_id,
'service_roles': self.service_roles,
})
return _DeprecatedPolicyValues(
{
'user_id': self.user_id,
'user_domain_id': self.user_domain_id,
'system_scope': self.system_scope,
'domain_id': self.domain_id,
'project_id': self.project_id,
'project_domain_id': self.project_domain_id,
'roles': self.roles,
'is_admin_project': self.is_admin_project,
'service_user_id': self.service_user_id,
'service_user_domain_id': self.service_user_domain_id,
'service_project_id': self.service_project_id,
'service_project_domain_id': self.service_project_domain_id,
'service_roles': self.service_roles,
}
)
def to_dict(self) -> ty.Dict[str, ty.Any]:
def to_dict(self) -> dict[str, ty.Any]:
"""Return a dictionary of context attributes."""
user_idt = self.user_idt_format.format(
user=self.user_id or '-',
@@ -280,36 +282,40 @@ class RequestContext:
p_domain=self.project_domain_id or '-',
)
return {'user': self.user_id,
'project_id': self.project_id,
'system_scope': self.system_scope,
'project': self.project_id,
'domain': self.domain_id,
'user_domain': self.user_domain_id,
'project_domain': self.project_domain_id,
'is_admin': self.is_admin,
'read_only': self.read_only,
'show_deleted': self.show_deleted,
'auth_token': self.auth_token,
'request_id': self.request_id,
'global_request_id': self.global_request_id,
'resource_uuid': self.resource_uuid,
'roles': self.roles,
'user_identity': user_idt,
'is_admin_project': self.is_admin_project}
return {
'user': self.user_id,
'project_id': self.project_id,
'system_scope': self.system_scope,
'project': self.project_id,
'domain': self.domain_id,
'user_domain': self.user_domain_id,
'project_domain': self.project_domain_id,
'is_admin': self.is_admin,
'read_only': self.read_only,
'show_deleted': self.show_deleted,
'auth_token': self.auth_token,
'request_id': self.request_id,
'global_request_id': self.global_request_id,
'resource_uuid': self.resource_uuid,
'roles': self.roles,
'user_identity': user_idt,
'is_admin_project': self.is_admin_project,
}
def get_logging_values(self) -> ty.Dict[str, ty.Any]:
def get_logging_values(self) -> dict[str, ty.Any]:
"""Return a dictionary of logging specific context attributes."""
values = {'user_name': self.user_name,
'project_name': self.project_name,
'domain_name': self.domain_name,
'user_domain_name': self.user_domain_name,
'project_domain_name': self.project_domain_name}
values = {
'user_name': self.user_name,
'project_name': self.project_name,
'domain_name': self.domain_name,
'user_domain_name': self.user_domain_name,
'project_domain_name': self.project_domain_name,
}
values.update(self.to_dict())
if self.auth_token:
# NOTE(jaosorior): Gotta obfuscate the token since this dict is
# meant for logging and we shouldn't leak it.
values['auth_token'] = '***' # nosec
values['auth_token'] = '***' # noqa: S105
else:
values['auth_token'] = None
# NOTE(bnemec: auth_token_info isn't defined in oslo.context, but it's
@@ -362,12 +368,12 @@ class RequestContext:
global_request_id=self.global_request_id,
system_scope=self.system_scope,
is_admin=self.is_admin,
**kwargs
**kwargs,
)
@classmethod
def from_dict(
cls, values: ty.Dict[str, ty.Any], **kwargs: ty.Any,
cls, values: dict[str, ty.Any], **kwargs: ty.Any
) -> ty_ext.Self:
"""Construct a context object from a provided dictionary."""
kwargs.setdefault('auth_token', values.get('auth_token'))
@@ -387,10 +393,12 @@ class RequestContext:
kwargs.setdefault('project_name', values.get('project_name'))
kwargs.setdefault('domain_name', values.get('domain_name'))
kwargs.setdefault('user_domain_name', values.get('user_domain_name'))
kwargs.setdefault('project_domain_name',
values.get('project_domain_name'))
kwargs.setdefault('is_admin_project',
values.get('is_admin_project', True))
kwargs.setdefault(
'project_domain_name', values.get('project_domain_name')
)
kwargs.setdefault(
'is_admin_project', values.get('is_admin_project', True)
)
kwargs.setdefault('system_scope', values.get('system_scope'))
for key in cls.FROM_DICT_EXTRA_KEYS:
kwargs.setdefault(key, values.get(key))
@@ -398,7 +406,7 @@ class RequestContext:
@classmethod
def from_environ(
cls, environ: ty.Dict[str, ty.Any], **kwargs: ty.Any,
cls, environ: dict[str, ty.Any], **kwargs: ty.Any
) -> ty_ext.Self:
"""Load a context object from a request environment.
@@ -444,18 +452,20 @@ class RequestContext:
def get_admin_context(show_deleted: bool = False) -> RequestContext:
"""Create an administrator context."""
context = RequestContext(None,
project_id=None,
is_admin=True,
show_deleted=show_deleted,
overwrite=False)
context = RequestContext(
None,
project_id=None,
is_admin=True,
show_deleted=show_deleted,
overwrite=False,
)
return context
def get_context_from_function_and_args(
function: ty.Callable[..., ty.Any],
args: ty.List[ty.Any],
kwargs: ty.Dict[str, ty.Any],
args: list[ty.Any],
kwargs: dict[str, ty.Any],
) -> ty.Optional[RequestContext]:
"""Find an arg of type RequestContext and return it.

View File

@@ -13,11 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import fixtures
import hashlib
import uuid
import warnings
import fixtures
from oslotest import base as test_base
from oslo_context import context
@@ -29,7 +29,6 @@ def generate_id(name):
class WarningsFixture(fixtures.Fixture):
def __init__(self, action="always", category=DeprecationWarning):
super().__init__()
self.action = action
@@ -59,6 +58,7 @@ class TestContext(context.RequestContext):
This is representative of how at least some of our consumers use the
RequestContext class in their projects.
"""
FROM_DICT_EXTRA_KEYS = ['auth_token_info']
def __init__(self, auth_token_info=None, **kwargs):
@@ -72,7 +72,6 @@ class TestContext(context.RequestContext):
class ContextTest(test_base.BaseTestCase):
def setUp(self):
super().setUp()
self.warnings = self.useFixture(WarningsFixture())
@@ -143,7 +142,7 @@ class ContextTest(test_base.BaseTestCase):
"global_request_id": "req-uuid",
"resource_uuid": "instance1",
"extra_data": "foo",
"system_scope": "all"
"system_scope": "all",
}
ctx = context.RequestContext.from_dict(dct)
self.assertEqual(dct['auth_token'], ctx.auth_token)
@@ -172,7 +171,7 @@ class ContextTest(test_base.BaseTestCase):
"read_only": True,
"roles": "role1,role2,role3", # future review provides this
"color": "red",
"unknown": ""
"unknown": "",
}
ctx = context.RequestContext.from_dict(dct)
self.assertEqual("token1", ctx.auth_token)
@@ -189,7 +188,7 @@ class ContextTest(test_base.BaseTestCase):
"read_only": True,
"roles": "role1,role2,role3",
"color": "red",
"unknown": ""
"unknown": "",
}
ctx = context.RequestContext.from_dict(
dct,
@@ -289,14 +288,17 @@ class ContextTest(test_base.BaseTestCase):
self.assertEqual(service_user_id, ctx.service_user_id)
self.assertEqual(service_user_name, ctx.service_user_name)
self.assertEqual(service_user_domain_id, ctx.service_user_domain_id)
self.assertEqual(service_user_domain_name,
ctx.service_user_domain_name)
self.assertEqual(
service_user_domain_name, ctx.service_user_domain_name
)
self.assertEqual(service_project_id, ctx.service_project_id)
self.assertEqual(service_project_name, ctx.service_project_name)
self.assertEqual(service_project_domain_id,
ctx.service_project_domain_id)
self.assertEqual(service_project_domain_name,
ctx.service_project_domain_name)
self.assertEqual(
service_project_domain_id, ctx.service_project_domain_id
)
self.assertEqual(
service_project_domain_name, ctx.service_project_domain_name
)
self.assertEqual(service_roles, ctx.service_roles)
def test_from_environ_no_roles(self):
@@ -338,37 +340,36 @@ class ContextTest(test_base.BaseTestCase):
new = uuid.uuid4().hex
override = uuid.uuid4().hex
environ = {'HTTP_X_USER': old,
'HTTP_X_USER_ID': new}
environ = {'HTTP_X_USER': old, 'HTTP_X_USER_ID': new}
ctx = context.RequestContext.from_environ(environ=environ)
self.assertEqual(new, ctx.user_id)
ctx = context.RequestContext.from_environ(
environ=environ, user_id=override,
environ=environ, user_id=override
)
self.assertEqual(override, ctx.user_id)
environ = {'HTTP_X_TENANT': old,
'HTTP_X_PROJECT_ID': new}
environ = {'HTTP_X_TENANT': old, 'HTTP_X_PROJECT_ID': new}
ctx = context.RequestContext.from_environ(environ=environ)
self.assertEqual(new, ctx.project_id)
ctx = context.RequestContext.from_environ(
environ=environ, project_id=override,
environ=environ, project_id=override
)
self.assertEqual(override, ctx.project_id)
environ = {'HTTP_X_TENANT_NAME': old,
'HTTP_X_PROJECT_NAME': new}
environ = {'HTTP_X_TENANT_NAME': old, 'HTTP_X_PROJECT_NAME': new}
ctx = context.RequestContext.from_environ(environ=environ)
self.assertEqual(new, ctx.project_name)
def test_from_environ_strip_roles(self):
environ = {'HTTP_X_ROLES': ' abc\t,\ndef\n,ghi\n\n',
'HTTP_X_SERVICE_ROLES': ' jkl\t,\nmno\n,pqr\n\n'}
environ = {
'HTTP_X_ROLES': ' abc\t,\ndef\n,ghi\n\n',
'HTTP_X_SERVICE_ROLES': ' jkl\t,\nmno\n,pqr\n\n',
}
ctx = context.RequestContext.from_environ(environ=environ)
self.assertEqual(['abc', 'def', 'ghi'], ctx.roles)
self.assertEqual(['jkl', 'mno', 'pqr'], ctx.service_roles)
@@ -491,8 +492,7 @@ class ContextTest(test_base.BaseTestCase):
self.assertEqual(show_deleted, d['show_deleted'])
self.assertEqual(request_id, d['request_id'])
self.assertEqual(resource_uuid, d['resource_uuid'])
user_identity = "{} {} {} {} {}".format(
user_id, project_id, domain_id, user_domain_id, project_domain_id)
user_identity = f"{user_id} {project_id} {domain_id} {user_domain_id} {project_domain_id}"
self.assertEqual(user_identity, d['user_identity'])
self.assertEqual([], d['roles'])
@@ -535,7 +535,8 @@ class ContextTest(test_base.BaseTestCase):
auth_token_info={'auth_token': 'topsecret'},
service_token="1234567",
auth_token="8901234",
user_id=userid)
user_id=userid,
)
safe_ctxt = ctx.redacted_copy()
self.assertIsNone(safe_ctxt.auth_token_info)
self.assertIsNone(safe_ctxt.service_token)

View File

@@ -17,7 +17,6 @@ from oslo_context import fixture
class ClearRequestContextTest(test_base.BaseTestCase):
# def setUp(self):
# super(ContextTest, self).setUp()
# self.useFixture(fixture.ClearRequestContext())

View File

@@ -1,3 +1,16 @@
[tool.ruff]
line-length = 79
[tool.ruff.format]
quote-style = "preserve"
docstring-code-format = true
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "S", "U"]
[tool.ruff.lint.per-file-ignores]
"oslo_context/tests/*" = ["S"]
[tool.mypy]
python_version = "3.9"
show_column_numbers = true

View File

@@ -186,9 +186,13 @@ htmlhelp_basename = 'oslo.contextReleaseNotesDoc'
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'oslo.contextReleaseNotes.tex',
'oslo.context Release Notes Documentation',
'oslo.context Developers', 'manual'),
(
'index',
'oslo.contextReleaseNotes.tex',
'oslo.context Release Notes Documentation',
'oslo.context Developers',
'manual',
),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -216,9 +220,13 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'oslo.contextReleaseNotes',
'oslo.context Release Notes Documentation',
['oslo.context Developers'], 1)
(
'index',
'oslo.contextReleaseNotes',
'oslo.context Release Notes Documentation',
['oslo.context Developers'],
1,
)
]
# If true, show URL addresses after external links.
@@ -230,11 +238,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'oslo.contextReleaseNotes',
'oslo.context Release Notes Documentation',
'oslo.context Developers', 'oslo.contextReleaseNotes',
'One line description of project.',
'Miscellaneous'),
(
'index',
'oslo.contextReleaseNotes',
'oslo.context Release Notes Documentation',
'oslo.context Developers',
'oslo.contextReleaseNotes',
'One line description of project.',
'Miscellaneous',
),
]
# Documents to append as an appendix to all manuals.

View File

@@ -17,4 +17,5 @@ import setuptools
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)
pbr=True,
)

10
tox.ini
View File

@@ -46,10 +46,12 @@ commands =
coverage xml -o cover/coverage.xml
[flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = true
ignore = E123,E125
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
# We only enable the hacking (H) checks
select = H
# H301 Black will put commas after imports that can't fit on one line
# H404 Docstrings don't always start with a newline
# H405 Multiline docstrings are okay
ignore = H301,H403,H404,H405
[hacking]
import_exceptions =