diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a61f3439..12654852 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -228,7 +228,7 @@ jobs: cache: pip cache-dependency-path: '**/requirements.dev.txt' - run: pip install -r python/requirements.dev.txt - - run: python -m black --check --diff --color . + - run: python -m black --check --diff --color . working-directory: ./python - run: maturin build -m python/Cargo.toml - run: pip install --no-index --find-links=target/wheels/ pyoxigraph @@ -245,6 +245,8 @@ jobs: working-directory: ./python - run: python -m mypy generate_stubs.py tests --strict working-directory: ./python + - run: python -m ruff check . + working-directory: ./python python_msv: runs-on: ubuntu-latest diff --git a/python/generate_stubs.py b/python/generate_stubs.py index a865f80e..3c2054d6 100644 --- a/python/generate_stubs.py +++ b/python/generate_stubs.py @@ -5,7 +5,7 @@ import inspect import logging import re import subprocess -from typing import Set, List, Mapping, Any, Tuple, Union, Optional, Dict +from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union def _path_to_type(*elements: str) -> ast.AST: @@ -107,7 +107,7 @@ def class_stubs( methods: List[ast.AST] = [] magic_methods: List[ast.AST] = [] for member_name, member_value in inspect.getmembers(cls_def): - current_element_path = element_path + [member_name] + current_element_path = [*element_path, member_name] if member_name == "__init__": try: inspect.signature(cls_def) # we check it actually exists @@ -118,13 +118,14 @@ def class_stubs( current_element_path, types_to_import, in_class=True, - ) - ] + methods + ), + *methods, + ] except ValueError as e: if "no signature found" not in str(e): raise ValueError( - f"Error while parsing signature of {cls_name}.__init__: {e}" - ) + f"Error while parsing signature of {cls_name}.__init_" + ) from e elif ( member_value == OBJECT_MEMBERS.get(member_name) or BUILTINS.get(member_name, ()) is None @@ -256,7 +257,8 @@ def arguments_stub( for match in re.findall(r"^ *:type *([a-z_]+): ([^\n]*) *$", doc, re.MULTILINE): if match[0] not in real_parameters: raise ValueError( - f"The parameter {match[0]} of {'.'.join(element_path)} is defined in the documentation but not in the function signature" + f"The parameter {match[0]} of {'.'.join(element_path)} " + "is defined in the documentation but not in the function signature" ) type = match[1] if type.endswith(", optional"): @@ -277,7 +279,8 @@ def arguments_stub( for param in real_parameters.values(): if param.name != "self" and param.name not in parsed_param_types: raise ValueError( - f"The parameter {param.name} of {'.'.join(element_path)} has no type definition in the function documentation" + f"The parameter {param.name} of {'.'.join(element_path)} " + "has no type definition in the function documentation" ) param_ast = ast.arg( arg=param.name, annotation=parsed_param_types.get(param.name) @@ -288,11 +291,13 @@ def arguments_stub( default_ast = ast.Constant(param.default) if param.name not in optional_params: raise ValueError( - f"Parameter {param.name} of {'.'.join(element_path)} is optional according to the type but not flagged as such in the doc" + f"Parameter {param.name} of {'.'.join(element_path)} " + "is optional according to the type but not flagged as such in the doc" ) elif param.name in optional_params: raise ValueError( - f"Parameter {param.name} of {'.'.join(element_path)} is optional according to the documentation but has no default value" + f"Parameter {param.name} of {'.'.join(element_path)} " + "is optional according to the documentation but has no default value" ) if param.kind == param.POSITIONAL_ONLY: @@ -329,14 +334,14 @@ def returns_stub( if isinstance(builtin, tuple) and builtin[1] is not None: return builtin[1] raise ValueError( - f"The return type of {'.'.join(element_path)} has no type definition using :rtype: in the function documentation" + f"The return type of {'.'.join(element_path)} " + "has no type definition using :rtype: in the function documentation" ) - elif len(m) == 1: - return convert_type_from_doc(m[0], element_path, types_to_import) - else: + if len(m) > 1: raise ValueError( f"Multiple return type annotations found with :rtype: for {'.'.join(element_path)}" ) + return convert_type_from_doc(m[0], element_path, types_to_import) def convert_type_from_doc( @@ -368,9 +373,9 @@ def parse_type_to_ast( stack: List[List[Any]] = [[]] for token in tokens: if token == "(": - l: List[str] = [] - stack[-1].append(l) - stack.append(l) + children: List[str] = [] + stack[-1].append(children) + stack.append(children) elif token == ")": stack.pop() else: @@ -435,13 +440,12 @@ def parse_type_to_ast( def build_doc_comment(doc: str) -> ast.Expr: - lines = [l.strip() for l in doc.split("\n")] + lines = [line.strip() for line in doc.split("\n")] clean_lines = [] - for l in lines: - if l.startswith(":type") or l.startswith(":rtype"): + for line in lines: + if line.startswith((":type", ":rtype")): continue - else: - clean_lines.append(l) + clean_lines.append(line) return ast.Expr(value=ast.Constant("\n".join(clean_lines).strip())) diff --git a/python/pyproject.toml b/python/pyproject.toml index 3a37fa08..8b7cdb91 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -28,3 +28,29 @@ Documentation = "https://pyoxigraph.readthedocs.io/" Homepage = "https://pyoxigraph.readthedocs.io/" Source = "https://github.com/oxigraph/oxigraph/tree/main/python" Tracker = "https://github.com/oxigraph/oxigraph/issues" + +[tool.ruff] +line-length = 120 +select = [ + "ARG", + "B", + "C40", + "E", + "F", + "FBT", + "I", + "ICN", + "ISC", + "N", + "PIE", + "PTH", + "RET", + "RUF", + "SIM", + "T10", + "TCH", + "TID", + "UP", + "W", + "YTT" +] diff --git a/python/requirements.dev.txt b/python/requirements.dev.txt index 3a9b9382..6bc286d4 100644 --- a/python/requirements.dev.txt +++ b/python/requirements.dev.txt @@ -2,4 +2,5 @@ black~=23.1 furo maturin~=0.14.0 mypy~=1.0 +ruff~=0.0.255 sphinx~=5.3 diff --git a/python/tests/test_io.py b/python/tests/test_io.py index a5d47309..5dda57ca 100644 --- a/python/tests/test_io.py +++ b/python/tests/test_io.py @@ -1,9 +1,8 @@ import unittest -from io import StringIO, BytesIO, UnsupportedOperation +from io import BytesIO, StringIO, UnsupportedOperation from tempfile import NamedTemporaryFile, TemporaryFile -from pyoxigraph import * - +from pyoxigraph import Literal, NamedNode, Quad, Triple, parse, serialize EXAMPLE_TRIPLE = Triple( NamedNode("http://example.com/foo"), NamedNode("http://example.com/p"), Literal("1") @@ -55,9 +54,8 @@ class TestParse(unittest.TestCase): ) def test_parse_io_error(self) -> None: - with self.assertRaises(UnsupportedOperation) as _: - with TemporaryFile("wb") as fp: - list(parse(fp, mime_type="application/n-triples")) + with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp: + list(parse(fp, mime_type="application/n-triples")) def test_parse_quad(self) -> None: self.assertEqual( @@ -89,9 +87,8 @@ class TestSerialize(unittest.TestCase): ) def test_serialize_io_error(self) -> None: - with self.assertRaises(UnsupportedOperation) as _: - with TemporaryFile("rb") as fp: - serialize([EXAMPLE_TRIPLE], fp, "text/turtle") + with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("rb") as fp: + serialize([EXAMPLE_TRIPLE], fp, "text/turtle") def test_serialize_quad(self) -> None: output = BytesIO() diff --git a/python/tests/test_model.py b/python/tests/test_model.py index b879cefb..63619274 100644 --- a/python/tests/test_model.py +++ b/python/tests/test_model.py @@ -1,6 +1,14 @@ import unittest -from pyoxigraph import * +from pyoxigraph import ( + BlankNode, + DefaultGraph, + Literal, + NamedNode, + Quad, + Triple, + Variable, +) XSD_STRING = NamedNode("http://www.w3.org/2001/XMLSchema#string") XSD_INTEGER = NamedNode("http://www.w3.org/2001/XMLSchema#integer") diff --git a/python/tests/test_store.py b/python/tests/test_store.py index a04ca979..bd7bb770 100644 --- a/python/tests/test_store.py +++ b/python/tests/test_store.py @@ -1,10 +1,21 @@ -import os import unittest from io import BytesIO, UnsupportedOperation +from pathlib import Path from tempfile import NamedTemporaryFile, TemporaryDirectory, TemporaryFile from typing import Any -from pyoxigraph import * +from pyoxigraph import ( + BlankNode, + DefaultGraph, + NamedNode, + Quad, + QuerySolution, + QuerySolutions, + QueryTriples, + Store, + Triple, + Variable, +) foo = NamedNode("http://foo") bar = NamedNode("http://bar") @@ -259,13 +270,12 @@ class TestStore(unittest.TestCase): fp.write(b" .") store = Store() store.load(file_name, mime_type="application/n-quads") - os.remove(file_name) + Path(file_name).unlink() self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) def test_load_with_io_error(self) -> None: - with self.assertRaises(UnsupportedOperation) as _: - with TemporaryFile("wb") as fp: - Store().load(fp, mime_type="application/n-triples") + with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp: + Store().load(fp, mime_type="application/n-triples") def test_dump_ntriples(self) -> None: store = Store() @@ -304,17 +314,14 @@ class TestStore(unittest.TestCase): store = Store() store.add(Quad(foo, bar, baz, graph)) store.dump(file_name, "application/n-quads") - with open(file_name, "rt") as fp: - file_content = fp.read() self.assertEqual( - file_content, + Path(file_name).read_text(), " .\n", ) def test_dump_with_io_error(self) -> None: - with self.assertRaises(OSError) as _: - with TemporaryFile("rb") as fp: - Store().dump(fp, mime_type="application/rdf+xml") + with self.assertRaises(OSError) as _, TemporaryFile("rb") as fp: + Store().dump(fp, mime_type="application/rdf+xml") def test_write_in_read(self) -> None: store = Store()