Skip to content

Commit 15a339d

Browse files
committed
feat: Python bindings
1 parent 1f397c4 commit 15a339d

File tree

17 files changed

+719
-1
lines changed

17 files changed

+719
-1
lines changed

.github/workflows/build.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,34 @@ jobs:
9999
with:
100100
command: clippy
101101
args: -- -D warnings
102+
103+
test-python:
104+
strategy:
105+
fail-fast: false
106+
matrix:
107+
os: [ubuntu-latest, macos-latest, windows-latest]
108+
python-version: ['3.5', '3.6', '3.7', '3.8']
109+
110+
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
111+
runs-on: ${{ matrix.os }}
112+
steps:
113+
- uses: actions/checkout@v2
114+
with:
115+
submodules: true
116+
- uses: actions/setup-python@v2
117+
with:
118+
python-version: ${{ matrix.python-version }}
119+
architecture: x64
120+
121+
- run: python -m pip install tox
122+
working-directory: ./python
123+
124+
- uses: actions-rs/toolchain@v1
125+
with:
126+
# TODO: Using nightly makes `cargo build` fail (at least it happens with 2020-06-12 nightly compiler)
127+
toolchain: nightly-2020-06-11
128+
override: true
129+
130+
- name: Run ${{ matrix.python }} tox job
131+
run: tox -e py
132+
working-directory: ./python

.github/workflows/python-release.yml

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: Python Release
2+
3+
on:
4+
push:
5+
tags:
6+
- python-v*
7+
8+
jobs:
9+
create_macos_and_windows_wheels:
10+
name: Wheels for Python ${{ matrix.python-version }} / ${{ matrix.os }}
11+
strategy:
12+
matrix:
13+
os: [macos-latest, windows-latest]
14+
python-version: ['3.5', '3.6', '3.7', '3.8']
15+
architecture: [x86, x64]
16+
exclude:
17+
- os: macos-latest
18+
architecture: x86
19+
- os: windows-latest
20+
# TODO: Re-enable windows 32bits
21+
architecture: x86
22+
runs-on: ${{ matrix.os }}
23+
steps:
24+
- uses: actions/checkout@v2
25+
- uses: actions/setup-python@v2
26+
with:
27+
python-version: ${{ matrix.python-version }}
28+
architecture: ${{ matrix.architecture }}
29+
- uses: actions-rs/toolchain@v1
30+
with:
31+
profile: minimal
32+
toolchain: nightly
33+
override: true
34+
- name: Install Tox
35+
run: pip install tox
36+
- name: Build wheel
37+
working-directory: ./python
38+
run: tox -e build-wheel
39+
- uses: actions/upload-artifact@v2
40+
with:
41+
name: Distribution Artifacts
42+
path: python/dist/
43+
44+
create_wheels_manylinux:
45+
name: Wheels for Python ${{ matrix.PYTHON_IMPLEMENTATION_ABI }} / Linux
46+
strategy:
47+
fail-fast: false
48+
matrix:
49+
# List of the language-implementation API pairs to publish wheels for
50+
# The list of supported is obtainable by running `docker run quay.io/pypa/manylinux2014_x86_64 ls /opt/python`
51+
PYTHON_IMPLEMENTATION_ABI: [cp35-cp35m, cp36-cp36m, cp37-cp37m, cp38-cp38]
52+
runs-on: ubuntu-latest
53+
container: quay.io/pypa/manylinux2014_x86_64 # Builds wheels on CentOS 7 (supported until 2024)
54+
env:
55+
# Variable needed for PyO3 to properly identify the python interpreter
56+
PYTHON_SYS_EXECUTABLE: /opt/python/${{ matrix.PYTHON_IMPLEMENTATION_ABI }}/bin/python
57+
steps:
58+
- uses: actions/checkout@v2
59+
- name: Install/Update OpenSSL
60+
run: |
61+
retryCount=0
62+
# yum install seems to be flakey (due to network timeouts)
63+
# retry up to 5 times with a 10s sleep in case of failure
64+
until yum install openssl-devel --assumeyes --noplugins; do
65+
# For some reason the install has failed
66+
if [ ${retryCount} -eq 5 ]; then
67+
false
68+
else
69+
retryCount=$((${retryCount}+1))
70+
fi
71+
sleep 10
72+
done
73+
- uses: actions-rs/toolchain@v1
74+
with:
75+
profile: minimal
76+
toolchain: nightly
77+
override: true
78+
- name: Install Tox
79+
run: ${{ env.PYTHON_SYS_EXECUTABLE }} -m pip install tox
80+
- name: Build wheel
81+
working-directory: ./python
82+
run: |
83+
${{ env.PYTHON_SYS_EXECUTABLE }} -m tox -e build-wheel
84+
# Ensure that the wheel is tagged as manylinux2014 platform
85+
auditwheel repair \
86+
--wheel-dir=./dist \
87+
--plat manylinux2014_x86_64 \
88+
./dist/css_inline-*-${{ matrix.PYTHON_IMPLEMENTATION_ABI }}-linux_x86_64.whl
89+
# Remove `linux_x86_64` tagged wheels as they are not supported by https://pypi.org
90+
# Example https://github.com/Stranger6667/jsonschema-rs/runs/766075274
91+
rm ./dist/css_inline-*-${{ matrix.PYTHON_IMPLEMENTATION_ABI }}-linux_x86_64.whl
92+
- uses: actions/upload-artifact@v2
93+
with:
94+
name: Distribution Artifacts
95+
path: python/dist/
96+
97+
create_source_dist:
98+
name: Create sdist package
99+
runs-on: ubuntu-latest
100+
steps:
101+
- uses: actions/checkout@v2
102+
- uses: actions/setup-python@v2
103+
with:
104+
python-version: 3.7
105+
- uses: actions-rs/toolchain@v1
106+
with:
107+
profile: minimal
108+
toolchain: nightly
109+
override: true
110+
- name: Install Tox
111+
run: pip install tox
112+
- name: Build sdist
113+
working-directory: ./python
114+
run: tox -e build-sdist
115+
- uses: actions/upload-artifact@v2
116+
with:
117+
name: Distribution Artifacts
118+
path: python/dist/
119+
120+
upload_to_pypi:
121+
needs:
122+
- create_macos_and_windows_wheels
123+
- create_wheels_manylinux
124+
- create_source_dist
125+
name: Upload Artifacts to PyPi
126+
runs-on: ubuntu-latest
127+
steps:
128+
- uses: actions/download-artifact@v2
129+
with:
130+
name: Distribution Artifacts
131+
path: python/dist/
132+
- name: Publish distribution package to PyPI
133+
uses: pypa/[email protected]
134+
with:
135+
user: ${{ secrets.PYPI_USERNAME }}
136+
password: ${{ secrets.PYPI_PASSWORD }}
137+
packages_dir: python/dist/

