diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44707b3..3e2113e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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] diff --git a/doc/source/conf.py b/doc/source/conf.py index cc95df1..f625568 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -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 diff --git a/doc/source/user/examples/usage.py b/doc/source/user/examples/usage.py index e2c3e04..b34cd72 100644 --- a/doc/source/user/examples/usage.py +++ b/doc/source/user/examples/usage.py @@ -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) diff --git a/doc/source/user/examples/usage_user_identity.py b/doc/source/user/examples/usage_user_identity.py index 056bc40..af40bec 100644 --- a/doc/source/user/examples/usage_user_identity.py +++ b/doc/source/user/examples/usage_user_identity.py @@ -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") diff --git a/oslo_context/context.py b/oslo_context/context.py index 3bae3ee..09133e2 100644 --- a/oslo_context/context.py +++ b/oslo_context/context.py @@ -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. diff --git a/oslo_context/tests/test_context.py b/oslo_context/tests/test_context.py index 1595830..5c0b1d9 100644 --- a/oslo_context/tests/test_context.py +++ b/oslo_context/tests/test_context.py @@ -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) diff --git a/oslo_context/tests/test_fixture.py b/oslo_context/tests/test_fixture.py index e566b52..598c54b 100644 --- a/oslo_context/tests/test_fixture.py +++ b/oslo_context/tests/test_fixture.py @@ -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()) diff --git a/pyproject.toml b/pyproject.toml index f292f69..a5c07db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 570ddf1..5c0e20c 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -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. diff --git a/setup.py b/setup.py index cd35c3c..e5bafb0 100644 --- a/setup.py +++ b/setup.py @@ -17,4 +17,5 @@ import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], - pbr=True) + pbr=True, +) diff --git a/tox.ini b/tox.ini index 857b8f0..c20c196 100644 --- a/tox.ini +++ b/tox.ini @@ -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 =