From c7fedebcf9c6653b0338b7558b5ca44f1ae23344 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Tue, 1 Jul 2025 19:25:36 +0200 Subject: [PATCH] Use ruff for linting --- .editorconfig | 12 ++ .isort.cfg | 4 - pre-commit.githook | 65 --------- pyproject.toml | 2 +- ruff.toml | 150 +++++++++++++++++++++ setup.cfg | 2 - tox.ini | 8 +- ursh/__init__.py | 1 - ursh/blueprints/__init__.py | 9 +- ursh/blueprints/api/blueprint.py | 16 ++- ursh/blueprints/api/handlers.py | 6 +- ursh/blueprints/api/resources.py | 9 +- ursh/blueprints/misc.py | 1 - ursh/blueprints/redirection.py | 1 - ursh/cli/core.py | 5 +- ursh/cli/key.py | 22 ++-- ursh/cli/util.py | 2 +- ursh/core/app.py | 3 +- ursh/core/db.py | 3 +- ursh/models.py | 5 +- ursh/schemas.py | 2 + ursh/testing/conftest.py | 32 ++--- ursh/testing/test_resources.py | 220 +++++++++++++++---------------- ursh/util/db.py | 4 +- ursh/util/nested_query_parser.py | 5 +- ursh/wsgi.py | 3 +- 26 files changed, 340 insertions(+), 252 deletions(-) create mode 100644 .editorconfig delete mode 100644 .isort.cfg delete mode 100755 pre-commit.githook create mode 100644 ruff.toml delete mode 100644 setup.cfg diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0d24981 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root=true + +[*] +indent_style=space +indent_size=4 +end_of_line=lf +charset=utf-8 +trim_trailing_whitespace=true +insert_final_newline=true + +[{*.yml,*.yaml,*.json}] +indent_size=2 diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 0457a7d..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[isort] -line_length=120 -multi_line_output=0 -lines_after_imports=2 diff --git a/pre-commit.githook b/pre-commit.githook deleted file mode 100755 index d82133a..0000000 --- a/pre-commit.githook +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -# vim: et ts=4 sw=4 ft=sh - -# Interesting post on max line length: -# http://stackoverflow.com/questions/88942/why-should-python-pep-8-specify-a-maximum-line-length-of-79-characters - -PEP8_CMD='flake8' -PEP8_OPTIONS='--max-line-length=120' - - -RED=$(echo -e $"\033[1;31m") -YELLOW=$(echo -e $"\033[0;33m") -CYAN=$(echo -e $"\033[0;36m") -RESET=$(echo -e $"\033[0;0m") -BRIGHTYELLOW=$(echo -e $"\033[1;33m") -WHITE=$(echo -e $"\033[1;37m") - -RE="s/\([^:]*\):\([0-9]*\):\([0-9]*\): \([EW][0-9]*\) \(.*\)/$WHITE[$CYAN\1$RESET $BRIGHTYELLOW\2:\3$WHITE] $RED\4 $YELLOW\5$RESET/g" -STATUS=0 - -_get_files() { - local i - unset FILES - while IFS= read -r -d $'\0' file; do - FILES[i++]="$file" - done < <(git diff --name-only --diff-filter=ACMR --staged -z "$1") -} - -# Flake8 -if ! RESULT=$(flake8 $(git status -s | grep -E '\.py$' | cut -c 4-)); then - echo "${RED}There are PEP8 issues in your code:${RESET}" - STATUS=1 -fi -if [[ -n "$RESULT" ]] ; then - echo "$RESULT" | sed -e "$RE" - echo -fi - - -# isort -if ! RESULT=$(python -c 'import sys; from isort.hooks import git_hook; sys.exit(git_hook(strict=True))'); then - STATUS=1 - echo "${RED}There are isort issues in your code:${RESET}" - echo "$RESULT" - echo - echo "${CYAN}Run this command to sort the imports:${RESET}" - echo "git diff --staged --name-only --diff-filter '*.py' | xargs isort" -fi - - -if [[ $STATUS != 0 ]] ; then - # claim stdin back - exec < /dev/tty - echo - read -r -p "${RED}Do you wish to commit it anyway ${CYAN}[${WHITE}y${CYAN}/${WHITE}N${CYAN}]${RESET}? " yn - case $yn in - [Yy]* ) exit 0;; - [Nn]* ) exit $STATUS;; - * ) exit $STATUS;; - esac - # close stdin - exec <&- -fi - -exit $STATUS diff --git a/pyproject.toml b/pyproject.toml index 630edd6..fe2c1e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ requires-python = '~=3.12' ursh = 'ursh.cli.core:cli' [project.optional-dependencies] -dev = ['pytest', 'isort'] +dev = ['pytest', 'ruff'] [project.urls] Issues = 'https://github.com/indico/ursh/issues' diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..2075e57 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,150 @@ +target-version = 'py312' +line-length = 120 + +[lint] +preview = true + +select = [ + 'E', # pycodestyle (errors) + 'W', # pycodestyle (warnings) + 'F', # pyflakes + 'N', # pep8-naming + 'Q', # flake8-quotes + 'RUF', # ruff + 'UP', # pyupgrade + 'D', # pydocstyle + 'S', # flake8-bandit + 'C4', # flake8-comprehensions + 'INT', # flake8-gettext + 'LOG', # flake8-logging + 'G', # flake8-logging-format + 'B', # flake8-bugbear + 'A001', # flake8-builtins + 'COM', # flake8-commas + 'T10', # flake8-debugger + 'EXE', # flake8-executable + 'ISC', # flake8-implicit-str-concat + 'PIE', # flake8-pie + 'PT', # flake8-pytest-style + 'RSE', # flake8-raise + 'RET504', # flake8-return + 'SIM', # flake8-simplify + 'TID', # flake8-tidy-imports + 'PGH', # pygrep-hooks + 'PL', # pylint + 'TRY', # tryceratops + 'PERF', # perflint + 'FURB', # refurb + 'I', # isort +] +ignore = [ + 'E226', # allow omitting whitespace around arithmetic operators + 'E731', # allow assigning lambdas (it's useful for single-line functions defined inside other functions) + 'N818', # not all our exceptions are errors + 'RUF012', # ultra-noisy and dicts in classvars are very common + 'RUF015', # not always more readable, and we don't do it for huge lists + 'RUF022', # autofix messes up our formatting instead of just sorting + 'UP038', # it looks kind of weird and it slower than a tuple + 'D205', # too many docstrings which have no summary line + 'D301', # https://github.com/astral-sh/ruff/issues/8696 + 'D1', # we have way too many missing docstrings :( + 'D400', # weird with openapi docstrings + 'D412', # we do not use section, and in click docstrings those blank lines are useful + 'S101', # we use asserts outside tests, and do not run python with `-O` (also see B011) + 'S113', # enforcing timeouts would likely require config in some places - maybe later + 'S311', # false positives, it does not care about the context + 'S324', # all our md5/sha1 usages are for non-security purposes + 'S404', # useless, triggers on *all* subprocess imports + 'S403', # there's already a warning on using pickle, no need to have one for the import + 'S405', # we don't use lxml in unsafe ways + 'S603', # useless, triggers on *all* subprocess calls: https://github.com/astral-sh/ruff/issues/4045 + 'S607', # we trust the PATH to be sane + 'B011', # we don't run python with `-O` (also see S101) + 'B904', # possibly useful but too noisy + 'PIE807', # `lambda: []` is much clearer for `load_default` in schemas + 'PT011', # very noisy + 'PT015', # nice for tests but not so nice elsewhere + 'PT018', # ^ likewise + 'SIM102', # sometimes nested ifs are more readable + 'SIM103', # sometimes this is more readable (especially when checking multiple conditions) + 'SIM105', # try-except-pass is faster and people are used to it + 'SIM108', # noisy ternary + 'SIM114', # sometimes separate ifs are more readable (especially if they just return a bool) + 'SIM117', # nested context managers may be more readable + 'PLC0415', # local imports are there for a reason + 'PLC2701', # some private imports are needed + 'PLR09', # too-many- is just noisy + 'PLR0913', # very noisy + 'PLR2004', # extremely noisy and generally annoying + 'PLR6201', # sets are faster (by a factor of 10!) but it's noisy and we're in nanoseconds territory + 'PLR6301', # extremely noisy and generally annoying + 'PLW0108', # a lambda often makes it more clear what you actually want + 'PLW1510', # we often do not care about the status code of commands + 'PLW1514', # we expect UTF8 environments everywhere + 'PLW1641', # false positives with SA comparator classes + 'PLW2901', # noisy and reassigning to the loop var is usually intentional + 'TRY002', # super noisy, and those exceptions are pretty exceptional anyway + 'TRY003', # super noisy and also useless w/ werkzeugs http exceptions + 'TRY300', # kind of strange in many cases + 'TRY301', # sometimes doing that is actually useful + 'TRY400', # not all exceptions need exception logging + 'PERF203', # noisy, false positives, and not applicable for 3.11+ + 'FURB113', # less readable + 'FURB140', # less readable and actually slower in 3.12+ + 'PLW0603', # globals are fine here + 'COM812', # formatter conflict + 'ISC001', # formatter conflict +] + +extend-safe-fixes = [ + 'RUF005', # we typically don't deal with objects overriding `__add__` or `__radd__` + 'C4', # they seem pretty safe + 'UP008', # ^ likewise + 'D200', # ^ likewise + 'D400', # ^ likewise + 'PT014', # duplicate test case parametrizations are never intentional + 'RSE102', # we do not use `raise func()` (with `func` returning the exception instance) + 'RET504', # looks pretty safe + 'SIM110', # ^ likewise + 'PERF102', # ^ likewise +] + +[format] +quote-style = 'single' + +[lint.flake8-builtins] +builtins-ignorelist = ['id', 'format', 'input', 'type', 'credits'] + +[lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false +parametrize-names-type = 'tuple' +parametrize-values-type = 'tuple' +parametrize-values-row-type = 'tuple' + +[lint.flake8-tidy-imports] +ban-relative-imports = 'all' + +[lint.flake8-quotes] +inline-quotes = 'single' +multiline-quotes = 'double' +docstring-quotes = 'double' +avoid-escape = true + +[lint.pep8-naming] +classmethod-decorators = [ + 'classmethod', + 'declared_attr', + 'expression', + 'comparator', +] + +[lint.pydocstyle] +convention = 'pep257' + +[lint.ruff] +parenthesize-tuple-in-subscript = true + +[lint.per-file-ignores] +# signals use wildcard imports to expose everything in `indico.core.signals` +'ursh/cli/*.py' = ['D401'] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6deafc2..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 120 diff --git a/tox.ini b/tox.ini index 6769ac0..1a28798 100644 --- a/tox.ini +++ b/tox.ini @@ -11,9 +11,5 @@ passenv = URSH_TEST_DATABASE_URI [testenv:style] skip_install = true -deps = - flake8 - isort -commands = - isort --diff --check-only ursh - flake8 ursh/ +deps = ruff +commands = ruff check --output-format github . diff --git a/ursh/__init__.py b/ursh/__init__.py index a6388c3..8939cd1 100644 --- a/ursh/__init__.py +++ b/ursh/__init__.py @@ -1,5 +1,4 @@ from ursh.core.db import db - __version__ = '0.0.dev0' __all__ = ('db',) diff --git a/ursh/blueprints/__init__.py b/ursh/blueprints/__init__.py index 4c8f575..7a426e8 100644 --- a/ursh/blueprints/__init__.py +++ b/ursh/blueprints/__init__.py @@ -1,6 +1,5 @@ -from .api.blueprint import bp as api -from .misc import bp as misc -from .redirection import bp as redirection +from ursh.blueprints.api.blueprint import bp as api +from ursh.blueprints.misc import bp as misc +from ursh.blueprints.redirection import bp as redirection - -__all__ = [redirection, api, misc] +__all__ = ['redirection', 'api', 'misc'] diff --git a/ursh/blueprints/api/blueprint.py b/ursh/blueprints/api/blueprint.py index fb4dee5..5155b74 100644 --- a/ursh/blueprints/api/blueprint.py +++ b/ursh/blueprints/api/blueprint.py @@ -1,16 +1,22 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from flask import Blueprint, g, request from sqlalchemy.exc import DataError, SQLAlchemyError from werkzeug.exceptions import BadRequest, Conflict, MethodNotAllowed, NotFound from ursh import db -from ursh.blueprints.api.handlers import (create_error_json, handle_bad_requests, handle_conflict, handle_db_errors, - handle_internal_exceptions, handle_method_not_allowed, handle_not_found) +from ursh.blueprints.api.handlers import ( + create_error_json, + handle_bad_requests, + handle_conflict, + handle_db_errors, + handle_internal_exceptions, + handle_method_not_allowed, + handle_not_found, +) from ursh.blueprints.api.resources import TokenResource, URLResource from ursh.models import Token - bp = Blueprint('urls', __name__, url_prefix='/api') tokens_view = TokenResource.as_view('tokens') @@ -35,7 +41,7 @@ def authorize_request(): if token is None or token.is_blocked: return error_json token.token_uses = Token.token_uses + 1 - token.last_access = datetime.now(timezone.utc) + token.last_access = datetime.now(UTC) db.session.commit() g.token = token diff --git a/ursh/blueprints/api/handlers.py b/ursh/blueprints/api/handlers.py index c8e9abf..380f6e4 100644 --- a/ursh/blueprints/api/handlers.py +++ b/ursh/blueprints/api/handlers.py @@ -22,7 +22,7 @@ def handle_conflict(error): def handle_internal_exceptions(error): - current_app.logger.exception('Unexpected error') + current_app.logger.exception('Unexpected error') # noqa: LOG004 return create_error_json(500, 'internal-error', 'Sorry, something went wrong') @@ -31,8 +31,8 @@ def create_error_json(status_code, error_code, message, **kwargs): 'status': status_code, 'error': { 'code': error_code, - 'description': message - } + 'description': message, + }, } message_dict['error'].update(kwargs) return jsonify(message_dict), status_code diff --git a/ursh/blueprints/api/resources.py b/ursh/blueprints/api/resources.py index a4b6a98..6024c5f 100644 --- a/ursh/blueprints/api/resources.py +++ b/ursh/blueprints/api/resources.py @@ -2,6 +2,7 @@ from flask import Response, current_app, g from flask_apispec import MethodResource, marshal_with, use_kwargs +from marshmallow import fields from sqlalchemy.exc import IntegrityError from werkzeug.exceptions import BadRequest, Conflict, MethodNotAllowed, NotFound @@ -31,6 +32,7 @@ class TokenResource(MethodResource): 401: description: not authorized """ + @admin_only @marshal_with(TokenSchema, code=201) @use_kwargs(TokenSchema) @@ -270,8 +272,7 @@ def get(self, api_key=None, **kwargs): if not api_key: filter_params = ['name', 'is_admin', 'is_blocked', 'callback_url'] filter_dict = {key: value for key, value in kwargs.items() if key in filter_params} - tokens = Token.query.filter_by(**filter_dict) - return tokens + return Token.query.filter_by(**filter_dict).all() try: UUID(api_key) except ValueError: @@ -302,6 +303,7 @@ class URLResource(MethodResource): 401: description: not authorized """ + @marshal_with(URLSchema, code=201) @use_kwargs(URLSchema) @use_kwargs(ShortcutSchemaRestricted, location='view_args') @@ -566,6 +568,9 @@ def delete(self, shortcut=None, **kwargs): @marshal_many_or_one(URLSchema, 'shortcut', code=200) @use_kwargs(URLSchema, location='query') + @use_kwargs({ + 'all': fields.Boolean(load_default=False), + }, location='query') @authorize_request_for_url def get(self, shortcut=None, **kwargs): """Obtain one or more URL objects. diff --git a/ursh/blueprints/misc.py b/ursh/blueprints/misc.py index e8c264e..9009d45 100644 --- a/ursh/blueprints/misc.py +++ b/ursh/blueprints/misc.py @@ -1,6 +1,5 @@ from flask import Blueprint, Response, current_app, redirect - bp = Blueprint('misc', __name__) diff --git a/ursh/blueprints/redirection.py b/ursh/blueprints/redirection.py index f513b9d..867d399 100644 --- a/ursh/blueprints/redirection.py +++ b/ursh/blueprints/redirection.py @@ -2,7 +2,6 @@ from ursh.models import URL - bp = Blueprint('redirection', __name__) diff --git a/ursh/cli/core.py b/ursh/cli/core.py index 9dd5221..419d34f 100644 --- a/ursh/cli/core.py +++ b/ursh/cli/core.py @@ -3,7 +3,6 @@ from ursh.cli.util import LazyGroup - _cli = AppGroup() cli_command = _cli.command cli_group = _cli.group @@ -14,7 +13,7 @@ def _get_ursh_version(ctx, param, value): if not value or ctx.resilient_parsing: return import ursh - message = 'ursh v{}'.format(ursh.__version__) + message = f'ursh v{ursh.__version__}' click.echo(message, ctx.color) ctx.exit() @@ -26,7 +25,7 @@ def _create_app(): @click.group(cls=FlaskGroup, create_app=_create_app) def cli(): - """ursh command line interface.""" + """ursh command line interface.""" # noqa: D403 @cli.group(cls=LazyGroup, import_name='ursh.cli.database:cli') diff --git a/ursh/cli/key.py b/ursh/cli/key.py index a3bbafe..b02f0e8 100644 --- a/ursh/cli/key.py +++ b/ursh/cli/key.py @@ -15,30 +15,30 @@ def _print_usage(command): def _success(msg): - click.echo('\n[SUCCESS] {}'.format(msg)) + click.echo(f'\n[SUCCESS] {msg}') sys.exit(0) def _failure(msg): - click.echo('\n[FAILURE] {}'.format(msg)) + click.echo(f'\n[FAILURE] {msg}') sys.exit(1) def _print_api_key(token): role = 'admin' if token.is_admin else 'user' - click.echo('Name: {name}'.format(name=token.name)) - click.echo('Role: {role}'.format(role=role)) - click.echo('API key: {key}'.format(key=token.api_key)) - click.echo('Blocked: {blocked}'.format(blocked=token.is_blocked)) + click.echo(f'Name: {token.name}') + click.echo(f'Role: {role}') + click.echo(f'API key: {token.api_key}') + click.echo(f'Blocked: {token.is_blocked}') def _create_api_key(role, name, blocked): - token = Token(name=name, is_admin=True if role == 'admin' else False, is_blocked=blocked) + token = Token(name=name, is_admin=(role == 'admin'), is_blocked=blocked) try: db.session.add(token) db.session.commit() except IntegrityError: - _failure('An API key with the same name ("{name}") already exists.'.format(name=name)) + _failure(f'An API key with the same name ("{name}") already exists.') _print_api_key(token) if blocked: _success('The above listed API key is blocked - you will not be able to use it until it is unblocked.') @@ -61,7 +61,7 @@ def _toggle_api_key_block(blocked, **kwargs): def _validate_filters_or_die(filters, command): if not any(filters.values()): _print_usage(command) - _failure('{method}: please specify at least one option.'.format(method=command.name)) + _failure(f'{command.name}: please specify at least one option.') @cli_group() @@ -110,8 +110,8 @@ def get(**kwargs): _failure('No API key was found for the specified filters.') -@cli.command() -def list(): +@cli.command('list') +def list_(): """List all API keys.""" tokens = Token.query.order_by(Token.name).all() for token in tokens: diff --git a/ursh/cli/util.py b/ursh/cli/util.py index e4c2c5a..1c2d10a 100644 --- a/ursh/cli/util.py +++ b/ursh/cli/util.py @@ -14,7 +14,7 @@ class LazyGroup(click.Group): def __init__(self, import_name, **kwargs): self._import_name = import_name - super(LazyGroup, self).__init__(**kwargs) + super().__init__(**kwargs) @cached_property def _impl(self): diff --git a/ursh/core/app.py b/ursh/core/app.py index f0e6380..3746983 100644 --- a/ursh/core/app.py +++ b/ursh/core/app.py @@ -15,7 +15,6 @@ from ursh.util.db import import_all_models from ursh.util.nested_query_parser import NestedQueryParser - CONFIG_OPTIONS = { 'SQLALCHEMY_DATABASE_URI': 'str', 'USE_PROXY': 'bool', @@ -32,7 +31,7 @@ def create_app(config_file=None, testing=False): - """Factory to create the Flask application + """Create the Flask application. :param config_file: A python file from which to load the config. If omitted, the config file must be set using diff --git a/ursh/core/db.py b/ursh/core/db.py index af1596c..9b76d22 100644 --- a/ursh/core/db.py +++ b/ursh/core/db.py @@ -1,6 +1,5 @@ from flask_sqlalchemy import SQLAlchemy - _naming_convention = { 'fk': 'fk_%(table_name)s_%(column_names)s_%(referred_table_name)s', 'pk': 'pk_%(table_name)s', @@ -8,7 +7,7 @@ 'ck': 'ck_%(table_name)s_%(constraint_name)s', 'uq': 'uq_%(table_name)s_%(column_names)s', 'column_names': lambda constraint, t: '_'.join((c if isinstance(c, str) else c.name) for c in constraint.columns), - 'unique_index': lambda constraint, t: 'uq_' if constraint.unique else '' + 'unique_index': lambda constraint, t: 'uq_' if constraint.unique else '', } diff --git a/ursh/models.py b/ursh/models.py index 7eaeaf7..2bc7960 100644 --- a/ursh/models.py +++ b/ursh/models.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from random import choices from uuid import uuid4 @@ -8,7 +8,6 @@ from ursh import db - ALPHABET_MANUAL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-' ALPHABET_RESTRICTED = '23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ' @@ -22,7 +21,7 @@ class Token(db.Model): is_admin = db.Column(db.Boolean, nullable=False, default=False) is_blocked = db.Column(db.Boolean, nullable=False, default=False) token_uses = db.Column(db.Integer, nullable=False, default=0) - last_access = db.Column(UtcDateTime, nullable=False, default=lambda: datetime.now(tz=timezone.utc)) + last_access = db.Column(UtcDateTime, nullable=False, default=lambda: datetime.now(tz=UTC)) callback_url = db.Column(db.String, nullable=True) urls = db.relationship('URL', back_populates='token') diff --git a/ursh/schemas.py b/ursh/schemas.py index c70e2ce..8be4195 100644 --- a/ursh/schemas.py +++ b/ursh/schemas.py @@ -25,6 +25,7 @@ def handle_error(error, data, **kwargs): class TokenSchema(SchemaBase): """Schema class to validate tokens.""" + api_key = fields.Str(description='The token API key - uniquely identifies the token') name = fields.Str(description='The token name') is_admin = fields.Boolean(description='Is this an admin token?') @@ -39,6 +40,7 @@ class URLSchema(SchemaBase): Note: use one of the sub-classes below for validation, depending on the shortcut requirements. """ + shortcut = fields.Str(description='The generated or manually set URL shortcut') url = fields.URL(description='The original URL (the short URL target)') short_url = fields.Method('_get_short_url', description='The short URL') diff --git a/ursh/testing/conftest.py b/ursh/testing/conftest.py index 6db046a..613beb5 100644 --- a/ursh/testing/conftest.py +++ b/ursh/testing/conftest.py @@ -10,7 +10,6 @@ from ursh.core.app import create_app from ursh.core.db import db as db_ - POSTGRES_MIN_VERSION = (9, 6) POSTGRES_VERSION = (12,) POSTGRES_PREFIX = '/usr/lib/postgresql/{version}/bin'.format(version='.'.join(map(str, POSTGRES_VERSION))) @@ -18,36 +17,36 @@ @pytest.fixture(scope='session') def app(): - """Creates the flask app""" + """Create the flask app""" return create_app(testing=True) @pytest.fixture(autouse=True) def app_context(app): - """Creates a flask app context""" + """Create a flask app context""" with app.app_context(): yield app @pytest.fixture def request_context(app_context): - """Creates a flask request context""" + """Create a flask request context""" with app_context.test_request_context(): yield @pytest.fixture def client(app): - yield app.test_client() + return app.test_client() @pytest.fixture(scope='session') def postgresql(): - """Provides a clean temporary PostgreSQL server/database. + """ + Provide a clean temporary PostgreSQL server/database. If the environment variable `URSH_TEST_DATABASE_URI` is set, this fixture will do nothing and simply return the connection string from that variable """ - # Use existing database if 'URSH_TEST_DATABASE_URI' in os.environ: yield os.environ['URSH_TEST_DATABASE_URI'] @@ -67,22 +66,22 @@ def postgresql(): version_output = subprocess.check_output([initdb_command, '--version']).decode('utf-8') pg_version = tuple(map(int, re.match(r'.*\(PostgreSQL\) ((?:\d+\.?)+).*', version_output).group(1).split('.'))) if pg_version < POSTGRES_MIN_VERSION: - pytest.skip('PostgreSQL version is too old: {}'.format(version_output)) + pytest.skip(f'PostgreSQL version is too old: {version_output}') except Exception as e: - pytest.skip('Could not retrieve PostgreSQL version: {}'.format(e)) + pytest.skip(f'Could not retrieve PostgreSQL version: {e}') # Prepare server instance and a test database temp_dir = tempfile.mkdtemp(prefix='indicotestpg.') - postgres_args = '-h "" -k "{}"'.format(temp_dir) + postgres_args = f'-h "" -k "{temp_dir}"' try: subprocess.check_call([initdb_command, '--encoding', 'utf8', temp_dir]) subprocess.check_call([pg_ctl_command, '-D', temp_dir, '-w', '-o', postgres_args, 'start']) subprocess.check_call(['createdb', '-h', temp_dir, db_name]) except Exception as e: shutil.rmtree(temp_dir) - pytest.skip('could not init/start postgresql: {}'.format(e)) + pytest.skip(f'could not init/start postgresql: {e}') - yield 'postgresql:///{}?host={}'.format(db_name, temp_dir) + yield f'postgresql:///{db_name}?host={temp_dir}' try: subprocess.check_call([pg_ctl_command, '-D', temp_dir, '-m', 'immediate', 'stop']) @@ -92,16 +91,17 @@ def postgresql(): with open(os.path.join(temp_dir, 'postmaster.pid')) as f: pid = int(f.readline().strip()) os.kill(pid, signal.SIGKILL) - pytest.skip('Could not stop postgresql; killed it instead: {}'.format(e)) + pytest.skip(f'Could not stop postgresql; killed it instead: {e}') except Exception as e: - pytest.skip('Could not stop/kill postgresql: {}'.format(e)) + pytest.skip(f'Could not stop/kill postgresql: {e}') finally: shutil.rmtree(temp_dir) @pytest.fixture(scope='session') def database(app, postgresql): - """Creates a test database which is destroyed afterwards + """ + Create a test database which is destroyed afterwards Used only internally, if you need to access the database use `db` instead to ensure your modifications are not persistent! """ @@ -121,7 +121,7 @@ def database(app, postgresql): @pytest.fixture def db(database, monkeypatch): - """Provides database access and ensures changes do not persist""" + """Provide database access and ensures changes do not persist""" # Prevent database/session modifications monkeypatch.setattr(database.session, 'commit', database.session.flush) monkeypatch.setattr(database.session, 'remove', lambda: None) diff --git a/ursh/testing/test_resources.py b/ursh/testing/test_resources.py index eaea096..1e2c502 100644 --- a/ursh/testing/test_resources.py +++ b/ursh/testing/test_resources.py @@ -1,4 +1,5 @@ import posixpath +from operator import itemgetter from urllib.parse import urlparse from uuid import uuid4 @@ -24,20 +25,20 @@ def create_user(db, name, is_admin=False, is_blocked=False): # Token tests -@pytest.mark.parametrize("admin,data,expected,status_code", [ +@pytest.mark.parametrize(('admin', 'data', 'expected', 'status_code'), ( ( # new token is issued True, {'name': 'abc', 'is_admin': True, 'callback_url': 'http://cern.ch'}, {'callback_url': 'http://cern.ch', 'is_admin': True, 'is_blocked': False, 'name': 'abc', 'token_uses': 0}, - 201 + 201, ), ( # new token is issued without callback True, {'name': 'abc', 'is_admin': True}, {'is_admin': True, 'is_blocked': False, 'name': 'abc', 'token_uses': 0}, - 201 + 201, ), ( # name is not mentioned @@ -45,14 +46,14 @@ def create_user(db, name, is_admin=False, is_blocked=False): {'is_admin': True, 'callback_url': 'http://cern.ch'}, {'error': {'args': ['name'], 'code': 'missing-args', 'description': 'New tokens need to mention the "name" attribute'}, 'status': 400}, - 400 + 400, ), ( # invalid callback URL True, {'name': 'abc', 'callback_url': 'fake'}, {'error': {'code': 'validation-error', 'messages': {'callback_url': ['Not a valid URL.']}}, 'status': 400}, - 400 + 400, ), ( # non-admin attempt @@ -60,9 +61,9 @@ def create_user(db, name, is_admin=False, is_blocked=False): {'name': 'abc', 'callback_url': 'http://cern.ch'}, {'error': {'code': 'insufficient-permissions', 'description': 'You are not allowed to make this request'}, 'status': 403}, - 403 - ) -]) + 403, + ), +)) def test_create_token(db, client, admin, data, expected, status_code): if admin: auth = make_auth(db, 'admin', is_admin=True, is_blocked=False) @@ -91,7 +92,7 @@ def test_create_token_name_exists(db, client): assert response.get_json() == expected -@pytest.mark.parametrize("admin,url,data,expected,status", [ +@pytest.mark.parametrize(('admin', 'url', 'data', 'expected', 'status'), ( ( # filter based on name True, @@ -99,20 +100,19 @@ def test_create_token_name_exists(db, client): {'name': 'abc'}, [{'callback_url': 'http://cern.ch', 'is_admin': False, 'is_blocked': False, 'name': 'abc', 'token_uses': 0}], - 200 + 200, ), ( # get all tokens True, '/api/tokens/', {}, - [{'callback_url': None, 'is_admin': False, 'is_blocked': False, 'name': 'non-admin', 'token_uses': 0}, + [{'callback_url': None, 'is_admin': True, 'is_blocked': False, 'name': 'admin', 'token_uses': 5}, {'callback_url': 'http://cern.ch', 'is_admin': False, 'is_blocked': False, 'name': 'abc', 'token_uses': 0}, {'callback_url': 'http://a.ch', 'is_admin': True, 'is_blocked': True, 'name': 'abcd', 'token_uses': 0}, {'callback_url': 'http://a.ch', 'is_admin': False, 'is_blocked': True, 'name': 'abcd1', 'token_uses': 0}, - {'callback_url': 'http://b.ch', 'is_admin': False, 'is_blocked': False, 'name': 'abcde', 'token_uses': 0}, - {'callback_url': None, 'is_admin': True, 'is_blocked': False, 'name': 'admin', 'token_uses': 5}], - 200 + {'callback_url': 'http://b.ch', 'is_admin': False, 'is_blocked': False, 'name': 'abcde', 'token_uses': 0}], + 200, ), ( # attempt to get token info by non-admin @@ -121,7 +121,7 @@ def test_create_token_name_exists(db, client): {}, {'error': {'code': 'insufficient-permissions', 'description': 'You are not allowed to make this request'}, 'status': 403}, - 403 + 403, ), ( # attempt to get specific token info with invalid UUID @@ -130,7 +130,7 @@ def test_create_token_name_exists(db, client): {}, {'error': {'args': ['api_key'], 'code': 'not-found', 'description': 'API key does not exist'}, 'status': 404}, - 404 + 404, ), ( # access specific token from non-admin @@ -139,7 +139,7 @@ def test_create_token_name_exists(db, client): {}, {'error': {'code': 'insufficient-permissions', 'description': 'You are not allowed to make this request'}, 'status': 403}, - 403 + 403, ), ( # filter based on other parameters @@ -147,9 +147,9 @@ def test_create_token_name_exists(db, client): '/api/tokens/', {'callback_url': 'http://a.ch', 'is_admin': False}, [{'callback_url': 'http://a.ch', 'is_admin': False, 'is_blocked': True, 'name': 'abcd1', 'token_uses': 0}], - 200 - ) -]) + 200, + ), +)) def test_get_tokens(db, client, admin, url, data, expected, status): if admin: auth = make_auth(db, 'admin', is_admin=True, is_blocked=False) @@ -167,9 +167,9 @@ def test_get_tokens(db, client, admin, url, data, expected, status): assert response.status_code == status if isinstance(expected, list): - parsed_response = sorted(parsed_response, key=lambda k: k['name']) - expected = sorted(expected, key=lambda k: k['name']) - for expected_token, returned_token in zip(expected, parsed_response): + parsed_response = sorted(parsed_response, key=itemgetter('name')) + expected = sorted(expected, key=itemgetter('name')) + for expected_token, returned_token in zip(expected, parsed_response, strict=True): for key, value in expected_token.items(): assert value == returned_token[key] if status == 200: @@ -183,14 +183,14 @@ def test_get_tokens(db, client, admin, url, data, expected, status): assert parsed_response.get('last_access') is not None -@pytest.mark.parametrize("admin,name,data,expected,status", [ +@pytest.mark.parametrize(('admin', 'name', 'data', 'expected', 'status'), ( ( # everything goes right True, 'abc', {'callback_url': 'http://www.google.com', 'is_admin': True, 'is_blocked': True}, {'callback_url': 'http://www.google.com', 'is_admin': True, 'is_blocked': True, 'name': 'abc', 'token_uses': 0}, - 200 + 200, ), ( # try to change the name @@ -199,7 +199,7 @@ def test_get_tokens(db, client, admin, url, data, expected, status): {'name': 'xyz', 'callback_url': 'http://www.google.com', 'is_admin': True, 'is_blocked': True}, {'callback_url': 'http://www.google.com', 'is_admin': True, 'is_blocked': True, 'name': 'abc', 'token_uses': 0}, - 200 + 200, ), ( # invalid name @@ -208,7 +208,7 @@ def test_get_tokens(db, client, admin, url, data, expected, status): {'callback_url': 'http://www.google.com', 'is_admin': True, 'is_blocked': True}, {'error': {'args': ['api_key'], 'code': 'not-found', 'description': 'API key does not exist'}, 'status': 404}, - 404 + 404, ), ( # non-existent api key @@ -216,7 +216,7 @@ def test_get_tokens(db, client, admin, url, data, expected, status): str(uuid4()), {'callback_url': 'http://www.google.com', 'is_admin': True, 'is_blocked': True}, {'error': {'args': ['api_key'], 'code': 'not-found', 'description': 'API key does not exist'}, 'status': 404}, - 404 + 404, ), ( # non-admin access attempt @@ -225,9 +225,9 @@ def test_get_tokens(db, client, admin, url, data, expected, status): {'callback_url': 'http://www.google.com', 'is_admin': True, 'is_blocked': True}, {'error': {'code': 'insufficient-permissions', 'description': 'You are not allowed to make this request'}, 'status': 403}, - 403 - ) -]) + 403, + ), +)) def test_token_patch(db, client, admin, name, data, expected, status): if admin: auth = make_auth(db, 'admin', is_admin=True, is_blocked=False) @@ -258,11 +258,11 @@ def test_token_patch(db, client, admin, name, data, expected, status): assert token.is_blocked == data.get('is_blocked') -@pytest.mark.parametrize("admin,expected,status", [ +@pytest.mark.parametrize(('admin', 'expected', 'status'), ( (True, '', 204), - (False, {"error": {"code": "insufficient-permissions", - "description": "You are not allowed to make this request"}, "status": 403}, 403) -]) + (False, {'error': {'code': 'insufficient-permissions', + 'description': 'You are not allowed to make this request'}, 'status': 403}, 403), +)) def test_token_delete(db, client, admin, expected, status): if admin: auth = make_auth(db, 'admin', is_admin=True, is_blocked=False) @@ -281,7 +281,7 @@ def test_token_delete(db, client, admin, expected, status): assert response.get_json() == expected -@pytest.mark.parametrize("method,url,data", [ +@pytest.mark.parametrize(('method', 'url', 'data'), ( ('post', '/api/tokens/', {'name': 'abc', 'callback_url': 'http://cern.ch'}), ('get', '/api/tokens/', {}), ('patch', '/api/tokens/abc', {}), @@ -290,9 +290,9 @@ def test_token_delete(db, client, admin, expected, status): ('get', '/api/urls/', {}), ('patch', '/api/urls/abc', {}), ('delete', '/api/urls/abc', {}), - ('put', '/api/urls/abc', {}) -]) -@pytest.mark.parametrize("blocked", [True, False]) + ('put', '/api/urls/abc', {}), +)) +@pytest.mark.parametrize('blocked', (True, False)) def test_blocked_or_unauthorized(db, client, method, url, data, blocked): headers = make_auth(db, 'blocked', is_admin=True, is_blocked=True) if blocked else None @@ -308,38 +308,38 @@ def test_blocked_or_unauthorized(db, client, method, url, data, blocked): # URL Tests -@pytest.mark.parametrize("data,expected,status", [ +@pytest.mark.parametrize(('data', 'expected', 'status'), ( ( # everything works right, a new url is created {'url': 'http://cern.ch'}, {'meta': {}, 'url': 'http://cern.ch'}, - 201 + 201, ), ( # everything works right, a new url is created with metadata {'url': 'http://cern.ch', 'meta': {'author': 'me', 'a': False}}, {'meta': {'author': 'me', 'a': False}, 'url': 'http://cern.ch'}, - 201 + 201, ), ( # invalid url {'url': 'fake'}, {'error': {'code': 'validation-error', 'messages': {'url': ['Not a valid URL.']}}, 'status': 400}, - 400 + 400, ), ( # empty url {'url': ''}, {'error': {'code': 'validation-error', 'messages': {'url': ['Not a valid URL.']}}, 'status': 400}, - 400 + 400, ), ( # allow_reuse=true {'url': 'http://existing.com', 'allow_reuse': True}, {'meta': {}, 'url': 'http://existing.com'}, - 400 - ) -]) + 400, + ), +)) def test_create_url(db, app, client, data, expected, status): auth = make_auth(db, 'non-admin', is_admin=False, is_blocked=False) @@ -367,52 +367,52 @@ def test_create_url(db, app, client, data, expected, status): assert response.status_code == status -@pytest.mark.parametrize("name,data,expected,status", [ +@pytest.mark.parametrize(('name', 'data', 'expected', 'status'), ( ( # everything goes right - "my-short-url", + 'my-short-url', {'url': 'http://google.com', 'meta': {'author': 'me'}}, {'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'my-short-url'), 'url': 'http://google.com'}, - 201 + 201, ), ( # invalid url - "my-short-url", + 'my-short-url', {'url': 'google.com', 'meta': {'author': 'me'}}, {'error': {'code': 'validation-error', 'messages': {'url': ['Not a valid URL.']}}, 'status': 400}, - 400 + 400, ), ( # empty url - "my-short-url", + 'my-short-url', {'url': '', 'meta': {'author': 'me'}}, {'error': {'code': 'validation-error', 'messages': {'url': ['Not a valid URL.']}}, 'status': 400}, - 400 + 400, ), ( # url with invalid characters - "my-short-url*", + 'my-short-url*', {'url': 'https://google.com', 'meta': {'author': 'me'}}, {'error': {'code': 'validation-error', 'messages': {'shortcut': ['Invalid value.']}}, 'status': 400}, - 400 + 400, ), ( # url with slash - "my-short-url/i-look-suspicious", + 'my-short-url/i-look-suspicious', {'url': 'https://google.com', 'meta': {'author': 'me'}}, {}, - 404 + 404, ), ( # blacklisted URL - "api", + 'api', {'url': 'https://google.com', 'meta': {'author': 'me'}}, {'error': {'code': 'validation-error', 'messages': {'shortcut': ['Invalid value.']}}, 'status': 400}, - 400 + 400, ), -]) +)) def test_put_url(db, client, name, data, expected, status): auth = make_auth(db, 'non-admin', is_admin=False, is_blocked=False) @@ -430,37 +430,37 @@ def test_put_url(db, client, name, data, expected, status): assert url is not None -@pytest.mark.parametrize("shortcut,data,expected,status", [ +@pytest.mark.parametrize(('shortcut', 'data', 'expected', 'status'), ( ( # everything goes right - "abc", + 'abc', {'meta': {'author': 'me'}, 'url': 'http://example.com'}, - {'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), + {'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), 'url': 'http://example.com'}, - 200 + 200, ), ( # nonexistent shortcut - "nonexistent", + 'nonexistent', {'meta': {'author': 'me'}, 'url': 'http://example.com'}, {'error': {'args': ['shortcut'], 'code': 'not-found', 'description': 'Shortcut does not exist'}, 'status': 404}, - 404 + 404, ), ( # invalid url - "abc", + 'abc', {'meta': {'author': 'me'}, 'url': 'example.com'}, {'error': {'code': 'validation-error', 'messages': {'url': ['Not a valid URL.']}}, 'status': 400}, - 400 + 400, ), ( # empty url - "abc", + 'abc', {'meta': {'author': 'me'}, 'url': ''}, {'error': {'code': 'validation-error', 'messages': {'url': ['Not a valid URL.']}}, 'status': 400}, - 400 + 400, ), -]) +)) def test_patch_url(db, client, shortcut, data, expected, status): auth1 = make_auth(db, 'non-admin-1', is_admin=False, is_blocked=False) auth2 = make_auth(db, 'non-admin-2', is_admin=False, is_blocked=False) @@ -482,20 +482,20 @@ def test_patch_url(db, client, shortcut, data, expected, status): assert url.token.name == parsed_response.get('owner') -@pytest.mark.parametrize("shortcut,expected,status", [ +@pytest.mark.parametrize(('shortcut', 'expected', 'status'), ( ( # everything goes right - "abc", + 'abc', {}, - 204 + 204, ), ( # invalid shortcut - "abcd", + 'abcd', {'error': {'args': ['shortcut'], 'code': 'not-found', 'description': 'Shortcut does not exist'}, 'status': 404}, - 404 + 404, ), -]) +)) def test_delete_url(db, client, shortcut, expected, status): auth = make_auth(db, 'non-admin', is_admin=False, is_blocked=False) @@ -514,63 +514,63 @@ def test_delete_url(db, client, shortcut, expected, status): assert value == parsed_response[key] -@pytest.mark.parametrize("url,data,expected,status", [ +@pytest.mark.parametrize(('url', 'data', 'expected', 'status'), ( ( # everything goes right '/api/urls/', {}, - [{'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), + [{'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), 'url': 'http://example.com'}, - {'meta': {"a": "b", "owner": "all"}, 'short_url': posixpath.join('http://localhost:5000/', 'def'), + {'meta': {'a': 'b', 'owner': 'all'}, 'short_url': posixpath.join('http://localhost:5000/', 'def'), 'url': 'http://example.com'}, - {'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'ghi'), + {'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'ghi'), 'url': 'http://cern.ch'}], - 200 + 200, ), ( # everything goes right, asking for specific shortcut '/api/urls/abc', {}, - {'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), + {'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), 'url': 'http://example.com'}, - 200 + 200, ), ( # filter based on url '/api/urls/', {'url': 'http://example.com'}, - [{'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), + [{'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), 'url': 'http://example.com'}, - {'meta': {"a": "b", "owner": "all"}, 'short_url': posixpath.join('http://localhost:5000/', 'def'), + {'meta': {'a': 'b', 'owner': 'all'}, 'short_url': posixpath.join('http://localhost:5000/', 'def'), 'url': 'http://example.com'}], - 200 + 200, ), ( # filter based on metadata fields '/api/urls/', {'meta.author': 'me'}, - [{'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), + [{'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), 'url': 'http://example.com'}, - {'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'ghi'), + {'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'ghi'), 'url': 'http://cern.ch'}], - 200 + 200, ), ( # filter based on both url and metadata fields '/api/urls/', {'url': 'http://example.com', 'meta.author': 'me'}, - [{'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), + [{'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), 'url': 'http://example.com'}], - 200 + 200, ), ( # invalid shortcut '/api/urls/xyz', {}, {'error': {'args': ['shortcut'], 'code': 'not-found', 'description': 'Shortcut does not exist'}, 'status': 404}, - 404 - ) -]) + 404, + ), +)) def test_get_url(db, client, url, data, expected, status): auth = make_auth(db, 'non-admin', is_admin=False, is_blocked=False) @@ -583,9 +583,9 @@ def test_get_url(db, client, url, data, expected, status): assert response.status_code == status if isinstance(expected, list): - parsed_response = sorted(parsed_response, key=lambda k: k['short_url']) - expected = sorted(expected, key=lambda k: k['short_url']) - for expected_url, returned_url in zip(expected, parsed_response): + parsed_response = sorted(parsed_response, key=itemgetter('short_url')) + expected = sorted(expected, key=itemgetter('short_url')) + for expected_url, returned_url in zip(expected, parsed_response, strict=True): for key, value in expected_url.items(): assert value == returned_url[key] if status == 200: @@ -611,24 +611,24 @@ def test_get_admin_all(db, client): client.put('/api/urls/ghi', json={'url': 'http://cern.ch', 'meta': {'author': 'me'}}, headers=admin_auth) response = client.get('/api/urls/', query_string={'all': True}, headers=admin_auth) parsed_response = response.get_json() - expected = [{'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), + expected = [{'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'abc'), 'url': 'http://example.com'}, - {'meta': {"a": "b", "owner": "all"}, 'short_url': posixpath.join('http://localhost:5000/', 'def'), + {'meta': {'a': 'b', 'owner': 'all'}, 'short_url': posixpath.join('http://localhost:5000/', 'def'), 'url': 'http://example.com'}, - {'meta': {"author": "me"}, 'short_url': posixpath.join('http://localhost:5000/', 'ghi'), + {'meta': {'author': 'me'}, 'short_url': posixpath.join('http://localhost:5000/', 'ghi'), 'url': 'http://cern.ch'}] assert response.status_code == 200 - parsed_response = sorted(parsed_response, key=lambda k: k['url']) - expected = sorted(expected, key=lambda k: k['url']) - for expected_token, returned_token in zip(expected, parsed_response): - for key, value in expected_token.items(): - assert value == returned_token[key] - assert returned_token.get('owner') is not None - assert returned_token.get('url') is not None + parsed_response = sorted(parsed_response, key=itemgetter('url')) + expected = sorted(expected, key=itemgetter('url')) + for expected_url, returned_url in zip(expected, parsed_response, strict=True): + for key, value in expected_url.items(): + assert value == returned_url[key] + assert returned_url.get('owner') is not None + assert returned_url.get('url') is not None -@pytest.mark.parametrize("method", ['patch', 'delete']) +@pytest.mark.parametrize('method', ('patch', 'delete')) def test_other_user(db, client, method): non_admin_auth1 = make_auth(db, 'non-admin-1', is_admin=False, is_blocked=False) non_admin_auth2 = make_auth(db, 'non-admin-2', is_admin=False, is_blocked=False) diff --git a/ursh/util/db.py b/ursh/util/db.py index a76d71b..d8718b0 100644 --- a/ursh/util/db.py +++ b/ursh/util/db.py @@ -30,10 +30,10 @@ def import_all_models(package_name): """ package_root = _get_package_root_path(package_name) modules = [] - for root, dirs, files in os.walk(package_root): + for root, _dirs, files in os.walk(package_root): if os.path.basename(root) == 'models': package = os.path.relpath(root, package_root).replace(os.sep, '.') - modules += ['{}.{}.{}'.format(package_name, package, name[:-3]) + modules += [f'{package_name}.{package}.{name[:-3]}' for name in files if name.endswith('.py') and name != 'blueprint.py' and not name.endswith('_test.py')] diff --git a/ursh/util/nested_query_parser.py b/ursh/util/nested_query_parser.py index dd5eebb..9e5a77f 100644 --- a/ursh/util/nested_query_parser.py +++ b/ursh/util/nested_query_parser.py @@ -1,7 +1,3 @@ -""" -Taken from the sample custom parser on http://webargs.readthedocs.io/en/latest/advanced.html#custom-parsers -""" - import re from webargs.flaskparser import FlaskParser @@ -12,6 +8,7 @@ def load_querystring(self, req, schema): return _structure_dict(req.args) +# Taken from the sample custom parser on http://webargs.readthedocs.io/en/latest/advanced.html#custom-parsers def _structure_dict(dict_): def structure_dict_pair(r, key, value): m = re.match(r'(\w+)\.(.*)', key) diff --git a/ursh/wsgi.py b/ursh/wsgi.py index 6855872..4fe1b9c 100644 --- a/ursh/wsgi.py +++ b/ursh/wsgi.py @@ -1,4 +1,3 @@ -from .core.app import create_app - +from ursh.core.app import create_app app = create_app()