.pre-commit-config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,15 @@ repos:
2929
- id: fmt
3030
- id: cargo-check
3131
- id: clippy
32+
33+
- repo: https://github.com/ambv/black
34+
rev: stable
35+
hooks:
36+
- id: black
37+
types: [python]
38+
39+
- repo: https://github.com/pre-commit/mirrors-isort
40+
rev: v4.3.21
41+
hooks:
42+
- id: isort
43+
additional_dependencies: ["isort[pyproject]"]

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@
2525

2626
- Initial public release
2727

28-
[Unreleased]: https://github.com/Stranger6667/jsonschema-rs/compare/v0.1.0...HEAD
28+
[Unreleased]: https://github.com/Stranger6667/css-inline/compare/v0.1.0...HEAD

python/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Rust
10+
/target
11+
12+
# Distribution / packaging
13+
/dist/
14+
/build/
15+
/.eggs/
16+
*.egg-info/
17+
18+
# Rust
19+
/target
20+
*.so
21+
22+
# Testing
23+
.hypothesis
24+
.tox

python/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Changelog
2+
3+
## [Unreleased]
4+
5+
## 0.1.0 - 2020-06-24
6+
7+
- Initial public release
8+
9+
[Unreleased]: https://github.com/Stranger6667/css-inline/compare/python-v0.1.0...HEAD

python/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "css-inline-python"
3+
version = "0.1.0"
4+
authors = ["Dmitry Dygalo <[email protected]>"]
5+
edition = "2018"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[lib]
10+
name = "css_inline"
11+
crate-type = ["cdylib"]
12+
13+
[build-dependencies]
14+
built = { version = "0.4", features = ["chrono"] }
15+
16+
[dependencies]
17+
css-inline = { path = "..", version = "= 0.1.0" }
18+
rayon = "1"
19+
pyo3 = { version = ">= 0.10", features = ["extension-module"] }
20+
pyo3-built = "0.4"
21+
22+
[profile.release]
23+
codegen-units = 1
24+
lto = "on"

