Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
875b745
Replace Pylint with Ruff linter
pronovic Aug 25, 2025
3881e9c
Remove unnecessary exclusions
pronovic Aug 25, 2025
f497af2
Fixes for FBT001
pronovic Aug 25, 2025
6d07d93
Fixes for FURB110
pronovic Aug 25, 2025
5be578d
Remove unnecessary exclusions
pronovic Aug 25, 2025
192bb3d
Fix rufflint regex
pronovic Aug 25, 2025
bb8d0f8
Fixes for PGH003
pronovic Aug 25, 2025
3c0215b
Remove unnecessary exclusions
pronovic Aug 25, 2025
83afb9d
Fixes for PLR6201
pronovic Aug 25, 2025
0d5bcaf
Fixes for PLW1514
pronovic Aug 25, 2025
0f0293d
Fixes for PT012
pronovic Aug 25, 2025
e9aa12c
Fixes for RET505
pronovic Aug 25, 2025
bac4a80
Remove unnecessary exclusions
pronovic Aug 25, 2025
b2044e5
Fixes for SIM113
pronovic Aug 25, 2025
c5bc263
Remove unnecessary exclusions
pronovic Aug 25, 2025
be55f2e
Fixes for TID252
pronovic Aug 25, 2025
a6e6831
Remove unnecessary exclusions
pronovic Aug 25, 2025
18c16d6
Fixes for UP006
pronovic Aug 25, 2025
b4c2fc7
Fixes for UP009
pronovic Aug 25, 2025
6f7780a
Fixes for UP015
pronovic Aug 25, 2025
d4fb9af
Fixes for UP031
pronovic Aug 25, 2025
dba0857
Fixes for UP035
pronovic Aug 25, 2025
f87429b
Fixes for UP045
pronovic Aug 25, 2025
239abe5
Remove 'from __future__ import annotations'
pronovic Aug 25, 2025
740e2ef
Fix rufflint error handling
pronovic Aug 25, 2025
149398b
Fix rufflint error handling
pronovic Aug 25, 2025
4534a6f
Merge branch 'ruff-linter' into ruff-linter-fixes
pronovic Aug 25, 2025
c9077ed
Update changelog
pronovic Aug 25, 2025
1825fe4
Un-exclude ERA
pronovic Aug 25, 2025
fbb8bfb
Adjust rufflint output
pronovic Aug 25, 2025
ce444e4
Adjust rufflint output
pronovic Aug 25, 2025
742b20c
Merge branch 'ruff-linter' into ruff-linter-fixes
pronovic Aug 25, 2025
dc44494
Fall back formatting changes
pronovic Aug 25, 2025
7dee2c1
merge with origin/main
pronovic Aug 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Version 0.1.27 unreleased
* Move unit tests from `tests` into `src/tests/uciparse`.
* Update the MyPy configuration so we're using latest rules.
* Replace black, isort, and pylint with the Ruff formatter and linter.
* Address Ruff linter warnings and modernize the code to 2025 standards.
* Update the jinja2 transitive dependency to address CVE-2025-27516.
* Update the requests transitive dependency to address CVE-2024-47081.
* Update the urllib3 transitive dependency to address CVE-2025-50181.
Expand Down
54 changes: 1 addition & 53 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ ignore = [
"D", # pydocstyle
"DJ", # flake8-django
"DOC", # pydoclint
"ERA", # eradicate
"EXE", # flake8-executable
"FIX", # flake8-fixme
"PTH", # flake8-use-pathlib
Expand All @@ -160,58 +159,7 @@ ignore = [
"SIM102", # allow nested `if` clauses; ruff suggestions often make the code less legible
"SIM117", # allow nested `with` clauses; ruff suggestions often make the code less legible
"TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit

# Exclusions of specific rules that we want to work toward being compliant with
"ANN001",
"ANN202",
"ANN204",
"ARG001",
"ARG002",
"BLE001",
"C414",
"C420",
"E303",
"E713",
"F841",
"FBT001",
"FBT002",
"FLY002",
"FURB101",
"FURB110",
"FURB118",
"FURB136",
"PERF401",
"PGH003",
"PIE808",
"PLR0911",
"PLR0912",
"PLR0913",
"PLR0917",
"PLR1702",
"PLR5501",
"PLR6201",
"PLR6301",
"PLW1514",
"PT012",
"RET505",
"RET506",
"S311",
"SIM110",
"SIM113",
"T201",
"TC001",
"TC003",
"TID252",
"TRY004",
"TRY201",
"TRY300",
"UP006",
"UP009",
"UP015",
"UP031",
"UP035",
"UP045",
"W291",
"UP031", # allow format-specifiers instead of f-strings in this codebase
]

[tool.ruff.lint.per-file-ignores]
Expand Down
1 change: 0 additions & 1 deletion src/tests/uciparse/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# vim: set ft=python ts=4 sw=4 expandtab:

from unittest.mock import MagicMock, call, patch
Expand Down
12 changes: 5 additions & 7 deletions src/tests/uciparse/test_uci.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# vim: set ft=python ts=4 sw=4 expandtab:

import os
from typing import Dict, List
from unittest.mock import MagicMock

import pytest
Expand All @@ -21,7 +19,7 @@
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), "fixtures/test_uci")


def load(path: str) -> Dict[str, List[str]]:
def load(path: str) -> dict[str, list[str]]:
data = {}
for f in os.listdir(path):
p = os.path.join(path, f)
Expand All @@ -32,22 +30,22 @@ def load(path: str) -> Dict[str, List[str]]:


@pytest.fixture
def original() -> Dict[str, List[str]]:
def original() -> dict[str, list[str]]:
return load(os.path.join(FIXTURE_DIR, "original"))


@pytest.fixture
def normalized() -> Dict[str, List[str]]:
def normalized() -> dict[str, list[str]]:
return load(os.path.join(FIXTURE_DIR, "normalized"))


@pytest.fixture
def invalid() -> Dict[str, List[str]]:
def invalid() -> dict[str, list[str]]:
return load(os.path.join(FIXTURE_DIR, "invalid"))


@pytest.fixture
def real() -> Dict[str, List[str]]:
def real() -> dict[str, list[str]]:
return load(os.path.join(FIXTURE_DIR, "real"))


Expand Down
1 change: 0 additions & 1 deletion src/uciparse/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
__all__ = [] # type: ignore
3 changes: 1 addition & 2 deletions src/uciparse/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# vim: set ft=python ts=4 sw=4 expandtab:

"""
Expand All @@ -9,7 +8,7 @@
import difflib
import sys

from .uci import UciFile, UciParseError
from uciparse.uci import UciFile, UciParseError


def parse() -> None:
Expand Down
70 changes: 33 additions & 37 deletions src/uciparse/uci.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# vim: set ft=python ts=4 sw=4 expandtab:

r"""
Expand Down Expand Up @@ -171,12 +170,11 @@
.. _UCI: https://openwrt.org/docs/guide-user/base-system/uci
"""

from __future__ import annotations # see: https://stackoverflow.com/a/33533514/2907667

import re
import typing
from abc import ABC, abstractmethod
from typing import List, Optional, Sequence, TextIO, Tuple
from collections.abc import Sequence
from typing import TextIO

# Standard indent of 4 spaces
_INDENT = " "
Expand Down Expand Up @@ -204,59 +202,59 @@ def _contains_single(string: str) -> bool:
return match is not None


def _parse_line(lineno: int, line: str) -> Optional[UciLine]:
def _parse_line(lineno: int, line: str) -> "UciLine | None":
"""Parse a line, raising UciParseError if it is not valid."""
match = _LINE_REGEX.match(line)
if not match:
raise UciParseError("Error on line %d: unrecognized line type" % lineno)
if match[4] == "#":
return _parse_comment(lineno, match[3], match[5])
elif match[8]:
if match[8]:
if match[8] == "package":
return _parse_package(lineno, match[10])
elif match[8] == "config":
if match[8] == "config":
return _parse_config(lineno, match[10])
elif match[8] == "option":
if match[8] == "option":
return _parse_option(lineno, match[10])
elif match[8] == "list":
if match[8] == "list":
return _parse_list(lineno, match[10])
return None


def _parse_package(lineno: int, remainder: str) -> UciPackageLine:
def _parse_package(lineno: int, remainder: str) -> "UciPackageLine":
"""Parse a package line, raising UciParseError if it is not valid."""
match = _PACKAGE_REGEX.match(remainder)
if not match:
raise UciParseError("Error on line %d: invalid package line" % lineno)
name = match[5] if match[5] else match[6]
name = match[5] or match[6]
comment = match[9]
return UciPackageLine(name=name, comment=comment)


def _parse_config(lineno: int, remainder: str) -> UciConfigLine:
def _parse_config(lineno: int, remainder: str) -> "UciConfigLine":
"""Parse a config line, raising UciParseError if it is not valid."""
match = _CONFIG_REGEX.match(remainder)
if not match:
raise UciParseError("Error on line %d: invalid config line" % lineno)
section = match[5] if match[5] else match[6]
name = match[12] if match[12] else match[9]
section = match[5] or match[6]
name = match[12] or match[9]
comment = match[16]
return UciConfigLine(section=section, name=name, comment=comment)


def _extract_data_of_remainder_match(match: typing.Match[str]) -> Tuple[str, str, str]:
def _extract_data_of_remainder_match(match: typing.Match[str]) -> tuple[str, str, str]:
"""Extracts a 3-tuple containing (name,value,comment) out of a {_OPTION_REGEX, LIST_REGEX} matcher"""
name = match[5] if match[5] else match[6]
name = match[5] or match[6]
value = ""
if match[11]:
value = match[11]
elif match[8] not in ('""', "''"):
elif match[8] not in {'""', "''"}:
value = match[8]
comment = match[15]
return name, value, comment


def _parse_option(lineno: int, remainder: str) -> UciOptionLine:
def _parse_option(lineno: int, remainder: str) -> "UciOptionLine":
"""Parse an option line, raising UciParseError if it is not valid."""
match = _OPTION_REGEX.match(remainder)
if not match:
Expand All @@ -265,7 +263,7 @@ def _parse_option(lineno: int, remainder: str) -> UciOptionLine:
return UciOptionLine(name=name, value=value, comment=comment)


def _parse_list(lineno: int, remainder: str) -> UciListLine:
def _parse_list(lineno: int, remainder: str) -> "UciListLine":
"""Parse a list line, raising UciParseError if it is not valid."""
match = LIST_REGEX.match(remainder)
if not match:
Expand All @@ -274,14 +272,14 @@ def _parse_list(lineno: int, remainder: str) -> UciListLine:
return UciListLine(name=name, value=value, comment=comment)


def _parse_comment(_lineno: int, prefix: str, remainder: str) -> UciCommentLine:
def _parse_comment(_lineno: int, prefix: str, remainder: str) -> "UciCommentLine":
"""Parse a comment-only line, raising UciParseError if it is not valid."""
indented = len(prefix) > 0 if prefix else False # all we care about is whether it's indented, not the actual indent
comment = "#%s" % remainder
return UciCommentLine(comment=comment, indented=indented)


def _serialize_identifier(prefix: str, identifier: Optional[str]) -> str:
def _serialize_identifier(prefix: str, identifier: str | None) -> str:
"""Serialize an identifier, which is never quoted."""
return "%s%s" % (prefix, identifier) if identifier else ""

Expand All @@ -292,7 +290,7 @@ def _serialize_value(prefix: str, value: str) -> str:
return "%s%s%s%s" % (prefix, quote, value, quote)


def _serialize_comment(prefix: str, comment: Optional[str]) -> str:
def _serialize_comment(prefix: str, comment: str | None) -> str:
"""Serialize a comment, with an optional prefix."""
return "%s%s" % (prefix, comment) if comment else ""

Expand All @@ -316,7 +314,7 @@ def normalized(self) -> str:
class UciPackageLine(UciLine):
"""A package line in a UCI config file."""

def __init__(self, name: str, comment: Optional[str] = None) -> None:
def __init__(self, name: str, comment: str | None = None) -> None:
self.name = name
self.comment = comment

Expand All @@ -330,7 +328,7 @@ def normalized(self) -> str:
class UciConfigLine(UciLine):
"""A config line in a UCI config file."""

def __init__(self, section: str, name: Optional[str] = None, comment: Optional[str] = None) -> None:
def __init__(self, section: str, name: str | None = None, comment: str | None = None) -> None:
self.section = section
self.name = name
self.comment = comment
Expand All @@ -346,7 +344,7 @@ def normalized(self) -> str:
class UciOptionLine(UciLine):
"""An option line in a UCI config file."""

def __init__(self, name: str, value: str, comment: Optional[str] = None) -> None:
def __init__(self, name: str, value: str, comment: str | None = None) -> None:
self.name = name
self.value = value
self.comment = comment
Expand All @@ -362,7 +360,7 @@ def normalized(self) -> str:
class UciListLine(UciLine):
"""A list line in a UCI config file."""

def __init__(self, name: str, value: str, comment: Optional[str] = None) -> None:
def __init__(self, name: str, value: str, comment: str | None = None) -> None:
self.name = name
self.value = value
self.comment = comment
Expand All @@ -378,7 +376,7 @@ def normalized(self) -> str:
class UciCommentLine(UciLine):
"""A comment line in a UCI config file."""

def __init__(self, comment: str, indented: bool = False) -> None:
def __init__(self, comment: str, *, indented: bool = False) -> None:
self.comment = comment
self.indented = indented

Expand All @@ -390,32 +388,30 @@ def normalized(self) -> str:


class UciFile:
def __init__(self, lines: List[UciLine]) -> None:
def __init__(self, lines: list[UciLine]) -> None:
self.lines = lines

def normalized(self) -> List[str]:
def normalized(self) -> list[str]:
"""Return a list of normalized lines comprising the file."""
# We join the lines first and then re-split so we don't end up with lines that have an embedded newline
return "".join([line.normalized() for line in self.lines]).splitlines(keepends=True)

@staticmethod
def from_file(path: str) -> UciFile:
def from_file(path: str) -> "UciFile":
"""Generate a UciFile from a file on disk."""
with open(path, "r") as fp:
with open(path, encoding=None) as fp: # use platform-specific encoding
return UciFile.from_fp(fp)

@staticmethod
def from_fp(fp: TextIO) -> UciFile:
def from_fp(fp: TextIO) -> "UciFile":
"""Generate a UciFile from the contents of a file pointer."""
return UciFile.from_lines(fp.readlines())

@staticmethod
def from_lines(lines: Sequence[str]) -> UciFile:
def from_lines(lines: Sequence[str]) -> "UciFile":
"""Generate a UciFile from a list of lines."""
lineno = 0
ucilines: List[UciLine] = []
for line in lines:
lineno += 1
ucilines: list[UciLine] = []
for lineno, line in enumerate(lines, start=1):
parsed = _parse_line(lineno, line)
if parsed:
ucilines.append(parsed)
Expand Down
Loading