diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3241c6f..3fb4966 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,15 +4,10 @@ repos: hooks: - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/psf/black - rev: 22.12.0 - hooks: - - id: black -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.274 + rev: v0.14.6 hooks: - - id: ruff + - id: ruff-check + - id: ruff-check + args: [--select, I, --fix] + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index a7b668b..63c0fff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,24 +26,17 @@ pre-commit = "^4.5.0" mypy = "^1.5" pytest = "^7.2.0" pytest-cov = "^4.0.0" -ruff = "0.0.274" +ruff = "0.14.6" [tool.poetry.group.coverage.dependencies] coverage = {version = "^7.0.0", extras = ["toml"]} -[tool.black] -line-length = 120 -target-version = ["py310"] - -[tool.isort] -profile = "black" -multi_line_output = 3 -line_length = 120 - [tool.ruff] line-length = 120 target-version = "py310" src = ["src"] + +[tool.ruff.lint] select = [ "F", "E", "W", "I", "N", "UP", "ANN0", "ANN2", "FBT", "B", "A", "C4", "FA", "ISC", "ICN", "G", "INP", "PIE", "T20", "PT", "Q", "RSE", "RET", "SIM", @@ -57,6 +50,9 @@ ignore = [ "TRY301", # alternatives might be worse ] +[tool.ruff.lint.isort] +split-on-trailing-comma = false + [tool.mypy] disallow_incomplete_defs = true disallow_untyped_defs = true diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index 87dc1ae..9d9e4a3 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -187,7 +187,7 @@ def _get_fields(cls: type) -> Iterator[tuple[Field, type]]: continue if isinstance(annotation, str): try: - annotation = eval(annotation, cls_globals, cls_locals) # noqa: PGH001 + annotation = eval(annotation, cls_globals, cls_locals) except NameError as ex: raise TypeError(f"Failed to parse annotation of field '{cls.__name__}.{name}': {ex}") from None yield field, annotation @@ -211,7 +211,6 @@ def _check_field(field: Field, field_type: type, context: str) -> None: @dataclass(slots=True) class _ClassInfo(Generic[T]): - _cache: ClassVar[MutableMapping[type[Any], _ClassInfo[Any]]] = WeakKeyDictionary() dataclass: type[T] @@ -260,7 +259,8 @@ class Binder(Generic[T]): Binds TOML data to a specific dataclass. """ - __slots__ = ("_dataclass", "_instance", "_class_info") + __slots__ = "_class_info", "_dataclass", "_instance" + _dataclass: type[T] _instance: T | None _class_info: _ClassInfo[T] @@ -270,12 +270,10 @@ def __class_getitem__(cls: type[Binder[T]], dataclass: type[T]) -> Binder[T]: return cls(dataclass) @overload - def __init__(self, class_or_instance: type[T]) -> None: - ... + def __init__(self, class_or_instance: type[T]) -> None: ... @overload - def __init__(self, class_or_instance: T) -> None: - ... + def __init__(self, class_or_instance: T) -> None: ... def __init__(self, class_or_instance: type[T] | T) -> None: if isinstance(class_or_instance, type): @@ -323,7 +321,7 @@ def _bind_to_single_type(self, value: object, field_type: type, context: str) -> elif issubclass(origin, Mapping): if not isinstance(value, dict): raise TypeError(f"Value for '{context}' has type '{type(value).__name__}', expected table") - key_type, elem_type = get_args(field_type) + _key_type, elem_type = get_args(field_type) mapping = { key: self._bind_to_field(elem, elem_type, None, f'{context}["{key}"]') for key, elem in value.items() } @@ -506,7 +504,7 @@ def _format_toml_table( origin = get_origin(field_type) if origin is not None: if issubclass(origin, Mapping): - key_type, value_type = get_args(field_type) + _key_type, value_type = get_args(field_type) if isinstance(value_type, Binder): if value is None: nested_map = {f"{key_fmt}.": None} @@ -568,12 +566,10 @@ def _format_toml_table( # These definitions exist to support the deprecated `Binder[DC]` syntax in mypy. @classmethod - def bind(cls, data: Mapping[str, Any]) -> T: - ... + def bind(cls, data: Mapping[str, Any]) -> T: ... @classmethod - def parse_toml(cls, file: BinaryIO | str | Path) -> T: - ... + def parse_toml(cls, file: BinaryIO | str | Path) -> T: ... else: diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 48d39c5..ffe5ed2 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -100,9 +100,9 @@ def round_trip_value(value: T, dc: type[Any]) -> T: "\"both\" 'quotes'", "embedded\nnewline", r"back\slash", - 'I\'m a string. "You can quote me". Name\tJos\u00E9\nLocation\tSF.', - "complex string with back\\slash, \"both\" 'quotes' and \u0000control\u007Fchars\u0007", - "\U0001F44D", + 'I\'m a string. "You can quote me". Name\tJos\u00e9\nLocation\tSF.', + "complex string with back\\slash, \"both\" 'quotes' and \u0000control\u007fchars\u0007", + "\U0001f44d", date(2022, 10, 5), datetime(2022, 10, 5, 19, 16, 29), time(19, 16, 29), @@ -240,9 +240,9 @@ def test_format_value_nested_dataclass(*, optional: bool, string: bool) -> None: def test_format_value_unsupported_type() -> None: - with pytest.raises(TypeError, match="^NoneType$"): + with pytest.raises(TypeError, match=r"^NoneType$"): format_toml_pair("unsupported", None) - with pytest.raises(TypeError, match="^NoneType$"): + with pytest.raises(TypeError, match=r"^NoneType$"): list(_iter_format_value(None)) @@ -821,7 +821,7 @@ def test_format_template_depth_first() -> None: ) -@pytest.fixture() +@pytest.fixture def sourceless_class() -> type[Any]: """A class for which no source code is available.""" diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 79a4a93..74f8eaa 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -659,7 +659,7 @@ def test_bind_timedelta_direct() -> None: duration = false """ ) as stream, - pytest.raises(TypeError, match="^Value for 'TimeDeltaConfig.duration' has type 'bool', expected time$"), + pytest.raises(TypeError, match=r"^Value for 'TimeDeltaConfig.duration' has type 'bool', expected time$"), ): Binder(TimeDeltaConfig).parse_toml(stream) @@ -693,7 +693,7 @@ def test_bind_timedelta_suffix() -> None: ) as stream, pytest.raises( TypeError, - match="^Value for 'TimeDeltaConfig.duration' with suffix 'weeks' has type 'bool', expected number$", + match=r"^Value for 'TimeDeltaConfig.duration' with suffix 'weeks' has type 'bool', expected number$", ), ): Binder(TimeDeltaConfig).parse_toml(stream)