Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor runtime_options and drop parser_precedence, hashing #66

Merged
merged 3 commits into from
Dec 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ Option | type | Description
static | `bool` | enable minifying static files css, less and js (default: `True`)
script_types | `list` | script types to limit js minification to (default: `[]`)
parsers | `dict` | parsers to handle minifying specific tags, mainly for advanced customization (default: `{}`)
parser_precedence | `bool` | allow parser specific options to take precedence over the extension (default: `False`)


#### - `bypass` and `bypass_caching`
Expand Down Expand Up @@ -107,9 +106,9 @@ when using the option include `''` (empty string) in the list to include script

#### - `parsers`

using `parser` allows to pass tag specific options to the module responsible for the minification, as well as replacing the default
parser with another included option or your own custom one. In the following example will replace the default `style` (handles CSS)
parser `rcssmin` with `lesscpy`:
allows passing tag specific options to the module responsible for the minification, as well as replacing the default parser with another included option or your own custom one.

In the following example will replace the default `style` (handles CSS) parser `rcssmin` with `lesscpy`:

```python
from flask_minify import minify
Expand Down
2 changes: 1 addition & 1 deletion flask_minify/about.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.33"
__version__ = "0.34"
__doc__ = "Flask extension to minify html, css, js and less."
__license__ = "MIT"
__author__ = "Mohamed Feddad"
Expand Down
29 changes: 7 additions & 22 deletions flask_minify/decorators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from functools import wraps

from flask_minify.parsers import Parser
from flask_minify.utils import get_optimized_hashing


def minify(
html=False,
Expand All @@ -8,7 +11,6 @@ def minify(
cache=True,
fail_safe=True,
parsers={},
parser_precedence=False,
):
"""Decorator to minify endpoint HTML output.

Expand All @@ -26,51 +28,34 @@ def minify(
silence encountered exceptions.
parsers: dict
parsers to handle minifying specific tags.
parser_options_take_precedence: bool
allow parser specific options to override the extension.

Returns
-------
String of minified HTML content.
"""
hashing = get_optimized_hashing()
parser = Parser(parsers, fail_safe)
parser.update_runtime_options(html, js, cssless)

def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
from .main import hashing
from .parsers import Parser

text = function(*args, **kwargs)
key = None
cache_key, cached = function.__dict__.get("minify", (None, None))
should_minify = isinstance(text, str) and any([html, js, cssless])
runtime_options = {
"html": {
"only_html_content": not html,
"minify_inline": {
"script": js,
"style": cssless,
},
},
}

if should_minify:
if cache:
key = hashing(text).hexdigest()

if cache_key != key or not cache:
parser = Parser(
runtime_options=runtime_options,
fail_safe=fail_safe,
parsers=parsers,
parser_precedence=parser_precedence,
)
text = parser.minify(text, "html")

if cache:
function.__dict__["minify"] = (key, text)

should_return_cached = all([cache_key == key, cache, should_minify])
should_return_cached = cache_key == key and cache and should_minify
return cached if should_return_cached else text

return wrapper
Expand Down
28 changes: 6 additions & 22 deletions flask_minify/main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
from itertools import tee
from re import compile as compile_re
from sys import maxsize

from flask import _app_ctx_stack, request
from xxhash import xxh32, xxh64

from flask_minify.parsers import Html, Jsmin, Lesscpy, Parser
from flask_minify.utils import is_cssless, is_html, is_js

# optimized hashing speed based on cpu architecture
hashing = xxh64 if maxsize > 2 ** 32 else xxh32
from flask_minify.parsers import Parser
from flask_minify.utils import get_optimized_hashing, is_cssless, is_html, is_js


class Minify:
Expand All @@ -29,7 +24,6 @@ def __init__(
static=True,
script_types=[],
parsers={},
parser_precedence=False,
):
"""Extension to minify flask response for html, javascript, css and less.

Expand Down Expand Up @@ -59,8 +53,6 @@ def __init__(
list of script types to limit js minification to.
parsers: dict
parsers to handle minifying specific tags.
parser_precedence: bool
allow parser specific options to take precedence over the extension.

Notes
-----
Expand Down Expand Up @@ -97,17 +89,9 @@ def root(id):
self._app = app
self.passive = passive
self.static = static
runtime_options = {
"html": {
"only_html_content": not html,
"script_types": script_types,
"minify_inline": {
"script": js,
"style": cssless,
},
},
}
self.parser = Parser(parsers, runtime_options, fail_safe, parser_precedence)
self.hashing = get_optimized_hashing()
self.parser = Parser(parsers, fail_safe)
self.parser.update_runtime_options(html, js, cssless, script_types)

app and self.init_app(app)

Expand Down Expand Up @@ -192,7 +176,7 @@ def get_minified_or_cached(self, content, tag):
def _cache_dict():
return self.cache.get(self.endpoint, {})

key = hashing(content.encode("utf-8")).hexdigest()
key = self.hashing(content.encode("utf-8")).hexdigest()
limit_reached = len(_cache_dict()) >= self.caching_limit
_, bypassed = self.get_endpoint_matches(self.endpoint, self.bypass_caching)

Expand Down
47 changes: 35 additions & 12 deletions flask_minify/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,38 @@
from flask_minify.utils import get_tag_contents


class Jsmin:
runtime_options = {"quote_chars": "'\"`"}
class ParserMixin:
# parser specific runtime option will take precedence over global
takes_precedence = False