python/MANIFEST.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include Cargo.toml
2+
include build.rs
3+
include pyproject.toml
4+
include rust-toolchain
5+
recursive-include src *

python/README.rst

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
css_inline
2+
==========
3+
4+
|Build| |Version| |Python versions| |License|
5+
6+
Fast CSS inlining for Python implemented in Rust.
7+
8+
Features:
9+
10+
- Removing ``style`` tags after inlining;
11+
- ... more features will be available soon
12+
13+
**NOTE**. This library is in active development and provides a small number of features at the moment, see ``Limitations`` sections below for more information.
14+
15+
Installation
16+
------------
17+
18+
To install ``css_inline`` via ``pip`` run the following command:
19+
20+
.. code:: bash
21+
22+
pip install css_inline
23+
24+
Usage
25+
-----
26+
27+
To inline CSS in a HTML document:
28+
29+
.. code:: python
30+
31+
import css_inline
32+
33+
HTML = """<html>
34+
<head>
35+
<title>Test</title>
36+
<style>
37+
h1, h2 { color:blue; }
38+
strong { text-decoration:none }
39+
p { font-size:2px }
40+
p.footer { font-size: 1px}
41+
</style>
42+
</head>
43+
<body>
44+
<h1>Big Text</h1>
45+
<p>
46+
<strong>Solid</strong>
47+
</p>
48+
<p class="footer">Foot notes</p>
49+
</body>
50+
</html>"""
51+
52+
inlined = css_inline.inline(HTML)
53+
54+
If you want to inline many HTML documents then you can utilize ``inline_many`` that processes the input in parallel.
55+
56+
.. code:: python
57+
58+
import css_inline
59+
60+
css_inline.inline_many(["...", "..."])
61+
62+
For customization options use ``CSSInliner`` class:
63+
64+
.. code:: python
65+
66+
import css_inline
67+
68+
inliner = css_inline.CSSInliner(remove_style_tags=True)
69+
inliner.inline("...")
70+
71+
Performance
72+
-----------
73+
74+
Due to the usage of efficient tooling from Mozilla's Servo project (``html5ever``, ``rust-cssparser`` and others) this
75+
library has good performance characteristics. In comparison with other Python projects, it is ~12-15x faster than the nearest competitor.
76+
77+
For inlining CSS in the html document from the ``Usage`` section above we have the following breakdown in our benchmarks:
78+
79+
- ``css_inline 0.1.0`` - 24.82 us
80+
- ``premailer 3.7.0`` - 356.59 us (**x14.37**)
81+
- ``inlinestyler 0.2.4`` - 2.62 ms (**x105.82**)
82+
- ``pynliner 0.8.0`` - 3.03 ms (**x122.44**)
83+
84+
You can take a look at the benchmarks' code at ``benches/bench.py`` file.
85+
The results above were measured with ``rustc 1.46``, ``Python 3.8`` on i8700K, and 32GB RAM.
86+
87+
Limitations
88+
-----------
89+
90+
Currently (as of ``0.1.0``) there are the following notable limitations:
91+
92+
- External stylesheets are not resolved (`#8 <https://github.com/Stranger6667/css-inline/issues/8>`_)
93+
- Inlined CSS is not minimized (`#12 <https://github.com/Stranger6667/css-inline/issues/12>`_)
94+
- `class` and `id` attributes are not removed (`#13 <https://github.com/Stranger6667/css-inline/issues/13>`_)
95+
96+
Python support
97+
--------------
98+
99+
``css_inline`` supports Python 3.5, 3.6, 3.7, and 3.8.
100+
101+
License
102+
-------
103+
104+
The code in this project is licensed under `MIT license`_.
105+
By contributing to ``css_inline``, you agree that your contributions
106+
will be licensed under its MIT license.
107+
108+
.. |Build| image:: https://github.com/Stranger6667/css-inline/workflows/ci/badge.svg
109+
:target: https://github.com/Stranger6667/css_inline/actions
110+
.. |Version| image:: https://img.shields.io/pypi/v/css_inline.svg
111+
:target: https://pypi.org/project/css_inline/
112+
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/css_inline.svg
113+
:target: https://pypi.org/project/css_inline/
114+
.. |License| image:: https://img.shields.io/pypi/l/css_inline.svg
115+
:target: https://opensource.org/licenses/MIT
116+
117+
.. _MIT license: https://opensource.org/licenses/MIT

0 commit comments

Comments
 (0)