mirror of
https://github.com/django-components/django-components.git
synced 2025-07-08 01:45:00 +00:00
feat: allow globs when specifynigg additionall JS and CSS (#1043)
* feat: allow globs when specifynigg additionall JS and CSS * refactor: fix tests and linter errors
This commit is contained in:
parent
73e94b6714
commit
ab75cfdb8f
12 changed files with 282 additions and 97 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,5 +1,19 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
|
## v0.132
|
||||||
|
|
||||||
|
#### Feat
|
||||||
|
|
||||||
|
- Allow to use glob patterns as paths for additional JS / CSS in
|
||||||
|
`Component.Media.js` and `Component.Media.css`
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Media:
|
||||||
|
js = ["*.js"]
|
||||||
|
css = ["*.css"]
|
||||||
|
```
|
||||||
|
|
||||||
## v0.131
|
## v0.131
|
||||||
|
|
||||||
#### Feat
|
#### Feat
|
||||||
|
|
|
@ -143,17 +143,22 @@ However, there's a few differences from Django's Media class:
|
||||||
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
|
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
|
||||||
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
|
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
|
||||||
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
|
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
|
||||||
|
3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
|
||||||
|
4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
|
||||||
|
it should be a list of [`Component`](../../../reference/api/#django_components.Component) classes.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class MyTable(Component):
|
class MyTable(Component):
|
||||||
class Media:
|
class Media:
|
||||||
js = [
|
js = [
|
||||||
"path/to/script.js",
|
"path/to/script.js",
|
||||||
|
"path/to/*.js", # Or as a glob
|
||||||
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
|
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
|
||||||
]
|
]
|
||||||
css = {
|
css = {
|
||||||
"all": [
|
"all": [
|
||||||
"path/to/style.css",
|
"path/to/style.css",
|
||||||
|
"path/to/*.css", # Or as a glob
|
||||||
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
|
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
|
||||||
],
|
],
|
||||||
"print": ["path/to/style2.css"],
|
"print": ["path/to/style2.css"],
|
||||||
|
|
|
@ -230,7 +230,9 @@ with a few differences:
|
||||||
|
|
||||||
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (see below).
|
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (see below).
|
||||||
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, [`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function.
|
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, [`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function.
|
||||||
3. If you set `Media.extend` to a list, it should be a list of `Component` classes.
|
3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
|
||||||
|
4. If you set [`Media.extend`](../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
|
||||||
|
it should be a list of [`Component`](../../reference/api/#django_components.Component) classes.
|
||||||
|
|
||||||
[Learn more](../../concepts/fundamentals/defining_js_css_html_files) about using Media.
|
[Learn more](../../concepts/fundamentals/defining_js_css_html_files) about using Media.
|
||||||
|
|
||||||
|
@ -245,10 +247,12 @@ class Calendar(Component):
|
||||||
class Media: # <--- new
|
class Media: # <--- new
|
||||||
js = [
|
js = [
|
||||||
"path/to/shared.js",
|
"path/to/shared.js",
|
||||||
|
"path/to/*.js", # Or as a glob
|
||||||
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
|
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
|
||||||
]
|
]
|
||||||
css = [
|
css = [
|
||||||
"path/to/shared.css",
|
"path/to/shared.css",
|
||||||
|
"path/to/*.css", # Or as a glob
|
||||||
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # Tailwind
|
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # Tailwind
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,26 @@
|
||||||
|
import glob
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Protocol,
|
||||||
|
Sequence,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
|
@ -16,7 +32,7 @@ from django.utils.safestring import SafeData
|
||||||
|
|
||||||
from django_components.util.loader import get_component_dirs, resolve_file
|
from django_components.util.loader import get_component_dirs, resolve_file
|
||||||
from django_components.util.logger import logger
|
from django_components.util.logger import logger
|
||||||
from django_components.util.misc import get_import_path
|
from django_components.util.misc import flatten, get_import_path, get_module_info, is_glob
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components.component import Component
|
from django_components.component import Component
|
||||||
|
@ -649,19 +665,19 @@ def _normalize_media(media: Type[ComponentMediaInput]) -> None:
|
||||||
_map_media_filepaths(media, _normalize_media_filepath)
|
_map_media_filepaths(media, _normalize_media_filepath)
|
||||||
|
|
||||||
|
|
||||||
def _map_media_filepaths(media: Type[ComponentMediaInput], map_fn: Callable[[Any], Any]) -> None:
|
def _map_media_filepaths(media: Type[ComponentMediaInput], map_fn: Callable[[Sequence[Any]], Iterable[Any]]) -> None:
|
||||||
if hasattr(media, "css") and media.css:
|
if hasattr(media, "css") and media.css:
|
||||||
if not isinstance(media.css, dict):
|
if not isinstance(media.css, dict):
|
||||||
raise ValueError(f"Media.css must be a dict, got {type(media.css)}")
|
raise ValueError(f"Media.css must be a dict, got {type(media.css)}")
|
||||||
|
|
||||||
for media_type, path_list in media.css.items():
|
for media_type, path_list in media.css.items():
|
||||||
media.css[media_type] = list(map(map_fn, path_list)) # type: ignore[assignment]
|
media.css[media_type] = list(map_fn(path_list)) # type: ignore[assignment]
|
||||||
|
|
||||||
if hasattr(media, "js") and media.js:
|
if hasattr(media, "js") and media.js:
|
||||||
if not isinstance(media.js, (list, tuple)):
|
if not isinstance(media.js, (list, tuple)):
|
||||||
raise ValueError(f"Media.css must be a list, got {type(media.css)}")
|
raise ValueError(f"Media.css must be a list, got {type(media.css)}")
|
||||||
|
|
||||||
media.js = list(map(map_fn, media.js))
|
media.js = list(map_fn(media.js))
|
||||||
|
|
||||||
|
|
||||||
def _is_media_filepath(filepath: Any) -> bool:
|
def _is_media_filepath(filepath: Any) -> bool:
|
||||||
|
@ -683,26 +699,33 @@ def _is_media_filepath(filepath: Any) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _normalize_media_filepath(filepath: ComponentMediaInputPath) -> Union[str, SafeData]:
|
def _normalize_media_filepath(filepaths: Sequence[ComponentMediaInputPath]) -> List[Union[str, SafeData]]:
|
||||||
if callable(filepath):
|
normalized: List[Union[str, SafeData]] = []
|
||||||
filepath = filepath()
|
for filepath in filepaths:
|
||||||
|
if callable(filepath):
|
||||||
|
filepath = filepath()
|
||||||
|
|
||||||
if isinstance(filepath, SafeData) or hasattr(filepath, "__html__"):
|
if isinstance(filepath, SafeData) or hasattr(filepath, "__html__"):
|
||||||
return filepath
|
normalized.append(filepath)
|
||||||
|
continue
|
||||||
|
|
||||||
if isinstance(filepath, (Path, os.PathLike)) or hasattr(filepath, "__fspath__"):
|
if isinstance(filepath, (Path, os.PathLike)) or hasattr(filepath, "__fspath__"):
|
||||||
# In case of Windows OS, convert to forward slashes
|
# In case of Windows OS, convert to forward slashes
|
||||||
filepath = Path(filepath.__fspath__()).as_posix()
|
filepath = Path(filepath.__fspath__()).as_posix()
|
||||||
|
|
||||||
if isinstance(filepath, bytes):
|
if isinstance(filepath, bytes):
|
||||||
filepath = filepath.decode("utf-8")
|
filepath = filepath.decode("utf-8")
|
||||||
|
|
||||||
if isinstance(filepath, str):
|
if isinstance(filepath, str):
|
||||||
return filepath
|
normalized.append(filepath)
|
||||||
|
continue
|
||||||
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Unknown filepath. Must be str, bytes, PathLike, SafeString, or a function that returns one of the former"
|
f"Unknown filepath {filepath} of type {type(filepath)}. Must be str, bytes, PathLike, SafeString,"
|
||||||
)
|
" or a function that returns one of the former"
|
||||||
|
)
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def _resolve_component_relative_files(
|
def _resolve_component_relative_files(
|
||||||
|
@ -730,12 +753,9 @@ def _resolve_component_relative_files(
|
||||||
return
|
return
|
||||||
|
|
||||||
component_name = comp_cls.__qualname__
|
component_name = comp_cls.__qualname__
|
||||||
# Derive the full path of the file where the component was defined
|
# Get the full path of the file where the component was defined
|
||||||
module_name = comp_cls.__module__
|
module, module_name, module_file_path = get_module_info(comp_cls)
|
||||||
module_obj = sys.modules[module_name]
|
if not module_file_path:
|
||||||
file_path = module_obj.__file__
|
|
||||||
|
|
||||||
if not file_path:
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Could not resolve the path to the file for component '{component_name}'."
|
f"Could not resolve the path to the file for component '{component_name}'."
|
||||||
" Paths for HTML, JS or CSS templates will NOT be resolved relative to the component file."
|
" Paths for HTML, JS or CSS templates will NOT be resolved relative to the component file."
|
||||||
|
@ -743,83 +763,147 @@ def _resolve_component_relative_files(
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the directory where the component class is defined
|
# Get the directory where the component class is defined
|
||||||
try:
|
matched_component_dir = _find_component_dir_containing_file(comp_dirs, module_file_path)
|
||||||
comp_dir_abs, comp_dir_rel = _get_dir_path_from_component_path(file_path, comp_dirs)
|
|
||||||
except RuntimeError:
|
# If no dir was found (e.g. the component was defined at runtime), we assume that the media paths
|
||||||
# If no dir was found, we assume that the path is NOT relative to the component dir
|
# are NOT relative.
|
||||||
|
if matched_component_dir is None:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"No component directory found for component '{component_name}' in {file_path}"
|
f"No component directory found for component '{component_name}' in {module_file_path}"
|
||||||
" If this component defines HTML, JS or CSS templates relatively to the component file,"
|
" If this component defines HTML, JS or CSS templates relatively to the component file,"
|
||||||
" then check that the component's directory is accessible from one of the paths"
|
" then check that the component's directory is accessible from one of the paths"
|
||||||
" specified in the Django's 'COMPONENTS.dirs' settings."
|
" specified in the Django's 'COMPONENTS.dirs' settings."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
matched_component_dir_abs = os.path.abspath(matched_component_dir)
|
||||||
|
# Derive the path from matched `COMPONENTS.dirs` to the dir where the current component file is.
|
||||||
|
component_module_dir_path_abs = os.path.dirname(module_file_path)
|
||||||
|
|
||||||
# Check if filepath refers to a file that's in the same directory as the component class.
|
# Check if filepath refers to a file that's in the same directory as the component class.
|
||||||
# If yes, modify the path to refer to the relative file.
|
# If yes, modify the path to refer to the relative file.
|
||||||
# If not, don't modify anything.
|
# If not, don't modify anything.
|
||||||
def resolve_media_file(filepath: Union[str, SafeData]) -> Union[str, SafeData]:
|
def resolve_relative_media_file(
|
||||||
if isinstance(filepath, str):
|
filepath: Union[str, SafeData],
|
||||||
filepath_abs = os.path.join(comp_dir_abs, filepath)
|
allow_glob: bool,
|
||||||
# NOTE: The paths to resources need to use POSIX (forward slashes) for Django to wor
|
) -> List[Union[str, SafeData]]:
|
||||||
# See https://github.com/django-components/django-components/issues/796
|
resolved_filepaths, has_matched = resolve_media_file(
|
||||||
filepath_rel_to_comp_dir = Path(os.path.join(comp_dir_rel, filepath)).as_posix()
|
filepath,
|
||||||
|
allow_glob,
|
||||||
if os.path.isfile(filepath_abs):
|
static_files_dir=matched_component_dir_abs,
|
||||||
# NOTE: It's important to use `repr`, so we don't trigger __str__ on SafeStrings
|
media_root_dir=component_module_dir_path_abs,
|
||||||
logger.debug(
|
|
||||||
f"Interpreting template '{repr(filepath)}' of component '{module_name}'"
|
|
||||||
" relatively to component file"
|
|
||||||
)
|
|
||||||
|
|
||||||
return filepath_rel_to_comp_dir
|
|
||||||
|
|
||||||
# If resolved absolute path does NOT exist or filepath is NOT a string, then return as is
|
|
||||||
logger.debug(
|
|
||||||
f"Interpreting template '{repr(filepath)}' of component '{module_name}'"
|
|
||||||
" relatively to components directory"
|
|
||||||
)
|
)
|
||||||
return filepath
|
|
||||||
|
# NOTE: It's important to use `repr`, so we don't trigger __str__ on SafeStrings
|
||||||
|
if has_matched:
|
||||||
|
logger.debug(
|
||||||
|
f"Interpreting file '{repr(filepath)}' of component '{module_name}'" " relatively to component file"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"Interpreting file '{repr(filepath)}' of component '{module_name}'"
|
||||||
|
" relatively to components directory"
|
||||||
|
)
|
||||||
|
|
||||||
|
return resolved_filepaths
|
||||||
|
|
||||||
|
# Check if filepath is a glob pattern that points to files relative to the components directory
|
||||||
|
# (as defined by `COMPONENTS.dirs` and `COMPONENTS.app_dirs` settings) in which the component is defined.
|
||||||
|
# If yes, modify the path to refer to the globbed files.
|
||||||
|
# If not, don't modify anything.
|
||||||
|
def resolve_static_media_file(
|
||||||
|
filepath: Union[str, SafeData],
|
||||||
|
allow_glob: bool,
|
||||||
|
) -> List[Union[str, SafeData]]:
|
||||||
|
resolved_filepaths, _ = resolve_media_file(
|
||||||
|
filepath,
|
||||||
|
allow_glob,
|
||||||
|
static_files_dir=matched_component_dir_abs,
|
||||||
|
media_root_dir=matched_component_dir_abs,
|
||||||
|
)
|
||||||
|
return resolved_filepaths
|
||||||
|
|
||||||
# Check if template name is a local file or not
|
# Check if template name is a local file or not
|
||||||
if getattr(comp_media, "template_file", None):
|
if getattr(comp_media, "template_file", None):
|
||||||
comp_media.template_file = resolve_media_file(comp_media.template_file)
|
comp_media.template_file = resolve_relative_media_file(comp_media.template_file, False)[0]
|
||||||
if getattr(comp_media, "js_file", None):
|
if getattr(comp_media, "js_file", None):
|
||||||
comp_media.js_file = resolve_media_file(comp_media.js_file)
|
comp_media.js_file = resolve_relative_media_file(comp_media.js_file, False)[0]
|
||||||
if getattr(comp_media, "css_file", None):
|
if getattr(comp_media, "css_file", None):
|
||||||
comp_media.css_file = resolve_media_file(comp_media.css_file)
|
comp_media.css_file = resolve_relative_media_file(comp_media.css_file, False)[0]
|
||||||
|
|
||||||
if hasattr(comp_media, "Media") and comp_media.Media:
|
if hasattr(comp_media, "Media") and comp_media.Media:
|
||||||
_map_media_filepaths(comp_media.Media, resolve_media_file)
|
_map_media_filepaths(
|
||||||
|
comp_media.Media,
|
||||||
|
# Media files can be defined as a glob patterns that match multiple files.
|
||||||
def _get_dir_path_from_component_path(
|
# Thus, flatten the list of lists returned by `resolve_relative_media_file`.
|
||||||
abs_component_file_path: str,
|
lambda filepaths: flatten(resolve_relative_media_file(f, True) for f in filepaths),
|
||||||
candidate_dirs: Union[List[str], List[Path]],
|
|
||||||
) -> Tuple[str, str]:
|
|
||||||
comp_dir_path_abs = os.path.dirname(abs_component_file_path)
|
|
||||||
|
|
||||||
# From all dirs defined in settings.COMPONENTS.dirs, find one that's the parent
|
|
||||||
# to the component file.
|
|
||||||
root_dir_abs = None
|
|
||||||
for candidate_dir in candidate_dirs:
|
|
||||||
candidate_dir_abs = os.path.abspath(candidate_dir)
|
|
||||||
if comp_dir_path_abs.startswith(candidate_dir_abs):
|
|
||||||
root_dir_abs = candidate_dir_abs
|
|
||||||
break
|
|
||||||
|
|
||||||
if root_dir_abs is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to resolve template directory for component file '{abs_component_file_path}'",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Derive the path from matched COMPONENTS.dirs to the dir where the current component file is.
|
# Go over the JS / CSS media files again, but this time, if there are still any globs,
|
||||||
comp_dir_path_rel = os.path.relpath(comp_dir_path_abs, candidate_dir_abs)
|
# try to resolve them relative to the root directory (as defined by `COMPONENTS.dirs
|
||||||
|
# and `COMPONENTS.app_dirs` settings).
|
||||||
|
_map_media_filepaths(
|
||||||
|
comp_media.Media,
|
||||||
|
# Media files can be defined as a glob patterns that match multiple files.
|
||||||
|
# Thus, flatten the list of lists returned by `resolve_static_media_file`.
|
||||||
|
lambda filepaths: flatten(resolve_static_media_file(f, True) for f in filepaths),
|
||||||
|
)
|
||||||
|
|
||||||
# Return both absolute and relative paths:
|
|
||||||
# - Absolute path is used to check if the file exists
|
# Check if filepath refers to a file that's in the same directory as the component class.
|
||||||
# - Relative path is used for defining the import on the component class
|
# If yes, modify the path to refer to the relative file.
|
||||||
return comp_dir_path_abs, comp_dir_path_rel
|
# If not, don't modify anything.
|
||||||
|
def resolve_media_file(
|
||||||
|
filepath: Union[str, SafeData],
|
||||||
|
allow_glob: bool,
|
||||||
|
static_files_dir: str,
|
||||||
|
media_root_dir: str,
|
||||||
|
) -> Tuple[List[Union[str, SafeData]], bool]:
|
||||||
|
# If filepath is NOT a string, then return as is
|
||||||
|
if not isinstance(filepath, str):
|
||||||
|
return [filepath], False
|
||||||
|
|
||||||
|
filepath_abs_or_glob = os.path.join(media_root_dir, filepath)
|
||||||
|
|
||||||
|
# The path may be a glob. So before we check if the file exists,
|
||||||
|
# we need to resolve the glob.
|
||||||
|
if allow_glob and is_glob(filepath_abs_or_glob):
|
||||||
|
matched_abs_filepaths = glob.glob(filepath_abs_or_glob)
|
||||||
|
else:
|
||||||
|
matched_abs_filepaths = [filepath_abs_or_glob]
|
||||||
|
|
||||||
|
# If there are no matches, return the original filepath
|
||||||
|
if not matched_abs_filepaths:
|
||||||
|
return [filepath], False
|
||||||
|
|
||||||
|
resolved_filepaths: List[str] = []
|
||||||
|
for matched_filepath_abs in matched_abs_filepaths:
|
||||||
|
# Derive the path from matched `COMPONENTS.dirs` to the media file.
|
||||||
|
# NOTE: The paths to resources need to use POSIX (forward slashes) for Django to work
|
||||||
|
# See https://github.com/django-components/django-components/issues/796
|
||||||
|
# NOTE: Since these paths matched the glob, we know that these files exist.
|
||||||
|
filepath_rel_to_comp_dir = Path(os.path.relpath(matched_filepath_abs, static_files_dir)).as_posix()
|
||||||
|
resolved_filepaths.append(filepath_rel_to_comp_dir)
|
||||||
|
|
||||||
|
return resolved_filepaths, True
|
||||||
|
|
||||||
|
|
||||||
|
def _find_component_dir_containing_file(
|
||||||
|
component_dirs: Sequence[Union[str, Path]],
|
||||||
|
target_file_path: str,
|
||||||
|
) -> Optional[Union[str, Path]]:
|
||||||
|
"""
|
||||||
|
From all directories that may contain components (such as those defined in `COMPONENTS.dirs`),
|
||||||
|
find the one that's the parent to the given file.
|
||||||
|
"""
|
||||||
|
abs_target_file_path = os.path.abspath(target_file_path)
|
||||||
|
|
||||||
|
for component_dir in component_dirs:
|
||||||
|
component_dir_abs = os.path.abspath(component_dir)
|
||||||
|
if abs_target_file_path.startswith(component_dir_abs):
|
||||||
|
return component_dir
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_asset(
|
def _get_asset(
|
||||||
|
|
|
@ -2,8 +2,9 @@ import re
|
||||||
import sys
|
import sys
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from itertools import chain
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Type, TypeVar, Union
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Tuple, Type, TypeVar, Union
|
||||||
|
|
||||||
from django_components.util.nanoid import generate
|
from django_components.util.nanoid import generate
|
||||||
|
|
||||||
|
@ -115,3 +116,15 @@ def hash_comp_cls(comp_cls: Type["Component"]) -> str:
|
||||||
full_name = get_import_path(comp_cls)
|
full_name = get_import_path(comp_cls)
|
||||||
comp_cls_hash = md5(full_name.encode()).hexdigest()[0:6]
|
comp_cls_hash = md5(full_name.encode()).hexdigest()[0:6]
|
||||||
return comp_cls.__name__ + "_" + comp_cls_hash
|
return comp_cls.__name__ + "_" + comp_cls_hash
|
||||||
|
|
||||||
|
|
||||||
|
# String is a glob if it contains at least one of `?`, `*`, or `[`
|
||||||
|
is_glob_re = re.compile(r"[?*[]")
|
||||||
|
|
||||||
|
|
||||||
|
def is_glob(filepath: str) -> bool:
|
||||||
|
return is_glob_re.search(filepath) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def flatten(lst: Iterable[Iterable[T]]) -> List[T]:
|
||||||
|
return list(chain.from_iterable(lst))
|
||||||
|
|
23
tests/components/glob/glob.py
Normal file
23
tests/components/glob/glob.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
|
|
||||||
|
# The Media JS / CSS glob and are relative to the component directory
|
||||||
|
class GlobComponent(Component):
|
||||||
|
template = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "glob_*.css"
|
||||||
|
js = "glob_*.js"
|
||||||
|
|
||||||
|
|
||||||
|
# The Media JS / CSS glob and are relative to the directory given in
|
||||||
|
# `COMPONENTS.dirs` and `COMPONENTS.app_dirs`
|
||||||
|
class GlobComponentRootDir(GlobComponent):
|
||||||
|
class Media:
|
||||||
|
css = "glob/glob_*.css"
|
||||||
|
js = "glob/glob_*.js"
|
3
tests/components/glob/glob_1.css
Normal file
3
tests/components/glob/glob_1.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.html-css-only {
|
||||||
|
color: blue;
|
||||||
|
}
|
1
tests/components/glob/glob_1.js
Normal file
1
tests/components/glob/glob_1.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
console.log("JS file");
|
3
tests/components/glob/glob_2.css
Normal file
3
tests/components/glob/glob_2.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.html-css-only {
|
||||||
|
color: blue;
|
||||||
|
}
|
1
tests/components/glob/glob_2.js
Normal file
1
tests/components/glob/glob_2.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
console.log("JS file");
|
|
@ -386,6 +386,34 @@ class TestComponentMedia:
|
||||||
assertInHTML('<link abc href="path/to/style2.css" media="print" rel="stylesheet">', rendered)
|
assertInHTML('<link abc href="path/to/style2.css" media="print" rel="stylesheet">', rendered)
|
||||||
assertInHTML('<link abc href="path/to/style3.css" media="screen" rel="stylesheet">', rendered)
|
assertInHTML('<link abc href="path/to/style3.css" media="screen" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
django_settings={
|
||||||
|
"INSTALLED_APPS": ("django_components", "tests"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_glob_pattern_relative_to_component(self):
|
||||||
|
from tests.components.glob.glob import GlobComponent
|
||||||
|
rendered = GlobComponent.render()
|
||||||
|
|
||||||
|
assertInHTML('<link href="glob/glob_1.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
assertInHTML('<link href="glob/glob_2.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
assertInHTML('<script src="glob/glob_1.js"></script>', rendered)
|
||||||
|
assertInHTML('<script src="glob/glob_2.js"></script>', rendered)
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
django_settings={
|
||||||
|
"INSTALLED_APPS": ("django_components", "tests"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_glob_pattern_relative_to_root_dir(self):
|
||||||
|
from tests.components.glob.glob import GlobComponentRootDir
|
||||||
|
rendered = GlobComponentRootDir.render()
|
||||||
|
|
||||||
|
assertInHTML('<link href="glob/glob_1.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
assertInHTML('<link href="glob/glob_2.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
assertInHTML('<script src="glob/glob_1.js"></script>', rendered)
|
||||||
|
assertInHTML('<script src="glob/glob_2.js"></script>', rendered)
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestMediaPathAsObject:
|
class TestMediaPathAsObject:
|
||||||
|
|
|
@ -247,6 +247,7 @@ class TestComponentFiles:
|
||||||
|
|
||||||
assert dot_paths == [
|
assert dot_paths == [
|
||||||
"components",
|
"components",
|
||||||
|
"components.glob.glob",
|
||||||
"components.multi_file.multi_file",
|
"components.multi_file.multi_file",
|
||||||
"components.relative_file.relative_file",
|
"components.relative_file.relative_file",
|
||||||
"components.relative_file_pathobj.relative_file_pathobj",
|
"components.relative_file_pathobj.relative_file_pathobj",
|
||||||
|
@ -260,15 +261,16 @@ class TestComponentFiles:
|
||||||
|
|
||||||
# NOTE: Compare parts so that the test works on Windows too
|
# NOTE: Compare parts so that the test works on Windows too
|
||||||
assert file_paths[0].parts[-3:] == ("tests", "components", "__init__.py")
|
assert file_paths[0].parts[-3:] == ("tests", "components", "__init__.py")
|
||||||
assert file_paths[1].parts[-4:] == ("tests", "components", "multi_file", "multi_file.py")
|
assert file_paths[1].parts[-4:] == ("tests", "components", "glob", "glob.py")
|
||||||
assert file_paths[2].parts[-4:] == ("tests", "components", "relative_file", "relative_file.py")
|
assert file_paths[2].parts[-4:] == ("tests", "components", "multi_file", "multi_file.py")
|
||||||
assert file_paths[3].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.py")
|
assert file_paths[3].parts[-4:] == ("tests", "components", "relative_file", "relative_file.py")
|
||||||
assert file_paths[4].parts[-3:] == ("tests", "components", "single_file.py")
|
assert file_paths[4].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.py")
|
||||||
assert file_paths[5].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.py")
|
assert file_paths[5].parts[-3:] == ("tests", "components", "single_file.py")
|
||||||
assert file_paths[6].parts[-3:] == ("tests", "components", "urls.py")
|
assert file_paths[6].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.py")
|
||||||
assert file_paths[7].parts[-3:] == ("django_components", "components", "__init__.py")
|
assert file_paths[7].parts[-3:] == ("tests", "components", "urls.py")
|
||||||
assert file_paths[8].parts[-3:] == ("django_components", "components", "dynamic.py")
|
assert file_paths[8].parts[-3:] == ("django_components", "components", "__init__.py")
|
||||||
assert file_paths[9].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py")
|
assert file_paths[9].parts[-3:] == ("django_components", "components", "dynamic.py")
|
||||||
|
assert file_paths[10].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py")
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
django_settings={
|
django_settings={
|
||||||
|
@ -282,6 +284,8 @@ class TestComponentFiles:
|
||||||
file_paths = [f.filepath for f in files]
|
file_paths = [f.filepath for f in files]
|
||||||
|
|
||||||
assert dot_paths == [
|
assert dot_paths == [
|
||||||
|
"components.glob.glob_1",
|
||||||
|
"components.glob.glob_2",
|
||||||
"components.relative_file.relative_file",
|
"components.relative_file.relative_file",
|
||||||
"components.relative_file_pathobj.relative_file_pathobj",
|
"components.relative_file_pathobj.relative_file_pathobj",
|
||||||
"components.staticfiles.staticfiles",
|
"components.staticfiles.staticfiles",
|
||||||
|
@ -289,10 +293,12 @@ class TestComponentFiles:
|
||||||
]
|
]
|
||||||
|
|
||||||
# NOTE: Compare parts so that the test works on Windows too
|
# NOTE: Compare parts so that the test works on Windows too
|
||||||
assert file_paths[0].parts[-4:] == ("tests", "components", "relative_file", "relative_file.js")
|
assert file_paths[0].parts[-4:] == ("tests", "components", "glob", "glob_1.js")
|
||||||
assert file_paths[1].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.js")
|
assert file_paths[1].parts[-4:] == ("tests", "components", "glob", "glob_2.js")
|
||||||
assert file_paths[2].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.js")
|
assert file_paths[2].parts[-4:] == ("tests", "components", "relative_file", "relative_file.js")
|
||||||
assert file_paths[3].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.js")
|
assert file_paths[3].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.js")
|
||||||
|
assert file_paths[4].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.js")
|
||||||
|
assert file_paths[5].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.js")
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue