diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 294479e..a72b889 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,4 @@ jobs: - name: Launch tests run: python -m pytest - name: Check coding style - run: python -m flake8 --exclude tests/css-parsing-tests - - name: Check imports order - run: python -m isort . --check --diff + run: python -m ruff check diff --git a/pyproject.toml b/pyproject.toml index a5d3ee6..d17557e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ Donation = 'https://opencollective.com/courtbouillon' [project.optional-dependencies] doc = ['sphinx', 'sphinx_rtd_theme'] -test = ['pytest', 'isort', 'flake8'] +test = ['pytest', 'ruff'] [tool.flit.sdist] exclude = ['.*'] @@ -56,7 +56,11 @@ include = ['tests/*', 'tinycss2/*'] exclude_lines = ['pragma: no cover', 'def __repr__', 'raise NotImplementedError'] omit = ['.*'] -[tool.isort] -default_section = 'FIRSTPARTY' -multi_line_output = 4 -extend_skip = ['tests/css-parsing-tests'] +[tool.ruff] +extend-exclude = ['tests/css-parsing-tests'] + +[tool.ruff.lint] +select = ['E', 'F', 'I'] + +[tool.ruff.lint.isort] +known-first-party = ['tinycss2'] diff --git a/tests/test_tinycss2.py b/tests/test_tinycss2.py index d67e8b9..f571338 100644 --- a/tests/test_tinycss2.py +++ b/tests/test_tinycss2.py @@ -4,19 +4,22 @@ from pathlib import Path import pytest +from webencodings import Encoding, lookup + from tinycss2 import ( - parse_component_value_list, parse_declaration_list, - parse_one_component_value, parse_one_declaration, parse_one_rule, - parse_rule_list, parse_stylesheet, parse_stylesheet_bytes, serialize) -from tinycss2.ast import ( - AtKeywordToken, AtRule, Comment, CurlyBracketsBlock, Declaration, - DimensionToken, FunctionBlock, HashToken, IdentToken, LiteralToken, - NumberToken, ParenthesesBlock, ParseError, PercentageToken, QualifiedRule, - SquareBracketsBlock, StringToken, UnicodeRangeToken, URLToken, - WhitespaceToken) + ast, + parse_component_value_list, + parse_declaration_list, + parse_one_component_value, + parse_one_declaration, + parse_one_rule, + parse_rule_list, + parse_stylesheet, + parse_stylesheet_bytes, + serialize, +) from tinycss2.color3 import RGBA, parse_color from tinycss2.nth import parse_nth -from webencodings import Encoding, lookup def generic(func): @@ -42,33 +45,33 @@ def numeric(t): list: lambda li: [to_json(el) for el in li], tuple: lambda li: [to_json(el) for el in li], Encoding: lambda e: e.name, - ParseError: lambda e: ['error', e.kind], - - Comment: lambda t: '/* … */', - WhitespaceToken: lambda t: ' ', - LiteralToken: lambda t: t.value, - IdentToken: lambda t: ['ident', t.value], - AtKeywordToken: lambda t: ['at-keyword', t.value], - HashToken: lambda t: ['hash', t.value, - 'id' if t.is_identifier else 'unrestricted'], - StringToken: lambda t: ['string', t.value], - URLToken: lambda t: ['url', t.value], - NumberToken: lambda t: ['number'] + numeric(t), - PercentageToken: lambda t: ['percentage'] + numeric(t), - DimensionToken: lambda t: ['dimension'] + numeric(t) + [t.unit], - UnicodeRangeToken: lambda t: ['unicode-range', t.start, t.end], - - CurlyBracketsBlock: lambda t: ['{}'] + to_json(t.content), - SquareBracketsBlock: lambda t: ['[]'] + to_json(t.content), - ParenthesesBlock: lambda t: ['()'] + to_json(t.content), - FunctionBlock: lambda t: ['function', t.name] + to_json(t.arguments), - - Declaration: lambda d: ['declaration', d.name, - to_json(d.value), d.important], - AtRule: lambda r: ['at-rule', r.at_keyword, to_json(r.prelude), - to_json(r.content)], - QualifiedRule: lambda r: ['qualified rule', to_json(r.prelude), - to_json(r.content)], + ast.ParseError: lambda e: ['error', e.kind], + + ast.Comment: lambda t: '/* … */', + ast.WhitespaceToken: lambda t: ' ', + ast.LiteralToken: lambda t: t.value, + ast.IdentToken: lambda t: ['ident', t.value], + ast.AtKeywordToken: lambda t: ['at-keyword', t.value], + ast.HashToken: lambda t: ['hash', t.value, + 'id' if t.is_identifier else 'unrestricted'], + ast.StringToken: lambda t: ['string', t.value], + ast.URLToken: lambda t: ['url', t.value], + ast.NumberToken: lambda t: ['number'] + numeric(t), + ast.PercentageToken: lambda t: ['percentage'] + numeric(t), + ast.DimensionToken: lambda t: ['dimension'] + numeric(t) + [t.unit], + ast.UnicodeRangeToken: lambda t: ['unicode-range', t.start, t.end], + + ast.CurlyBracketsBlock: lambda t: ['{}'] + to_json(t.content), + ast.SquareBracketsBlock: lambda t: ['[]'] + to_json(t.content), + ast.ParenthesesBlock: lambda t: ['()'] + to_json(t.content), + ast.FunctionBlock: lambda t: ['function', t.name] + to_json(t.arguments), + + ast.Declaration: lambda d: ['declaration', d.name, + to_json(d.value), d.important], + ast.AtRule: lambda r: ['at-rule', r.at_keyword, to_json(r.prelude), + to_json(r.content)], + ast.QualifiedRule: lambda r: ['qualified rule', to_json(r.prelude), + to_json(r.content)], RGBA: lambda v: [round(c, 10) for c in v], } diff --git a/tinycss2/nth.py b/tinycss2/nth.py index 0c2ca77..c5e924b 100644 --- a/tinycss2/nth.py +++ b/tinycss2/nth.py @@ -88,7 +88,7 @@ def parse_b(tokens, a): def parse_signless_b(tokens, a, b_sign): token = _next_significant(tokens) if (token.type == 'number' and token.is_integer and - not token.representation[0] in '-+'): + token.representation[0] not in '-+'): return parse_end(tokens, a, b_sign * token.int_value) diff --git a/tinycss2/tokenizer.py b/tinycss2/tokenizer.py index 30ccd14..03fe8d9 100644 --- a/tinycss2/tokenizer.py +++ b/tinycss2/tokenizer.py @@ -3,11 +3,7 @@ from webencodings import ascii_lower -from .ast import ( - AtKeywordToken, Comment, CurlyBracketsBlock, DimensionToken, FunctionBlock, - HashToken, IdentToken, LiteralToken, NumberToken, ParenthesesBlock, - ParseError, PercentageToken, SquareBracketsBlock, StringToken, - UnicodeRangeToken, URLToken, WhitespaceToken) +from . import ast from .serializer import serialize_string_value, serialize_url _NUMBER_RE = re.compile(r'[-+]?([0-9]*\.)?[0-9]+([eE][+-]?[0-9]+)?') @@ -53,21 +49,21 @@ def parse_component_value_list(css, skip_comments=False): while css.startswith((' ', '\n', '\t'), pos): pos += 1 value = css[token_start_pos:pos] - tokens.append(WhitespaceToken(line, column, value)) + tokens.append(ast.WhitespaceToken(line, column, value)) continue elif (c in 'Uu' and pos + 2 < length and css[pos + 1] == '+' and css[pos + 2] in '0123456789abcdefABCDEF?'): start, end, pos = _consume_unicode_range(css, pos + 2) - tokens.append(UnicodeRangeToken(line, column, start, end)) + tokens.append(ast.UnicodeRangeToken(line, column, start, end)) continue elif css.startswith('-->', pos): # Check before identifiers - tokens.append(LiteralToken(line, column, '-->')) + tokens.append(ast.LiteralToken(line, column, '-->')) pos += 3 continue elif _is_ident_start(css, pos): value, pos = _consume_ident(css, pos) if not css.startswith('(', pos): # Not a function - tokens.append(IdentToken(line, column, value)) + tokens.append(ast.IdentToken(line, column, value)) continue pos += 1 # Skip the '(' if ascii_lower(value) == 'url': @@ -85,12 +81,12 @@ def parse_component_value_list(css, skip_comments=False): else: assert error_key == 'eof-in-url' repr = repr[:-1] - tokens.append(URLToken(line, column, value, repr)) + tokens.append(ast.URLToken(line, column, value, repr)) if error is not None: - tokens.append(ParseError(line, column, *error)) + tokens.append(ast.ParseError(line, column, *error)) continue arguments = [] - tokens.append(FunctionBlock(line, column, value, arguments)) + tokens.append(ast.FunctionBlock(line, column, value, arguments)) stack.append((tokens, end_char)) end_char = ')' tokens = arguments @@ -104,22 +100,22 @@ def parse_component_value_list(css, skip_comments=False): int_value = int(repr_) if not any(match.groups()) else None if pos < length and _is_ident_start(css, pos): unit, pos = _consume_ident(css, pos) - tokens.append(DimensionToken( + tokens.append(ast.DimensionToken( line, column, value, int_value, repr_, unit)) elif css.startswith('%', pos): pos += 1 - tokens.append(PercentageToken( + tokens.append(ast.PercentageToken( line, column, value, int_value, repr_)) else: - tokens.append(NumberToken( + tokens.append(ast.NumberToken( line, column, value, int_value, repr_)) elif c == '@': pos += 1 if pos < length and _is_ident_start(css, pos): value, pos = _consume_ident(css, pos) - tokens.append(AtKeywordToken(line, column, value)) + tokens.append(ast.AtKeywordToken(line, column, value)) else: - tokens.append(LiteralToken(line, column, '@')) + tokens.append(ast.LiteralToken(line, column, '@')) elif c == '#': pos += 1 if pos < length and ( @@ -130,26 +126,26 @@ def parse_component_value_list(css, skip_comments=False): (css[pos] == '\\' and not css.startswith('\\\n', pos))): is_identifier = _is_ident_start(css, pos) value, pos = _consume_ident(css, pos) - tokens.append(HashToken(line, column, value, is_identifier)) + tokens.append(ast.HashToken(line, column, value, is_identifier)) else: - tokens.append(LiteralToken(line, column, '#')) + tokens.append(ast.LiteralToken(line, column, '#')) elif c == '{': content = [] - tokens.append(CurlyBracketsBlock(line, column, content)) + tokens.append(ast.CurlyBracketsBlock(line, column, content)) stack.append((tokens, end_char)) end_char = '}' tokens = content pos += 1 elif c == '[': content = [] - tokens.append(SquareBracketsBlock(line, column, content)) + tokens.append(ast.SquareBracketsBlock(line, column, content)) stack.append((tokens, end_char)) end_char = ']' tokens = content pos += 1 elif c == '(': content = [] - tokens.append(ParenthesesBlock(line, column, content)) + tokens.append(ast.ParenthesesBlock(line, column, content)) stack.append((tokens, end_char)) end_char = ')' tokens = content @@ -160,7 +156,7 @@ def parse_component_value_list(css, skip_comments=False): tokens, end_char = stack.pop() pos += 1 elif c in '}])': - tokens.append(ParseError(line, column, c, 'Unmatched ' + c)) + tokens.append(ast.ParseError(line, column, c, 'Unmatched ' + c)) pos += 1 elif c in ('"', "'"): value, pos, error = _consume_quoted_string(css, pos) @@ -168,35 +164,35 @@ def parse_component_value_list(css, skip_comments=False): repr = '"{}"'.format(serialize_string_value(value)) if error is not None: repr = repr[:-1] - tokens.append(StringToken(line, column, value, repr)) + tokens.append(ast.StringToken(line, column, value, repr)) if error is not None: - tokens.append(ParseError(line, column, *error)) + tokens.append(ast.ParseError(line, column, *error)) elif css.startswith('/*', pos): # Comment pos = css.find('*/', pos + 2) if pos == -1: if not skip_comments: tokens.append( - Comment(line, column, css[token_start_pos + 2:])) + ast.Comment(line, column, css[token_start_pos + 2:])) break if not skip_comments: tokens.append( - Comment(line, column, css[token_start_pos + 2:pos])) + ast.Comment(line, column, css[token_start_pos + 2:pos])) pos += 2 elif css.startswith('