diff --git a/CHANGES b/CHANGES index 9c398d22..19ec9ad8 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,10 @@ +### Development + +- Ruff: Add additional rules, fix linting issues (#353) + ## cihai 0.25.0 (2023-06-24) _Maintenance only, no bug fixes, or new features_ diff --git a/docs/conf.py b/docs/conf.py index fa264a8c..fcc488d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -176,9 +176,7 @@ autodoc_member_order = "groupwise" -def linkcode_resolve( - domain: str, info: t.Dict[str, str] -) -> t.Union[None, str]: # NOQA: C901 +def linkcode_resolve(domain: str, info: t.Dict[str, str]) -> t.Union[None, str]: """ Determine the URL corresponding to Python object diff --git a/examples/variant_ts_difficulties.py b/examples/variant_ts_difficulties.py index 3e0d1002..bdaa2c90 100755 --- a/examples/variant_ts_difficulties.py +++ b/examples/variant_ts_difficulties.py @@ -24,7 +24,7 @@ def run(unihan_options: t.Optional[t.Dict[str, object]] = None) -> None: print("3.7.1 bullet 4") for char in c.unihan.with_fields(["kTraditionalVariant", "kSimplifiedVariant"]): - print("Character: {}".format(char.char)) + print(f"Character: {char.char}") trad = set(char.untagged_vars("kTraditionalVariant")) simp = set(char.untagged_vars("kSimplifiedVariant")) Unihan = c.sql.base.classes.Unihan @@ -33,9 +33,9 @@ def run(unihan_options: t.Optional[t.Dict[str, object]] = None) -> None: else: print("Case 2 (non-idempotent)") for trad_var in trad: - print("s2t: {}".format(trad_var)) + print(f"s2t: {trad_var}") for simp_var in simp: - print("t2s: {}".format(simp_var)) + print(f"t2s: {simp_var}") if __name__ == "__main__": diff --git a/examples/variants.py b/examples/variants.py index 8d9413b2..c39a7f56 100755 --- a/examples/variants.py +++ b/examples/variants.py @@ -7,7 +7,7 @@ def variant_list(unihan: Unihan, field: str) -> None: for char in unihan.with_fields([field]): - print("Character: {}".format(char.char)) + print(f"Character: {char.char}") for var in char.untagged_vars(field): print(var) diff --git a/pyproject.toml b/pyproject.toml index ac789148..10ba9ecd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,26 @@ files = [ ] [tool.ruff] +target-version = "py38" +select = [ + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "Q", # flake8-quotes + "PTH", # flake8-use-pathlib + "ERA", # eradicate + "SIM", # flake8-simplify + "TRY", # Trycertatops + "PERF", # Perflint + "RUF" # Ruff-specific rules +] + +[tool.ruff.isort] +known-first-party = ["unihan_etl", "cihai"] +combine-as-imports = true [tool.ruff.per-file-ignores] "*/__init__.py" = ["F401"] diff --git a/src/cihai/__init__.py b/src/cihai/__init__.py index a6428785..56f51e7c 100644 --- a/src/cihai/__init__.py +++ b/src/cihai/__init__.py @@ -1 +1 @@ -from .__about__ import __version__ as __version__ # NOQA: F401 +from .__about__ import __version__ as __version__ diff --git a/src/cihai/_internal/config_reader.py b/src/cihai/_internal/config_reader.py index e23ae954..9e5d4260 100644 --- a/src/cihai/_internal/config_reader.py +++ b/src/cihai/_internal/config_reader.py @@ -12,6 +12,16 @@ FormatLiteral = t.Literal["json", "yaml"] +class ConfigFormatNotImplementedError(NotImplementedError): + def __init__(self, format: str): + return super().__init__(f"{format} not supported in configuration") + + +class ConfigExtensionNotImplementedError(NotImplementedError): + def __init__(self, ext: str, path: t.Union[str, pathlib.Path]): + return super().__init__(f"{ext} not supported in {path}") + + class ConfigReader: r"""Parse string data (YAML and JSON) into a dictionary. @@ -46,7 +56,7 @@ def _load(format: "FormatLiteral", content: str) -> t.Dict[str, t.Any]: elif format == "json": return t.cast(t.Dict[str, t.Any], json.loads(content)) else: - raise NotImplementedError(f"{format} not supported in configuration") + raise NotImplementedError(format=format) @classmethod def load(cls, format: "FormatLiteral", content: str) -> "ConfigReader": @@ -102,14 +112,14 @@ def _from_file(cls, path: pathlib.Path) -> t.Dict[str, t.Any]: {'session_name': 'my session'} """ assert isinstance(path, pathlib.Path) - content = open(path).read() + content = path.open().read() if path.suffix in [".yaml", ".yml"]: format: "FormatLiteral" = "yaml" elif path.suffix == ".json": format = "json" else: - raise NotImplementedError(f"{path.suffix} not supported in {path}") + raise ConfigExtensionNotImplementedError(ext=path.suffix, path=path) return cls._load( format=format, @@ -184,7 +194,7 @@ def _dump( indent=2, ) else: - raise NotImplementedError(f"{format} not supported in config") + raise ConfigFormatNotImplementedError(format=format) def dump(self, format: "FormatLiteral", indent: int = 2, **kwargs: t.Any) -> str: r"""Dump via ConfigReader instance. diff --git a/src/cihai/_internal/types.py b/src/cihai/_internal/types.py index 554bc210..c0361767 100644 --- a/src/cihai/_internal/types.py +++ b/src/cihai/_internal/types.py @@ -6,9 +6,9 @@ :class:`StrPath` and :class:`StrOrBytesPath` is based on `typeshed's`_. .. _typeshed's: https://github.com/python/typeshed/blob/5df8de7/stdlib/_typeshed/__init__.pyi#L115-L118 -""" # NOQA E501 -from os import PathLike +""" import typing as t +from os import PathLike if t.TYPE_CHECKING: from typing_extensions import TypeAlias diff --git a/src/cihai/config.py b/src/cihai/config.py index 2ceb2765..1be9e6a8 100644 --- a/src/cihai/config.py +++ b/src/cihai/config.py @@ -58,7 +58,9 @@ def expand_config(d: "UntypedDict", dirs: "AppDirs" = app_dirs) -> None: if isinstance(v, dict): expand_config(v, dirs) if isinstance(v, str): - d[k] = os.path.expanduser(os.path.expandvars(v).format(**context)) + d[k] = os.path.expanduser( # NOQA: PTH111 + os.path.expandvars(v).format(**context), + ) path = pathlib.Path(t.cast(str, d[k])) if path.exists() or any( diff --git a/src/cihai/constants.py b/src/cihai/constants.py index 71bcf16e..c8e8f1b3 100644 --- a/src/cihai/constants.py +++ b/src/cihai/constants.py @@ -26,5 +26,4 @@ UNIHAN_CONFIG: "UntypedDict" = { "datasets": {"unihan": "cihai.data.unihan.dataset.Unihan"}, # Turn off by default for using as a plugin example in examples/ - # "plugins": {"unihan": {"variants": "cihai.data.unihan.dataset.UnihanVariants"}}, } diff --git a/src/cihai/core.py b/src/cihai/core.py index 041f84ca..66fc1887 100644 --- a/src/cihai/core.py +++ b/src/cihai/core.py @@ -1,12 +1,11 @@ """Cihai core functionality.""" import inspect import logging -import os import pathlib import typing as t + from cihai._internal.config_reader import ConfigReader from cihai.data.unihan.dataset import Unihan - from unihan_etl.util import merge_dict from . import exc, extend @@ -17,6 +16,7 @@ if t.TYPE_CHECKING: from typing_extensions import TypeGuard + from cihai.types import ConfigDict, UntypedDict DS = t.TypeVar("DS", bound=t.Type[extend.Dataset]) @@ -25,6 +25,11 @@ log = logging.getLogger(__name__) +class CihaiConfigError(exc.CihaiException): + def __init__(self) -> None: + return super().__init__("Invalid exception with configuration") + + def is_valid_config(config: "UntypedDict") -> "TypeGuard[ConfigDict]": return True @@ -101,12 +106,14 @@ def __init__( expand_config(_config, app_dirs) if not is_valid_config(config=_config): - raise exc.CihaiException("Invalid exception with configuration") + raise CihaiConfigError() self.config = _config - if not os.path.exists(app_dirs.user_data_dir): - os.makedirs(app_dirs.user_data_dir) + user_data_dir = pathlib.Path(app_dirs.user_data_dir) + + if not user_data_dir.exists(): + user_data_dir.mkdir(parents=True) #: :class:`cihai.db.Database` : Database instance self.sql = Database(self.config) diff --git a/src/cihai/data/unihan/bootstrap.py b/src/cihai/data/unihan/bootstrap.py index ede4b2b5..de975221 100644 --- a/src/cihai/data/unihan/bootstrap.py +++ b/src/cihai/data/unihan/bootstrap.py @@ -1,7 +1,7 @@ import typing as t -import sqlalchemy.sql.schema import sqlalchemy +import sqlalchemy.sql.schema from sqlalchemy import Column, String, Table from unihan_etl import core as unihan @@ -40,19 +40,16 @@ def bootstrap_unihan( try: DEFAULT_FIELDS = [f for c, f in UNIHAN_MANIFEST.items() if c in ["Unihan"]] except Exception: - DEFAULT_FIELDS = [f for f in UNIHAN_MANIFEST.values()] + DEFAULT_FIELDS = list(UNIHAN_MANIFEST.values()) def is_bootstrapped(metadata: sqlalchemy.sql.schema.MetaData) -> bool: """Return True if cihai is correctly bootstrapped.""" fields = UNIHAN_FIELDS + DEFAULT_COLUMNS - if TABLE_NAME in metadata.tables.keys(): + if TABLE_NAME in metadata.tables: table = metadata.tables[TABLE_NAME] - if set(fields) == {c.name for c in table.columns}: - return True - else: - return False + return set(fields) == {c.name for c in table.columns} else: return False diff --git a/src/cihai/data/unihan/dataset.py b/src/cihai/data/unihan/dataset.py index 5e4cc888..347f83bf 100644 --- a/src/cihai/data/unihan/dataset.py +++ b/src/cihai/data/unihan/dataset.py @@ -10,6 +10,7 @@ if t.TYPE_CHECKING: from sqlalchemy.sql.schema import Table + from ...conversion import ParsedVars, UntaggedVars diff --git a/src/cihai/db.py b/src/cihai/db.py index 04eb74a5..0755ed07 100644 --- a/src/cihai/db.py +++ b/src/cihai/db.py @@ -1,5 +1,6 @@ """Cihai core functionality.""" import typing as t + from sqlalchemy import MetaData, create_engine from sqlalchemy.ext.automap import automap_base from sqlalchemy.orm import Session diff --git a/src/cihai/extend.py b/src/cihai/extend.py index 7678ab9c..c153fff6 100644 --- a/src/cihai/extend.py +++ b/src/cihai/extend.py @@ -17,12 +17,13 @@ from . import utils if t.TYPE_CHECKING: - from cihai.db import Database from sqlalchemy.engine import Engine from sqlalchemy.ext.automap import AutomapBase from sqlalchemy.orm.session import Session from sqlalchemy.sql.schema import MetaData + from cihai.db import Database + DSP = t.TypeVar("DSP", bound=t.Type["DatasetPlugin"]) @@ -106,10 +107,7 @@ def add_plugin( namespace: str, bootstrap: bool = True, ) -> None: - if isinstance(_cls, str): - cls = utils.import_string(_cls) - else: - cls = _cls + cls = utils.import_string(_cls) if isinstance(_cls, str) else _cls setattr(self, namespace, cls()) plugin = getattr(self, namespace) diff --git a/src/cihai/log.py b/src/cihai/log.py index 02b30874..2062ce4d 100644 --- a/src/cihai/log.py +++ b/src/cihai/log.py @@ -119,7 +119,7 @@ def template(self, record: logging.LogRecord) -> str: Style.RESET_ALL, " ", ] - module_funcName = [Fore.GREEN, Style.BRIGHT, "%(module)s.%(funcName)s()"] + module_funcname = [Fore.GREEN, Style.BRIGHT, "%(module)s.%(funcName)s()"] lineno = [ Fore.BLACK, Style.DIM, @@ -131,7 +131,7 @@ def template(self, record: logging.LogRecord) -> str: ] tpl = "".join( - reset + levelname + asctime + name + module_funcName + lineno + reset + reset + levelname + asctime + name + module_funcname + lineno + reset ) return tpl diff --git a/src/cihai/types.py b/src/cihai/types.py index ab4c0fee..9ae0baaa 100644 --- a/src/cihai/types.py +++ b/src/cihai/types.py @@ -5,6 +5,7 @@ if t.TYPE_CHECKING: from typing_extensions import TypeAlias + from cihai.extend import Dataset diff --git a/tests/conftest.py b/tests/conftest.py index 134124f0..c7800fb1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,12 @@ -import os import pathlib import typing as t import zipfile import pytest - import sqlalchemy from cihai.data.unihan.constants import UNIHAN_FILES - if t.TYPE_CHECKING: from .types import UnihanOptions @@ -25,8 +22,8 @@ def fixture_path(tests_path: pathlib.Path) -> pathlib.Path: @pytest.fixture -def test_config_file(fixture_path: str) -> str: - return os.path.join(fixture_path, "test_config.yml") +def test_config_file(fixture_path: pathlib.Path) -> pathlib.Path: + return fixture_path / "test_config.yml" @pytest.fixture diff --git a/tests/test_cihai.py b/tests/test_cihai.py index c0283160..9202ebdf 100644 --- a/tests/test_cihai.py +++ b/tests/test_cihai.py @@ -5,6 +5,7 @@ """ import typing as t + import sqlalchemy import cihai @@ -54,9 +55,11 @@ def test_unihan_options( ) assert "Unihan" in app.sql.metadata.tables assert app.sql.metadata.tables["Unihan"].columns - assert set(app.sql.metadata.tables["Unihan"].columns.keys()) == set( - unihan_constants.UNIHAN_FIELDS + ["ucn", "char"] - ) + assert set(app.sql.metadata.tables["Unihan"].columns.keys()) == { + *unihan_constants.UNIHAN_FIELDS, + "ucn", + "char", + } assert bootstrap.is_bootstrapped(app.sql.metadata) diff --git a/tests/test_config.py b/tests/test_config.py index 69220362..8fa6bf27 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,9 +1,11 @@ import os import pathlib import typing as t + +import pytest from appdirs import AppDirs + from cihai.config import expand_config -import pytest if t.TYPE_CHECKING: from cihai.types import UntypedDict diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 6fde4a79..a45a1733 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -11,7 +11,6 @@ import typing as t import pytest - import sqlalchemy from cihai import conversion diff --git a/tests/test_extend.py b/tests/test_extend.py index b82d2991..096a8e51 100644 --- a/tests/test_extend.py +++ b/tests/test_extend.py @@ -48,7 +48,7 @@ def test_add_dataset_unihan(unihan_options: t.Dict[str, object]) -> None: assert hasattr(c, "unihan") assert isinstance(c.unihan, extend.Dataset) - c.unihan.sql + assert c.unihan.sql is not None c.unihan.bootstrap(options=unihan_options) U = c.sql.base.classes.Unihan @@ -60,14 +60,14 @@ def test_add_dataset_unihan(unihan_options: t.Dict[str, object]) -> None: assert first_glyph is not None char = first_glyph.char - kDefQuery = c.unihan.lookup_char(char=char).first() - assert kDefQuery is not None - assert kDefQuery.kDefinition == first_glyph.kDefinition + kdef_query = c.unihan.lookup_char(char=char).first() + assert kdef_query is not None + assert kdef_query.kDefinition == first_glyph.kDefinition - charQuery = c.unihan.reverse_char(hints=[first_glyph.kDefinition]).first() - assert charQuery is not None + char_query = c.unihan.reverse_char(hints=[first_glyph.kDefinition]).first() + assert char_query is not None - assert charQuery.char == char, "works with list of column value matches" + assert char_query.char == char, "works with list of column value matches" reverse_query = c.unihan.reverse_char(hints=first_glyph.kDefinition).first() diff --git a/tests/test_middleware/simple/__init__.py b/tests/test_middleware/simple/__init__.py index fc4241f4..c093b5bc 100644 --- a/tests/test_middleware/simple/__init__.py +++ b/tests/test_middleware/simple/__init__.py @@ -1,6 +1,5 @@ import typing as t - if t.TYPE_CHECKING: from cihai.types import UntypedDict @@ -59,7 +58,7 @@ def reverse(self, request: str, response: "Response") -> "Response": """ dataset: "Response" = {"好": {"definition": "hao"}} - for char in dataset.keys(): + for char in dataset: for val in dataset[char].values(): assert isinstance(val, dict) if request in val: