Python: Uses Ruff linter

pull/428/head
Tpt 2 years ago committed by Thomas Tanon
parent fbcbd60c0e
commit 28def4001b
  1. 2
      .github/workflows/tests.yml
  2. 48
      python/generate_stubs.py
  3. 26
      python/pyproject.toml
  4. 1
      python/requirements.dev.txt
  5. 11
      python/tests/test_io.py
  6. 10
      python/tests/test_model.py
  7. 27
      python/tests/test_store.py

@ -245,6 +245,8 @@ jobs:
working-directory: ./python working-directory: ./python
- run: python -m mypy generate_stubs.py tests --strict - run: python -m mypy generate_stubs.py tests --strict
working-directory: ./python working-directory: ./python
- run: python -m ruff check .
working-directory: ./python
python_msv: python_msv:
runs-on: ubuntu-latest runs-on: ubuntu-latest

@ -5,7 +5,7 @@ import inspect
import logging import logging
import re import re
import subprocess 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: def _path_to_type(*elements: str) -> ast.AST:
@ -107,7 +107,7 @@ def class_stubs(
methods: List[ast.AST] = [] methods: List[ast.AST] = []
magic_methods: List[ast.AST] = [] magic_methods: List[ast.AST] = []
for member_name, member_value in inspect.getmembers(cls_def): 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__": if member_name == "__init__":
try: try:
inspect.signature(cls_def) # we check it actually exists inspect.signature(cls_def) # we check it actually exists
@ -118,13 +118,14 @@ def class_stubs(
current_element_path, current_element_path,
types_to_import, types_to_import,
in_class=True, in_class=True,
) ),
] + methods *methods,
]
except ValueError as e: except ValueError as e:
if "no signature found" not in str(e): if "no signature found" not in str(e):
raise ValueError( raise ValueError(
f"Error while parsing signature of {cls_name}.__init__: {e}" f"Error while parsing signature of {cls_name}.__init_"
) ) from e
elif ( elif (
member_value == OBJECT_MEMBERS.get(member_name) member_value == OBJECT_MEMBERS.get(member_name)
or BUILTINS.get(member_name, ()) is None 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): for match in re.findall(r"^ *:type *([a-z_]+): ([^\n]*) *$", doc, re.MULTILINE):
if match[0] not in real_parameters: if match[0] not in real_parameters:
raise ValueError( 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] type = match[1]
if type.endswith(", optional"): if type.endswith(", optional"):
@ -277,7 +279,8 @@ def arguments_stub(
for param in real_parameters.values(): for param in real_parameters.values():
if param.name != "self" and param.name not in parsed_param_types: if param.name != "self" and param.name not in parsed_param_types:
raise ValueError( 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( param_ast = ast.arg(
arg=param.name, annotation=parsed_param_types.get(param.name) arg=param.name, annotation=parsed_param_types.get(param.name)
@ -288,11 +291,13 @@ def arguments_stub(
default_ast = ast.Constant(param.default) default_ast = ast.Constant(param.default)
if param.name not in optional_params: if param.name not in optional_params:
raise ValueError( 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: elif param.name in optional_params:
raise ValueError( 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: if param.kind == param.POSITIONAL_ONLY:
@ -329,14 +334,14 @@ def returns_stub(
if isinstance(builtin, tuple) and builtin[1] is not None: if isinstance(builtin, tuple) and builtin[1] is not None:
return builtin[1] return builtin[1]
raise ValueError( 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: if len(m) > 1:
return convert_type_from_doc(m[0], element_path, types_to_import)
else:
raise ValueError( raise ValueError(
f"Multiple return type annotations found with :rtype: for {'.'.join(element_path)}" 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( def convert_type_from_doc(
@ -368,9 +373,9 @@ def parse_type_to_ast(
stack: List[List[Any]] = [[]] stack: List[List[Any]] = [[]]
for token in tokens: for token in tokens:
if token == "(": if token == "(":
l: List[str] = [] children: List[str] = []
stack[-1].append(l) stack[-1].append(children)
stack.append(l) stack.append(children)
elif token == ")": elif token == ")":
stack.pop() stack.pop()
else: else:
@ -435,13 +440,12 @@ def parse_type_to_ast(
def build_doc_comment(doc: str) -> ast.Expr: 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 = [] clean_lines = []
for l in lines: for line in lines:
if l.startswith(":type") or l.startswith(":rtype"): if line.startswith((":type", ":rtype")):
continue continue
else: clean_lines.append(line)
clean_lines.append(l)
return ast.Expr(value=ast.Constant("\n".join(clean_lines).strip())) return ast.Expr(value=ast.Constant("\n".join(clean_lines).strip()))

@ -28,3 +28,29 @@ Documentation = "https://pyoxigraph.readthedocs.io/"
Homepage = "https://pyoxigraph.readthedocs.io/" Homepage = "https://pyoxigraph.readthedocs.io/"
Source = "https://github.com/oxigraph/oxigraph/tree/main/python" Source = "https://github.com/oxigraph/oxigraph/tree/main/python"
Tracker = "https://github.com/oxigraph/oxigraph/issues" 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"
]

@ -2,4 +2,5 @@ black~=23.1
furo furo
maturin~=0.14.0 maturin~=0.14.0
mypy~=1.0 mypy~=1.0
ruff~=0.0.255
sphinx~=5.3 sphinx~=5.3

@ -1,9 +1,8 @@
import unittest import unittest
from io import StringIO, BytesIO, UnsupportedOperation from io import BytesIO, StringIO, UnsupportedOperation
from tempfile import NamedTemporaryFile, TemporaryFile from tempfile import NamedTemporaryFile, TemporaryFile
from pyoxigraph import * from pyoxigraph import Literal, NamedNode, Quad, Triple, parse, serialize
EXAMPLE_TRIPLE = Triple( EXAMPLE_TRIPLE = Triple(
NamedNode("http://example.com/foo"), NamedNode("http://example.com/p"), Literal("1") NamedNode("http://example.com/foo"), NamedNode("http://example.com/p"), Literal("1")
@ -55,8 +54,7 @@ class TestParse(unittest.TestCase):
) )
def test_parse_io_error(self) -> None: def test_parse_io_error(self) -> None:
with self.assertRaises(UnsupportedOperation) as _: with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp:
with TemporaryFile("wb") as fp:
list(parse(fp, mime_type="application/n-triples")) list(parse(fp, mime_type="application/n-triples"))
def test_parse_quad(self) -> None: def test_parse_quad(self) -> None:
@ -89,8 +87,7 @@ class TestSerialize(unittest.TestCase):
) )
def test_serialize_io_error(self) -> None: def test_serialize_io_error(self) -> None:
with self.assertRaises(UnsupportedOperation) as _: with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("rb") as fp:
with TemporaryFile("rb") as fp:
serialize([EXAMPLE_TRIPLE], fp, "text/turtle") serialize([EXAMPLE_TRIPLE], fp, "text/turtle")
def test_serialize_quad(self) -> None: def test_serialize_quad(self) -> None:

@ -1,6 +1,14 @@
import unittest 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_STRING = NamedNode("http://www.w3.org/2001/XMLSchema#string")
XSD_INTEGER = NamedNode("http://www.w3.org/2001/XMLSchema#integer") XSD_INTEGER = NamedNode("http://www.w3.org/2001/XMLSchema#integer")

@ -1,10 +1,21 @@
import os
import unittest import unittest
from io import BytesIO, UnsupportedOperation from io import BytesIO, UnsupportedOperation
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory, TemporaryFile from tempfile import NamedTemporaryFile, TemporaryDirectory, TemporaryFile
from typing import Any from typing import Any
from pyoxigraph import * from pyoxigraph import (
BlankNode,
DefaultGraph,
NamedNode,
Quad,
QuerySolution,
QuerySolutions,
QueryTriples,
Store,
Triple,
Variable,
)
foo = NamedNode("http://foo") foo = NamedNode("http://foo")
bar = NamedNode("http://bar") bar = NamedNode("http://bar")
@ -259,12 +270,11 @@ class TestStore(unittest.TestCase):
fp.write(b"<http://foo> <http://bar> <http://baz> <http://graph>.") fp.write(b"<http://foo> <http://bar> <http://baz> <http://graph>.")
store = Store() store = Store()
store.load(file_name, mime_type="application/n-quads") 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)}) self.assertEqual(set(store), {Quad(foo, bar, baz, graph)})
def test_load_with_io_error(self) -> None: def test_load_with_io_error(self) -> None:
with self.assertRaises(UnsupportedOperation) as _: with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp:
with TemporaryFile("wb") as fp:
Store().load(fp, mime_type="application/n-triples") Store().load(fp, mime_type="application/n-triples")
def test_dump_ntriples(self) -> None: def test_dump_ntriples(self) -> None:
@ -304,16 +314,13 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, baz, graph))
store.dump(file_name, "application/n-quads") store.dump(file_name, "application/n-quads")
with open(file_name, "rt") as fp:
file_content = fp.read()
self.assertEqual( self.assertEqual(
file_content, Path(file_name).read_text(),
"<http://foo> <http://bar> <http://baz> <http://graph> .\n", "<http://foo> <http://bar> <http://baz> <http://graph> .\n",
) )
def test_dump_with_io_error(self) -> None: def test_dump_with_io_error(self) -> None:
with self.assertRaises(OSError) as _: with self.assertRaises(OSError) as _, TemporaryFile("rb") as fp:
with TemporaryFile("rb") as fp:
Store().dump(fp, mime_type="application/rdf+xml") Store().dump(fp, mime_type="application/rdf+xml")
def test_write_in_read(self) -> None: def test_write_in_read(self) -> None:

Loading…
Cancel
Save