Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-tinycss2 for openSUSE:Factory
checked in at 2026-01-21 14:11:24
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-tinycss2 (Old)
and /work/SRC/openSUSE:Factory/.python-tinycss2.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-tinycss2"
Wed Jan 21 14:11:24 2026 rev:15 rq:1328174 version:1.5.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-tinycss2/python-tinycss2.changes
2024-11-06 16:50:37.132726945 +0100
+++
/work/SRC/openSUSE:Factory/.python-tinycss2.new.1928/python-tinycss2.changes
2026-01-21 14:11:37.552505419 +0100
@@ -1,0 +2,9 @@
+Tue Jan 20 06:42:21 UTC 2026 - Daniel Garcia <[email protected]>
+
+- Update to 1.5.1
+ * Include parsing tests in source tarball
+- 1.5.0:
+ * Support most of CSS Color Level 5
+ * Fix tokenizer crash on escaped Dimension units and Function names
+
+-------------------------------------------------------------------
Old:
----
tinycss2-1.4.0.tar.gz
New:
----
tinycss2-1.5.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-tinycss2.spec ++++++
--- /var/tmp/diff_new_pack.vvx2qh/_old 2026-01-21 14:11:38.164530938 +0100
+++ /var/tmp/diff_new_pack.vvx2qh/_new 2026-01-21 14:11:38.168531105 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-tinycss2
#
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-tinycss2
-Version: 1.4.0
+Version: 1.5.1
Release: 0
Summary: A tiny CSS parser
License: BSD-3-Clause
++++++ tinycss2-1.4.0.tar.gz -> tinycss2-1.5.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/PKG-INFO new/tinycss2-1.5.1/PKG-INFO
--- old/tinycss2-1.4.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
+++ new/tinycss2-1.5.1/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
@@ -1,11 +1,11 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: tinycss2
-Version: 1.4.0
+Version: 1.5.1
Summary: A tiny CSS parser
Keywords: css,parser
Author-email: Simon Sapin <[email protected]>
Maintainer-email: CourtBouillon <[email protected]>
-Requires-Python: >=3.8
+Requires-Python: >=3.10
Description-Content-Type: text/x-rst
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
@@ -14,17 +14,18 @@
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Text Processing
+License-File: LICENSE
Requires-Dist: webencodings >=0.4
Requires-Dist: sphinx ; extra == "doc"
-Requires-Dist: sphinx_rtd_theme ; extra == "doc"
+Requires-Dist: furo ; extra == "doc"
Requires-Dist: pytest ; extra == "test"
Requires-Dist: ruff ; extra == "test"
Project-URL: Changelog, https://github.com/Kozea/tinycss2/releases
@@ -45,7 +46,7 @@
CSS modules.
* Free software: BSD license
-* For Python 3.8+, tested on CPython and PyPy
+* For Python 3.10+, tested on CPython and PyPy
* Documentation: https://doc.courtbouillon.org/tinycss2
* Changelog: https://github.com/Kozea/tinycss2/releases
* Code, issues, tests: https://github.com/Kozea/tinycss2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/README.rst
new/tinycss2-1.5.1/README.rst
--- old/tinycss2-1.4.0/README.rst 2023-12-18 21:14:28.869470600 +0100
+++ new/tinycss2-1.5.1/README.rst 2025-11-23 11:28:53.919480600 +0100
@@ -7,7 +7,7 @@
CSS modules.
* Free software: BSD license
-* For Python 3.8+, tested on CPython and PyPy
+* For Python 3.10+, tested on CPython and PyPy
* Documentation: https://doc.courtbouillon.org/tinycss2
* Changelog: https://github.com/Kozea/tinycss2/releases
* Code, issues, tests: https://github.com/Kozea/tinycss2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/docs/api_reference.rst
new/tinycss2-1.5.1/docs/api_reference.rst
--- old/tinycss2-1.4.0/docs/api_reference.rst 2024-06-18 22:56:02.664717400
+0200
+++ new/tinycss2-1.5.1/docs/api_reference.rst 2025-11-23 11:28:53.919480600
+0100
@@ -38,31 +38,49 @@
.. autofunction:: serialize_identifier
+Color Level 3
+-------------
+
.. module:: tinycss2.color3
+.. autofunction:: parse_color
+.. autoclass:: RGBA
-Color
------
+Color Level 4
+-------------
+.. module:: tinycss2.color4
.. autofunction:: parse_color
-.. autoclass:: RGBA
+.. autoclass:: Color
+ :members:
+.. autodata:: COLOR_SPACES
+.. autodata:: D50
+.. autodata:: D65
+Color Level 5
+-------------
-.. module:: tinycss2.nth
+.. module:: tinycss2.color5
+.. autofunction:: parse_color
+.. autoclass:: Color
+ :show-inheritance:
+.. autodata:: COLOR_SPACES
+.. autodata:: D50
+.. autodata:: D65
<An+B>
------
+.. module:: tinycss2.nth
.. autofunction:: parse_nth
-.. module:: tinycss2.ast
-
-
AST nodes
---------
+.. module:: tinycss2.ast
+
Various parsing functions return a **node** or a list of nodes. Some types of
nodes contain nested nodes which may in turn contain more nodes, forming
together an **abstract syntax tree**.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/docs/changelog.rst
new/tinycss2-1.5.1/docs/changelog.rst
--- old/tinycss2-1.4.0/docs/changelog.rst 2024-10-24 16:57:27.160828400
+0200
+++ new/tinycss2-1.5.1/docs/changelog.rst 2025-11-23 11:28:53.919480600
+0100
@@ -2,10 +2,27 @@
=========
+Version 1.5.1
+-------------
+
+Released on 2025-11-23.
+
+* Include parsing tests in source tarball
+
+
+Version 1.5.0
+-------------
+
+Released on 2025-11-19.
+
+* Support most of CSS Color Level 5
+* Fix tokenizer crash on escaped Dimension units and Function names
+
+
Version 1.4.0
-------------
-Released on 2024-10-24
+Released on 2024-10-24.
* Support CSS Color Level 4
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/docs/conf.py
new/tinycss2-1.5.1/docs/conf.py
--- old/tinycss2-1.4.0/docs/conf.py 2023-12-18 19:45:27.561892700 +0100
+++ new/tinycss2-1.5.1/docs/conf.py 2025-11-23 11:28:53.920480500 +0100
@@ -46,10 +46,12 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'sphinx_rtd_theme'
+html_theme = 'furo'
html_theme_options = {
- 'collapse_navigation': False,
+ 'top_of_page_buttons': ['edit'],
+ 'source_edit_link':
+ 'https://github.com/CourtBouillon/pydyf/edit/main/docs/{filename}',
}
# Favicon URL
@@ -63,7 +65,7 @@
# These paths are either relative to html_static_path
# or fully qualified paths (eg. https://...)
html_css_files = [
- 'https://www.courtbouillon.org/static/docs.css',
+ 'https://www.courtbouillon.org/static/docs-furo.css',
]
# Output file base name for HTML help builder.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/docs/index.rst
new/tinycss2-1.5.1/docs/index.rst
--- old/tinycss2-1.4.0/docs/index.rst 2021-09-05 11:30:30.528626400 +0200
+++ new/tinycss2-1.5.1/docs/index.rst 2025-11-23 11:28:53.920480500 +0100
@@ -7,7 +7,7 @@
.. toctree::
:caption: Documentation
- :maxdepth: 3
+ :maxdepth: 2
first_steps
common_use_cases
@@ -16,7 +16,7 @@
.. toctree::
:caption: Extra Information
- :maxdepth: 3
+ :maxdepth: 2
changelog
contribute
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/pyproject.toml
new/tinycss2-1.5.1/pyproject.toml
--- old/tinycss2-1.4.0/pyproject.toml 2024-10-24 16:57:27.160828400 +0200
+++ new/tinycss2-1.5.1/pyproject.toml 2025-11-23 11:28:53.920480500 +0100
@@ -8,7 +8,7 @@
keywords = ['css', 'parser']
authors = [{name = 'Simon Sapin', email = '[email protected]'}]
maintainers = [{name = 'CourtBouillon', email = '[email protected]'}]
-requires-python = '>=3.8'
+requires-python = '>=3.10'
readme = {file = 'README.rst', content-type = 'text/x-rst'}
license = {file = 'LICENSE'}
dependencies = ['webencodings >=0.4']
@@ -20,11 +20,11 @@
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
- 'Programming Language :: Python :: 3.8',
- '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 :: Text Processing',
@@ -40,7 +40,7 @@
Donation = 'https://opencollective.com/courtbouillon'
[project.optional-dependencies]
-doc = ['sphinx', 'sphinx_rtd_theme']
+doc = ['sphinx', 'furo']
test = ['pytest', 'ruff']
[tool.flit.sdist]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/tinycss2-1.4.0/tests/css-parsing-tests/color_functions_5.json
new/tinycss2-1.5.1/tests/css-parsing-tests/color_functions_5.json
--- old/tinycss2-1.4.0/tests/css-parsing-tests/color_functions_5.json
1970-01-01 01:00:00.000000000 +0100
+++ new/tinycss2-1.5.1/tests/css-parsing-tests/color_functions_5.json
2025-11-23 11:28:54.545486700 +0100
@@ -0,0 +1,22 @@
+[
+ "device-cmyk(0 81% 81% 30%)", "color(device-cmyk 0 0.81 0.81 0.3)",
+ "device-cmyk(0, 81%, 81%, 30%)", null,
+ "device-cmyk(0, 0.81, 0.81, 0.3)", "color(device-cmyk 0 0.81 0.81 0.3)",
+ "device-cmyk(0 81% 81% 30% / 50%)", "color(device-cmyk 0 0.81 0.81 0.3 /
0.5)",
+ "device-cmyk(0 81% 81% 30% / 200%)", "color(device-cmyk 0 0.81 0.81 0.3)",
+ "device-cmyk(0 81% 81% 30% / -200%)", "color(device-cmyk 0 0.81 0.81 0.3 /
0)",
+ "device-cmyk(0 81% 81%)", null,
+ "device-cmyk(0 81% 81% 130%)", "color(device-cmyk 0 0.81 0.81 1)",
+ "device-cmyk(0 81% 81% -30%)", "color(device-cmyk 0 0.81 0.81 0)",
+ "color(unvalid 0 0 0 0)", null,
+ "color(--valid 0 0 0 0)", "color(--valid 0 0 0 0)",
+ "color(--valid 10% 10% 10% 0)", "color(--valid 0.1 0.1 0.1 0)",
+ "color(--valid 0 0 0 0 / 50%)", "color(--valid 0 0 0 0 / 0.5)",
+ "color(--valid 0 0 0 0 / 0.5)", "color(--valid 0 0 0 0 / 0.5)",
+ "color(--valid 0 0 0 0 / 150%)", "color(--valid 0 0 0 0)",
+ "color(--valid 0 0 0 0 / -150%)", "color(--valid 0 0 0 0 / 0)",
+ "light-dark(white, black)", ["color(srgb 1 1 1)", "color(srgb 0 0 0)"],
+ "light-dark(device-cmyk(0 81% 81% 30%), color(--valid 0 0 0 0))",
["color(device-cmyk 0 0.81 0.81 0.3)", "color(--valid 0 0 0 0)"],
+ "light-dark(device-cmyk(0 81% 81%), color(unvalid 0 0 0 0))", [null, null],
+ "light-dark(color(display-p3 0% 0% 0%), color(srgb 0% 0% 0% / 50%))",
["color(display-p3 0 0 0)", "color(srgb 0 0 0 / 0.5)"]
+]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/tests/test_tinycss2.py
new/tinycss2-1.5.1/tests/test_tinycss2.py
--- old/tinycss2-1.4.0/tests/test_tinycss2.py 2024-10-24 16:57:27.167828600
+0200
+++ new/tinycss2-1.5.1/tests/test_tinycss2.py 2025-11-23 11:28:53.920480500
+0100
@@ -20,6 +20,7 @@
from tinycss2.color3 import parse_color as parse_color3 # isort:skip
from tinycss2.color4 import Color # isort:skip
from tinycss2.color4 import parse_color as parse_color4 # isort:skip
+from tinycss2.color5 import parse_color as parse_color5 # isort:skip
from tinycss2.nth import parse_nth # isort:skip
@@ -159,6 +160,19 @@
return str(int(value) if value.is_integer() else value)
+def _build_color(color):
+ if color is None:
+ return
+ (*coordinates, alpha) = color
+ result = f'color({color.space}'
+ for coordinate in coordinates:
+ result += f' {_number(coordinate)}'
+ if alpha != 1:
+ result += f' / {_number(alpha)}'
+ result += ')'
+ return result
+
+
def test_color_currentcolor_3():
for value in ('currentcolor', 'currentColor', 'CURRENTCOLOR'):
assert parse_color3(value) == 'currentColor'
@@ -169,23 +183,48 @@
assert parse_color4(value) == 'currentcolor'
+def test_color_currentcolor_5():
+ for value in ('currentcolor', 'currentColor', 'CURRENTCOLOR'):
+ assert parse_color5(value) == 'currentcolor'
+
+
@json_test()
def test_color_function_4(input):
- if not (color := parse_color4(input)):
+ return _build_color(parse_color4(input))
+
+
+@json_test(filename='color_function_4.json')
+def test_color_function_4_with_5(input):
+ return _build_color(parse_color5(input))
+
+
+@json_test()
+def test_color_functions_5(input):
+ if input.startswith('light-dark'):
+ result = []
+ result.append(_build_color(parse_color5(input, ('light',))))
+ result.append(_build_color(parse_color5(input, ('dark',))))
+ else:
+ result = _build_color(parse_color5(input))
+ return result
+
+
+@json_test()
+def test_color_hexadecimal_3(input):
+ if not (color := parse_color3(input)):
return None
(*coordinates, alpha) = color
- result = f'color({color.space}'
- for coordinate in coordinates:
- result += f' {_number(coordinate)}'
+ result = f'rgb{"a" if alpha != 1 else ""}('
+ result += f'{", ".join(_number(coordinate * 255) for coordinate in
coordinates)}'
if alpha != 1:
- result += f' / {_number(alpha)}'
+ result += f', {_number(alpha)}'
result += ')'
return result
-@json_test()
-def test_color_hexadecimal_3(input):
- if not (color := parse_color3(input)):
+@json_test(filename='color_hexadecimal_3.json')
+def test_color_hexadecimal_3_with_4(input):
+ if not (color := parse_color4(input)):
return None
(*coordinates, alpha) = color
result = f'rgb{"a" if alpha != 1 else ""}('
@@ -210,9 +249,23 @@
return result
+@json_test(filename='color_hexadecimal_4.json')
+def test_color_hexadecimal_4_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ assert color.space == 'srgb'
+ (*coordinates, alpha) = color
+ result = f'rgb{"a" if alpha != 1 else ""}('
+ result += f'{", ".join(_number(coordinate * 255) for coordinate in
coordinates)}'
+ if alpha != 1:
+ result += f', {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test(filename='color_hexadecimal_3.json')
-def test_color_hexadecimal_3_with_4(input):
- if not (color := parse_color4(input)):
+def test_color_hexadecimal_3_with_5(input):
+ if not (color := parse_color5(input)):
return None
assert color.space == 'srgb'
(*coordinates, alpha) = color
@@ -251,6 +304,20 @@
return result
+@json_test(filename='color_hsl_3.json')
+def test_color_hsl_3_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ assert color.space == 'hsl'
+ (*coordinates, alpha) = color.to('srgb')
+ result = f'rgb{"a" if alpha != 1 else ""}('
+ result += f'{", ".join(_number(coordinate * 255) for coordinate in
coordinates)}'
+ if alpha != 1:
+ result += f', {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_color_hsl_4(input):
if not (color := parse_color4(input)):
@@ -265,6 +332,20 @@
return result
+@json_test(filename='color_hsl_4.json')
+def test_color_hsl_4_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ assert color.space == 'hsl'
+ (*coordinates, alpha) = color.to('srgb')
+ result = f'rgb{"a" if alpha != 1 else ""}('
+ result += f'{", ".join(_number(coordinate * 255) for coordinate in
coordinates)}'
+ if alpha != 1:
+ result += f', {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_color_hwb_4(input):
if not (color := parse_color4(input)):
@@ -279,6 +360,20 @@
return result
+@json_test(filename='color_hwb_4.json')
+def test_color_hwb_4_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ assert color.space == 'hwb'
+ (*coordinates, alpha) = color.to('srgb')
+ result = f'rgb{"a" if alpha != 1 else ""}('
+ result += f'{", ".join(_number(coordinate * 255) for coordinate in
coordinates)}'
+ if alpha != 1:
+ result += f', {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_color_keywords_3(input):
if not (color := parse_color3(input)):
@@ -310,6 +405,22 @@
return result
+@json_test(filename='color_keywords_3.json')
+def test_color_keywords_3_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ elif isinstance(color, str):
+ return color
+ assert color.space == 'srgb'
+ (*coordinates, alpha) = color
+ result = f'rgb{"a" if alpha != 1 else ""}('
+ result += f'{", ".join(_number(coordinate * 255) for coordinate in
coordinates)}'
+ if alpha != 1:
+ result += f', {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_color_keywords_4(input):
if not (color := parse_color4(input)):
@@ -326,6 +437,22 @@
return result
+@json_test(filename='color_keywords_4.json')
+def test_color_keywords_4_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ elif isinstance(color, str):
+ return color
+ assert color.space == 'srgb'
+ (*coordinates, alpha) = color
+ result = f'rgb{"a" if alpha != 1 else ""}('
+ result += f'{", ".join(_number(coordinate * 255) for coordinate in
coordinates)}'
+ if alpha != 1:
+ result += f', {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_color_lab_4(input):
if not (color := parse_color4(input)):
@@ -342,6 +469,22 @@
return result
+@json_test(filename='color_lab_4.json')
+def test_color_lab_4_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ elif isinstance(color, str):
+ return color
+ assert color.space == 'lab'
+ (*coordinates, alpha) = color
+ result = f'{color.space}('
+ result += f'{" ".join(_number(coordinate) for coordinate in coordinates)}'
+ if alpha != 1:
+ result += f' / {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_color_oklab_4(input):
if not (color := parse_color4(input)):
@@ -358,6 +501,22 @@
return result
+@json_test(filename='color_oklab_4.json')
+def test_color_oklab_4_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ elif isinstance(color, str):
+ return color
+ assert color.space == 'oklab'
+ (*coordinates, alpha) = color
+ result = f'{color.space}('
+ result += f'{" ".join(_number(coordinate) for coordinate in coordinates)}'
+ if alpha != 1:
+ result += f' / {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_color_lch_4(input):
if not (color := parse_color4(input)):
@@ -374,6 +533,22 @@
return result
+@json_test(filename='color_lch_4.json')
+def test_color_lch_4_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ elif isinstance(color, str):
+ return color
+ assert color.space == 'lch'
+ (*coordinates, alpha) = color
+ result = f'{color.space}('
+ result += f'{" ".join(_number(coordinate) for coordinate in coordinates)}'
+ if alpha != 1:
+ result += f' / {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_color_oklch_4(input):
if not (color := parse_color4(input)):
@@ -390,6 +565,22 @@
return result
+@json_test(filename='color_oklch_4.json')
+def test_color_oklch_4_with_5(input):
+ if not (color := parse_color5(input)):
+ return None
+ elif isinstance(color, str):
+ return color
+ assert color.space == 'oklch'
+ (*coordinates, alpha) = color
+ result = f'{color.space}('
+ result += f'{" ".join(_number(coordinate) for coordinate in coordinates)}'
+ if alpha != 1:
+ result += f' / {_number(alpha)}'
+ result += ')'
+ return result
+
+
@json_test()
def test_stylesheet_bytes(kwargs):
kwargs['css_bytes'] = kwargs['css_bytes'].encode('latin1')
@@ -474,6 +665,27 @@
assert serialize(tokens) == source
-def test_bad_unicode():
- parse_one_declaration('background:\udca9')
- parse_rule_list('@\udca9')
+def test_escape_in_at_rule():
+ at_rule, = parse_rule_list('@\udca9')
+ assert at_rule.type == 'at-rule'
+ assert at_rule.at_keyword == '\udca9'
+
+
+def test_escape_in_ident():
+ declaration = parse_one_declaration('background:\udca9')
+ assert declaration.type == 'declaration'
+ value, = declaration.value
+ assert value.value == '\udca9'
+
+
+def test_escape_in_dimension_token():
+ dimension, = parse_component_value_list('0\\dddf')
+ assert dimension.type == 'dimension'
+ assert dimension.int_value == 0
+ assert dimension.unit == '\udddf'
+
+
+def test_escape_in_function_name():
+ function, = parse_component_value_list('\\dddf()')
+ assert function.type == 'function'
+ assert function.name == '\udddf'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/tinycss2/__init__.py
new/tinycss2-1.5.1/tinycss2/__init__.py
--- old/tinycss2-1.4.0/tinycss2/__init__.py 2024-10-24 16:57:27.168828500
+0200
+++ new/tinycss2-1.5.1/tinycss2/__init__.py 2025-11-23 11:28:53.920480500
+0100
@@ -15,4 +15,4 @@
from .serializer import serialize, serialize_identifier # noqa
from .tokenizer import parse_component_value_list # noqa
-VERSION = __version__ = '1.4.0'
+VERSION = __version__ = '1.5.1'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/tinycss2/ast.py
new/tinycss2-1.5.1/tinycss2/ast.py
--- old/tinycss2-1.4.0/tinycss2/ast.py 2024-06-18 23:24:18.884527000 +0200
+++ new/tinycss2-1.5.1/tinycss2/ast.py 2025-11-23 11:28:53.921480700 +0100
@@ -48,7 +48,7 @@
.. automethod:: serialize
"""
- __slots__ = ['source_line', 'source_column']
+ __slots__ = ['source_column', 'source_line']
def __init__(self, source_line, source_column):
self.source_line = source_line
@@ -234,7 +234,7 @@
This is the value to use when comparing to a CSS keyword.
"""
- __slots__ = ['value', 'lower_value']
+ __slots__ = ['lower_value', 'value']
type = 'ident'
repr_format = '<{self.__class__.__name__} {self.value}>'
@@ -274,7 +274,7 @@
if node.type == 'at-keyword' and node.lower_value == 'import':
"""
- __slots__ = ['value', 'lower_value']
+ __slots__ = ['lower_value', 'value']
type = 'at-keyword'
repr_format = '<{self.__class__.__name__} @{self.value}>'
@@ -311,7 +311,7 @@
(Only such hash tokens are valid ID selectors.)
"""
- __slots__ = ['value', 'is_identifier']
+ __slots__ = ['is_identifier', 'value']
type = 'hash'
repr_format = '<{self.__class__.__name__} #{self.value}>'
@@ -342,7 +342,7 @@
The unescaped value, as a Unicode string, without the quotes.
"""
- __slots__ = ['value', 'representation']
+ __slots__ = ['representation', 'value']
type = 'string'
repr_format = '<{self.__class__.__name__} {self.representation}>'
@@ -370,7 +370,7 @@
markers.
"""
- __slots__ = ['value', 'representation']
+ __slots__ = ['representation', 'value']
type = 'url'
repr_format = '<{self.__class__.__name__} {self.representation}>'
@@ -398,7 +398,7 @@
Same as :attr:`start` if the source only specified one value.
"""
- __slots__ = ['start', 'end']
+ __slots__ = ['end', 'start']
type = 'unicode-range'
repr_format = '<{self.__class__.__name__} {self.start} {self.end}>'
@@ -437,7 +437,7 @@
The CSS representation of the value, as a Unicode string.
"""
- __slots__ = ['value', 'int_value', 'is_integer', 'representation']
+ __slots__ = ['int_value', 'is_integer', 'representation', 'value']
type = 'number'
repr_format = '<{self.__class__.__name__} {self.representation}>'
@@ -481,7 +481,7 @@
as a Unicode string.
"""
- __slots__ = ['value', 'int_value', 'is_integer', 'representation']
+ __slots__ = ['int_value', 'is_integer', 'representation', 'value']
type = 'percentage'
repr_format = '<{self.__class__.__name__} {self.representation}%>'
@@ -540,8 +540,14 @@
if node.type == 'dimension' and node.lower_unit == 'px':
"""
- __slots__ = ['value', 'int_value', 'is_integer', 'representation',
- 'unit', 'lower_unit']
+ __slots__ = [
+ 'int_value',
+ 'is_integer',
+ 'lower_unit',
+ 'representation',
+ 'unit',
+ 'value',
+ ]
type = 'dimension'
repr_format = ('<{self.__class__.__name__} '
'{self.representation}{self.unit}>')
@@ -553,7 +559,10 @@
self.is_integer = int_value is not None
self.representation = representation
self.unit = unit
- self.lower_unit = ascii_lower(unit)
+ try:
+ self.lower_unit = ascii_lower(unit)
+ except UnicodeEncodeError:
+ self.lower_unit = unit
def _serialize_to(self, write):
write(self.representation)
@@ -680,14 +689,17 @@
in the list.
"""
- __slots__ = ['name', 'lower_name', 'arguments']
+ __slots__ = ['arguments', 'lower_name', 'name']
type = 'function'
repr_format = '<{self.__class__.__name__} {self.name}( … )>'
def __init__(self, line, column, name, arguments):
Node.__init__(self, line, column)
self.name = name
- self.lower_name = ascii_lower(name)
+ try:
+ self.lower_name = ascii_lower(name)
+ except UnicodeEncodeError:
+ self.lower_name = name
self.arguments = arguments
def _serialize_to(self, write):
@@ -743,7 +755,7 @@
this flag, such as non-property descriptor declarations.
"""
- __slots__ = ['name', 'lower_name', 'value', 'important']
+ __slots__ = ['important', 'lower_name', 'name', 'value']
type = 'declaration'
repr_format = '<{self.__class__.__name__} {self.name}: …>'
@@ -788,7 +800,7 @@
as a list of :term:`component values`.
"""
- __slots__ = ['prelude', 'content']
+ __slots__ = ['content', 'prelude']
type = 'qualified-rule'
repr_format = ('<{self.__class__.__name__} '
'… {{ … }}>')
@@ -849,7 +861,7 @@
or :obj:`None` for at-rules ending with a semicolon.
"""
- __slots__ = ['at_keyword', 'lower_at_keyword', 'prelude', 'content']
+ __slots__ = ['at_keyword', 'content', 'lower_at_keyword', 'prelude']
type = 'at-rule'
repr_format = ('<{self.__class__.__name__} '
'@{self.at_keyword} … {{ … }}>')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/tinycss2/color3.py
new/tinycss2-1.5.1/tinycss2/color3.py
--- old/tinycss2-1.4.0/tinycss2/color3.py 2024-10-24 16:57:27.168828500
+0200
+++ new/tinycss2-1.5.1/tinycss2/color3.py 2025-11-23 11:28:53.921480700
+0100
@@ -142,10 +142,10 @@
_HASH_REGEXPS = (
- (2, re.compile('^{}$'.format(4 * '([\\da-f])'), re.I).match),
- (1, re.compile('^{}$'.format(4 * '([\\da-f]{2})'), re.I).match),
- (2, re.compile('^{}$'.format(3 * '([\\da-f])'), re.I).match),
- (1, re.compile('^{}$'.format(3 * '([\\da-f]{2})'), re.I).match),
+ (2, re.compile(f'^{4 * "([0-9a-f])"}$', re.I).match),
+ (1, re.compile(f'^{4 * "([0-9a-f]{2})"}$', re.I).match),
+ (2, re.compile(f'^{3 * "([0-9a-f])"}$', re.I).match),
+ (1, re.compile(f'^{3 * "([0-9a-f]{2})"}$', re.I).match),
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/tinycss2/color4.py
new/tinycss2-1.5.1/tinycss2/color4.py
--- old/tinycss2-1.4.0/tinycss2/color4.py 2024-10-24 16:57:27.169828400
+0200
+++ new/tinycss2-1.5.1/tinycss2/color4.py 2025-11-23 11:28:53.921480700
+0100
@@ -4,13 +4,16 @@
from .color3 import _BASIC_COLOR_KEYWORDS, _EXTENDED_COLOR_KEYWORDS,
_HASH_REGEXPS
from .parser import parse_one_component_value
+#: XYZ values of the D50 white point, normalized to Y=1.
D50 = (0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585)
+#: XYZ values of the D65 white point, normalized to Y=1.
D65 = (0.3127 / 0.3290, 1, (1 - 0.3127 - 0.3290) / 0.3290)
_FUNCTION_SPACES = {
'srgb', 'srgb-linear',
'display-p3', 'a98-rgb', 'prophoto-rgb', 'rec2020',
'xyz', 'xyz-d50', 'xyz-d65'
}
+#: Supported color spaces.
COLOR_SPACES = _FUNCTION_SPACES | {'hsl', 'hwb', 'lab', 'lch', 'oklab',
'oklch'}
@@ -23,8 +26,11 @@
to [0, 1]. Coordinates can also be set to ``None`` when undefined.
"""
+ COLOR_SPACES = COLOR_SPACES
+
def __init__(self, space, coordinates, alpha):
- assert space in COLOR_SPACES, f"{space} is not a supported color space"
+ if self.COLOR_SPACES:
+ assert space in self.COLOR_SPACES, f"{space} is not a supported
color space"
self.space = space
self.coordinates = tuple(
None if coordinate is None else float(coordinate)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/tinycss2/color5.py
new/tinycss2-1.5.1/tinycss2/color5.py
--- old/tinycss2-1.4.0/tinycss2/color5.py 1970-01-01 01:00:00.000000000
+0100
+++ new/tinycss2-1.5.1/tinycss2/color5.py 2025-11-23 11:28:53.921480700
+0100
@@ -0,0 +1,135 @@
+from . import color4
+
+#: Supported color spaces.
+COLOR_SPACES = color4.COLOR_SPACES | {'device-cmyk'}
+#: Supported color schemes.
+COLOR_SCHEMES = {'light', 'dark'}
+#: XYZ values of the D50 white point, normalized to Y=1.
+D50 = color4.D50
+#: XYZ values of the D65 white point, normalized to Y=1.
+D65 = color4.D65
+
+
+class Color(color4.Color):
+ COLOR_SPACES = None
+
+
+def parse_color(input, color_schemes=None):
+ """Parse a color value as defined in CSS Color Level 5.
+
+ https://www.w3.org/TR/css-color-5/
+
+ :type input: :obj:`str` or :term:`iterable`
+ :param input: A string or an iterable of :term:`component values`.
+ :type color_schemes: :obj:`str` or :term:`iterable`
+ :param color_schemes: the ``'normal'`` string, or an iterable of color
+ schemes used to resolve the ``light-dark()`` function.
+ :returns:
+ * :obj:`None` if the input is not a valid color value.
+ (No exception is raised.)
+ * The string ``'currentcolor'`` for the ``currentcolor`` keyword
+ * A :class:`Color` object for every other values, including keywords.
+
+ """
+ color = color4.parse_color(input)
+
+ if color:
+ return color
+
+ if color_schemes is None or color_schemes == 'normal':
+ color_scheme = 'light'
+ else:
+ for color_scheme in color_schemes:
+ if color_scheme in COLOR_SCHEMES:
+ break
+ else:
+ color_scheme = 'light'
+
+ if isinstance(input, str):
+ token = color4.parse_one_component_value(input, skip_comments=True)
+ else:
+ token = input
+
+ if token.type == 'function':
+ tokens = [
+ token for token in token.arguments
+ if token.type not in ('whitespace', 'comment')]
+ name = token.lower_name
+ alpha = []
+
+ if name == 'color':
+ space, *tokens = tokens
+
+ old_syntax = all(token == ',' for token in tokens[1::2])
+ if old_syntax:
+ tokens = tokens[::2]
+ else:
+ for index, token in enumerate(tokens):
+ if token == '/':
+ alpha = tokens[index + 1:]
+ tokens = tokens[:index]
+ break
+
+ if name == 'device-cmyk':
+ return _parse_device_cmyk(tokens, color4._parse_alpha(alpha),
old_syntax)
+ elif name == 'color':
+ return _parse_color(space, tokens, color4._parse_alpha(alpha))
+ elif name == 'light-dark':
+ return _parse_light_dark(tokens, color_scheme)
+ else:
+ return
+
+
+def _parse_device_cmyk(args, alpha, old_syntax):
+ """Parse a list of CMYK channels.
+
+ If args is a list of 4 NUMBER or PERCENTAGE tokens, return
+ device-cmyk :class:`Color`. Otherwise, return None.
+
+ Input C, M, Y, K ranges are [0, 1], output are [0, 1].
+
+ """
+ if old_syntax:
+ if color4._types(args) != {'number'}:
+ return
+ else:
+ if not color4._types(args) <= {'number', 'percentage'}:
+ return
+ if len(args) != 4:
+ return
+ cmyk = [
+ arg.value if arg.type == 'number' else
+ arg.value / 100 if arg.type == 'percentage' else None
+ for arg in args]
+ cmyk = [max(0., min(1., float(channel))) for channel in cmyk]
+ return Color('device-cmyk', cmyk, alpha)
+
+
+def _parse_light_dark(args, color_scheme):
+ colors = []
+ for arg in args:
+ if color := parse_color(arg, color_scheme):
+ colors.append(color)
+ if len(colors) == 2:
+ if color_scheme == 'light':
+ return colors[0]
+ else:
+ return colors[1]
+ return
+
+
+def _parse_color(space, args, alpha):
+ """Parse a color space name list of coordinates.
+
+ Ranges are [0, 1].
+
+ """
+ if not color4._types(args) <= {'number', 'percentage'}:
+ return
+ if space.type != 'ident' or not space.value.startswith('--'):
+ return
+ coordinates = [
+ arg.value if arg.type == 'number' else
+ arg.value / 100 if arg.type == 'percentage' else None
+ for arg in args]
+ return Color(space.value, coordinates, alpha)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/tinycss2/serializer.py
new/tinycss2-1.5.1/tinycss2/serializer.py
--- old/tinycss2-1.4.0/tinycss2/serializer.py 2024-06-18 23:16:32.129732600
+0200
+++ new/tinycss2-1.5.1/tinycss2/serializer.py 2025-11-23 11:28:53.921480700
+0100
@@ -1,3 +1,6 @@
+import re
+
+
def serialize(nodes):
"""Serialize nodes to CSS syntax.
@@ -66,16 +69,19 @@
)
+_replacement_string_value = {
+ '"': r'\"',
+ '\\': r'\\',
+ '\n': r'\A ',
+ '\r': r'\D ',
+ '\f': r'\C ',
+}
+_re_string_value = ''.join(re.escape(char) for char in
_replacement_string_value)
+_re_string_value = re.compile(f'[{_re_string_value}]', re.MULTILINE)
+def _serialize_string_value_match(match):
+ return _replacement_string_value[match.group(0)]
def serialize_string_value(value):
- return ''.join(
- r'\"' if c == '"' else
- r'\\' if c == '\\' else
- r'\A ' if c == '\n' else
- r'\D ' if c == '\r' else
- r'\C ' if c == '\f' else
- c
- for c in value
- )
+ return _re_string_value.sub(_serialize_string_value_match, value)
def serialize_url(value):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tinycss2-1.4.0/tinycss2/tokenizer.py
new/tinycss2-1.5.1/tinycss2/tokenizer.py
--- old/tinycss2-1.4.0/tinycss2/tokenizer.py 2024-06-18 22:56:02.665717600
+0200
+++ new/tinycss2-1.5.1/tinycss2/tokenizer.py 2025-11-23 11:28:53.921480700
+0100
@@ -70,14 +70,18 @@
tokens.append(IdentToken(line, column, value))
continue
pos += 1 # Skip the '('
- if ascii_lower(value) == 'url':
+ try:
+ is_url = ascii_lower(value) == 'url'
+ except UnicodeEncodeError:
+ is_url = False
+ if is_url:
url_pos = pos
while css.startswith((' ', '\n', '\t'), url_pos):
url_pos += 1
if url_pos >= length or css[url_pos] not in ('"', "'"):
value, pos, error = _consume_url(css, pos)
if value is not None:
- repr = 'url({})'.format(serialize_url(value))
+ repr = f'url({serialize_url(value)})'
if error is not None:
error_key = error[0]
if error_key == 'eof-in-string':
@@ -163,7 +167,7 @@
elif c in ('"', "'"):
value, pos, error = _consume_quoted_string(css, pos)
if value is not None:
- repr = '"{}"'.format(serialize_string_value(value))
+ repr = f'"{serialize_string_value(value)}"'
if error is not None:
repr = repr[:-1]
tokens.append(StringToken(line, column, value, repr))