diff --git a/.bandit b/.bandit deleted file mode 100644 index fba0d25..0000000 --- a/.bandit +++ /dev/null @@ -1,2 +0,0 @@ -[bandit] -exclude: tests diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index e030a5a..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: codingjoe -custom: https://www.paypal.me/codingjoe diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d232a23..dd93a24 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,10 @@ version: 2 updates: -- package-ecosystem: pip - directory: "/" - schedule: - interval: daily -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: daily + - package-ecosystem: pip + directory: "/" + schedule: + interval: daily + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65443d6..358a97f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,107 +1,50 @@ name: CI - on: push: branches: - main pull_request: - schedule: - - cron: '0 0 * * *' - jobs: - - lint: - strategy: - fail-fast: false - matrix: - lint-command: - - "bandit ." - - "black --check --diff ." - - "flake8 ." - - "isort --check-only --diff ." - - "pydocstyle ." + dist: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.x" - cache: 'pip' - cache-dependency-path: 'pyproject.toml' - - run: python -m pip install -e .[lint] - - run: ${{ matrix.lint-command }} - - readme: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.x" - - name: Install Python dependencies - run: python -m pip install --upgrade pip build wheel twine readme-renderer - - run: python -m build --sdist --wheel - - run: python -m twine check dist/* + - uses: astral-sh/setup-uv@v7 + - run: uvx --from build pyproject-build --sdist --wheel + - run: uvx twine check dist/* - uses: actions/upload-artifact@v6 with: path: dist/* - pytest: - runs-on: ubuntu-latest needs: - - readme - - lint + - dist + runs-on: ubuntu-latest strategy: matrix: python-version: - "3.10" - "3.11" - "3.12" + - "3.13" + - "3.14" django-version: - "4.2" - - "5.0" - - "5.1" + - "5.2" + - "6.0" + exclude: + - python-version: "3.14" + django-version: "4.2" + - python-version: "3.10" + django-version: "6.0" + - python-version: "3.11" + django-version: "6.0" steps: - - uses: actions/checkout@v6 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - run: python -m pip install .[test] - - name: Install Django ${{ matrix.django-version }} - run: python -m pip install "django~=${{ matrix.django-version }}.0" - - name: Run tests - run: python -m pytest - - uses: codecov/codecov-action@v5 - - analyze: - name: CodeQL Analyze - needs: [ pytest ] - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ python ] - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v7 with: - languages: ${{ matrix.language }} - queries: +security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v4 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + python-version: ${{ matrix.python-version }} + - run: uv run --with django~=${{ matrix.django-version }}.0 pytest -m "not selenium" + - uses: codecov/codecov-action@v5 with: - category: "/language:${{ matrix.language }}" \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} + flags: python-${{ matrix.python-version }}-django-${{ matrix.django-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5611912..f6eabda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,23 +1,28 @@ name: Release - on: release: types: [published] - + workflow_dispatch: jobs: - PyPI: + pypi-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: astral-sh/setup-uv@v7 + - run: uvx --from build pyproject-build --sdist --wheel + - uses: actions/upload-artifact@v6 with: - python-version: "3.10" - - name: Install Python dependencies - run: python -m pip install --upgrade pip build wheel twine - - name: Build dist packages - run: python -m build --sdist --wheel - - name: Upload packages - run: python -m twine upload dist/* - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + name: release-dists + path: dist/ + pypi-publish: + runs-on: ubuntu-latest + needs: + - pypi-build + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v7 + with: + name: release-dists + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index c11dca2..a8351db 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,7 @@ venv.bak/ .mypy_cache/ # setuptools_scm -_version.py \ No newline at end of file +_version.py + +# uv +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..01f6a5e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,45 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: check-merge-conflict + - id: check-ast + - id: check-toml + - id: check-yaml + - id: check-symlinks + - id: debug-statements + - id: end-of-file-fixer + - id: no-commit-to-branch + args: [--branch, main] + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.29.1 + hooks: + - id: django-upgrade + - repo: https://github.com/hukkin/mdformat + rev: 1.0.0 + hooks: + - id: mdformat + additional_dependencies: + - mdformat-ruff + - mdformat-footnote + - mdformat-gfm + - mdformat-gfm-alerts + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.9 + hooks: + - id: ruff-check + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + - repo: https://github.com/google/yamlfmt + rev: v0.20.0 + hooks: + - id: yamlfmt +ci: + autoupdate_schedule: weekly + skip: + - no-commit-to-branch diff --git a/README.md b/README.md index 38b9b9b..795efde 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,10 @@ from django.db import models from dynamic_filenames import FilePattern upload_to_pattern = FilePattern( - filename_pattern='{app_label:.25}/{model_name:.30}/{instance.created:%Y-%m-%d}/{uuid:base32}{ext}' + filename_pattern="{app_label:.25}/{model_name:.30}/{instance.created:%Y-%m-%d}/{uuid:base32}{ext}" ) + class FileModel(models.Model): my_file = models.FileField(upload_to=upload_to_pattern) created = models.DateTimeField(auto_now_add=True) @@ -36,73 +37,75 @@ Auto slug example: `ext` -: File extension including the dot. +: File extension including the dot. `name` -: Filename excluding the folders. +: Filename excluding the folders. `model_name` -: Name of the Django model. +: Name of the Django model. `app_label` -: App label of the Django model. +: App label of the Django model. `instance` -: Instance of the model before it has been saved. You may not have a - primary key at this point. +: Instance of the model before it has been saved. You may not have a +primary key at this point. `uuid` -: UUID version 4 that supports multiple type specifiers. The UUID will - be the same should you use it twice in the same string, but - different on each invocation of the `upload_to` callable. +: UUID version 4 that supports multiple type specifiers. The UUID will +be the same should you use it twice in the same string, but +different on each invocation of the `upload_to` callable. - The type specifiers allow you to format the UUID in different ways, - e.g. `{uuid:x}` will give you a with a hexadecimal UUID. +``` +The type specifiers allow you to format the UUID in different ways, +e.g. `{uuid:x}` will give you a with a hexadecimal UUID. - The supported type specifiers are: +The supported type specifiers are: - `s` +`s` - : String representation of a UUID including dashes. +: String representation of a UUID including dashes. - `i` +`i` - : Integer representation of a UUID. Like to `UUID.int`. +: Integer representation of a UUID. Like to `UUID.int`. - `x` +`x` - : Hexadecimal (Base16) representation of a UUID. Like to - `UUID.hex`. +: Hexadecimal (Base16) representation of a UUID. Like to + `UUID.hex`. - `X` +`X` - : Upper case hexadecimal representation of a UUID. Like to - `UUID.hex`. +: Upper case hexadecimal representation of a UUID. Like to + `UUID.hex`. - `base32` +`base32` - : Base32 representation of a UUID without padding. +: Base32 representation of a UUID without padding. - `base64` +`base64` - : Base64 representation of a UUID without padding. +: Base64 representation of a UUID without padding. - :::: warning - ::: title - Warning - ::: + :::: warning + ::: title + Warning + ::: - Not all file systems support Base64 file names. - :::: + Not all file systems support Base64 file names. + :::: - All type specifiers also support precisions to cut the string, e.g. - `{{uuid:.2base32}}` would only return the first 2 characters of a - Base32 encoded UUID. +All type specifiers also support precisions to cut the string, e.g. +`{{uuid:.2base32}}` would only return the first 2 characters of a +Base32 encoded UUID. +``` ### Type specifiers @@ -115,9 +118,10 @@ from django.db import models from dynamic_filenames import FilePattern upload_to_pattern = FilePattern( - filename_pattern='{app_label:.25}/{model_name:.30}/{instance.title:.40slug}{ext}' + filename_pattern="{app_label:.25}/{model_name:.30}/{instance.title:.40slug}{ext}" ) + class FileModel(models.Model): title = models.CharField(max_length=100) my_file = models.FileField(upload_to=upload_to_pattern) diff --git a/dynamic_filenames/__init__.py b/dynamic_filenames/__init__.py index f0b9e56..df5128e 100644 --- a/dynamic_filenames/__init__.py +++ b/dynamic_filenames/__init__.py @@ -8,7 +8,7 @@ from django.utils.text import slugify -from . import _version # noqa +from . import _version __version__ = _version.__version__ VERSION = _version.VERSION_TUPLE @@ -132,7 +132,7 @@ def __init__(self, **kwargs): def deconstruct(self): """Destruct callable to support Django migrations.""" - path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) + path = f"{self.__class__.__module__}.{self.__class__.__name__}" return path, [], self.kwargs @staticmethod diff --git a/pyproject.toml b/pyproject.toml index 11e955f..7b1214c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,34 +26,36 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Framework :: Django", "Framework :: Django :: 4.2", - "Framework :: Django :: 5.0", - "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", + "Framework :: Django :: 6.0", ] requires-python = ">=3.10" dependencies = ["django>=4.2.0"] -[project.optional-dependencies] +[dependency-groups] +dev = [ + { include-group = "test" }, +] test = [ "pytest", "pytest-cov", "pytest-django", ] -lint = [ - "bandit==1.9.2", - "black==25.12.0", - "flake8==7.3.0", - "isort==7.0.0", - "pydocstyle[toml]==6.3.0", -] [project.urls] -Project-URL = "https://github.com/codingjoe/django-dynamic-filenames" +# https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels +Homepage = "https://github.com/codingjoe/django-dynamic-filenames" Changelog = "https://github.com/codingjoe/django-dynamic-filenames/releases" Source = "https://github.com/codingjoe/django-dynamic-filenames" -Documentation = "https://github.com/codingjoe/django-dynamic-filenames#django-dynamic-filenames" -Issue-Tracker = "https://github.com/codingjoe/django-dynamic-filenames/issues" +Releasenotes = "https://github.com/codingjoe/django-dynamic-filenames/releases/latest" +Documentation = "https://github.com/codingjoe/django-dynamic-filenames?tab=readme-ov-file" +Issues = "https://github.com/codingjoe/django-dynamic-filenames/issues" +Funding = "https://github.com/sponsors/codingjoe" + [tool.flit.module] name = "dynamic_filenames" @@ -63,7 +65,7 @@ write_to = "dynamic_filenames/_version.py" [tool.pytest.ini_options] minversion = "6.0" -addopts = "--cov --tb=short -rxs" +addopts = "--cov --cov-report=xml --cov-report=term --tb=short -rxs" testpaths = ["tests"] DJANGO_SETTINGS_MODULE = "tests.testapp.settings" @@ -72,16 +74,51 @@ source = ["dynamic_filenames"] [tool.coverage.report] show_missing = true -omit = ["dynamic_filenames/_version.py", "tests/*"] - -[tool.isort] -atomic = true -line_length = 88 -known_first_party = "dynamic_filenames, tests" -include_trailing_comma = true -default_section = "THIRDPARTY" -combine_as_imports = true -skip = ["dynamic_filenames/_version.py"] - -[tool.pydocstyle] -add_ignore = "D1" +skip_covered = true + +[tool.ruff] +src = ["dynamic_filenames", "tests"] +line-length = 88 +indent-width = 4 + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +preview = true + +[tool.ruff.lint] +select = [ + "D", # pydocstyle + "E", # pycodestyle errors + "EXE", # flake8-executable + "F", # pyflakes + "I", # isort + "PGH", # pygrep-hooks + "PT", # flake8-pytest-style + "RET", # flake8-return + "S", # flake8-bandit + "SIM", # flake8-simplify + "UP", # pyupgrade + "W", # pycodestyle warnings +] + +ignore = ["D1", "E501", "PT012"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S"] + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.isort] +combine-as-imports = true +split-on-trailing-comma = true +section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] +force-wrap-aliases = true +known-first-party = ["dynamic_filenames", "tests"] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6f60592..0000000 --- a/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length=88 -select = C,E,F,W,B,B950 -ignore = E203, E501, W503, E731 diff --git a/setup.py b/setup.py deleted file mode 100755 index 95067a6..0000000 --- a/setup.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python - -from setuptools import setup - -setup( - name="django-dynamic-filenames", - py_modules=["dynamic_filenames"], - use_scm_version=True, -) diff --git a/tests/testapp/.gitignore b/tests/testapp/.gitignore index 05c35d0..62616c7 100644 --- a/tests/testapp/.gitignore +++ b/tests/testapp/.gitignore @@ -1 +1 @@ -migrations/ \ No newline at end of file +migrations/