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/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/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/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('