@property
def options_changed(self):
return self._o != self.runtime_options


class Jsmin(ParserMixin):
runtime_options = _o = {"quote_chars": "'\"`"}
executer = staticmethod(jsmin)


class Rcssmin:
runtime_options = {"keep_bang_comments": False}
class Rcssmin(ParserMixin):
runtime_options = _o = {"keep_bang_comments": False}
executer = staticmethod(cssmin)


class Lesscpy:
runtime_options = {"minify": True, "xminify": True}
class Lesscpy(ParserMixin):
runtime_options = _o = {"minify": True, "xminify": True}

def executer(self, content, **options):
return compile_less(StringIO(content), **options)


class Html:
class Html(ParserMixin):
_default_tags = {
"script": False,
"style": False,
}
runtime_options = {
runtime_options = _o = {
"remove_comments": True,
"remove_optional_attribute_quotes": False,
"only_html_content": False,
Expand Down Expand Up @@ -58,14 +67,26 @@ class Parser:
def __init__(
self,
parsers={},
runtime_options={},
fail_safe=False,
parser_precedence=False,
runtime_options={},
):
self.parsers = {**self._default_parsers, **parsers}
self.runtime_options = {**runtime_options}
self.fail_safe = fail_safe
self.parser_precedence = parser_precedence

def update_runtime_options(
self, html=False, js=False, cssless=False, script_types=[]
):
self.runtime_options.setdefault("html", {}).update(
{
"only_html_content": not html,
"script_types": script_types,
"minify_inline": {
"script": js,
"style": cssless,
},
}
)

def minify(self, content, tag):
if tag not in self.parsers:
Expand All @@ -74,7 +95,9 @@ def minify(self, content, tag):
parser = self.parsers[tag]()
parser.parser = self

if self.parser_precedence:
if parser.options_changed:
runtime_options = parser.runtime_options
elif parser.takes_precedence:
runtime_options = {
**self.runtime_options.get(tag, {}),
**parser.runtime_options,
Expand Down
8 changes: 8 additions & 0 deletions flask_minify/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from re import DOTALL
from re import compile as compile_re
from re import sub
from sys import maxsize

from xxhash import xxh32, xxh64


def is_empty(content):
Expand Down Expand Up @@ -129,3 +132,8 @@ def is_cssless(response):
content_type = getattr(response, "content_type", "")

return "css" in content_type.lower() or "less" in content_type.lower()


def get_optimized_hashing():
"""Gets optimized hashing module based on cpu architecture"""
return xxh64 if maxsize > 2 ** 32 else xxh32
2 changes: 2 additions & 0 deletions tests/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,5 @@
]
).encode("utf-8")
)

COMPILED_LESS_RAW = "body {\n color: red;\n}"
20 changes: 17 additions & 3 deletions tests/units.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from os import path
from sys import path as sys_path
from unittest import mock

from flask_minify import minify, parsers
from flask_minify.utils import is_cssless, is_empty, is_html, is_js

from .constants import CSS_EDGE_CASES, MINIFIED_CSS_EDGE_CASES
from .constants import (
COMPILED_LESS_RAW,
CSS_EDGE_CASES,
LESS_RAW,
MINIFIED_CSS_EDGE_CASES,
)


class TestUtils:
Expand Down Expand Up @@ -81,3 +84,14 @@ def test_css_edge_cases_with_rcssmin(self):
minified = parser.minify(CSS_EDGE_CASES, "style")

assert minified == MINIFIED_CSS_EDGE_CASES

def test_overriding_parser_options(self):
new_options = {"minify": False}

class CustomParser(parsers.Lesscpy):
runtime_options = new_options

parser = parsers.Parser({"style": CustomParser})
minified = parser.minify(LESS_RAW, "style")

assert minified == COMPILED_LESS_RAW