Skip to content
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
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release new version
on:
push:
tags:
- v*

jobs:
pypi-publish:
name: Upload release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/weasyprint
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Install requirements
run: python -m pip install flit
- name: Build packages
run: flit build
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
add-version:
name: Add version to GitHub
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install requirements
run: sudo apt-get install pandoc
- name: Generate content
run: |
pandoc docs/changelog.rst -f rst -t gfm | csplit - /##/ "{1}" -f .part
sed -r "s/^([A-Z].*)\:\$/## \1/" .part01 | sed -r "s/^ *//" | sed -rz "s/([^\n])\n([^\n^-])/\1 \2/g" | tail -n +5 > .body
- name: Create Release
uses: softprops/action-gh-release@v2
with:
body_path: .body
10 changes: 4 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.13']
python-version: ['3.14']
include:
- os: ubuntu-latest
python-version: '3.9'
python-version: '3.11'
- os: ubuntu-latest
python-version: 'pypy-3.9'
python-version: 'pypy-3.11'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand All @@ -41,6 +41,4 @@ jobs:
env:
DYLD_FALLBACK_LIBRARY_PATH: /opt/homebrew/lib
- name: Check coding style
run: python -m flake8
- name: Check imports order
run: python -m isort . --check --diff
run: python -m ruff check
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Flask-WeasyPrint generates PDF files out of your Flask website thanks to
WeasyPrint.

* Free software: BSD license
* For Python 3.9+, tested on CPython and PyPy
* For Python 3.11+, tested on CPython and PyPy
* Documentation: https://doc.courtbouillon.org/flask-weasyprint
* Changelog: https://github.com/Kozea/Flask-WeasyPrint/releases
* Code, issues, tests: https://github.com/Kozea/Flask-WeasyPrint
Expand Down
78 changes: 39 additions & 39 deletions flask_weasyprint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from io import BytesIO
from urllib.parse import urljoin, urlsplit
from urllib.request import BaseHandler

from flask import current_app, has_request_context, request, send_file
from werkzeug.test import Client, ClientRedirectError, EnvironBuilder
from werkzeug.test import Client, EnvironBuilder
from werkzeug.wrappers import Response

VERSION = __version__ = '1.1.0'
Expand Down Expand Up @@ -73,7 +74,7 @@ def dispatch(url_string):


def make_url_fetcher(dispatcher=None, next_fetcher=True):
"""Return an function suitable as a ``url_fetcher`` in WeasyPrint.
"""Return a URL fetcher that handles the Flask app routes internally.

You generally don’t need to call this directly.

Expand All @@ -92,45 +93,44 @@ def make_url_fetcher(dispatcher=None, next_fetcher=True):
Typically ``base_url + path`` is equivalent to the passed URL.

"""
from weasyprint.urls import URLFetcher, URLFetcherResponse # lazy loading

if next_fetcher is True:
from weasyprint import default_url_fetcher # lazy loading
next_fetcher = default_url_fetcher
next_fetcher = URLFetcher

if dispatcher is None:
dispatcher = make_flask_url_dispatcher()

def flask_url_fetcher(url):
redirect_chain = set()
while True:
result = dispatcher(url)
if result is None:
return next_fetcher(url)
app, base_url, path = result
client = Client(app, response_wrapper=Response)
if has_request_context() and request.cookies:
server_name = EnvironBuilder(
path, base_url=base_url).server_name
for cookie_key, cookie_value in request.cookies.items():
client.set_cookie(
cookie_key, cookie_value, domain=server_name)
response = client.get(path, base_url=base_url)
if response.status_code == 200:
return {
'string': response.data, 'mime_type': response.mimetype,
'encoding': 'utf-8', 'redirected_url': url}
# The test client can follow redirects, but do it ourselves
# to get access to the redirected URL.
elif response.status_code in (301, 302, 303, 305, 307, 308):
redirect_chain.add(url)
url = urljoin(url, response.location)
if url in redirect_chain:
raise ClientRedirectError('loop detected')
else:
raise ValueError(
'Flask-WeasyPrint got HTTP status '
f'{response.status} for {urljoin(base_url, path)}')

return flask_url_fetcher
class FlaskHandler(BaseHandler):
def default_open(self, req):
url = req.full_url
if result := dispatcher(url):
app, base_url, path = result
client = Client(app, response_wrapper=Response)
if has_request_context() and request.cookies:
server_name = EnvironBuilder(path, base_url=base_url).server_name
for cookie_key, cookie_value in request.cookies.items():
client.set_cookie(cookie_key, cookie_value, domain=server_name)
response = client.get(path, base_url=base_url)
response = URLFetcherResponse(
url, response.data, response.headers, response.status_code)
response.msg = ''
return response

class FlaskFetcher(next_fetcher or URLFetcher):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_handler(FlaskHandler())

def fetch(self, url, headers=None):
if dispatcher(url) is None:
if next_fetcher:
return super().fetch(url, headers)
else:
raise ValueError(f'Unknown Flask app URL: {url}')
return URLFetcher.fetch(self, url, headers)

return FlaskFetcher()


def _wrapper(class_, *args, **kwargs):
Expand All @@ -148,7 +148,7 @@ def _wrapper(class_, *args, **kwargs):
return class_(guess, *args, **kwargs)


def HTML(*args, **kwargs):
def HTML(*args, **kwargs): # noqa: N802
"""Like :class:`weasyprint.HTML` but:

