Skip to content

Commit

Permalink
Add contrib fixture app
Browse files Browse the repository at this point in the history
Closes #25
  • Loading branch information
RoTranDo committed Dec 12, 2023
1 parent c6b10a1 commit b3b59c2
Show file tree
Hide file tree
Showing 16 changed files with 492 additions and 0 deletions.
Empty file.
36 changes: 36 additions & 0 deletions anfema_django_testutils/contrib/fixtures/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""The :mod:`anfema_django_testutils.contrib.fixtures` app provides to find fixture and fixture media files from each
of your applications and any other specified places.
Settings
========
``FIXTURE_FINDERS``
``FIXTURE_DIRS``
``FIXTURE_MEDIAFILE_FINDERS``
``FIXTURE_MEDIAFILE_DIRS``
Management Commands
===================
:mod:`anfema_django_testutils.contrib.fixtures` exposes three management commands.
findfixture
-----------
*django-admin findfixture FILE [FILE ...]*
Finds the absolute paths for given fixture file(s).
findfixturemedia
----------------
*django-admin findfixturemedia FILE [FILE ...]*
Finds the absolute paths for given fixture media file(s).
collectfixturemedia
-------------------
*django-admin collectfixturemedia*
Collect fixture media files in a single location.
"""
22 changes: 22 additions & 0 deletions anfema_django_testutils/contrib/fixtures/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import os

from django.apps import AppConfig
from django.core.checks import register
from django.utils.translation import gettext_lazy as _

from .checks import check_config


class FixturesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "anfema_django_testutils.contrib.fixtures"
label = "fixtures"
verbose_name = _("fixtures")

fixture_source_dir = "fixtures"
fixture_suffixes = [".json", ".yaml", ".yml", ".xml"]

fixturemedia_source_dir = os.path.join(fixture_source_dir, "media")

def ready(self):
register(check_config, self.name)
86 changes: 86 additions & 0 deletions anfema_django_testutils/contrib/fixtures/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from pathlib import Path

from django.core.checks import Error, Warning

from .settings import get_config


def check_config(app_configs=None, **kwargs):
"""Check the configuration settings for correctness."""
from .apps import FixturesConfig

errors = []

app_label = FixturesConfig.label
config = get_config()

if not isinstance(config["FIXTURE_FINDERS"], (tuple, list)):
errors.append(
Error(
"The FIXTURE_FINDERS setting must be a list or tuple.",
id=f"{app_label}.E001",
obj="Improper Configuration",
),
)
if not isinstance(config["FIXTURE_DIRS"], (tuple, list)):
errors.append(
Error(
"The FIXTURE_DIRS setting must be a a list or tuple.",
id=f"{app_label}.E001",
obj="Improper Configuration",
),
)
elif not all([isinstance(i, str) for i in config["FIXTURE_DIRS"]]):
errors.append(
Error(
f"At least one item of FIXTURE_DIRS settings is not a string.",
id=f"{app_label}.E001",
obj="Improper Configuration",
),
)
else:
for path in filter(lambda p: not Path(p).exists(), config["FIXTURE_DIRS"]):
errors.append(
Warning(
f"The FIXTURE_DIRS settings contains a non-existing path: {path!r}.",
id=f"{app_label}.W001",
obj="Improper Configuration",
),
)

if not isinstance(config["FIXTURE_MEDIAFILE_FINDERS"], (tuple, list)):
errors.append(
Error(
"The FIXTURE_MEDIAFILE_FINDERS setting must be a list or tuple.",
id=f"{app_label}.E001",
obj="Improper Configuration",
),
)

if not isinstance(config["FIXTURE_MEDIAFILE_DIRS"], (tuple, list)):
errors.append(
Error(
"The FIXTURE_MEDIAFILE_DIRS setting must be a a list or tuple.",
id=f"{app_label}.E001",
obj="Improper Configuration",
),
)
elif not all([isinstance(i, str) for i in config["FIXTURE_MEDIAFILE_DIRS"]]):
errors.append(
Error(
f"At least one item of FIXTURE_MEDIAFILE_DIRS settings is not a string.",
id=f"{app_label}.E001",
obj="Improper Configuration",
),
)
else:
for path in filter(lambda p: not Path(p).exists(), config["FIXTURE_MEDIAFILE_DIRS"]):
errors.append(
Warning(
f"The FIXTURE_MEDIAFILE_DIRS settings contains a non-existing path: {path!r}.",
id=f"{app_label}.W001",
obj="Improper Configuration",
),
)

return errors
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import fixture, media
59 changes: 59 additions & 0 deletions anfema_django_testutils/contrib/fixtures/finders/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Generator, Union

from django.contrib.staticfiles.finders import (
AppDirectoriesFinder,
BaseFinder,
FileSystemFinder,
FileSystemStorage,
get_finder,
)


def find_file(finders, path: str, all: bool = False) -> Union[str, list[str], None]:
"""Find a fixture media file with the given path using all enabled fixture media files finders.
:param str path: Path to search for.
:param bool all: Defines whether to return only the first match or search for all matches.
:return: If ``all`` is ``False`` (default), return the first matching absolute path (or ``None``
if no match). Otherwise return a list.
"""
matches = []
for finder in get_finders(finders):
result = finder.find(path, all=all)
if not all and result:
return result
matches.extend([result] if not isinstance(result, (list, tuple)) else result)
return matches or ([] if all else None)


def get_finders(finders: list[str]) -> Generator[BaseFinder, None, None]:
"""Yield enabled finder classes listed in *finders*."""
yield from map(get_finder, finders)


class BaseFileSystemFinder(FileSystemFinder):
search_dirs: list[str]

def __init__(self, app_names=None, *args, **kwargs):
self.locations = []
self.storages = {}
for root in self.search_dirs:
if isinstance(root, (list, tuple)):
prefix, root = root
else:
prefix = ""
if (prefix, root) not in self.locations:
self.locations.append((prefix, root))
for prefix, root in self.locations:
filesystem_storage = FileSystemStorage(location=root)
filesystem_storage.prefix = prefix
self.storages[root] = filesystem_storage
super(BaseFinder, self).__init__(*args, **kwargs)

def check(self):
raise NotImplementedError


class BaseAppDirectoriesFinder(AppDirectoriesFinder):
def check(self):
raise NotImplementedError
85 changes: 85 additions & 0 deletions anfema_django_testutils/contrib/fixtures/finders/fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from typing import Generator, Union

import django.contrib.staticfiles.finders
from django.apps import apps
from django.contrib.staticfiles.finders import BaseFinder
from django.contrib.staticfiles.utils import matches_patterns
from django.core.files.storage import FileSystemStorage

from ..settings import get_config
from .base import BaseAppDirectoriesFinder, BaseFileSystemFinder


searched_locations = django.contrib.staticfiles.finders.searched_locations


class FileSystemFinder(BaseFileSystemFinder):
"""A fixture files finder that that uses the ``FIXTURES_DIRS`` setting to locate fixture files."""

search_dirs = get_config()["FIXTURE_DIRS"]

def list(
self, fixture_suffixes: list[str] = None, ignore_patterns: list[str] = None
) -> Generator[tuple[str, FileSystemStorage], None, None]:
"""Yields all fixture files found in the folder defined by the ``FIXTURES_DIRS`` setting.
:param list fixture_suffixes: A list of file suffixes to match for fixture files.
:param list ignore_patterns: A list of patterns to ignore when searching for fixtures.
"""
patterns = [f"*{suffix}" for suffix in (fixture_suffixes or apps.get_app_config("fixtures").fixture_suffixes)]
yield from filter(lambda p: matches_patterns(p[0], patterns), super().list(ignore_patterns))


class AppDirectoriesFinder(BaseAppDirectoriesFinder):
"""A fixture files finder that looks in the :file:`fixtures` folder of each app."""

def __init__(self, app_names=None, *args, **kwargs):
self.source_dir = apps.get_app_config("fixtures").fixture_source_dir
super().__init__(app_names=None, *args, **kwargs)

def list(
self, fixture_suffixes: list[str] = None, ignore_patterns: list[str] = None
) -> Generator[tuple[str, FileSystemStorage], None, None]:
"""Yields all fixture files found in the :file:`fixtures` folder of each app.
:param list fixture_suffixes: A list of file suffixes to match for fixture files.
:param list ignore_patterns: A list of patterns to ignore when searching for fixtures.
"""

patterns = [f"*{suffix}" for suffix in (fixture_suffixes or apps.get_app_config("fixtures").fixture_suffixes)]
yield from filter(lambda p: matches_patterns(p[0], patterns), super().list(ignore_patterns))


def find(path: str, all: bool = False) -> Union[str, list[str], None]:
"""Find a fixture file with the given path using all enabled fixture files finders.
:param str path: Path to search for.
:param bool all: Defines whether to return only the first match or search for all matches.
:return: If ``all`` is ``False`` (default), return the first matching absolute path (or ``None``
if no match). Otherwise return a list.
"""
searched_locations[:] = []
matches = []
for finder in get_finders():
result = finder.find(path, all=all)

if not all and result:
return result
if not isinstance(result, (list, tuple)):
result = [result]
matches.extend(result)
return matches or ([] if all else None)


def get_finders() -> Generator[BaseFinder, None, None]:
"""Yield enabled fixture files finder classes."""
finders = get_config()["FIXTURE_FINDERS"]
yield from map(get_finder, finders)


def get_finder(import_path) -> BaseFinder:
"""Import a given fixture files finder class.
:param str import_path: The full Python path to the class to be imported
"""
return django.contrib.staticfiles.finders.get_finder(import_path)
33 changes: 33 additions & 0 deletions anfema_django_testutils/contrib/fixtures/finders/media.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from typing import Union

from django.apps import apps

from ..settings import get_config
from .base import BaseAppDirectoriesFinder, BaseFileSystemFinder, find_file


class FileSystemFinder(BaseFileSystemFinder):
"""A media files finder that uses the ``FIXTURE_MEDIAFILE_DIRS`` setting to locate files."""

search_dirs = get_config()["FIXTURE_MEDIAFILE_DIRS"]


class AppDirectoriesFinder(BaseAppDirectoriesFinder):
"""A media files finder that looks in the :file:`fixtures/media` folder of each app."""

def __init__(self, app_names=None, *args, **kwargs):
self.source_dir = apps.get_app_config("fixtures").fixturemedia_source_dir
super().__init__(app_names=None, *args, **kwargs)


def find(path: str, all: bool = False) -> Union[str, list[str], None]:
"""Find a fixture media file with the given path using all enabled fixture media files finders.
:param str path: Path to search for.
:param bool all: Defines whether to return only the first match or search for all matches.
:return: If ``all`` is ``False`` (default), return the first matching absolute path (or ``None``
if no match). Otherwise return a list.
"""
return find_file(get_config()["FIXTURE_MEDIAFILE_FINDERS"], path, all)
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.core.files.storage import FileSystemStorage
from django.core.management.base import BaseCommand

from anfema_django_testutils.contrib.fixtures.finders.base import get_finders
from anfema_django_testutils.contrib.fixtures.settings import get_config


class Command(BaseCommand):
"""Copies fixture media files from different locations to the settings.MEDIA_ROOT."""

help = "Collect fixture media files in a single location."
verbosity: int

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.media_storage = FileSystemStorage()
self.ignore_patterns = []

def log(self, msg: str, *, level: int = 1, style=None) -> None:
"""Small log helper"""
if self.verbosity >= level:
self.stdout.write((style or str)(msg))

def set_options(self, **options) -> None:
self.verbosity = options["verbosity"]

def handle(self, **options):
self.set_options(**options)

finders = get_config()["FIXTURE_MEDIAFILE_FINDERS"]
for finder_class in get_finders(finders):
for path, storage in finder_class.list(self.ignore_patterns):
source_path = storage.path(path)

self.media_storage.delete(path)
with storage.open(source_path) as fp:
self.log(f"Copy {source_path} -> {self.media_storage.path(path)}")
self.media_storage.save(path, fp)
Loading

0 comments on commit b3b59c2

Please sign in to comment.