diff --git a/.editorconfig b/.editorconfig index fbe3f00e..356385d7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# EditorConfig: http://EditorConfig.org +# http://editorconfig.org root = true @@ -7,8 +7,8 @@ indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true -end_of_line = lf charset = utf-8 +end_of_line = lf [*.py] indent_size = 4 @@ -27,8 +27,6 @@ max_line_length = off indent_size = 4 max_line_length = off -# Tests don't get a line width restriction. It's still a good idea to follow -# the 99 character rule, but in the interests of clarity, tests often need to -# violate it. +# Tests can violate line width restrictions in the interest of clarity. [**/test_*.py] max_line_length = off diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2486597..d15fe8da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,7 +169,7 @@ jobs: - name: Upload HTML report uses: actions/upload-artifact@v4 with: - name: html-report + name: coverage-report path: htmlcov publish-docs: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e75a753b..7b518631 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,9 @@ +ci: + autoupdate_schedule: monthly + repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: "v4.6.0" hooks: - id: check-added-large-files - id: check-case-conflict @@ -11,11 +14,11 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.4 + rev: "2.2.4" hooks: - id: pyproject-fmt - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: "v3.17.0" hooks: - id: pyupgrade args: [--py39-plus] @@ -25,16 +28,7 @@ repos: - id: pycln args: [--all] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.21.0 + rev: "1.21.0" hooks: - id: django-upgrade args: [--target-version, "3.2"] - - repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.6.8 - hooks: - # Run the linter. - - id: ruff - args: [--fix] - # Run the formatter. - - id: ruff-format diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index a57e5db1..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -prune docs -prune scripts -exclude .readthedocs.yaml -exclude CHANGELOG.rst -include LICENSE -include pyproject.toml -include README.rst diff --git a/docs/src/contributing.md b/docs/src/contributing.md index ca958d72..ce1f2bb9 100644 --- a/docs/src/contributing.md +++ b/docs/src/contributing.md @@ -45,6 +45,12 @@ By utilizing `hatch`, the following commands are available to manage the develop | `hatch run precommit:update` | Update the [`pre-commit`](https://pre-commit.com/) hooks configured within this repository | | `hatch run pyproject:format` | Format the `pyproject.toml` file using [`pyproject-fmt`](https://github.com/tox-dev/pyproject-fmt) | +??? tip "Configure your IDE for linting" + + This repository uses `hatch fmt` for linting and formatting, which is a [modestly customized](https://hatch.pypa.io/latest/config/internal/static-analysis/#default-settings) version of [`ruff`](https://github.com/astral-sh/ruff). + + You can install `ruff` as a plugin to your preferred code editor to create a similar environment. + ### Documentation | Command | Description | @@ -54,8 +60,16 @@ By utilizing `hatch`, the following commands are available to manage the develop | `hatch run docs:linkcheck` | Check for broken links in the documentation | | `hatch run scripts\validate_changelog.py` | Check if the changelog meets the [Keep A Changelog](https://keepachangelog.com/en/1.1.0/) specification | -??? tip "Configure your IDE for linting" +### Environment Management - This repository uses `hatch fmt` for linting and formatting, which is a [modestly customized](https://hatch.pypa.io/latest/config/internal/static-analysis/#default-settings) version of [`ruff`](https://github.com/astral-sh/ruff). +| Command | Description | +| --- | --- | +| `hatch build --clean` | Build the package from source | +| `hatch env prune` | Delete all virtual environments created by `hatch` | +| `hatch python install 3.12` | Install a specific Python version to your system | - You can install `ruff` as a plugin to your preferred code editor to create a similar environment. +??? tip "Check out Hatch for all available commands!" + + This documentation only covers commonly used commands for ServeStatic. + + You can type `hatch --help` to see all available commands. diff --git a/pyproject.toml b/pyproject.toml index a9baa7b9..8442fdd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ requires = [ "hatchling" ] name = "servestatic" description = "Production-grade static file server for Python web apps." readme = "README.md" -keywords = [ "Django" ] +keywords = [ "asgi", "django", "http", "server", "static", "staticfiles", "wsgi" ] license = "MIT" authors = [ { name = "Mark Bakhit" } ] requires-python = ">=3.9" @@ -26,7 +26,6 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", ] dynamic = [ "version" ] -dependencies = [ "django" ] optional-dependencies.brotli = [ "brotli" ] urls.Changelog = "https://archmonger.github.io/ServeStatic/latest/changelog/" urls.Documentation = "https://archmonger.github.io/ServeStatic/" @@ -36,26 +35,22 @@ urls.Homepage = "https://github.com/Archmonger/ServeStatic" path = "src/servestatic/__init__.py" [tool.hatch.build.targets.sdist] -include = [ "/src", "/tests" ] - -[tool.hatch.build.targets.wheel] -nclude = [ "/src", "/tests" ] +include = [ "/src" ] [tool.hatch.metadata] -license-files = { paths = [ "LICENSE" ] } +license-files = { paths = [ "LICENSE.md" ] } [tool.hatch.envs.default] installer = "uv" -# -# Testing -# +# >>> Hatch Tests <<< + [tool.hatch.envs.hatch-test] extra-dependencies = [ "pytest-sugar", "requests", "brotli" ] randomize = true matrix-name-format = "{variable}-{value}" -# Django 3.2 LTS +# Django 3.2 [[tool.hatch.envs.hatch-test.matrix]] python = [ "3.9", "3.10" ] django = [ "3.2" ] @@ -107,9 +102,8 @@ matrix.django.dependencies = [ ], value = "django~=5.1" }, ] -# -# Documentation -# +# >>> Hatch Documentation <<< + [tool.hatch.envs.docs] template = "docs" detached = true @@ -136,9 +130,8 @@ linkcheck = [ deploy_latest = [ "cd docs && mike deploy --push --update-aliases {args} latest" ] deploy_develop = [ "cd docs && mike deploy --push develop" ] -# -# pre-commit -# +# >>> Hatch pre-commit <<< + [tool.hatch.envs.precommit] template = "pre-commit" detached = true @@ -148,9 +141,8 @@ dependencies = [ "pre-commit>=3,<4" ] check = [ "pre-commit run --all-files" ] update = [ "pre-commit autoupdate" ] -# -# pyproject-format -# +# >>> Hatch pyproject-format <<< + [tool.hatch.envs.pyproject] template = "pyproject" detached = true @@ -159,9 +151,7 @@ dependencies = [ "pyproject-fmt" ] [tool.hatch.envs.pyproject.scripts] format = [ "pyproject-fmt pyproject.toml" ] -# -# Tools -# +# >>> Generic Tools <<< [tool.black] target-version = [ "py39" ] @@ -197,6 +187,3 @@ source = [ "src/" ] [tool.coverage.report] show_missing = true - -[tool.rstcheck] -report_level = "ERROR" diff --git a/scripts/generate_default_media_types.py b/scripts/generate_default_media_types.py old mode 100755 new mode 100644 index 0b870ed1..02a4e125 --- a/scripts/generate_default_media_types.py +++ b/scripts/generate_default_media_types.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +# pragma: no cover from __future__ import annotations import argparse @@ -34,16 +34,14 @@ def main() -> int: EXTRA_MIMETYPES = { - # nginx file uses application/javascript, but HTML specification recommends - # text/javascript: + # Nginx uses application/javascript, but HTML specification recommends text/javascript: ".js": "text/javascript", ".md": "text/markdown", ".mjs": "text/javascript", ".woff": "application/font-woff", ".woff2": "font/woff2", "apple-app-site-association": "application/pkc7-mime", - # Adobe Products - see: - # https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/xdomain.html#policy-file-host-basics + # Adobe: https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/xdomain.html#policy-file-host-basics "crossdomain.xml": "text/x-cross-domain-policy", } @@ -75,8 +73,7 @@ def get_types_map() -> dict[str, str]: types_map = {} for match in matches: media_type = match[0] - # This is the default media type anyway, no point specifying - # it explicitly + # This is the default media type anyway, no point specifying it explicitly if media_type == "application/octet-stream": continue diff --git a/scripts/validate_changelog.py b/scripts/validate_changelog.py index c4cc130b..1aec4112 100644 --- a/scripts/validate_changelog.py +++ b/scripts/validate_changelog.py @@ -1,11 +1,11 @@ -"""Parses Keep a Changelog format and ensures it is valid""" +# pragma: no cover +# ruff: noqa: PERF401 # /// script # requires-python = ">=3.11" # dependencies = [] # /// -# ruff: noqa: PERF401 import re import sys @@ -30,7 +30,7 @@ def validate_changelog(changelog_path="CHANGELOG.md"): with open(changelog_path, encoding="UTF-8") as file: changelog = file.read() - # Remove HTML comments + # Remove markdown comments changelog = re.sub(HTML_COMMENT_RE[0], "", changelog, flags=HTML_COMMENT_RE[1]) # Replace duplicate newlines with a single newline changelog = re.sub(r"\n+", "\n", changelog) @@ -171,8 +171,25 @@ def validate_changelog(changelog_path="CHANGELOG.md"): f"Section '{line}' is out of order in version '{version_header}'. " "Expected section order: [Added, Changed, Deprecated, Removed, Fixed, Security]" ) + # Additional check for duplicate sections + if section_position == current_position_in_order: + errors.append(f"Duplicate section '{line}' found in version '{version_header}'.") current_position_in_order = section_position + # Find sections with missing bullet points + changelog_header_and_bullet_lines = [ + line for line in changelog.split("\n") if line.startswith(("### ", "## ", "-")) + ] + current_version = "UNKNOWN" + for position, line in enumerate(changelog_header_and_bullet_lines): + if line.startswith("## "): + current_version = line + # If it's an h3 header, report an error if the next line is not a bullet point, or if there is no next line + if line.startswith("### ") and ( + position + 1 == len(changelog_header_and_bullet_lines) + or not changelog_header_and_bullet_lines[position + 1].startswith("-") + ): + errors.append(f"Section '{line}' in version '{current_version}' is missing bullet points") return errors diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 827cf2cd..00000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 88 -extend-ignore = E203 -per-file-ignores = - src/servestatic/media_types.py:E501 diff --git a/src/servestatic/utils.py b/src/servestatic/utils.py index 1131cf2e..dac7ac90 100644 --- a/src/servestatic/utils.py +++ b/src/servestatic/utils.py @@ -9,7 +9,7 @@ from concurrent.futures import ThreadPoolExecutor from typing import TYPE_CHECKING, Callable -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from collections.abc import AsyncIterable from io import IOBase diff --git a/tests/django_urls.py b/tests/django_urls.py index 157b9b17..7a286b6a 100644 --- a/tests/django_urls.py +++ b/tests/django_urls.py @@ -3,7 +3,7 @@ from django.urls import path -def avoid_django_default_welcome_page(): +def avoid_django_default_welcome_page(): # pragma: no cover pass diff --git a/tests/test_string_utils.py b/tests/test_string_utils.py index 28329768..c5ee2095 100644 --- a/tests/test_string_utils.py +++ b/tests/test_string_utils.py @@ -3,21 +3,25 @@ from servestatic.utils import ensure_leading_trailing_slash -class EnsureLeadingTrailingSlashTests: - def test_none(self): - assert ensure_leading_trailing_slash(None) == "/" +def test_none(): + assert ensure_leading_trailing_slash(None) == "/" - def test_empty(self): - assert ensure_leading_trailing_slash("") == "/" - def test_slash(self): - assert ensure_leading_trailing_slash("/") == "/" +def test_empty(): + assert ensure_leading_trailing_slash("") == "/" - def test_contents(self): - assert ensure_leading_trailing_slash("/foo/") == "/foo/" - def test_leading(self): - assert ensure_leading_trailing_slash("/foo") == "/foo" +def test_slash(): + assert ensure_leading_trailing_slash("/") == "/" - def test_trailing(self): - assert ensure_leading_trailing_slash("foo/") == "/foo" + +def test_contents(): + assert ensure_leading_trailing_slash("/foo/") == "/foo/" + + +def test_leading(): + assert ensure_leading_trailing_slash("/foo") == "/foo/" + + +def test_trailing(): + assert ensure_leading_trailing_slash("foo/") == "/foo/" diff --git a/tests/utils.py b/tests/utils.py index 3b8f8730..2136e221 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -51,7 +51,7 @@ def __init__(self, application): self.application = application async def __call__(self, scope, receive, send): - if scope["type"] != "http": + if scope["type"] != "http": # pragma: no cover msg = "Incorrect response type!" raise RuntimeError(msg) @@ -120,7 +120,7 @@ def __init__(self, scope_overrides: dict | None = None): "type": "http", } - if scope_overrides: + if scope_overrides: # pragma: no cover scope.update(scope_overrides) super().__init__(scope)