Skip to content

Commit 0abd10f

Browse files
authored
Make cchardet optional (#211)
Cchardet is not compatible with Python 3.11+ (there is an alpha release that works on 3.11 and 3.12, but it is now old too), and we need to make and keep this package easier to install, update, and secure. Cchardet seemed very dependable for a long time, but its maintainer eventually took a long break. It's pretty frozen, and we cannot depend on it as a hard requirement. The detection test had to be amended to a different encoding in order to get something that is supported and reliably detected across all detector options. Partially covers #196.
1 parent 9d375b4 commit 0abd10f

6 files changed

Lines changed: 63 additions & 11 deletions

File tree

.circleci/config.yml

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ commands:
1515
description: Install docs dependencies
1616
type: boolean
1717
default: false
18+
chardet:
19+
type: string
20+
default: ""
1821
steps:
1922
- restore_cache:
2023
keys:
24+
- cache-v4-{{ arch }}-python<< parameters.python-version >>-<< parameters.chardet >>-{{ checksum "requirements.txt" }}-{{ checksum "requirements-server.txt" }}-{{ checksum "requirements-dev.txt" }}-{{ checksum "requirements-experimental.txt" }}-{{ checksum "requirements-docs.txt" }}
2125
- cache-v4-{{ arch }}-python<< parameters.python-version >>-{{ checksum "requirements.txt" }}-{{ checksum "requirements-server.txt" }}-{{ checksum "requirements-dev.txt" }}-{{ checksum "requirements-experimental.txt" }}-{{ checksum "requirements-docs.txt" }}
2226
- cache-v4-{{ arch }}-python<< parameters.python-version >>-{{ checksum "requirements.txt" }}-{{ checksum "requirements-server.txt" }}-{{ checksum "requirements-dev.txt" }}-{{ checksum "requirements-experimental.txt" }}-
2327
- cache-v4-{{ arch }}-python<< parameters.python-version >>-{{ checksum "requirements.txt" }}-{{ checksum "requirements-server.txt" }}-{{ checksum "requirements-dev.txt" }}-
@@ -35,6 +39,14 @@ commands:
3539
pip install .[server,dev] --no-binary lxml
3640
pip install -r requirements-experimental.txt
3741
42+
- when:
43+
condition: << parameters.chardet >>
44+
steps:
45+
- run:
46+
command: |
47+
. ~/venv/bin/activate
48+
pip install << parameters.chardet >>
49+
3850
# Docs dependencies are only compatible on Python 3.10+, so only install
3951
# them on demand.
4052
- when:
@@ -46,7 +58,7 @@ commands:
4658
pip install -r requirements-docs.txt
4759
4860
- save_cache:
49-
key: cache-v4-{{ arch }}-python<< parameters.python-version >>-{{ checksum "requirements.txt" }}-{{ checksum "requirements-server.txt" }}-{{ checksum "requirements-dev.txt" }}-{{ checksum "requirements-experimental.txt" }}-{{ checksum "requirements-docs.txt" }}
61+
key: cache-v4-{{ arch }}-python<< parameters.python-version >>-<< parameters.chardet >>-{{ checksum "requirements.txt" }}-{{ checksum "requirements-server.txt" }}-{{ checksum "requirements-dev.txt" }}-{{ checksum "requirements-experimental.txt" }}-{{ checksum "requirements-docs.txt" }}
5062
paths:
5163
- ~/venv
5264

@@ -55,13 +67,17 @@ jobs:
5567
parameters:
5668
python-version:
5769
type: string
70+
chardet:
71+
type: string
72+
default: ""
5873
working_directory: ~/web-monitoring-diff
5974
docker:
6075
- image: cimg/python:<< parameters.python-version >>
6176
steps:
6277
- checkout
6378
- setup_pip:
6479
python-version: << parameters.python-version >>
80+
chardet: << parameters.chardet >>
6581
- run:
6682
name: Tests
6783
command: |
@@ -179,7 +195,23 @@ workflows:
179195
only: /^v.*/
180196
matrix:
181197
parameters:
182-
python-version: ["3.7", "3.8", "3.9", "3.10"]
198+
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
199+
chardet: ["", "cchardet", "faust-cchardet"]
200+
exclude:
201+
- python-version: "3.7"
202+
chardet: "faust-cchardet"
203+
- python-version: "3.8"
204+
chardet: "faust-cchardet"
205+
- python-version: "3.9"
206+
chardet: "faust-cchardet"
207+
- python-version: "3.11"
208+
chardet: "cchardet"
209+
# - python-version: "3.12"
210+
# chardet: "cchardet"
211+
# - python-version: "3.13"
212+
# chardet: "cchardet"
213+
# - python-version: "3.14"
214+
# chardet: "cchardet"
183215
- lint:
184216
filters:
185217
tags:

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# We separate them out so that the final `release` image can layer on top of
77
# this one without needing compiler-related packages.
88
##
9-
FROM python:3.10.11-slim as base
9+
FROM python:3.10.17-slim as base
1010
LABEL maintainer="enviroDGI@gmail.com"
1111

1212
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -29,6 +29,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
2929
WORKDIR /app
3030

3131
RUN pip install --upgrade pip
32+
RUN pip install cchardet
3233
# Copy the requirements.txt alone into the container at /app
3334
# so that they can be cached more aggressively than the rest of the source.
3435
ADD requirements.txt /app

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ This package was originally built as a component of EDGI’s [Web Monitoring Pro
7272
pip install -r requirements-experimental.txt
7373
```
7474

75+
4. (Optional) If you are using the diff *server* and want high-performance character encoding detection, install [`cchardet`](https://pypi.org/project/cchardet). Note that it only supports Python 3.10 and *earlier* at the time of this writing. An alpha release supports up to Python 3.12.
76+
77+
```sh
78+
pip install cchardet
79+
80+
# Or, for Python 3.11 or 3.12:
81+
pip install cchardet==2.2.0a2
82+
```
83+
7584

7685
## Basic Usage
7786

requirements-server.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#
88
# It only exists to keep the list of dependencies in a separate file from
99
# setup.py.
10-
cchardet >=2.1.6,<3
10+
chardet >=5.0.0,<6
1111
pycurl >=7.43,<8
1212
sentry-sdk >=1.0.0,<2.0
1313
tornado >=6.0.0,<7

web_monitoring_diff/server/server.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import os
1111
import pycurl
1212
import re
13-
import cchardet
1413
import sentry_sdk
1514
import signal
1615
import sys
@@ -25,6 +24,17 @@
2524
from ..exceptions import UndiffableContentError, UndecodableContentError
2625
from ..utils import shutdown_executor_in_loop, Signal
2726

27+
# Where possible, use cchardet (or faust-cchardet) for performance.
28+
# Unfortunately these aren't supported in the latest Python vesions, so we also
29+
# fall back to something in pure Python for those cases. There are a few
30+
# options there, but `chardet` offers the best accuracy/performance tradeoff
31+
# when operating on truncated/small content (we always truncate).
32+
# (Performance measurements as of mid-2024.)
33+
try:
34+
import cchardet as chardet
35+
except ImportError:
36+
import chardet
37+
2838
logger = logging.getLogger(__name__)
2939

3040
# Track errors with Sentry.io. It will automatically detect the `SENTRY_DSN`
@@ -641,11 +651,11 @@ def _extract_encoding(headers, content):
641651
if encoding:
642652
encoding = encoding.strip()
643653
if not encoding and content:
644-
# try to identify encoding using cchardet. Use up to 18kb of the
654+
# try to identify encoding using chardet. Use up to 18kb of the
645655
# content for detection. Its not necessary to use the full content
646656
# as it could be huge. Also, if you use too little, detection is not
647657
# accurate.
648-
detected = cchardet.detect(content[:18432])
658+
detected = chardet.detect(content[:18432])
649659
if detected:
650660
detected_encoding = detected.get('encoding')
651661
if detected_encoding:

web_monitoring_diff/tests/test_server_exc_handling.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -342,10 +342,10 @@ def test_extract_encoding_from_body(self):
342342
# Polish content without any content-type headers or meta tag.
343343
headers = {}
344344
body = """<html><head><title>TITLE</title></head>
345-
<i>czyli co zrobić aby zobaczyć w tekstach polskie litery.</i>
346-
Obowiązku czytania nie ma, ale wiele może wyjaśnić.
347-
<body></body>""".encode('iso-8859-2')
348-
assert df._extract_encoding(headers, body) == 'iso-8859-2'
345+
<i>И така, какво да направите, за да видите българските букви в текстовете?</i>
346+
Четенето не е задължително, но може да обясни много.
347+
<body></body>""".encode('iso-8859-5')
348+
assert df._extract_encoding(headers, body) == 'iso-8859-5'
349349

350350
def test_diff_content_with_null_bytes(self):
351351
response = self.fetch('/html_source_dmp?format=json&'

0 commit comments

Comments
 (0)