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))

Reply via email to