* :func:`make_url_fetcher` is used to create an ``url_fetcher``
Expand All @@ -165,7 +165,7 @@ def HTML(*args, **kwargs):
return _wrapper(HTML, *args, **kwargs)


def CSS(*args, **kwargs):
def CSS(*args, **kwargs): # noqa: N802
from weasyprint import CSS # lazy loading
return _wrapper(CSS, *args, **kwargs)

Expand Down Expand Up @@ -200,5 +200,5 @@ def render_pdf(html, stylesheets=None, download_filename=None,
pdf = html.write_pdf(stylesheets=stylesheets, **options)
as_attachment = automatic_download if download_filename else False
return send_file(
BytesIO(pdf), mimetype="application/pdf", as_attachment=as_attachment,
BytesIO(pdf), mimetype='application/pdf', as_attachment=as_attachment,
download_name=download_filename)
19 changes: 11 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ description = 'Make PDF in your Flask app with WeasyPrint'
keywords = ['html', 'css', 'pdf', 'converter', 'flask', 'weasyprint']
authors = [{name = 'Simon Sapin', email = 'simon.sapin@exyr.org'}]
maintainers = [{name = 'CourtBouillon', email = 'contact@courtbouillon.org'}]
requires-python = '>=3.9'
requires-python = '>=3.11'
readme = {file = 'README.rst', content-type = 'text/x-rst'}
license = {file = 'LICENSE'}
dependencies = [
'flask >=2.3.0',
'weasyprint >=53.0',
'weasyprint >=68.0',
]
classifiers = [
'Development Status :: 5 - Production/Stable',
Expand All @@ -24,11 +24,10 @@ classifiers = [
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.9',
'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',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
Expand All @@ -46,7 +45,7 @@ Donation = 'https://opencollective.com/courtbouillon'

[project.optional-dependencies]
doc = ['sphinx', 'sphinx_rtd_theme']
test = ['pytest', 'isort', 'flake8']
test = ['pytest', 'ruff']

[tool.flit.sdist]
exclude = ['.*']
Expand All @@ -59,6 +58,10 @@ include = ['tests/*', 'flask_weasyprint/*']
exclude_lines = ['pragma: no cover', 'def __repr__', 'raise NotImplementedError']
omit = ['.*']

[tool.isort]
default_section = 'FIRSTPARTY'
multi_line_output = 4
[tool.ruff.lint]
select = ['E', 'W', 'F', 'I', 'N', 'RUF', 'T20', 'PIE', 'PT', 'RSE', 'UP', 'Q']
ignore = ['RUF001', 'RUF002', 'RUF003']

[tool.ruff.lint.flake8-quotes]
inline-quotes = 'single'
multiline-quotes = 'single'
6 changes: 3 additions & 3 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

@app.config.from_object
class Config:
GRAPH_COLORS = ['#0C3795', '#752641', '#E47F00']
GRAPH_COLORS = ('#0C3795', '#752641', '#E47F00')


@app.route('/')
Expand Down Expand Up @@ -115,8 +115,8 @@ def static(filename):
abort(404)


@app.route(u'/Unïĉodé/<stuff>')
@app.route(u'/foo bar/<stuff>')
@app.route('/Unïĉodé/<stuff>')
@app.route('/foo bar/<stuff>')
def funky_urls(stuff):
return stuff

Expand Down
Loading
Loading