From 78c631499d1b238963a41f4da36b223f7d5eca94 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Fri, 11 Jul 2025 16:50:09 +0530 Subject: [PATCH 1/3] Fix linter issues; fix ValidCoordinates and add OptionalCoordinates validator --- .pre-commit-config.yaml | 55 ++------ docs/baseframe/conf.py | 5 +- pyproject.toml | 98 ++++++++++++- setup.py | 2 +- src/baseframe/__init__.py | 6 +- src/baseframe/blueprint.py | 4 +- src/baseframe/extensions.py | 5 +- src/baseframe/filters.py | 17 +-- src/baseframe/forms/__init__.py | 7 +- src/baseframe/forms/auto.py | 4 +- src/baseframe/forms/fields.py | 68 +++++---- src/baseframe/forms/filters.py | 2 +- src/baseframe/forms/form.py | 16 +-- src/baseframe/forms/parsleyjs.py | 80 +++++------ src/baseframe/forms/validators.py | 132 +++++++++++------- src/baseframe/forms/widgets.py | 41 +++--- src/baseframe/statsd.py | 6 +- src/baseframe/utils.py | 10 +- src/baseframe/views.py | 54 +++---- tests/baseframe_tests/__init__.py | 1 + tests/baseframe_tests/conftest.py | 13 +- tests/baseframe_tests/filters_test.py | 92 ++++++------ tests/baseframe_tests/forms/__init__.py | 1 + tests/baseframe_tests/forms/fields_test.py | 29 ++-- tests/baseframe_tests/forms/form_test.py | 35 +++-- .../baseframe_tests/forms/sqlalchemy_test.py | 14 +- .../baseframe_tests/forms/validators_test.py | 124 ++++++++++++++-- tests/baseframe_tests/statsd_test.py | 77 ++++++---- tests/baseframe_tests/utils_test.py | 8 +- 29 files changed, 616 insertions(+), 390 deletions(-) create mode 100644 tests/baseframe_tests/forms/__init__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ec720b87..aa86f20d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,56 +1,32 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks ci: - skip: ['yesqa', 'no-commit-to-branch'] + skip: ['no-commit-to-branch'] repos: - repo: https://github.com/pre-commit-ci/pre-commit-ci-config rev: v1.6.1 hooks: - id: check-pre-commit-ci-config - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.1 - hooks: - - id: ruff - args: ['--fix', '--exit-non-zero-on-fix'] - # Extra args, only after removing flake8 and yesqa: '--extend-select', 'RUF100' - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade args: ['--keep-runtime-typing', '--py39-plus'] - - repo: https://github.com/asottile/yesqa - rev: v1.5.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.2 hooks: - - id: yesqa - additional_dependencies: &flake8deps - - bandit - - flake8-assertive - - flake8-blind-except - - flake8-bugbear - - flake8-builtins - - flake8-comprehensions - - flake8-docstrings - - flake8-isort - - flake8-logging-format - - flake8-mutable - - flake8-plugin-utils - - flake8-print - - flake8-pytest-style - - pep8-naming - - toml - - tomli + - id: ruff-check + args: ['--fix', '--exit-non-zero-on-fix'] + # Extra args, only after removing flake8 and yesqa: '--extend-select', 'RUF100' + - id: ruff-format - repo: https://github.com/PyCQA/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort additional_dependencies: - toml - - repo: https://github.com/psf/black - rev: 24.10.0 - hooks: - - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 + rev: v1.16.1 hooks: - id: mypy # warn-unused-ignores is unsafe with pre-commit, see @@ -65,13 +41,8 @@ repos: - types-pytz - types-requests - typing-extensions - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 - hooks: - - id: flake8 - additional_dependencies: *flake8deps - repo: https://github.com/PyCQA/pylint - rev: v3.3.3 + rev: v3.3.7 hooks: - id: pylint args: [ @@ -82,7 +53,7 @@ repos: additional_dependencies: - tomli - repo: https://github.com/PyCQA/bandit - rev: 1.8.2 + rev: 1.8.6 hooks: - id: bandit language_version: python3 @@ -90,7 +61,7 @@ repos: additional_dependencies: - 'bandit[toml]' - repo: https://github.com/pycontribs/mirrors-prettier - rev: v3.4.2 + rev: v3.6.2 hooks: - id: prettier - repo: https://github.com/ducminh-phan/reformat-gherkin diff --git a/docs/baseframe/conf.py b/docs/baseframe/conf.py index 1ae2cc58..8286949c 100644 --- a/docs/baseframe/conf.py +++ b/docs/baseframe/conf.py @@ -1,6 +1,5 @@ """Sphinx configuration.""" - -# flake8: noqa +# ruff: noqa: A001,INP001 # # baseframe documentation build configuration file, created by # sphinx-quickstart on Thu Nov 15 12:31:40 2012. @@ -52,7 +51,7 @@ # General information about the project. project = 'baseframe' -copyright = '2012-22, Hasgeek' +copyright = '2012-25, Hasgeek' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/pyproject.toml b/pyproject.toml index 678f3f9f..adb3ac5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,11 +168,21 @@ disable = [ ] [tool.bandit] -exclude_dirs = ['node_modules'] +exclude_dirs = ['node_modules', 'build'] +skips = [ + 'B113', # Handled by pylint; bandit incorrectly flags requests_mock for timeout +] [tool.bandit.assert_used] skips = ['*/*_test.py', '*/test_*.py'] +[tool.bandit.markupsafe_xss] +allowed_calls = [ + 'flask.render_template', + 'coaster.utils.markdown', + 'render_field_options', +] + [tool.ruff] # Exclude a variety of commonly ignored directories. exclude = [ @@ -209,21 +219,90 @@ docstring-code-format = true quote-style = "preserve" [tool.ruff.lint] -# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. -select = ["E", "F"] -ignore = ["E402", "E501"] - +select = [ + "A", # flake8-builtins + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "ASYNC", # flake8-async + "ASYNC1", # flake8-trio + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C", # pylint convention + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", # Error + "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RET", # flake8-return + "RUF", # ruff + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "T20", # flake8-print + "UP", # pyupgrade + "W", # Warnings + "YTT", # flake8-2020 +] +ignore = [ + "A005", # Shadowing a Python standard-library module is okay as they're namespaced + "ANN002", # `*args` is implicit `Any` + "ANN003", # `**kwargs` is implicit `Any` + "ANN401", # Allow `Any` type + "C901", + "D101", + "D102", + "D103", + "D105", # Magic methods don't need docstrings + "D106", # Nested classes don't need docstrings + "D107", # `__init__` doesn't need a docstring + "D203", # No blank lines before class docstring + "D212", # Allow multiline docstring to start on next line after quotes + "D213", # But also allow multiline docstring to start right after quotes + "E402", # Allow top-level imports after statements + "E501", # Allow long lines if the formatter can't fix it + "EM101", # Allow Exception("string") + "EM102", # Allow Exception(f"string") + "ISC001", # Allow implicitly concatenated string literals (required for formatter) + "PLR2004", # Too many false positives + "PLR0911", # Alow multiple return statements + "PLR0912", # Some functions are complex + "PLR0913", # Some functions need many args + "PLR0915", # Too many statements are okay + "RUF012", # Allow mutable ClassVar without annotation (conflicts with SQLAlchemy) + "SLOT000", # Don't require `__slots__` for subclasses of str +] # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] +# Allow these characters in strings +allowed-confusables = ["‘", "’", "–"] + [tool.ruff.lint.extend-per-file-ignores] "__init__.py" = ["E402"] # Allow non-top-level imports "tests/**.py" = [ - "S101", # Allow assert + "ARG001", # Context manager fixtures may not be used within a test "ANN001", # Args don't need types (usually fixtures) + "D401", # Fixture docstrings shouldn't be imperative "N802", # Fixture returning a class may be named per class name convention "N803", # Args don't require naming convention (fixture could be a class) + "N999", # Module name may have a CamelCased class name in it + "S101", # Allow assert ] [tool.ruff.lint.isort] @@ -251,3 +330,10 @@ mark-parentheses = false [tool.ruff.lint.pyupgrade] keep-runtime-typing = true + +[tool.ruff.lint.flake8-bandit] +allowed-markup-calls = [ + 'flask.render_template', + 'coaster.utils.markdown', + 'baseframe.filters.render_field_options', +] diff --git a/setup.py b/setup.py index bb516536..5cca2499 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def run(self) -> None: if not self._dry_run: curdir = os.getcwd() os.chdir(os.path.join(self.build_lib, 'baseframe')) - os.system('make') # nosec + os.system('make') # nosec B605 B607 # noqa: S605,S607 os.chdir(curdir) diff --git a/src/baseframe/__init__.py b/src/baseframe/__init__.py index 67e6155b..6cec5b51 100644 --- a/src/baseframe/__init__.py +++ b/src/baseframe/__init__.py @@ -36,18 +36,19 @@ # TODO: baseframe_js and baseframe_css are defined in deprecated.py # and pending removal after an audit of all apps __all__ = [ + 'Bundle', + 'Version', '_', '__', '__version__', '__version_info__', 'assets', 'babel', + 'baseframe', 'baseframe_css', 'baseframe_js', 'baseframe_translations', - 'baseframe', 'blueprint', - 'Bundle', 'cache', 'ctx_has_locale', 'deprecated', @@ -62,6 +63,5 @@ 'signals', 'statsd', 'utils', - 'Version', 'views', ] diff --git a/src/baseframe/blueprint.py b/src/baseframe/blueprint.py index 84fcbd85..4e6fe7a3 100644 --- a/src/baseframe/blueprint.py +++ b/src/baseframe/blueprint.py @@ -17,7 +17,7 @@ from sentry_sdk.integrations.rq import RqIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration -from coaster.app import RotatingKeySecureCookieSessionInterface +from coaster.app import FlaskRotatingKeySecureCookieSessionInterface from coaster.assets import split_namespec from .assets import assets @@ -139,7 +139,7 @@ def init_app( app.config['SECRET_KEY'] = app.config['SECRET_KEYS'][0] if app.config.get('SECRET_KEY') != app.config.get('SECRET_KEYS', [None])[0]: raise ValueError("App has misconfigured secret keys") - app.session_interface = RotatingKeySecureCookieSessionInterface() + app.session_interface = FlaskRotatingKeySecureCookieSessionInterface() if app.config.get('MATOMO_URL') and app.config.get('MATOMO_ID'): # Default .js and tracking file for Matomo diff --git a/src/baseframe/extensions.py b/src/baseframe/extensions.py index 67b60f2f..7cdfc5f7 100644 --- a/src/baseframe/extensions.py +++ b/src/baseframe/extensions.py @@ -56,10 +56,7 @@ def __call__(self, string: LiteralString, **variables) -> str: ... cache = Cache() babel = Babel() statsd = Statsd() -if DebugToolbarExtension is not None: # pragma: no cover - toolbar = DebugToolbarExtension() -else: # pragma: no cover - toolbar = None +toolbar = DebugToolbarExtension() if DebugToolbarExtension is not None else None baseframe_translations = Domain( os.path.join(os.path.dirname(__file__), 'translations'), domain='baseframe' diff --git a/src/baseframe/filters.py b/src/baseframe/filters.py index 944f5bb3..2a4e4099 100644 --- a/src/baseframe/filters.py +++ b/src/baseframe/filters.py @@ -2,7 +2,7 @@ import os.path from datetime import date, datetime, time, timedelta -from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Literal, Optional, Union from urllib.parse import urlsplit, urlunsplit import grapheme @@ -113,14 +113,9 @@ def avatar_url( return user.avatar + '&size=' + str(size) return user.avatar + '?size=' + str(size) return user.avatar - email = user.email + email = str(user.email or '') if email: - if isinstance(email, str): - # Flask-Lastuser's User model has email as a string - ehash = md5sum(user.email) - else: - # Lastuser's User model has email as a UserEmail object - ehash = email.md5sum + ehash = md5sum(email) gravatar = '//www.gravatar.com/avatar/' + ehash + '?d=mm' if size: gravatar += '&s=' + str(size) @@ -130,12 +125,12 @@ def avatar_url( @baseframe.app_template_filter('render_field_options') -def render_field_options(field: WTField, **kwargs: Any) -> str: +def render_field_options(field: WTField, **kwargs: Any) -> Markup: """Remove HTML attributes with falsy values before rendering a field.""" d = {k: v for k, v in kwargs.items() if v is not None and v is not False} if field.render_kw: d.update(field.render_kw) - return cast(str, field(**d)) + return field(**d) # TODO: Only used in renderfield.mustache. Re-check whether this is necessary at all. @@ -228,7 +223,7 @@ def preview(html: str, min: int = 50, max: int = 158) -> str: # noqa: A002 @baseframe.app_template_filter('cdata') def cdata(text: str) -> str: """Convert text to a CDATA sequence.""" - return Markup('', ']]]]>') + ']]>') + return Markup('', ']]]]>') + ']]>') # nosec: B704 # noqa: S704 # TODO: Used only in Hasjob. Move there? diff --git a/src/baseframe/forms/__init__.py b/src/baseframe/forms/__init__.py index 34d2b066..6cb76802 100644 --- a/src/baseframe/forms/__init__.py +++ b/src/baseframe/forms/__init__.py @@ -1,8 +1,5 @@ -# flake8: noqa - -""" -Baseframe forms -""" +"""Baseframe forms.""" +# ruff: noqa: F403 from .auto import * from .fields import * diff --git a/src/baseframe/forms/auto.py b/src/baseframe/forms/auto.py index 9acc7738..4dffbc8f 100644 --- a/src/baseframe/forms/auto.py +++ b/src/baseframe/forms/auto.py @@ -15,7 +15,7 @@ request, url_for, ) -from markupsafe import Markup, escape +from markupsafe import Markup from werkzeug.wrappers import Response from coaster.utils import buid @@ -110,7 +110,7 @@ def render_message(title: str, message: str, code: int = 200) -> Response: """Render a message.""" template = THEME_FILES[current_app.config['theme']]['message.html.jinja2'] if request_is_xhr(): - return make_response(Markup(f'

{escape(message)}

'), code) + return make_response(Markup('

{}

').format(message), code) return make_response(render_template(template, title=title, message=message), code) diff --git a/src/baseframe/forms/fields.py b/src/baseframe/forms/fields.py index 54d11616..1fd46d49 100644 --- a/src/baseframe/forms/fields.py +++ b/src/baseframe/forms/fields.py @@ -1,6 +1,6 @@ """Form fields.""" - -# pylint: disable=attribute-defined-outside-init +# ruff: noqa: ARG002 +# pylint: disable=attribute-defined-outside-init,unused-argument from __future__ import annotations @@ -49,32 +49,30 @@ ) __all__ = [ - # Imported from WTForms - 'Field', - 'FieldList', - 'FileField', - 'Label', - 'RecaptchaField', - 'SelectMultipleField', - 'SubmitField', - # Baseframe fields (many of these are extensions of WTForms fields) + 'SANITIZE_ATTRIBUTES', + 'SANITIZE_TAGS', 'AnnotatedTextField', 'AutocompleteField', 'AutocompleteMultipleField', 'CoordinatesField', 'DateTimeField', 'EnumSelectField', + 'Field', + 'FieldList', + 'FileField', 'FormField', 'GeonameSelectField', 'GeonameSelectMultiField', 'ImgeeField', 'JsonField', + 'Label', 'MarkdownField', 'RadioMatrixField', - 'SANITIZE_ATTRIBUTES', - 'SANITIZE_TAGS', + 'RecaptchaField', 'SelectField', + 'SelectMultipleField', 'StylesheetField', + 'SubmitField', 'TextListField', 'TinyMce4Field', 'UserSelectField', @@ -129,17 +127,16 @@ class SelectField(SelectFieldBase): Here is an example of how the data is laid out:: ( - ("Fruits", ( - ('apple', "Apple"), - ('peach', "Peach"), - ('pear', "Pear") - )), - ("Vegetables", ( - ('cucumber', "Cucumber"), - ('potato', "Potato"), - ('tomato', "Tomato"), - )), - ('other', "None of the above") + ("Fruits", (('apple', "Apple"), ('peach', "Peach"), ('pear', "Pear"))), + ( + "Vegetables", + ( + ('cucumber', "Cucumber"), + ('potato', "Potato"), + ('tomato', "Tomato"), + ), + ), + ('other', "None of the above"), ) It's a little strange that the tuples are (value, label) except for groups which are @@ -185,11 +182,8 @@ def __init__( ) -> None: super().__init__(*args, **kwargs) - if tinymce_options is None: - tinymce_options = {} - else: - # Clone the dict to preserve local edits - tinymce_options = dict(tinymce_options) + # Clone the dict to preserve local edits + tinymce_options = {} if tinymce_options is None else dict(tinymce_options) # Set defaults for TinyMCE tinymce_options.setdefault( @@ -539,7 +533,7 @@ def process_formdata(self, valuelist: list[str]) -> None: # Convert strings into Tag objects self.data = valuelist - def pre_validate(self, form: WTForm) -> None: # pylint: disable=unused-argument + def pre_validate(self, form: WTForm) -> None: """Do not validate data.""" return @@ -690,7 +684,7 @@ class ImgeeField(URLField): validators=[validators.DataRequired()], profile='foo', img_label='logos', - img_size='100x75' + img_size='100x75', ) """ @@ -746,11 +740,11 @@ def process_formdata(self, valuelist: list[str]) -> None: if valuelist and len(valuelist) == 2: try: latitude = Decimal(valuelist[0]) - except DecimalError: + except (DecimalError, TypeError): latitude = None try: longitude = Decimal(valuelist[1]) - except DecimalError: + except (DecimalError, TypeError): longitude = None self.data = latitude, longitude @@ -758,8 +752,12 @@ def process_formdata(self, valuelist: list[str]) -> None: self.data = None, None def _value(self) -> tuple[str, str]: - if self.data is not None and self.data != (None, None): - return str(self.data[0]), str(self.data[1]) + """Return HTML render value.""" + if self.data is not None: + return ( + str(self.data[0]) if self.data[0] is not None else '', + str(self.data[1]) if self.data[1] is not None else '', + ) return '', '' diff --git a/src/baseframe/forms/filters.py b/src/baseframe/forms/filters.py index 60f93e18..733d6f95 100644 --- a/src/baseframe/forms/filters.py +++ b/src/baseframe/forms/filters.py @@ -23,7 +23,7 @@ from coaster.utils import unicode_extended_whitespace -__all__ = ['lower', 'upper', 'strip', 'lstrip', 'rstrip', 'strip_each', 'none_if_empty'] +__all__ = ['lower', 'lstrip', 'none_if_empty', 'rstrip', 'strip', 'strip_each', 'upper'] def lower() -> Callable[[Optional[str]], Optional[str]]: diff --git a/src/baseframe/forms/form.py b/src/baseframe/forms/form.py index b65cc94a..c1ebe3af 100644 --- a/src/baseframe/forms/form.py +++ b/src/baseframe/forms/form.py @@ -24,12 +24,12 @@ from .typing import FilterCallable, ValidatorCallable, ValidatorList, WidgetProtocol __all__ = [ - 'field_registry', - 'widget_registry', - 'validator_registry', 'Form', 'FormGenerator', 'RecaptchaForm', + 'field_registry', + 'validator_registry', + 'widget_registry', ] # Use a hardcoded list to control what is available to user-facing apps @@ -151,9 +151,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) # Finally, populate the ``choices`` attr of selection fields - if callable(post_init := getattr(self, '__post_init__', None)): - post_init() # pylint: disable=not-callable - elif callable(post_init := getattr(self, 'set_queries', None)): + if callable(post_init := getattr(self, '__post_init__', None)) or callable( + post_init := getattr(self, 'set_queries', None) + ): post_init() # pylint: disable=not-callable def __json__(self) -> Any: @@ -307,8 +307,8 @@ def generate(self, formstruct: dict) -> type[Form]: class DynamicForm(Form): pass - for fielddata in formstruct: - fielddata = dict(fielddata) # Make a copy + for _fielddata in formstruct: + fielddata = dict(_fielddata) # Make a copy name = fielddata.pop('name', None) type_ = fielddata.pop('type', None) if not name: diff --git a/src/baseframe/forms/parsleyjs.py b/src/baseframe/forms/parsleyjs.py index 6fbd3398..2d2812a7 100644 --- a/src/baseframe/forms/parsleyjs.py +++ b/src/baseframe/forms/parsleyjs.py @@ -66,37 +66,33 @@ __author__ = 'Johannes Gehrs (jgehrs@gmail.com)' __all__ = [ - # ParsleyJS helpers - 'parsley_kwargs', - 'ParsleyInputMixin', - # Widgets - 'TextInput', - 'PasswordInput', - 'HiddenInput', - 'TextArea', + 'BooleanField', 'CheckboxInput', - 'Select', - 'ListWidget', - 'TelInput', - 'URLInput', - 'EmailInput', + 'DateField', 'DateInput', - 'NumberInput', - # Fields - 'StringField', - 'IntegerField', - 'RadioField', - 'BooleanField', 'DecimalField', + 'EmailField', + 'EmailInput', 'FloatField', - 'PasswordField', 'HiddenField', - 'TextAreaField', + 'HiddenInput', + 'IntegerField', + 'ListWidget', + 'NumberInput', + 'PasswordField', + 'PasswordInput', + 'RadioField', + 'Select', 'SelectField', + 'StringField', 'TelField', + 'TelInput', + 'TextArea', + 'TextAreaField', + 'TextInput', 'URLField', - 'EmailField', - 'DateField', + 'URLInput', + 'parsley_kwargs', ] @@ -143,7 +139,7 @@ def parsley_kwargs(field: WTField, kwargs: Any, extend: bool = True) -> dict[str return new_kwargs -def _email_kwargs(kwargs: dict[str, Any], vali: ValidatorCallable) -> None: +def _email_kwargs(kwargs: dict[str, Any], _vali: ValidatorCallable) -> None: kwargs['data-parsley-type'] = 'email' @@ -151,7 +147,7 @@ def _equal_to_kwargs(kwargs: dict[str, Any], vali: EqualTo) -> None: kwargs['data-parsley-equalto'] = '#' + vali.fieldname -def _ip_address_kwargs(kwargs: dict[str, Any], vali: IPAddress) -> None: +def _ip_address_kwargs(kwargs: dict[str, Any], _vali: IPAddress) -> None: # Regexp from http://stackoverflow.com/a/4460645 kwargs['data-parsley-regexp'] = ( r'^\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' @@ -199,7 +195,7 @@ def _regexp_kwargs(kwargs: dict[str, Any], vali: Regexp) -> None: kwargs['data-parsley-regexp'] = regex_string -def _url_kwargs(kwargs: dict[str, Any], vali: URL) -> None: +def _url_kwargs(kwargs: dict[str, Any], _vali: URL) -> None: kwargs['data-parsley-type'] = 'url' @@ -232,68 +228,68 @@ def _message_kwargs(kwargs: dict[str, Any], message: str) -> None: kwargs['data-parsley-error-message'] = message -class ParsleyInputMixin: - def __call__(self, field: WTField, **kwargs: Any) -> str: +class ParsleyWidgetMixin: + def __call__(self, field: WTField, **kwargs: Any) -> Markup: kwargs = parsley_kwargs(field, kwargs) return super().__call__(field, **kwargs) # type: ignore[misc] -class TextInput(ParsleyInputMixin, _TextInput): +class TextInput(ParsleyWidgetMixin, _TextInput): pass -class PasswordInput(ParsleyInputMixin, _PasswordInput): +class PasswordInput(ParsleyWidgetMixin, _PasswordInput): pass -class HiddenInput(ParsleyInputMixin, _HiddenInput): +class HiddenInput(ParsleyWidgetMixin, _HiddenInput): pass -class TextArea(ParsleyInputMixin, _TextArea): +class TextArea(ParsleyWidgetMixin, _TextArea): pass -class CheckboxInput(ParsleyInputMixin, _CheckboxInput): +class CheckboxInput(ParsleyWidgetMixin, _CheckboxInput): pass -class TelInput(ParsleyInputMixin, _TelInput): +class TelInput(ParsleyWidgetMixin, _TelInput): pass -class URLInput(ParsleyInputMixin, _URLInput): +class URLInput(ParsleyWidgetMixin, _URLInput): pass -class EmailInput(ParsleyInputMixin, _EmailInput): +class EmailInput(ParsleyWidgetMixin, _EmailInput): pass -class DateInput(ParsleyInputMixin, _DateInput): +class DateInput(ParsleyWidgetMixin, _DateInput): pass -class NumberInput(ParsleyInputMixin, _NumberInput): +class NumberInput(ParsleyWidgetMixin, _NumberInput): pass -class Select(ParsleyInputMixin, _Select): +class Select(ParsleyWidgetMixin, _Select): pass class ListWidget(_ListWidget): - def __call__(self, field: WTField, **kwargs: Any) -> str: + def __call__(self, field: WTField, **kwargs: Any) -> Markup: sub_kwargs = parsley_kwargs(field, kwargs, extend=False) kwargs.setdefault('id', field.id) html = [f'<{self.html_tag} {html_params(**kwargs)}>'] - for subfield in field: + for subfield in field: # pyright: ignore[reportGeneralTypeIssues] if self.prefix_label: html.append(f'
  • {subfield.label} {subfield(**sub_kwargs)}
  • ') else: html.append(f'
  • {subfield(**sub_kwargs)} {subfield.label}
  • ') html.append(f'') - return Markup(''.join(html)) + return Markup(''.join(html)) # nosec: B704 # noqa: S704 class StringField(_StringField): diff --git a/src/baseframe/forms/validators.py b/src/baseframe/forms/validators.py index b80a4328..e226993d 100644 --- a/src/baseframe/forms/validators.py +++ b/src/baseframe/forms/validators.py @@ -1,4 +1,5 @@ """WTForms validators.""" +# ruff: noqa: ARG002 from __future__ import annotations @@ -38,31 +39,31 @@ from .typing import ValidatorList __all__ = [ + 'URL', # WTForms 'AllUrlsValid', + 'AllowedIf', + 'DataRequired', # WTForms + 'EqualTo', # WTForms + 'ForEach', + 'InputRequired', # WTForms 'IsEmoji', 'IsNotPublicEmailDomain', 'IsPublicEmailDomain', + 'Length', # WTForms 'NoObfuscatedEmail', - 'AllowedIf', + 'NumberRange', # WTForms + 'Optional', # WTForms + 'OptionalCoordinates', 'OptionalIf', + 'Recaptcha', 'RequiredIf', + 'StopValidation', # WTForms 'ValidCoordinates', 'ValidEmail', 'ValidEmailDomain', 'ValidName', 'ValidUrl', - 'ForEach', - 'Recaptcha', - # WTForms validators - 'DataRequired', - 'EqualTo', - 'InputRequired', - 'Length', - 'NumberRange', - 'Optional', - 'StopValidation', - 'URL', - 'ValidationError', + 'ValidationError', # WTForms ] @@ -91,7 +92,7 @@ def is_empty(value: Any) -> bool: return value not in _zero_values and not value -FakeField = namedtuple( +FakeField = namedtuple( # noqa: PYI024 'FakeField', ['data', 'raw_data', 'errors', 'gettext', 'ngettext'] ) @@ -110,13 +111,12 @@ def __init__(self, validators: ValidatorList) -> None: def __call__(self, form: WTForm, field: WTField) -> None: for element in field.data: fake_field = FakeField(element, element, [], field.gettext, field.ngettext) - for validator in self.validators: - try: + try: + for validator in self.validators: validator(form, fake_field) - except StopValidation as exc: - if exc.args and exc.args[0]: - field.errors.append(exc.args[0]) - break + except StopValidation as exc: + if exc.args and exc.args[0]: + field.errors.append(exc.args[0]) class AllowedIf: @@ -135,11 +135,10 @@ def __init__(self, fieldname: str, message: OptionalType[str] = None) -> None: self.message = message or self.default_message def __call__(self, form: WTForm, field: WTField) -> None: - if field.data: - if is_empty(form[self.fieldname].data): - raise StopValidation( - self.message.format(field=form[self.fieldname].label.text) - ) + if field.data and is_empty(form[self.fieldname].data): + raise StopValidation( + self.message.format(field=form[self.fieldname].label.text) + ) class OptionalIf(Optional): @@ -149,10 +148,12 @@ class OptionalIf(Optional): If this field is required when the other field is empty, chain it with :class:`DataRequired`:: - field = forms.StringField("Field", + field = forms.StringField( + "Field", validators=[ - forms.validators.OptionalIf('other'), forms.validators.DataRequired() - ] + forms.validators.OptionalIf('other'), + forms.validators.DataRequired(), + ], ) :param str fieldname: Name of the other field @@ -178,10 +179,12 @@ class RequiredIf(DataRequired): If this field is also optional when the other field is empty, chain it with :class:`Optional`:: - field = forms.StringField("Field", + field = forms.StringField( + "Field", validators=[ - forms.validators.RequiredIf('other'), forms.validators.Optional() - ] + forms.validators.RequiredIf('other'), + forms.validators.Optional(), + ], ) :param str fieldname: Name of the other field @@ -214,8 +217,7 @@ def __call__(self, form: WTForm, field: WTField) -> None: other = form[self.fieldname] if not self.compare(field.data, other.data): d = { - 'other_label': hasattr(other, 'label') - and other.label.text + 'other_label': (hasattr(other, 'label') and other.label.text) or self.fieldname, 'other_name': self.fieldname, } @@ -486,16 +488,14 @@ def check_url( method to the next. """ urlparts = urlparse(url) - if allowed_schemes: - if urlparts.scheme not in allowed_schemes: - return self.message_schemes.format( - url=url, schemes=_(', ').join(allowed_schemes) - ) - if allowed_domains: - if urlparts.netloc.lower() not in allowed_domains: - return self.message_domains.format( - url=url, domains=_(', ').join(allowed_domains) - ) + if allowed_schemes and urlparts.scheme not in allowed_schemes: + return self.message_schemes.format( + url=url, schemes=_(', ').join(allowed_schemes) + ) + if allowed_domains and urlparts.netloc.lower() not in allowed_domains: + return self.message_domains.format( + url=url, domains=_(', ').join(allowed_domains) + ) if urlparts.scheme not in ('http', 'https') or not self.visit_url: # The rest of this function only validates HTTP urls. @@ -522,7 +522,7 @@ def check_url( r = requests.get( url, timeout=5, - verify=False, # nosec # skipcq: BAN-B501 + verify=False, # nosec: B501 # noqa: S501 headers={'User-Agent': self.user_agent}, ) code = r.status_code @@ -536,7 +536,7 @@ def check_url( requests.exceptions.Timeout, ): code = None - except Exception as exc: # noqa: B902 # pylint: disable=broad-except + except Exception as exc: # pylint: disable=broad-except # noqa: BLE001 exception_catchall.send(exc) code = None @@ -676,7 +676,7 @@ def __call__(self, form: WTForm, field: WTField) -> None: diagnosis = is_email(email, check_dns=True, diagnose=True) if diagnosis.code == 0: raise StopValidation(self.message) - except (dns.resolver.Timeout, dns.resolver.NoNameservers): + except (dns.resolver.Timeout, dns.resolver.NoNameservers): # noqa: PERF203 pass @@ -710,11 +710,47 @@ def __init__( self.message_longitude = message_longitude or self.default_message_longitude def __call__(self, form: WTForm, field: WTField) -> None: + if not field.data or len(field.data) != 2: + # Don't allow `None`, `()` or `[]`, or lists not of size two. + # While this rejects `0` for being falsy, that too is not a valid value + raise StopValidation(self.message) + latitude, longitude = field.data + if latitude is None or (not -90 <= latitude <= 90): + raise StopValidation(self.message_latitude) + if longitude is None or (not -180 <= longitude <= 180): + raise StopValidation(self.message_longitude) + + +class OptionalCoordinates: + default_message = __("Valid latitude and longitude expected") + default_message_latitude = __("Latitude must be within ± 90 degrees") + default_message_longitude = __("Longitude must be within ± 180 degrees") + + def __init__( + self, + message: OptionalType[str] = None, + message_latitude: OptionalType[str] = None, + message_longitude: OptionalType[str] = None, + ) -> None: + self.message = message or self.default_message + self.message_latitude = message_latitude or self.default_message_latitude + self.message_longitude = message_longitude or self.default_message_longitude + + def __call__(self, form: WTForm, field: WTField) -> None: + if ( + field.data is None + or field.data == '' + or tuple(field.data) + in [(), (None,), ('',), (None, None), ('', None), (None, ''), ('', '')] + ): + # No value provided, treat as optional + return if len(field.data) != 2: raise StopValidation(self.message) - if not -90 <= field.data[0] <= 90: + latitude, longitude = field.data + if latitude is None or (not -90 <= latitude <= 90): raise StopValidation(self.message_latitude) - if not -180 <= field.data[1] <= 180: + if longitude is None or (not -180 <= longitude <= 180): raise StopValidation(self.message_longitude) diff --git a/src/baseframe/forms/widgets.py b/src/baseframe/forms/widgets.py index af59e83e..3e845906 100644 --- a/src/baseframe/forms/widgets.py +++ b/src/baseframe/forms/widgets.py @@ -9,21 +9,22 @@ from furl import furl from markupsafe import Markup, escape from wtforms import Field as WTField, SelectFieldBase +from wtforms.fields import RadioField from wtforms.widgets import RadioInput, Select, html_params from ..extensions import _ __all__ = [ - 'TinyMce4', - 'SubmitInput', - 'DateTimeInput', 'CoordinatesInput', - 'RadioMatrixInput', + 'DateTimeInput', 'ImgeeWidget', 'InlineListWidget', 'RadioInput', - 'SelectWidget', + 'RadioMatrixInput', 'Select2Widget', + 'SelectWidget', + 'SubmitInput', + 'TinyMce4', ] @@ -55,7 +56,7 @@ def __call__(self, field: SelectFieldBase, **kwargs: Any) -> Markup: label = item2 html.append(self.render_option(val, label, val == field.data)) html.append('') - return Markup(''.join(html)) + return Markup(''.join(html)) # nosec: B704 # noqa: S704 class Select2Widget(Select): @@ -76,7 +77,7 @@ def __call__(self, field: SelectFieldBase, **kwargs: Any) -> Markup: for val, label, selected, render_kw in field.iter_choices(): html.append(self.render_option(val, label, selected, **render_kw)) html.append('') - return Markup(''.join(html)) + return Markup(''.join(html)) # nosec: B704 # noqa: S704 class TinyMce4(wtforms.widgets.TextArea): @@ -121,7 +122,7 @@ def __call__(self, field: WTField, **kwargs: Any) -> Markup: value = field._value() or '' class_ = kwargs.pop('class', kwargs.pop('class_', '')) input_attrs = html_params(name=field.name, id=field_id, value=value, **kwargs) - return Markup( + return Markup( # nosec: B704 # noqa: S704 f' {field.tzname}' ) @@ -149,7 +150,7 @@ def __call__(self, field: WTField, **kwargs: Any) -> Markup: if len(value) < 2: value.append('') - return Markup( + return Markup( # nosec: B704 # noqa: S704 # pylint: disable=consider-using-f-string ' '.format( self.html_params( @@ -202,7 +203,7 @@ def __call__(self, field: RadioMatrixField, **kwargs: Any) -> Markup: rendered.append('') rendered.append('') - return Markup('\n'.join(rendered)) + return Markup('\n'.join(rendered)) # nosec: B704 # noqa: S704 class InlineListWidget: @@ -228,20 +229,20 @@ def __init__( self.class_ = class_ self.class_prefix = class_prefix - def __call__(self, field: WTField, **kwargs: Any) -> Markup: + def __call__(self, field: RadioField, **kwargs: Any) -> Markup: kwargs.setdefault('id', field.id) kwargs['class_'] = ( kwargs.pop('class_', kwargs.pop('class', '')).strip() + ' ' + self.class_ ).strip() html = [f'<{escape(self.html_tag)} {html_params(**kwargs)}>'] - for subfield in field: - html.append( - f'' - ) + html.extend( + f'' + for subfield in field + ) html.append(f'') - return Markup('\n'.join(html)) + return Markup('\n'.join(html)) # nosec: B704 # noqa: S704 class ImgeeWidget(wtforms.widgets.Input): @@ -265,7 +266,7 @@ def __call__(self, field: WTField, **kwargs: Any) -> Markup: value = furl.url # pylint: disable=consider-using-f-string - iframe_html = Markup( + iframe_html = Markup( # nosec: B704 # noqa: S704 ''.format( self.html_params( id='iframe_' + id_ + '_upload', @@ -275,7 +276,7 @@ def __call__(self, field: WTField, **kwargs: Any) -> Markup: ) ) - field_html = Markup( + field_html = Markup( # nosec: B704 # noqa: S704 ' '.format( self.html_params(id='img_' + id_, src=value, width='200', **kwargs), self.html_params( diff --git a/src/baseframe/statsd.py b/src/baseframe/statsd.py index 2f9a102c..b850d38f 100644 --- a/src/baseframe/statsd.py +++ b/src/baseframe/statsd.py @@ -206,7 +206,7 @@ def gauge( delta=delta, ) - def set( # noqa: A003 + def set( self, stat: str, value: str, @@ -258,14 +258,14 @@ def _request_finished(self, app: Flask, response: Response) -> None: self.incr(metric_name, tags=tags) @staticmethod - def _before_render_template(app: Flask, template: Template, **kwargs) -> None: + def _before_render_template(app: Flask, template: Template, **_kwargs) -> None: """Record start time when rendering a template.""" if app.config['STATSD_RATE'] != 0: if not hasattr(g, TEMPLATE_START_TIME_ATTR): setattr(g, TEMPLATE_START_TIME_ATTR, {}) getattr(g, TEMPLATE_START_TIME_ATTR)[template] = time.time() - def _template_rendered(self, app: Flask, template: Template, **kwargs) -> None: + def _template_rendered(self, app: Flask, template: Template, **_kwargs) -> None: """Calculate time to render a template and log to statsd.""" start_time = getattr(g, TEMPLATE_START_TIME_ATTR, {}).get(template) if not start_time: diff --git a/src/baseframe/utils.py b/src/baseframe/utils.py index 3b23b5c8..a2f88241 100644 --- a/src/baseframe/utils.py +++ b/src/baseframe/utils.py @@ -27,12 +27,12 @@ __all__ = [ 'MxLookupError', - 'request_timestamp', 'is_public_email_domain', - 'localized_country_list', 'localize_timezone', - 'request_is_xhr', + 'localized_country_list', 'request_checked_xhr', + 'request_is_xhr', + 'request_timestamp', ] @@ -105,9 +105,7 @@ def is_public_email_domain( raise return default - if any(p['public'] for p in sniffedmx['providers']): - return True - return False + return bool(any(p['public'] for p in sniffedmx['providers'])) def localized_country_list() -> list[tuple[str, str]]: diff --git a/src/baseframe/views.py b/src/baseframe/views.py index 7c672952..240a4bf5 100644 --- a/src/baseframe/views.py +++ b/src/baseframe/views.py @@ -1,4 +1,5 @@ """Baseframe views and view support helpers.""" +# ruff: noqa: ARG001 import os import os.path @@ -115,16 +116,14 @@ def asset_path(bundle_key: str) -> str: @baseframe.app_context_processor def baseframe_context() -> dict[str, Any]: """Add Baseframe helper functions to Jinja2 template context.""" - return { - 'networkbar_links': networkbar_links, - 'asset_path': asset_path, - } + return {'networkbar_links': networkbar_links, 'asset_path': asset_path} @baseframe.route('/favicon.ico', subdomain='') @baseframe.route('/favicon.ico', defaults={'subdomain': None}) def favicon(subdomain: Optional[str] = None) -> Response: """Render a favicon from the app's static folder, falling back to default icon.""" + app_icon_path: Optional[str] = None app_icon_folder = current_app.static_folder # Does the app have a favicon.ico in /static? if not os.path.exists( @@ -132,13 +131,17 @@ def favicon(subdomain: Optional[str] = None) -> Response: ): # Nope? Is it in /static/img? app_icon_path = os.path.join( - current_app.static_folder, 'img' # type: ignore[arg-type] + current_app.static_folder or '', + 'img', ) if not os.path.exists(os.path.join(app_icon_path, 'favicon.ico')): # Still nope? Serve default favicon from baseframe app_icon_path = os.path.join( - baseframe.static_folder, 'img' # type: ignore[arg-type] + baseframe.static_folder or '', + 'img', ) + if not app_icon_path: + abort(404) return send_from_directory( app_icon_path, 'favicon.ico', mimetype='image/vnd.microsoft.icon' ) @@ -153,7 +156,8 @@ def humans(subdomain: Optional[str] = None) -> Response: current_app.static_folder if os.path.exists( os.path.join( - current_app.static_folder, 'humans.txt' # type: ignore[arg-type] + current_app.static_folder or '', + 'humans.txt', # type: ignore[arg-type] ) ) else baseframe.static_folder @@ -172,7 +176,8 @@ def robots(subdomain: Optional[str] = None) -> Response: current_app.static_folder if os.path.exists( os.path.join( - current_app.static_folder, 'robots.txt' # type: ignore[arg-type] + current_app.static_folder or '', + 'robots.txt', # type: ignore[arg-type] ) ) else baseframe.static_folder @@ -187,7 +192,8 @@ def robots(subdomain: Optional[str] = None) -> Response: def well_known(filename: str, subdomain: Optional[str] = None) -> Response: """Render .well-known folder contents from app's static folder.""" well_known_path = os.path.join( - current_app.static_folder, '.well-known' # type: ignore[arg-type] + current_app.static_folder or '', + '.well-known', # type: ignore[arg-type] ) return send_from_directory(well_known_path, filename) @@ -223,11 +229,10 @@ def csrf_refresh(subdomain: Optional[str] = None) -> ReturnRenderWith: """Serve a refreshed CSRF token to ensure HTML forms never expire.""" parsed_host = urlparse(request.url_root) origin = parsed_host.scheme + '://' + parsed_host.netloc - if 'Origin' in request.headers: - # Origin is present in (a) cross-site requests and (b) same site requests in - # some browsers. Therefore, if Origin is present, confirm it matches our domain. - if request.headers['Origin'] != origin: - abort(403) + # Origin is present in (a) cross-site requests and (b) same site requests in some + # browsers. Therefore, if Origin is present, confirm it matches our domain + if 'Origin' in request.headers and request.headers['Origin'] != origin: + abort(403) return ( {'csrf_token': generate_csrf()}, @@ -245,12 +250,14 @@ def csrf_refresh(subdomain: Optional[str] = None) -> ReturnRenderWith: @baseframe.after_app_request def process_response(response: Response) -> Response: """Process response objects to add additional headers.""" - if request.endpoint in ('static', 'baseframe.static'): - if 'Access-Control-Allow-Origin' not in response.headers: - # This is required for webfont resources - # Note: We do not serve static assets in production, nginx does. - # That means this piece of code will never be called in production. - response.headers['Access-Control-Allow-Origin'] = '*' + if ( + request.endpoint in ('static', 'baseframe.static') + and 'Access-Control-Allow-Origin' not in response.headers + ): + # This is required for webfont resources + # Note: We do not serve static assets in production, nginx does. + # That means this piece of code will never be called in production. + response.headers['Access-Control-Allow-Origin'] = '*' # If Babel was accessed in this request, the response's contents will vary with # the accepted language @@ -273,10 +280,9 @@ def process_response(response: Response) -> Response: # 'ALLOW' is an unofficial signal from the app to Baseframe. # It signals us to remove the header and not set a default response.headers.pop('X-Frame-Options') - else: - if request_has_auth() and getattr(current_auth, 'login_required', False): - # Protect only login_required pages from appearing in frames - response.headers['X-Frame-Options'] = 'SAMEORIGIN' + elif request_has_auth() and getattr(current_auth, 'login_required', False): + # Protect only login_required pages from appearing in frames + response.headers['X-Frame-Options'] = 'SAMEORIGIN' # In memoriam. http://www.gnuterrypratchett.com/ response.headers['X-Clacks-Overhead'] = 'GNU Terry Pratchett' diff --git a/tests/baseframe_tests/__init__.py b/tests/baseframe_tests/__init__.py index e69de29b..b0dd0a94 100644 --- a/tests/baseframe_tests/__init__.py +++ b/tests/baseframe_tests/__init__.py @@ -0,0 +1 @@ +"""Baseframe tests.""" diff --git a/tests/baseframe_tests/conftest.py b/tests/baseframe_tests/conftest.py index 46a692e2..be113cc3 100644 --- a/tests/baseframe_tests/conftest.py +++ b/tests/baseframe_tests/conftest.py @@ -1,31 +1,34 @@ """Test configuration.""" - # pylint: disable=redefined-outer-name +from collections.abc import Generator + import pytest from flask import Flask +from flask.ctx import AppContext +from flask.testing import FlaskClient from baseframe import baseframe @pytest.fixture -def app(): +def app() -> Flask: """App fixture.""" fixture_app = Flask(__name__) fixture_app.config['CACHE_TYPE'] = 'SimpleCache' - fixture_app.config['SECRET_KEY'] = 'test secret' # nosec + fixture_app.config['SECRET_KEY'] = 'test secret' # nosec: B105 # noqa: S105 baseframe.init_app(fixture_app, requires=['baseframe']) return fixture_app @pytest.fixture -def ctx(app): +def ctx(app: Flask) -> Generator[AppContext, None, None]: with app.app_context() as context: yield context @pytest.fixture -def client(app): +def client(app: Flask) -> Generator[FlaskClient, None, None]: """App client fixture.""" with app.test_client() as test_client: yield test_client diff --git a/tests/baseframe_tests/filters_test.py b/tests/baseframe_tests/filters_test.py index a4c58216..50632fd8 100644 --- a/tests/baseframe_tests/filters_test.py +++ b/tests/baseframe_tests/filters_test.py @@ -7,6 +7,7 @@ from typing import Optional import pytest +from flask import Flask from pytz import UTC, timezone from coaster.utils import md5sum @@ -15,7 +16,7 @@ @pytest.fixture(params=[True, False]) -def times(request): +def times(request: pytest.FixtureRequest) -> SimpleNamespace: """Sample times fixture.""" now = datetime.now(UTC) if request.param else datetime.utcnow() return SimpleNamespace( @@ -27,7 +28,7 @@ def times(request): ) -def test_dt_filters_age(times) -> None: +def test_dt_filters_age(times: SimpleNamespace) -> None: age = filters.age(times.now) assert age == 'now' @@ -52,14 +53,18 @@ def test_dt_filters_age(times) -> None: assert age == '2 years ago' -def test_dt_filters_shortdate_date_with_threshold(app, times) -> None: +def test_dt_filters_shortdate_date_with_threshold( + app: Flask, times: SimpleNamespace +) -> None: app.config['SHORTDATE_THRESHOLD_DAYS'] = 10 testdate = times.now.date() - timedelta(days=5) with app.test_request_context('/'): assert filters.shortdate(testdate) == testdate.strftime('%e %b') -def test_dt_filters_shortdate_date_without_threshold(app, times) -> None: +def test_dt_filters_shortdate_date_without_threshold( + app: Flask, times: SimpleNamespace +) -> None: app.config['SHORTDATE_THRESHOLD_DAYS'] = 0 testdate = times.now.date() - timedelta(days=5) with app.test_request_context('/'): @@ -68,14 +73,18 @@ def test_dt_filters_shortdate_date_without_threshold(app, times) -> None: ) -def test_dt_filters_shortdate_datetime_with_threshold(app, times) -> None: +def test_dt_filters_shortdate_datetime_with_threshold( + app: Flask, times: SimpleNamespace +) -> None: app.config['SHORTDATE_THRESHOLD_DAYS'] = 10 testdate = times.now - timedelta(days=5) with app.test_request_context('/'): assert filters.shortdate(testdate) == testdate.strftime('%e %b') -def test_dt_filters_shortdate_datetime_without_threshold(app, times) -> None: +def test_dt_filters_shortdate_datetime_without_threshold( + app: Flask, times: SimpleNamespace +) -> None: testdate = times.now - timedelta(days=5) with app.test_request_context('/'): assert filters.shortdate(testdate).replace("’", "'") == testdate.strftime( @@ -83,7 +92,9 @@ def test_dt_filters_shortdate_datetime_without_threshold(app, times) -> None: ) -def test_dt_filters_shortdate_datetime_with_tz(app, times) -> None: +def test_dt_filters_shortdate_datetime_with_tz( + app: Flask, times: SimpleNamespace +) -> None: testdate = times.now with app.test_request_context('/'): assert filters.shortdate(testdate).replace("’", "'") == testdate.strftime( @@ -91,47 +102,53 @@ def test_dt_filters_shortdate_datetime_with_tz(app, times) -> None: ) -def test_dt_filters_longdate_date(app, times) -> None: +def test_dt_filters_longdate_date(app: Flask, times: SimpleNamespace) -> None: testdate = times.now.date() with app.test_request_context('/'): assert filters.longdate(testdate) == testdate.strftime('%e %B %Y') -def test_dt_filters_longdate_datetime(app, times) -> None: +def test_dt_filters_longdate_datetime(app: Flask, times: SimpleNamespace) -> None: testdate = times.now with app.test_request_context('/'): assert filters.longdate(testdate) == testdate.strftime('%e %B %Y') -def test_dt_filters_longdate_datetime_with_tz(app, times) -> None: +def test_dt_filters_longdate_datetime_with_tz( + app: Flask, times: SimpleNamespace +) -> None: testdate = times.now with app.test_request_context('/'): assert filters.longdate(testdate) == testdate.strftime('%e %B %Y') -def test_dt_filters_date_filter(app, times) -> None: +def test_dt_filters_date_filter(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/'): assert ( filters.date_filter(times.date, 'yyyy-MM-dd', usertz=False) == '2020-01-31' ) -def test_dt_filters_date_localized_short_hi(app, times) -> None: +def test_dt_filters_date_localized_short_hi(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert filters.date_filter(times.date, format='short') == '31/1/20' -def test_dt_filters_date_localized_medium_hi(app, times) -> None: +def test_dt_filters_date_localized_medium_hi( + app: Flask, times: SimpleNamespace +) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert filters.date_filter(times.date, format='medium') == '31 जन॰ 2020' -def test_dt_filters_date_localized_long_hi(app, times) -> None: +def test_dt_filters_date_localized_long_hi(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert filters.date_filter(times.date, format='long') == '31 जनवरी 2020' -def test_dt_filters_time_localized_hi_medium(app, times) -> None: +def test_dt_filters_time_localized_hi_medium( + app: Flask, times: SimpleNamespace +) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert ( filters.datetime_filter(times.datetime, format='medium') @@ -139,32 +156,32 @@ def test_dt_filters_time_localized_hi_medium(app, times) -> None: ) -def test_dt_filters_month_localized_hi(app, times) -> None: +def test_dt_filters_month_localized_hi(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert filters.date_filter(times.date, "MMMM") == 'जनवरी' -def test_dt_filters_month_localized_en(app, times) -> None: +def test_dt_filters_month_localized_en(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/'): assert filters.date_filter(times.date, "MMMM") == 'January' -def test_dt_filters_time_localized_short(app, times) -> None: +def test_dt_filters_time_localized_short(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert filters.time_filter(times.datetime, format='short') == '12:00 am' -def test_dt_filters_time_localized_medium(app, times) -> None: +def test_dt_filters_time_localized_medium(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert filters.time_filter(times.datetime, format='medium') == '12:00:00 am' -def test_dt_filters_time_localized_long(app, times) -> None: +def test_dt_filters_time_localized_long(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert filters.time_filter(times.datetime, format='long') == '12:00:00 am UTC' -def test_dt_filters_time_localized_hi_full(app, times) -> None: +def test_dt_filters_time_localized_hi_full(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert ( filters.time_filter(times.time, format='full') @@ -172,7 +189,7 @@ def test_dt_filters_time_localized_hi_full(app, times) -> None: ) -def test_dt_filters_time_localized_en_full(app, times) -> None: +def test_dt_filters_time_localized_en_full(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'en'}): assert filters.time_filter(times.time, format='full') in ( '11:59:59 PM Coordinated Universal Time', @@ -180,7 +197,7 @@ def test_dt_filters_time_localized_en_full(app, times) -> None: ) -def test_dt_filters_datetime_with_usertz(app, times) -> None: +def test_dt_filters_datetime_with_usertz(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/'): assert filters.datetime_filter( times.datetimeEST, format='full', usertz=False @@ -190,7 +207,7 @@ def test_dt_filters_datetime_with_usertz(app, times) -> None: ) -def test_dt_filters_datetime_without_usertz(app, times) -> None: +def test_dt_filters_datetime_without_usertz(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/'): assert filters.datetime_filter( times.datetimeEST, format='full', usertz=True @@ -200,7 +217,7 @@ def test_dt_filters_datetime_without_usertz(app, times) -> None: ) -def test_dt_filters_date_dmy(app, times) -> None: +def test_dt_filters_date_dmy(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/'): assert ( filters.date_filter(times.datetime, format='short', locale='en_GB') @@ -208,7 +225,7 @@ def test_dt_filters_date_dmy(app, times) -> None: ) -def test_dt_filters_date_mdy(app, times) -> None: +def test_dt_filters_date_mdy(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/'): assert ( filters.date_filter(times.datetime, format='short', locale='en_US') @@ -216,12 +233,12 @@ def test_dt_filters_date_mdy(app, times) -> None: ) -def test_dt_filters_timestamp(app, times) -> None: +def test_dt_filters_timestamp(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/'): assert filters.timestamp_filter(times.datetime) == 1580428800.0 -def test_dt_filters_timestamp_filter(app, times) -> None: +def test_dt_filters_timestamp_filter(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/'): assert filters.timedelta_filter(times.now) == "1 second ago" assert filters.timedelta_filter(1) == "1 second" @@ -261,7 +278,7 @@ def test_dt_filters_timestamp_filter(app, times) -> None: ) -def test_dt_filters_timestamp_filter_hi(app, times) -> None: +def test_dt_filters_timestamp_filter_hi(app: Flask, times: SimpleNamespace) -> None: with app.test_request_context('/', headers={'Accept-Language': 'hi'}): assert filters.timedelta_filter(times.now) == "1 सेकंड पहले" assert filters.timedelta_filter(1) == "1 सेकंड" @@ -329,7 +346,7 @@ def test_initials() -> None: assert initial == '' -def test_usessl(app) -> None: +def test_usessl(app: Flask) -> None: with app.test_request_context('/'): app.config['USE_SSL'] = False ssled = filters.usessl('http://hasgeek.com') @@ -346,7 +363,7 @@ def test_usessl(app) -> None: assert ssled == 'https://localhost/static/test' -def test_nossl(app) -> None: +def test_nossl(app: Flask) -> None: with app.test_request_context('/'): nossled = filters.nossl('https://hasgeek.com') assert nossled == 'http://hasgeek.com' @@ -438,9 +455,8 @@ def test_preview() -> None: == "Hello all, Here is …" ) # Strips HTML tags and attributes when returning text - assert ( - filters.preview( - """ + assert filters.preview( + """

    Example.org is a reserved TLD for tests. @@ -449,11 +465,9 @@ def test_preview() -> None: Anyone may use it for any example use case.

    """ - ) - == ( - "Example.org is a reserved TLD for tests." - " Anyone may use it for any example use case." - ) + ) == ( + "Example.org is a reserved TLD for tests." + " Anyone may use it for any example use case." ) # Counts by Unicode graphemes, not code points, to avoid mangling letters assert filters.preview('हिंदी टायपिंग', min=1, max=3) == 'हिंदी…' diff --git a/tests/baseframe_tests/forms/__init__.py b/tests/baseframe_tests/forms/__init__.py new file mode 100644 index 00000000..c141d5e4 --- /dev/null +++ b/tests/baseframe_tests/forms/__init__.py @@ -0,0 +1 @@ +"""Baseframe form tests.""" diff --git a/tests/baseframe_tests/forms/fields_test.py b/tests/baseframe_tests/forms/fields_test.py index 8d9ef06b..f6fad229 100644 --- a/tests/baseframe_tests/forms/fields_test.py +++ b/tests/baseframe_tests/forms/fields_test.py @@ -6,6 +6,7 @@ from decimal import Decimal import pytest +from flask.ctx import AppContext from pytz import timezone, utc from werkzeug.datastructures import MultiDict @@ -48,7 +49,7 @@ class DateTimeForm(forms.Form): @pytest.fixture -def enum_form(ctx): +def enum_form(ctx: AppContext) -> EnumForm: """Enum form fixture.""" return EnumForm(meta={'csrf': False}) @@ -89,30 +90,30 @@ def test_enum_render(enum_form) -> None: @pytest.fixture -def json_form(ctx): +def json_form(ctx: AppContext) -> JsonForm: """JSON form fixture.""" return JsonForm(meta={'csrf': False}) -def test_json_default(json_form) -> None: +def test_json_default(json_form: JsonForm) -> None: assert json_form.jsondata.data == DEFAULT_JSONDATA assert json_form.jsondata_empty_default.data == {} assert json_form.jsondata_no_default.data is None -def test_json_valid(json_form) -> None: +def test_json_valid(json_form: JsonForm) -> None: json_form.process(formdata=MultiDict({'jsondata': '{"key": "val"}'})) assert json_form.validate() is True -def test_json_invalid(json_form) -> None: +def test_json_invalid(json_form: JsonForm) -> None: json_form.process( formdata=MultiDict({'jsondata': '{"key"; "val"}'}) ) # invalid JSON assert json_form.validate() is False -def test_json_empty_default(json_form) -> None: +def test_json_empty_default(json_form: JsonForm) -> None: json_form.process( formdata=MultiDict( { @@ -127,7 +128,7 @@ def test_json_empty_default(json_form) -> None: assert json_form.jsondata_no_default.data is None -def test_json_nondict(json_form) -> None: +def test_json_nondict(json_form: JsonForm) -> None: json_form.process(formdata=MultiDict({'jsondata': '43'})) assert json_form.validate() is False json_form.process(formdata=MultiDict({'jsondata': 'true'})) @@ -139,18 +140,18 @@ def test_json_nondict(json_form) -> None: assert json_form.validate() is True -def test_json_unicode(json_form) -> None: +def test_json_unicode(json_form: JsonForm) -> None: json_form.process(formdata=MultiDict({'jsondata': '{"key": "val😡"}'})) assert json_form.validate() is True assert json_form.jsondata.data == {"key": "val😡"} -def test_json_unicode_dumps(json_form) -> None: +def test_json_unicode_dumps(json_form: JsonForm) -> None: json_form.jsondata.data = {"key": "val😡"} assert json_form.jsondata._value() == '{\n "key": "val😡"\n}' -def test_json_decimal(json_form) -> None: +def test_json_decimal(json_form: JsonForm) -> None: json_form.jsondata.data = {"key": Decimal('1.2')} assert json_form.validate() is True assert json_form.jsondata._value() == '{\n "key": "1.2"\n}' @@ -164,7 +165,7 @@ def test_json_decimal(json_form) -> None: assert json_form.jsondata.data == {"key": 1.2} -def test_json_array(json_form) -> None: +def test_json_array(json_form: JsonForm) -> None: json_form.process( formdata=MultiDict({'jsondata': '[{"key": "val"}, {"key2": "val2"}]'}) ) @@ -177,7 +178,7 @@ def test_json_array(json_form) -> None: assert json_form.jsondata_no_dict.data == [{"key": "val"}, {"key2": "val2"}] -def test_json_comment(json_form) -> None: +def test_json_comment(json_form: JsonForm) -> None: json_form.process( formdata=MultiDict( { @@ -192,7 +193,7 @@ def test_json_comment(json_form) -> None: assert json_form.validate() is False -def test_json_non_serializable(json_form) -> None: +def test_json_non_serializable(json_form: JsonForm) -> None: json_form.jsondata.data = {"key": complex(1, 2)} with pytest.raises(TypeError): json_form.jsondata._value() @@ -321,7 +322,7 @@ def test_date_time_field(test_input, expected_naive, expected_aware) -> None: '100000-01-01', ], ) -def test_date_time_field_badvalue(test_input) -> None: +def test_date_time_field_badvalue(test_input: str) -> None: """Assert bad datetime input is recorded as a ValidationError.""" form = DateTimeForm(meta={'csrf': False}) form.process(formdata=MultiDict({'naive': test_input, 'aware': test_input})) diff --git a/tests/baseframe_tests/forms/form_test.py b/tests/baseframe_tests/forms/form_test.py index 96ca6fea..6487bd03 100644 --- a/tests/baseframe_tests/forms/form_test.py +++ b/tests/baseframe_tests/forms/form_test.py @@ -1,6 +1,8 @@ """Test forms.""" - # pylint: disable=redefined-outer-name +# ruff: noqa: ARG002 + +from typing import Any import pytest from werkzeug.datastructures import MultiDict @@ -17,7 +19,7 @@ class SimpleUser: company: str pw_hash: str - def _set_password(self, value: str): + def _set_password(self, value: str) -> None: self.pw_hash = str(password_hash(value)) password = property(fset=_set_password) @@ -38,40 +40,41 @@ class GetSetForm(forms.Form): password = forms.PasswordField("Password") confirm_password = forms.PasswordField("Confirm password") - def get_firstname(self, obj): + def get_firstname(self, obj: SimpleUser) -> str: return obj.fullname.split(' ', 1)[0] - def get_lastname(self, obj): + def get_lastname(self, obj: SimpleUser) -> str: parts = obj.fullname.split(' ', 1) if len(parts) > 1: return parts[-1] return '' - def get_password(self, obj): + def get_password(self, obj: SimpleUser) -> str: return '' - def get_confirm_password(self, obj): + def get_confirm_password(self, obj: SimpleUser) -> str: return '' - def set_firstname(self, obj): + def set_firstname(self, obj: SimpleUser) -> None: pass - def set_lastname(self, obj): - obj.fullname = self.firstname.data + " " + self.lastname.data + def set_lastname(self, obj: SimpleUser) -> None: + obj.fullname = (self.firstname.data or '') + " " + (self.lastname.data or '') - def set_password(self, obj): + def set_password(self, obj: SimpleUser) -> None: obj.password = self.password.data - def set_confirm_password(self, obj): + def set_confirm_password(self, obj: SimpleUser) -> None: pass class InitOrderForm(forms.Form): __expects__ = ('expected_item',) + expected_item: Any has_context = forms.StringField("Has context") - def get_has_context(self, obj): + def get_has_context(self, obj: Any) -> Any: return self.expected_item @@ -82,7 +85,9 @@ class FieldRenderForm(forms.Form): @pytest.fixture def user() -> SimpleUser: return SimpleUser( # nosec - fullname="Test user", company="Test company", password="test" + fullname="Test user", + company="Test company", + password="test", # noqa: S106 ) @@ -216,7 +221,7 @@ def __post_init__(self) -> None: @pytest.mark.usefixtures('ctx') def test_set_queries_gets_called() -> None: - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match='is deprecated'): class TestForm(forms.Form): set_queries_called: bool = False @@ -230,7 +235,7 @@ def set_queries(self) -> None: @pytest.mark.usefixtures('ctx') def test_only_post_init_called() -> None: - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match='is deprecated'): class TestForm(forms.Form): post_init_called: bool = False diff --git a/tests/baseframe_tests/forms/sqlalchemy_test.py b/tests/baseframe_tests/forms/sqlalchemy_test.py index 4130d2ec..b0478935 100644 --- a/tests/baseframe_tests/forms/sqlalchemy_test.py +++ b/tests/baseframe_tests/forms/sqlalchemy_test.py @@ -2,9 +2,13 @@ # pylint: disable=redefined-outer-name +from collections.abc import Generator + import pytest +from flask import Flask +from flask.ctx import AppContext from flask_sqlalchemy import SQLAlchemy -from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import DeclarativeBase, scoped_session from coaster.sqlalchemy import ModelBase, Query @@ -40,7 +44,7 @@ class DocumentForm(forms.Form): @pytest.fixture -def database(app): +def database(app: Flask) -> Generator[SQLAlchemy, None, None]: """Database structure.""" app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False @@ -53,7 +57,7 @@ def database(app): @pytest.fixture -def db_session(database): +def db_session(database: SQLAlchemy) -> Generator[scoped_session, None, None]: """Database session fixture.""" savepoint = database.session.begin_nested() yield database.session @@ -62,11 +66,11 @@ def db_session(database): @pytest.fixture -def form(ctx): +def form(ctx: AppContext) -> DocumentForm: return DocumentForm(model=Document, meta={'csrf': False}) -def test_available_attr(form, db_session) -> None: +def test_available_attr(form: DocumentForm, db_session: scoped_session) -> None: """Test AvailableAttr SQLAlchemy validator.""" d1 = Document() form.process(name='d1', title='t1') diff --git a/tests/baseframe_tests/forms/validators_test.py b/tests/baseframe_tests/forms/validators_test.py index 1c3e8f7a..96174ccf 100644 --- a/tests/baseframe_tests/forms/validators_test.py +++ b/tests/baseframe_tests/forms/validators_test.py @@ -4,12 +4,15 @@ import re import warnings +from collections.abc import Generator from types import SimpleNamespace from typing import Any import pytest import requests_mock import urllib3 +from flask import Flask +from flask.ctx import AppContext from werkzeug.datastructures import MultiDict from baseframe import forms @@ -63,7 +66,7 @@ class PublicEmailDomainFormTest(forms.Form): @pytest.fixture -def tforms(ctx): +def tforms(ctx: AppContext) -> Generator[SimpleNamespace, None, None]: urllib3.disable_warnings() yield SimpleNamespace( url_form=UrlFormTest(meta={'csrf': False}), @@ -86,35 +89,35 @@ def test_is_empty() -> None: assert forms.validators.is_empty(None) is True -def test_valid_url(app, tforms) -> None: +def test_valid_url(app: Flask, tforms: SimpleNamespace) -> None: with app.test_request_context('/'): url = 'https://hasgeek.com/' tforms.url_form.process(url=url) assert tforms.url_form.validate() -def test_invalid_url(app, tforms) -> None: +def test_invalid_url(app: Flask, tforms: SimpleNamespace) -> None: with app.test_request_context('/'): url = 'https://hasgeek' tforms.url_form.process(url=url) assert not tforms.url_form.validate() -def test_valid_emoji(app, tforms) -> None: +def test_valid_emoji(app: Flask, tforms: SimpleNamespace) -> None: with app.test_request_context('/'): dat = '👍' tforms.emoji_form.process(emoji=dat) assert tforms.emoji_form.validate() is True -def test_invalid_emoji(app, tforms) -> None: +def test_invalid_emoji(app: Flask, tforms: SimpleNamespace) -> None: with app.test_request_context('/'): dat = 'eviltext' tforms.emoji_form.process(emoji=dat) assert tforms.emoji_form.validate() is False -def test_public_email_domain(app, tforms) -> None: +def test_public_email_domain(app: Flask, tforms: SimpleNamespace) -> None: with app.test_request_context('/'): # both valid tforms.webmail_form.process( @@ -154,7 +157,7 @@ def test_public_email_domain(app, tforms) -> None: assert 'not_webmail_domain' not in tforms.webmail_form.errors -def test_public_email_domain_helper(app) -> None: +def test_public_email_domain_helper(app: Flask) -> None: with app.test_request_context('/'): assert is_public_email_domain('gmail.com', default=False) assert not is_public_email_domain('google.com', default=False) @@ -179,21 +182,21 @@ def test_public_email_domain_helper(app) -> None: ) -def test_url_without_protocol(app, tforms) -> None: +def test_url_without_protocol(app: Flask, tforms: SimpleNamespace) -> None: with app.test_request_context('/'): url = 'hasgeek.com' tforms.url_form.process(url=url) assert not tforms.url_form.validate() -def test_inaccessible_url(app, tforms) -> None: +def test_inaccessible_url(app: Flask, tforms: SimpleNamespace) -> None: with app.test_request_context('/'): url = 'http://4dc1f6f0e7bc44f2b5b44f00abea4eae.com/' tforms.url_form.process(url=url) assert not tforms.url_form.validate() -def test_disallowed_url(app, tforms) -> None: +def test_disallowed_url(app: Flask, tforms: SimpleNamespace) -> None: with app.test_request_context('/'): url = 'https://example.com/' tforms.url_form.process(url=url) @@ -203,7 +206,7 @@ def test_disallowed_url(app, tforms) -> None: assert not tforms.url_form.validate() -def test_html_snippet_valid_urls(app, tforms) -> None: +def test_html_snippet_valid_urls(app: Flask, tforms: SimpleNamespace) -> None: url1 = 'https://hasgeek.com/' url2 = 'https://hasjob.co/' with app.test_request_context('/'): @@ -217,7 +220,7 @@ def test_html_snippet_valid_urls(app, tforms) -> None: assert tforms.all_urls_form.validate() -def test_html_snippet_invalid_urls(app, tforms) -> None: +def test_html_snippet_invalid_urls(app: Flask, tforms: SimpleNamespace) -> None: url1 = 'https://hasgeek.com/' url2 = 'https://hasjob' with app.test_request_context('/'): @@ -534,7 +537,7 @@ class TestFormBase: form: forms.Form @pytest.fixture(autouse=True) - def _setup(self, app): + def _setup(self, app: Flask) -> Generator[None, None, None]: with app.app_context(): self.form = self.Form(meta={'csrf': False}) yield @@ -786,3 +789,98 @@ class Form(forms.Form): other_empty = None # '' is not valid in IntegerField other_not_empty = 0 + + +class TestValidCoordinates(TestFormBase): + class Form(forms.Form): + """Test form.""" + + coordinates = forms.CoordinatesField( + "Location", + validators=[forms.validators.ValidCoordinates()], + ) + + def test_none(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': None})) + assert not self.form.validate() + + def test_empty(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': ()})) + assert not self.form.validate() + + def test_nones(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (None, None)})) + assert not self.form.validate() + + def test_blanks(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': ('', '')})) + assert not self.form.validate() + + def test_no_lat(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (None, 0)})) + assert not self.form.validate() + + def test_no_lon(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (0, None)})) + assert not self.form.validate() + + def test_invalid_lat(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (-100, 0)})) + assert not self.form.validate() + + def test_invalid_lon(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (0, -200)})) + assert not self.form.validate() + + def test_valid_latlon(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (13, 77.6)})) + assert self.form.validate() + + +class TestOptionalCoordinates(TestFormBase): + class Form(forms.Form): + """Test form.""" + + coordinates = forms.CoordinatesField( + "Location", + validators=[ + forms.validators.OptionalCoordinates(), + ], + ) + + def test_none(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': None})) + assert self.form.validate() + + def test_empty(self) -> None: + self.form.process(fromdata=MultiDict({'coordinates': ''})) + assert self.form.validate() + + def test_nones(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (None, None)})) + self.form.validate() + assert self.form.errors == {} + + def test_blanks(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': ('', '')})) + assert self.form.validate() + + def test_no_lat(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (None, 0)})) + assert not self.form.validate() + + def test_no_lon(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (0, None)})) + assert not self.form.validate() + + def test_invalid_lat(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (-100, 0)})) + assert not self.form.validate() + + def test_invalid_lon(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (0, -200)})) + assert not self.form.validate() + + def test_valid_latlon(self) -> None: + self.form.process(formdata=MultiDict({'coordinates': (13, 77.6)})) + assert self.form.validate() diff --git a/tests/baseframe_tests/statsd_test.py b/tests/baseframe_tests/statsd_test.py index 04886a9e..6e8bb561 100644 --- a/tests/baseframe_tests/statsd_test.py +++ b/tests/baseframe_tests/statsd_test.py @@ -4,11 +4,16 @@ # Tests adapted from https://github.com/bbelyeu/flask-statsdclient +from __future__ import annotations + +from collections.abc import Callable from datetime import timedelta from unittest.mock import patch import pytest from flask import Flask, render_template_string +from flask.ctx import AppContext +from flask.typing import ResponseReturnValue from flask_babel import Babel from statsd.client.timer import Timer from statsd.client.udp import Pipeline @@ -18,29 +23,29 @@ @pytest.fixture -def app(): +def app() -> Flask: """Redefine app without Baseframe for statsd tests.""" return Flask(__name__) @pytest.fixture -def statsd(app): +def statsd(app: Flask) -> Statsd: s = Statsd() s.init_app(app) return s @pytest.fixture -def view(app): +def view(app: Flask) -> Callable[[], ResponseReturnValue]: @app.route('/') - def index(): + def index() -> ResponseReturnValue: return 'index' return index @pytest.fixture -def form(app): +def form(app: Flask) -> forms.Form: Babel(app) # Needed for form validator message translations class SimpleForm(forms.Form): @@ -51,12 +56,12 @@ class SimpleForm(forms.Form): return SimpleForm(meta={'csrf': False}) -def test_default_config(app, statsd) -> None: +def test_default_config(app: Flask, statsd: Statsd) -> None: # pylint: disable=protected-access assert app.extensions['statsd_core']._addr == ('127.0.0.1', 8125) -def test_custom_config(app) -> None: +def test_custom_config(app: Flask) -> None: # pylint: disable=protected-access app.config['STATSD_HOST'] = '1.2.3.4' app.config['STATSD_PORT'] = 12345 @@ -71,7 +76,7 @@ def test_custom_config(app) -> None: # 3. Insert tags if tags are enabled and specified -def test_incr(ctx, statsd) -> None: +def test_incr(ctx: AppContext, statsd) -> None: with patch('statsd.StatsClient.incr') as mock_incr: statsd.incr('test.counter') mock_incr.assert_called_once_with( @@ -89,7 +94,7 @@ def test_incr(ctx, statsd) -> None: ) -def test_decr(ctx, statsd) -> None: +def test_decr(ctx: AppContext, statsd: Statsd) -> None: with patch('statsd.StatsClient.decr') as mock_decr: statsd.decr('test.counter') mock_decr.assert_called_once_with( @@ -107,7 +112,7 @@ def test_decr(ctx, statsd) -> None: ) -def test_gauge(ctx, statsd) -> None: +def test_gauge(ctx: AppContext, statsd: Statsd) -> None: with patch('statsd.StatsClient.gauge') as mock_gauge: statsd.gauge('test.gauge', 5) mock_gauge.assert_called_once_with( @@ -125,7 +130,7 @@ def test_gauge(ctx, statsd) -> None: ) -def test_set(ctx, statsd) -> None: +def test_set(ctx: AppContext, statsd: Statsd) -> None: with patch('statsd.StatsClient.set') as mock_set: statsd.set('test.set', 'item') mock_set.assert_called_once_with( @@ -138,7 +143,7 @@ def test_set(ctx, statsd) -> None: ) -def test_timing(ctx, statsd) -> None: +def test_timing(ctx: AppContext, statsd: Statsd) -> None: with patch('statsd.StatsClient.timing') as mock_timing: statsd.timing('test.timing', 10) mock_timing.assert_called_once_with( @@ -153,7 +158,7 @@ def test_timing(ctx, statsd) -> None: ) -def test_timer(ctx, statsd) -> None: +def test_timer(ctx: AppContext, statsd: Statsd) -> None: timer = statsd.timer('test.timer', rate=1) assert isinstance(timer, Timer) assert timer.stat == 'flask_app.baseframe_tests.statsd_test.test.timer' @@ -165,12 +170,12 @@ def test_timer(ctx, statsd) -> None: assert timer.rate == 0.5 -def test_pipeline(ctx, statsd) -> None: +def test_pipeline(ctx: AppContext, statsd: Statsd) -> None: pipeline = statsd.pipeline() assert isinstance(pipeline, Pipeline) -def test_custom_rate(app, ctx, statsd) -> None: +def test_custom_rate(app: Flask, ctx: AppContext, statsd: Statsd) -> None: app.config['STATSD_RATE'] = 0.3 with patch('statsd.StatsClient.incr') as mock_incr: statsd.incr('test.counter') @@ -184,7 +189,7 @@ def test_custom_rate(app, ctx, statsd) -> None: ) -def test_tags(app, ctx, statsd) -> None: +def test_tags(app: Flask, ctx: AppContext, statsd: Statsd) -> None: # Tags are converted into buckets if statsd doesn't support them with patch('statsd.StatsClient.incr') as mock_incr: statsd.incr('test.counter', tags={'tag': 'value'}) @@ -236,7 +241,9 @@ def test_tags(app, ctx, statsd) -> None: ) -def test_request_handler_notags(app, statsd, view) -> None: +def test_request_handler_notags( + app: Flask, statsd: Statsd, view: Callable[[], ResponseReturnValue] +) -> None: """Test request_handlers logging with tags disabled.""" with ( patch('statsd.StatsClient.incr') as mock_incr, @@ -261,7 +268,9 @@ def test_request_handler_notags(app, statsd, view) -> None: mock_timing.assert_called() -def test_request_handler_tags(app, statsd, view) -> None: +def test_request_handler_tags( + app: Flask, statsd: Statsd, view: Callable[[], ResponseReturnValue] +) -> None: """Test request_handlers logging with tags enabled.""" app.config['STATSD_TAGS'] = ',' with ( @@ -279,7 +288,9 @@ def test_request_handler_tags(app, statsd, view) -> None: mock_timing.assert_called_once() -def test_request_handler_disabled(app, view) -> None: +def test_request_handler_disabled( + app: Flask, view: Callable[[], ResponseReturnValue] +) -> None: """Test request_handlers logging disabled.""" app.config['STATSD_REQUEST_LOG'] = False Statsd(app) @@ -293,7 +304,7 @@ def test_request_handler_disabled(app, view) -> None: mock_timing.assert_not_called() -def test_render_template_notags(app, statsd) -> None: +def test_render_template_notags(app: Flask, statsd: Statsd) -> None: """Test render_template logging with tags disabled.""" with ( patch('statsd.StatsClient.incr') as mock_incr, @@ -305,17 +316,15 @@ def test_render_template_notags(app, statsd) -> None: assert mock_timing.call_count == 2 assert [c[0][0] for c in mock_incr.call_args_list] == [ 'flask_app.baseframe_tests.statsd_test.render_template.template__str', - 'flask_app.baseframe_tests.statsd_test.render_template' - '.template__overall', + 'flask_app.baseframe_tests.statsd_test.render_template.template__overall', ] assert [c[0][0] for c in mock_incr.call_args_list] == [ 'flask_app.baseframe_tests.statsd_test.render_template.template__str', - 'flask_app.baseframe_tests.statsd_test.render_template' - '.template__overall', + 'flask_app.baseframe_tests.statsd_test.render_template.template__overall', ] -def test_render_template_tags(app, statsd) -> None: +def test_render_template_tags(app: Flask, statsd: Statsd) -> None: """Test render_template logging with tags enabled.""" app.config['STATSD_TAGS'] = ',' with ( @@ -336,7 +345,9 @@ def test_render_template_tags(app, statsd) -> None: ) -def test_render_template_disabled(app, view) -> None: +def test_render_template_disabled( + app: Flask, view: Callable[[], ResponseReturnValue] +) -> None: """Test render_template logging disabled.""" app.config['STATSD_RENDERTEMPLATE_LOG'] = False Statsd(app) @@ -350,7 +361,9 @@ def test_render_template_disabled(app, view) -> None: mock_timing.assert_not_called() -def test_form_success(ctx, app, statsd, form) -> None: +def test_form_success( + ctx: AppContext, app: Flask, statsd: Statsd, form: forms.Form +) -> None: app.config['STATSD_TAGS'] = ',' with patch('statsd.StatsClient.incr') as mock_incr: form.field.data = "test" @@ -363,7 +376,7 @@ def test_form_success(ctx, app, statsd, form) -> None: ) -def test_form_error(ctx, app, statsd, form) -> None: +def test_form_error(ctx, app: Flask, statsd: Statsd, form: forms.Form) -> None: app.config['STATSD_TAGS'] = ',' with patch('statsd.StatsClient.incr') as mock_incr: form.field.data = None @@ -376,7 +389,9 @@ def test_form_error(ctx, app, statsd, form) -> None: ) -def test_form_nolog(ctx, app, statsd, form) -> None: +def test_form_nolog( + ctx: AppContext, app: Flask, statsd: Statsd, form: forms.Form +) -> None: app.config['STATSD_TAGS'] = ',' app.config['STATSD_FORM_LOG'] = False with patch('statsd.StatsClient.incr') as mock_incr: @@ -388,7 +403,9 @@ def test_form_nolog(ctx, app, statsd, form) -> None: mock_incr.assert_not_called() -def test_form_signals_off(ctx, app, statsd, form) -> None: +def test_form_signals_off( + ctx: AppContext, app: Flask, statsd: Statsd, form: forms.Form +) -> None: app.config['STATSD_TAGS'] = ',' app.config['STATSD_FORM_LOG'] = True with patch('statsd.StatsClient.incr') as mock_incr: diff --git a/tests/baseframe_tests/utils_test.py b/tests/baseframe_tests/utils_test.py index 252bb994..6978b296 100644 --- a/tests/baseframe_tests/utils_test.py +++ b/tests/baseframe_tests/utils_test.py @@ -1,8 +1,10 @@ """Test utils.""" - # pylint: disable=redefined-outer-name +from collections.abc import Callable + import pytest +from flask import Flask from baseframe.utils import ( _localized_country_list_inner, @@ -15,11 +17,11 @@ @pytest.fixture -def testview(app): +def testview(app: Flask) -> Callable[[], str]: """Add a view to the app for testing.""" @app.route('/localetest') - def locale_testview(): + def locale_testview() -> str: country_list = localized_country_list() return dict(country_list)['DE'] From 18aba93ec6650a1de2b5125c8816c61714b94495 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:22:11 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/baseframe/assets.py | 298 ++-- src/baseframe/static/css/baseframe-bs3.css | 8 +- src/baseframe/static/css/baseframe.css | 8 +- .../static/css/bootstrap3/bootstrap.css | 18 +- src/baseframe/static/css/codemirror.css | 3 +- src/baseframe/static/css/editor.css | 5 +- src/baseframe/static/css/mui.css | 5 +- src/baseframe/static/css/rrssb.css | 5 +- src/baseframe/static/js/dropzone.js | 4 +- src/baseframe/static/js/footable-filter.js | 28 +- src/baseframe/static/js/footable-paginate.js | 62 +- src/baseframe/static/js/footable-sort.js | 26 +- src/baseframe/static/js/footable.js | 98 +- src/baseframe/static/js/jQRangeSlider-min.js | 339 ++--- src/baseframe/static/js/jquery-easytabs.js | 4 +- src/baseframe/static/js/jquery-modal.js | 48 +- src/baseframe/static/js/jquery.expander.js | 4 +- src/baseframe/static/js/jquery.succinct.js | 4 +- src/baseframe/static/js/jquery.truncate8.js | 34 +- .../static/js/jquery.ui.touch-punch.js | 16 +- src/baseframe/static/js/jquery_jeditable.js | 77 +- src/baseframe/static/js/leaflet-search.js | 84 +- src/baseframe/static/js/leaflet.js | 1166 +++++++-------- src/baseframe/static/js/mui.js | 142 +- src/baseframe/static/js/pace.js | 95 +- .../static/js/ractive-transitions-fly.js | 4 +- src/baseframe/static/js/ractive.js | 1263 +++++++++-------- src/baseframe/static/js/validate.js | 8 +- .../static/less/bootstrap/mixins.less | 6 +- src/baseframe/static/sass/mui/_custom.scss | 7 +- src/baseframe/static/sass/rrssb.scss | 5 +- 31 files changed, 1986 insertions(+), 1888 deletions(-) diff --git a/src/baseframe/assets.py b/src/baseframe/assets.py index 8fd4feb6..438e1db4 100644 --- a/src/baseframe/assets.py +++ b/src/baseframe/assets.py @@ -10,16 +10,16 @@ assets = VersionedAssets() assets['baseframe-networkbar.js'][Version(__version__)] = 'baseframe/js/networkbar.js' -assets['baseframe-networkbar.css'][ - Version(__version__) -] = 'baseframe/css/networkbar.css' -assets['baseframe-base.js'][ - Version(__version__) -] = 'baseframe/js/baseframe-bootstrap.js' +assets['baseframe-networkbar.css'][Version(__version__)] = ( + 'baseframe/css/networkbar.css' +) +assets['baseframe-base.js'][Version(__version__)] = ( + 'baseframe/js/baseframe-bootstrap.js' +) assets['baseframe-base.css'][Version(__version__)] = 'baseframe/css/baseframe.css' -assets['baseframe-base-bs3.css'][ - Version(__version__) -] = 'baseframe/css/baseframe-bs3.css' +assets['baseframe-base-bs3.css'][Version(__version__)] = ( + 'baseframe/css/baseframe-bs3.css' +) # Bootstrap 3.3.1 assets['bootstrap.css'][Version('3.3.1')] = 'baseframe/css/bootstrap3/bootstrap.css' @@ -72,9 +72,9 @@ assets['jquery.ui.sortable.js'][Version('1.12.1')] = ( 'baseframe/js/jquery-ui-sortable-1.12.1.js', ) -assets['jquery.ui.sortable.css'][ - Version('1.12.1') -] = 'baseframe/css/jquery-ui-sortable-1.12.1.css' +assets['jquery.ui.sortable.css'][Version('1.12.1')] = ( + 'baseframe/css/jquery-ui-sortable-1.12.1.css' +) assets['jquery.ui.touch-punch.js'][Version('0.2.3')] = ( 'baseframe/js/jquery.ui.touch-punch.js', ) @@ -98,9 +98,9 @@ assets['jquery.nouislider.js'][Version('7.0.10')] = ( 'baseframe/js/jquery.nouislider.min.js', ) -assets['jquery.nouislider.css'][ - Version('7.0.10') -] = 'baseframe/css/jquery.nouislider.min.css' +assets['jquery.nouislider.css'][Version('7.0.10')] = ( + 'baseframe/css/jquery.nouislider.min.css' +) # textarea-expander is deprecated. Use autosize instead assets['jquery.textarea-expander.js'][Version('1.0.0')] = ( @@ -169,84 +169,84 @@ assets['select2-material.js'][Version('4.0.3')] = {'requires': ['select2.js==4.0.3']} -assets['bootstrap-multiselect.css'][ - Version('0.9.13') -] = 'baseframe/css/bootstrap-multiselect.css' -assets['bootstrap-multiselect.js'][ - Version('0.9.13') -] = 'baseframe/js/bootstrap-multiselect.js' +assets['bootstrap-multiselect.css'][Version('0.9.13')] = ( + 'baseframe/css/bootstrap-multiselect.css' +) +assets['bootstrap-multiselect.js'][Version('0.9.13')] = ( + 'baseframe/js/bootstrap-multiselect.js' +) assets['codemirror.js'][Version('4.11.0')] = 'baseframe/js/codemirror/lib/codemirror.js' -assets['codemirror.css'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/lib/codemirror.css' - -assets['codemirror.mode.markdown.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/mode/markdown/markdown.js' -assets['codemirror.mode.gfm.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/mode/gfm/gfm.js' -assets['codemirror.mode.htmlmixed.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/mode/htmlmixed/htmlmixed.js' -assets['codemirror.mode.css.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/mode/css/css.js' -assets['codemirror.mode.javascript.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/mode/javascript/javascript.js' -assets['codemirror.addon.display.fullscreen.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/display/fullscreen.js' -assets['codemirror.addon.display.fullscreen.css'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/display/fullscreen.css' -assets['codemirror.addon.display.rulers.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/display/rulers.js' -assets['codemirror.addon.mode.overlay.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/mode/overlay.js' -assets['codemirror.addon.edit.continuelist.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/edit/continuelist.js' -assets['codemirror.addon.edit.closebrackets.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/edit/closebrackets.js' -assets['codemirror.addon.edit.matchbrackets.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/edit/matchbrackets.js' -assets['codemirror.addon.hint.show-hint.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/hint/show-hint.js' -assets['codemirror.addon.hint.show-hint.css'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/hint/show-hint.css' -assets['codemirror.addon.hint.css-hint.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/hint/css-hint.js' -assets['codemirror.addon.hint.javascript-hint.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/hint/javascript-hint.js' -assets['codemirror.addon.lint.css-lint.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/lint/css-lint.js' -assets['codemirror.addon.lint.json-lint.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/lint/json-lint.js' -assets['codemirror.addon.fold.brace-fold.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/fold/brace-fold.js' -assets['codemirror.addon.fold.foldcode.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/fold/foldcode.js' -assets['codemirror.addon.fold.foldgutter.js'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/fold/foldgutter.js' -assets['codemirror.addon.fold.foldgutter.css'][ - Version('4.11.0') -] = 'baseframe/js/codemirror/addon/fold/foldgutter.css' +assets['codemirror.css'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/lib/codemirror.css' +) + +assets['codemirror.mode.markdown.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/mode/markdown/markdown.js' +) +assets['codemirror.mode.gfm.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/mode/gfm/gfm.js' +) +assets['codemirror.mode.htmlmixed.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/mode/htmlmixed/htmlmixed.js' +) +assets['codemirror.mode.css.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/mode/css/css.js' +) +assets['codemirror.mode.javascript.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/mode/javascript/javascript.js' +) +assets['codemirror.addon.display.fullscreen.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/display/fullscreen.js' +) +assets['codemirror.addon.display.fullscreen.css'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/display/fullscreen.css' +) +assets['codemirror.addon.display.rulers.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/display/rulers.js' +) +assets['codemirror.addon.mode.overlay.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/mode/overlay.js' +) +assets['codemirror.addon.edit.continuelist.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/edit/continuelist.js' +) +assets['codemirror.addon.edit.closebrackets.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/edit/closebrackets.js' +) +assets['codemirror.addon.edit.matchbrackets.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/edit/matchbrackets.js' +) +assets['codemirror.addon.hint.show-hint.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/hint/show-hint.js' +) +assets['codemirror.addon.hint.show-hint.css'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/hint/show-hint.css' +) +assets['codemirror.addon.hint.css-hint.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/hint/css-hint.js' +) +assets['codemirror.addon.hint.javascript-hint.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/hint/javascript-hint.js' +) +assets['codemirror.addon.lint.css-lint.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/lint/css-lint.js' +) +assets['codemirror.addon.lint.json-lint.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/lint/json-lint.js' +) +assets['codemirror.addon.fold.brace-fold.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/fold/brace-fold.js' +) +assets['codemirror.addon.fold.foldcode.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/fold/foldcode.js' +) +assets['codemirror.addon.fold.foldgutter.js'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/fold/foldgutter.js' +) +assets['codemirror.addon.fold.foldgutter.css'][Version('4.11.0')] = ( + 'baseframe/js/codemirror/addon/fold/foldgutter.css' +) assets['codemirror-markdown.js'][Version('4.11.0')] = { 'requires': [ @@ -305,46 +305,46 @@ ] } -assets['codemirror.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/codemirror.min.js' -assets['codemirror.css'][ - Version('5.53.2') -] = 'baseframe/css/codemirror-5.53.2/codemirror-basic.css' - -assets['codemirror.mode.markdown.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/mode/markdown/markdown.min.js' -assets['codemirror.mode.gfm.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/mode/gfm/gfm.min.js' -assets['codemirror.mode.htmlmixed.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/mode/htmlmixed/htmlmixed.min.js' -assets['codemirror.mode.css.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/mode/css/css.min.js' -assets['codemirror.addon.mode.overlay.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/addon/mode/overlay.min.js' -assets['codemirror.addon.edit.continuelist.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/addon/edit/continuelist.min.js' -assets['codemirror.addon.edit.closebrackets.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/addon/edit/closebrackets.min.js' -assets['codemirror.addon.edit.matchbrackets.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/addon/edit/matchbrackets.min.js' -assets['codemirror.addon.hint.css-hint.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/addon/hint/css-hint.min.js' -assets['codemirror.addon.lint.css-lint.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/addon/lint/css-lint.min.js' -assets['codemirror.addon.display.placeholder.js'][ - Version('5.53.2') -] = 'baseframe/js/codemirror-5.53.2/addon/display/placeholder.js' +assets['codemirror.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/codemirror.min.js' +) +assets['codemirror.css'][Version('5.53.2')] = ( + 'baseframe/css/codemirror-5.53.2/codemirror-basic.css' +) + +assets['codemirror.mode.markdown.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/mode/markdown/markdown.min.js' +) +assets['codemirror.mode.gfm.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/mode/gfm/gfm.min.js' +) +assets['codemirror.mode.htmlmixed.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/mode/htmlmixed/htmlmixed.min.js' +) +assets['codemirror.mode.css.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/mode/css/css.min.js' +) +assets['codemirror.addon.mode.overlay.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/addon/mode/overlay.min.js' +) +assets['codemirror.addon.edit.continuelist.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/addon/edit/continuelist.min.js' +) +assets['codemirror.addon.edit.closebrackets.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/addon/edit/closebrackets.min.js' +) +assets['codemirror.addon.edit.matchbrackets.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/addon/edit/matchbrackets.min.js' +) +assets['codemirror.addon.hint.css-hint.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/addon/hint/css-hint.min.js' +) +assets['codemirror.addon.lint.css-lint.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/addon/lint/css-lint.min.js' +) +assets['codemirror.addon.display.placeholder.js'][Version('5.53.2')] = ( + 'baseframe/js/codemirror-5.53.2/addon/display/placeholder.js' +) assets['codemirror-markdown.js'][Version('5.53.2')] = { 'requires': [ @@ -440,9 +440,9 @@ assets['ractive.js'][Version('0.7.3')] = 'baseframe/js/ractive.js' # Ractive fly transition -assets['ractive-transitions-fly.js'][ - Version('0.3.0') -] = 'baseframe/js/ractive-transitions-fly.js' +assets['ractive-transitions-fly.js'][Version('0.3.0')] = ( + 'baseframe/js/ractive-transitions-fly.js' +) # Validate assets['validate.js'][Version('2.0.1')] = 'baseframe/js/validate.js' @@ -465,9 +465,9 @@ assets['moment.js'][Version('2.24.0')] = 'baseframe/js/moment.js' # To use moment timezone in the browser, zone data needs to be loaded. -assets['moment-timezone-data.js'][ - Version('0.5.25') -] = 'baseframe/js/moment-timezone-with-data-10-year-range.js' +assets['moment-timezone-data.js'][Version('0.5.25')] = ( + 'baseframe/js/moment-timezone-with-data-10-year-range.js' +) assets['leaflet.js'][Version('1.3.4')] = 'baseframe/js/leaflet.js' assets['leaflet.css'][Version('1.3.4')] = 'baseframe/css/leaflet.css' @@ -481,13 +481,13 @@ assets['rrssb.js'][Version('1.8.5')] = 'baseframe/js/rrssb.js' assets['rrssb.css'][Version('1.8.5')] = 'baseframe/css/rrssb.css' -assets['baseframe-firasans.css'][ - Version('1.0.0') -] = 'baseframe/css/baseframe-firasans.css' +assets['baseframe-firasans.css'][Version('1.0.0')] = ( + 'baseframe/css/baseframe-firasans.css' +) -assets['getdevicepixelratio.js'][ - Version('1.0.0') -] = 'baseframe/js/getdevicepixelratio.js' +assets['getdevicepixelratio.js'][Version('1.0.0')] = ( + 'baseframe/js/getdevicepixelratio.js' +) assets['pace.js'][Version('1.0.0')] = 'baseframe/js/pace.js' @@ -555,9 +555,9 @@ } assets['mui.js'][Version('0.9.21')] = 'baseframe/js/mui.js' -assets['baseframe-material.js'][ - Version('0.9.21') -] = 'baseframe/js/baseframe-material.js' +assets['baseframe-material.js'][Version('0.9.21')] = ( + 'baseframe/js/baseframe-material.js' +) assets['baseframe-mui.js'][Version(__version__)] = { 'requires': [ 'extra-material.js', @@ -571,6 +571,6 @@ 'requires': ['jquery-modal.css', 'select2-material.css', 'mui.css'] } -assets['font-awesome5-sprite.svg'][ - Version('5.10.2') -] = 'baseframe/img/font-awesome5-sprite.svg' +assets['font-awesome5-sprite.svg'][Version('5.10.2')] = ( + 'baseframe/img/font-awesome5-sprite.svg' +) diff --git a/src/baseframe/static/css/baseframe-bs3.css b/src/baseframe/static/css/baseframe-bs3.css index 93fecab1..5595d966 100644 --- a/src/baseframe/static/css/baseframe-bs3.css +++ b/src/baseframe/static/css/baseframe-bs3.css @@ -406,8 +406,9 @@ table.mceLayout.focus { -moz-box-shadow: none !important; -webkit-box-shadow: none !important; box-shadow: none !important; - font-family: 'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, - Arial, 'Lucida Grande', sans-serif !important; + font-family: + 'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, + 'Lucida Grande', sans-serif !important; } .mce-window { @@ -517,7 +518,8 @@ input[name='title'], input[type='url'], input[name='name'] { - font-family: Consolas, Inconsolata, Menlo, 'DejaVu Sans Mono', + font-family: + Consolas, Inconsolata, Menlo, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; } diff --git a/src/baseframe/static/css/baseframe.css b/src/baseframe/static/css/baseframe.css index b6cfbf7c..2c8739f1 100644 --- a/src/baseframe/static/css/baseframe.css +++ b/src/baseframe/static/css/baseframe.css @@ -655,8 +655,9 @@ table.mceLayout.focus { -moz-box-shadow: none !important; -webkit-box-shadow: none !important; box-shadow: none !important; - font-family: 'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, - Arial, 'Lucida Grande', sans-serif !important; + font-family: + 'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, + 'Lucida Grande', sans-serif !important; } .mce-window { @@ -766,7 +767,8 @@ input[name='title'], input[type='url'], input[name='name'] { - font-family: Consolas, Inconsolata, Menlo, 'DejaVu Sans Mono', + font-family: + Consolas, Inconsolata, Menlo, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; } diff --git a/src/baseframe/static/css/bootstrap3/bootstrap.css b/src/baseframe/static/css/bootstrap3/bootstrap.css index fce80631..b8e68a3c 100644 --- a/src/baseframe/static/css/bootstrap3/bootstrap.css +++ b/src/baseframe/static/css/bootstrap3/bootstrap.css @@ -269,8 +269,9 @@ html { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } body { - font-family: 'Source Sans Pro', 'Lucida Grande', 'DejaVu Sans', - 'Bitstream Vera Sans', Arial, sans-serif; + font-family: + 'Source Sans Pro', 'Lucida Grande', 'DejaVu Sans', 'Bitstream Vera Sans', + Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333333; @@ -707,7 +708,8 @@ code, kbd, pre, samp { - font-family: Consolas, Inconsolata, Menlo, 'DejaVu Sans Mono', + font-family: + Consolas, Inconsolata, Menlo, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; } code { @@ -5341,8 +5343,9 @@ button.close { z-index: 1070; display: block; visibility: visible; - font-family: 'Source Sans Pro', 'Lucida Grande', 'DejaVu Sans', - 'Bitstream Vera Sans', Arial, sans-serif; + font-family: + 'Source Sans Pro', 'Lucida Grande', 'DejaVu Sans', 'Bitstream Vera Sans', + Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 1.4; @@ -5449,8 +5452,9 @@ button.close { display: none; max-width: 276px; padding: 1px; - font-family: 'Source Sans Pro', 'Lucida Grande', 'DejaVu Sans', - 'Bitstream Vera Sans', Arial, sans-serif; + font-family: + 'Source Sans Pro', 'Lucida Grande', 'DejaVu Sans', 'Bitstream Vera Sans', + Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.5; diff --git a/src/baseframe/static/css/codemirror.css b/src/baseframe/static/css/codemirror.css index ba196c92..780f5ac0 100644 --- a/src/baseframe/static/css/codemirror.css +++ b/src/baseframe/static/css/codemirror.css @@ -2,7 +2,8 @@ .CodeMirror { height: auto; min-height: 6em; - font-family: Consolas, Inconsolata, Menlo, 'DejaVu Sans Mono', + font-family: + Consolas, Inconsolata, Menlo, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; border: 1px solid #ccc; border-radius: 4px; diff --git a/src/baseframe/static/css/editor.css b/src/baseframe/static/css/editor.css index ec871d82..d2ea3807 100644 --- a/src/baseframe/static/css/editor.css +++ b/src/baseframe/static/css/editor.css @@ -1,7 +1,8 @@ @import url('//fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,400italic,600italic'); body { - font-family: 'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, - Arial, 'Lucida Grande', sans-serif; + font-family: + 'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, + 'Lucida Grande', sans-serif; font-size: 14px; line-height: 20px; color: #333; diff --git a/src/baseframe/static/css/mui.css b/src/baseframe/static/css/mui.css index 90749bd7..213e75ff 100644 --- a/src/baseframe/static/css/mui.css +++ b/src/baseframe/static/css/mui.css @@ -1690,8 +1690,9 @@ html { } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, - Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif, 'Noto Sans Math', + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, + Cantarell, 'Helvetica Neue', Arial, sans-serif, 'Noto Sans Math', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; font-weight: 400; diff --git a/src/baseframe/static/css/rrssb.css b/src/baseframe/static/css/rrssb.css index d4659c7a..16b1ad5c 100644 --- a/src/baseframe/static/css/rrssb.css +++ b/src/baseframe/static/css/rrssb.css @@ -1,7 +1,8 @@ .rrssb-buttons { box-sizing: border-box; - font-family: 'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, - Arial, 'Lucida Grande', sans-serif; + font-family: + 'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, + 'Lucida Grande', sans-serif; height: 36px; margin: 0; margin-bottom: 12px; diff --git a/src/baseframe/static/js/dropzone.js b/src/baseframe/static/js/dropzone.js index 8a3d0430..0b7f7673 100644 --- a/src/baseframe/static/js/dropzone.js +++ b/src/baseframe/static/js/dropzone.js @@ -656,9 +656,9 @@ extend = function () { var key, object, objects, target, val, _i, _len; - (target = arguments[0]), + ((target = arguments[0]), (objects = - 2 <= arguments.length ? __slice.call(arguments, 1) : []); + 2 <= arguments.length ? __slice.call(arguments, 1) : [])); for (_i = 0, _len = objects.length; _i < _len; _i++) { object = objects[_i]; for (key in object) { diff --git a/src/baseframe/static/js/footable-filter.js b/src/baseframe/static/js/footable-filter.js index 87873b54..1b4bb8e7 100644 --- a/src/baseframe/static/js/footable-filter.js +++ b/src/baseframe/static/js/footable-filter.js @@ -15,11 +15,11 @@ !(function (t, e, i) { function a() { var e = this; - (e.name = 'Footable Filter'), + ((e.name = 'Footable Filter'), (e.init = function (i) { if (((e.footable = i), i.options.filter.enabled === !0)) { if (t(i.table).data('filter') === !1) return; - i.timers.register('filter'), + (i.timers.register('filter'), t(i.table) .unbind('.filtering') .bind({ @@ -35,26 +35,26 @@ r.data('filter-disable-enter') || i.options.filter.disableEnter, }; - l.disableEnter && + (l.disableEnter && t(l.input).keypress(function (t) { return window.event ? 13 !== window.event.keyCode : 13 !== t.which; }), r.bind('footable_clear_filter', function () { - t(l.input).val(''), e.clearFilter(); + (t(l.input).val(''), e.clearFilter()); }), r.bind('footable_filter', function (t, i) { e.filter(i.filter); }), t(l.input).keyup(function (a) { - i.timers.filter.stop(), + (i.timers.filter.stop(), 27 === a.which && t(l.input).val(''), i.timers.filter.start(function () { var i = t(l.input).val() || ''; e.filter(i); - }, l.timeout); - }); + }, l.timeout)); + })); }, 'footable_redrawn.filtering': function (a) { var r = t(i.table), @@ -62,7 +62,7 @@ l && e.filter(l); }, }) - .data('footable-filter', e); + .data('footable-filter', e)); } }), (e.filter = function (i) { @@ -77,30 +77,30 @@ var f = n.filter.split(' '); r.find('> tbody > tr').hide().addClass('footable-filtered'); var s = r.find('> tbody > tr:not(.footable-row-detail)'); - t.each(f, function (t, e) { + (t.each(f, function (t, e) { e && e.length > 0 && (r.data('current-filter', e), (s = s.filter(a.options.filter.filterFunction))); }), s.each(function () { - e.showRow(this, a), t(this).removeClass('footable-filtered'); + (e.showRow(this, a), t(this).removeClass('footable-filtered')); }), r.data('filter-string', n.filter), - a.raise('footable_filtered', { filter: n.filter, clear: !1 }); + a.raise('footable_filtered', { filter: n.filter, clear: !1 })); } }), (e.clearFilter = function () { var i = e.footable, a = t(i.table); - a + (a .find('> tbody > tr:not(.footable-row-detail)') .removeClass('footable-filtered') .each(function () { e.showRow(this, i); }), a.removeData('filter-string'), - i.raise('footable_filtered', { clear: !0 }); + i.raise('footable_filtered', { clear: !0 })); }), (e.showRow = function (e, i) { var a = t(e), @@ -111,7 +111,7 @@ r.hasClass('footable-row-detail') ? (a.add(r).show(), i.createOrUpdateDetailRow(e)) : a.show(); - }); + })); } if (e.footable === i || null === e.footable) throw new Error( diff --git a/src/baseframe/static/js/footable-paginate.js b/src/baseframe/static/js/footable-paginate.js index de0b4598..47af30c2 100644 --- a/src/baseframe/static/js/footable-paginate.js +++ b/src/baseframe/static/js/footable-paginate.js @@ -16,7 +16,7 @@ function i(e) { var t = a(e.table), i = t.data(); - (this.pageNavigation = i.pageNavigation || e.options.pageNavigation), + ((this.pageNavigation = i.pageNavigation || e.options.pageNavigation), (this.pageSize = i.pageSize || e.options.pageSize), (this.firstText = i.firstText || e.options.firstText), (this.previousText = i.previousText || e.options.previousText), @@ -32,15 +32,15 @@ (this.limit = this.limitNavigation > 0), (this.currentPage = i.currentPage || 0), (this.pages = []), - (this.control = !1); + (this.control = !1)); } function n() { var e = this; - (e.name = 'Footable Paginate'), + ((e.name = 'Footable Paginate'), (e.init = function (t) { if (t.options.paginate === !0) { if (a(t.table).data('page') === !1) return; - (e.footable = t), + ((e.footable = t), a(t.table) .unbind('.paging') .bind({ @@ -49,16 +49,16 @@ e.setupPaging(); }, }) - .data('footable-paging', e); + .data('footable-paging', e)); } }), (e.setupPaging = function () { var t = e.footable, n = a(t.table).find('> tbody'); - (t.pageInfo = new i(t)), + ((t.pageInfo = new i(t)), e.createPages(t, n), e.createNavigation(t, n), - e.fillPage(t, n, t.pageInfo.currentPage); + e.fillPage(t, n, t.pageInfo.currentPage)); }), (e.createPages = function (e, t) { var i = 1, @@ -68,11 +68,11 @@ g = []; n.pages = []; var s = t.find('> tr:not(.footable-filtered,.footable-row-detail)'); - s.each(function (a, e) { - l.push(e), + (s.each(function (a, e) { + (l.push(e), a === o - 1 ? (n.pages.push(l), i++, (o = i * n.pageSize), (l = [])) - : a >= s.length - (s.length % n.pageSize) && g.push(e); + : a >= s.length - (s.length % n.pageSize) && g.push(e)); }), g.length > 0 && n.pages.push(g), n.currentPage >= n.pages.length && @@ -80,7 +80,7 @@ n.currentPage < 0 && (n.currentPage = 0), 1 === n.pages.length ? a(e.table).addClass('no-paging') - : a(e.table).removeClass('no-paging'); + : a(e.table).removeClass('no-paging')); }), (e.createNavigation = function (t, i) { var n = a(t.table).find(t.pageInfo.pageNavigation); @@ -96,12 +96,12 @@ console.error('More than one pagination control was found!'); } if (0 !== n.length) { - n.is('ul') || + (n.is('ul') || (0 === n.find('ul:first').length && n.append('