Skip to content

Commit

Permalink
Enable the rest of the ruff rulesets.
Browse files Browse the repository at this point in the history
Bring the rest of the scaffolding up to date.
  • Loading branch information
Julian committed Dec 17, 2023
1 parent d0ba48e commit 98e1c7c
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 56 deletions.
123 changes: 115 additions & 8 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,159 @@
from pathlib import Path
from tempfile import TemporaryDirectory
import os

import nox

ROOT = Path(__file__).parent
TESTS = ROOT / "tests"
PYPROJECT = ROOT / "pyproject.toml"
DOCS = ROOT / "docs"

REQUIREMENTS = dict(
docs=DOCS / "requirements.txt",
tests=TESTS / "requirements.txt",
)
REQUIREMENTS_IN = [ # this is actually ordered, as files depend on each other
path.parent / f"{path.stem}.in" for path in REQUIREMENTS.values()
]

SUPPORTED = ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"]
LATEST = "3.12"

nox.options.sessions = []


def session(default=True, **kwargs):
def session(default=True, python=LATEST, **kwargs): # noqa: D103
def _session(fn):
if default:
nox.options.sessions.append(kwargs.get("name", fn.__name__))
return nox.session(**kwargs)(fn)
return nox.session(python=python, **kwargs)(fn)

return _session


@session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3"])
@session(python=SUPPORTED)
def tests(session):
session.install(ROOT, "-r", TESTS / "requirements.txt")
if session.posargs == ["coverage"]:
"""
Run the test suite with a corresponding Python version.
"""
session.install("-r", REQUIREMENTS["tests"])

if session.posargs and session.posargs[0] == "coverage":
if len(session.posargs) > 1 and session.posargs[1] == "github":
github = Path(os.environ["GITHUB_STEP_SUMMARY"])
else:
github = None

session.install("coverage[toml]")
session.run("coverage", "run", "-m", "pytest")
session.run("coverage", "report")
session.run("coverage", "run", "-m", "pytest", TESTS)
if github is None:
session.run("coverage", "report")
else:
with github.open("a") as summary:
summary.write("### Coverage\n\n")
summary.flush() # without a flush, output seems out of order.
session.run(
"coverage",
"report",
"--format=markdown",
stdout=summary,
)
else:
session.run("pytest", *session.posargs, TESTS)


@session()
def audit(session):
"""
Audit dependencies for vulnerabilities.
"""
session.install("pip-audit", ROOT)
session.run("python", "-m", "pip_audit")


@session(tags=["build"])
def build(session):
"""
Build a distribution suitable for PyPI and check its validity.
"""
session.install("build", "twine")
with TemporaryDirectory() as tmpdir:
session.run("python", "-m", "build", ROOT, "--outdir", tmpdir)
session.run("twine", "check", "--strict", tmpdir + "/*")


@session(tags=["style"])
def style(session):
"""
Check Python code style.
"""
session.install("ruff")
session.run("ruff", "check", ROOT)


@session(tags=["docs"])
@nox.parametrize(
"builder",
[
nox.param(name, id=name)
for name in [
"dirhtml",
"doctest",
"linkcheck",
"man",
"spelling",
]
],
)
def docs(session, builder):
"""
Build the documentation using a specific Sphinx builder.
"""
session.install("-r", REQUIREMENTS["docs"])
with TemporaryDirectory() as tmpdir_str:
tmpdir = Path(tmpdir_str)
argv = ["-n", "-T", "-W"]
if builder != "spelling":
argv += ["-q"]
posargs = session.posargs or [tmpdir / builder]
session.run(
"python",
"-m",
"sphinx",
"-b",
builder,
DOCS,
*argv,
*posargs,
)


@session(tags=["docs", "style"], name="docs(style)")
def docs_style(session):
"""
Check the documentation style.
"""
session.install(
"doc8",
"pygments",
"pygments-github-lexers",
)
session.run("python", "-m", "doc8", "--config", PYPROJECT, DOCS)


@session(default=False)
def requirements(session):
"""
Update the project's pinned requirements. Commit the result.
"""
session.install("pip-tools")
for each in [TESTS / "requirements.in"]:
for each in REQUIREMENTS_IN:
session.run(
"pip-compile",
"--resolver",
"backtracking",
"--strip-extras",
"-U",
each.relative_to(ROOT),
)
97 changes: 96 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ build-backend = "maturin"
[project]
name = "rpds-py"
description = "Python bindings to Rust's persistent data structures (rpds)"
requires-python = ">=3.8"
readme = "README.rst"
license = {text = "MIT"}
requires-python = ">=3.8"
keywords = ["data structures", "rust", "persistent"]
authors = [
{email = "Julian+rpds@GrayVines.com"},
Expand All @@ -31,6 +31,7 @@ classifiers = [
dynamic = ["version"]

[project.urls]
Documentation = "https://rpds.readthedocs.io/"
Homepage = "https://github.com/crate-py/rpds"
Issues = "https://github.com/crate-py/rpds/issues/"
Funding = "https://github.com/sponsors/Julian"
Expand All @@ -40,12 +41,106 @@ Source = "https://github.com/crate-py/rpds"
[tool.black]
line-length = 79

[tool.coverage.html]
show_contexts = true
skip_covered = false

[tool.coverage.run]
branch = true
dynamic_context = "test_function"

[tool.coverage.report]
exclude_also = [
"if TYPE_CHECKING:",
"\\s*\\.\\.\\.\\s*",
]
fail_under = 100
show_missing = true
skip_covered = true

[tool.doc8]
ignore = [
"D000", # see PyCQA/doc8#125
"D001", # one sentence per line, so max length doesn't make sense
]

[tool.isort]
combine_as_imports = true
ensure_newline_before_comments = true
from_first = true
include_trailing_comma = true
multi_line_output = 3
known_first_party = ["rpds"]
use_parentheses = true

[tool.maturin]
features = ["pyo3/extension-module"]

[tool.pyright]
reportUnnecessaryTypeIgnoreComment = true
strict = ["**/*"]
exclude = [
"**/tests/__init__.py",
"**/tests/test_*.py",
]

[tool.ruff]
line-length = 79
select = ["ALL"]
ignore = [
"A001", # It's fine to shadow builtins
"A002",
"A003",
"ARG", # This is all wrong whenever an interface is involved
"ANN", # Just let the type checker do this
"B008", # It's totally OK to call functions for default arguments.
"B904", # raise SomeException(...) is fine.
"B905", # No need for explicit strict, this is simply zip's default behavior
"C408", # Calling dict is fine when it saves quoting the keys
"C901", # Not really something to focus on
"D105", # It's fine to not have docstrings for magic methods.
"D107", # __init__ especially doesn't need a docstring
"D200", # This rule makes diffs uglier when expanding docstrings
"D203", # No blank lines before docstrings.
"D212", # Start docstrings on the second line.
"D400", # This rule misses sassy docstrings ending with ! or ?
"D401", # This rule is too flaky.
"D406", # Section headers should end with a colon not a newline
"D407", # Underlines aren't needed
"D412", # Plz spaces after section headers
"EM101", # These don't bother me, it's fine there's some duplication.
"EM102",
"FBT", # It's worth avoiding boolean args but I don't care to enforce it
"FIX", # Yes thanks, if I could it wouldn't be there
"I001", # We can't yet use ruff's isort
"N", # These naming rules are silly
"PLR0912", # These metrics are fine to be aware of but not to enforce
"PLR0913",
"PLR0915",
"PLW2901", # Shadowing for loop variables is occasionally fine.
"PT006", # pytest parametrize takes strings as well
"PYI025", # wat, I'm not confused, thanks.
"RET502", # Returning None implicitly is fine
"RET503",
"RET505", # These push you to use `if` instead of `elif`, but for no reason
"RET506",
"RSE102", # Ha, what, who even knew you could leave the parens off. But no.
"SIM300", # Not sure what heuristic this uses, but it's easily incorrect
"SLF001", # Private usage within this package itself is fine
"TD", # These TODO style rules are also silly
"UP007", # We support 3.8 + 3.9
]
[tool.ruff.lint.flake8-pytest-style]
mark-parentheses = false

[tool.ruff.flake8-quotes]
docstring-quotes = "double"

[tool.ruff.lint.isort]
combine-as-imports = true
from-first = true

[tool.ruff.per-file-ignores]
"noxfile.py" = ["ANN", "D100", "S101", "T201"]
"docs/*" = ["ANN", "D", "INP001"]
"tests/*" = ["ANN", "B018", "D", "PLR", "RUF012", "S", "SIM", "TRY"]
73 changes: 37 additions & 36 deletions rpds.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import (
FrozenSet,
ItemsView,
Iterable,
Iterator,
Expand All @@ -9,55 +8,57 @@ from typing import (
ValuesView,
)

T = TypeVar("T")
KT = TypeVar("KT", covariant=True)
VT = TypeVar("VT", covariant=True)
_T = TypeVar("_T")
_KT_co = TypeVar("_KT_co", covariant=True)
_VT_co = TypeVar("_VT_co", covariant=True)

class HashTrieMap(Mapping[KT, VT]):
class HashTrieMap(Mapping[_KT_co, _VT_co]):
def __init__(
self,
value: Mapping[KT, VT] | Iterable[tuple[KT, VT]] = {},
**kwds: Mapping[KT, VT],
value: Mapping[_KT_co, _VT_co] | Iterable[tuple[_KT_co, _VT_co]] = {},
**kwds: Mapping[_KT_co, _VT_co],
): ...
def __getitem__(self, key: KT) -> VT: ...
def __iter__(self) -> Iterator[KT]: ...
def __getitem__(self, key: _KT_co) -> _VT_co: ...
def __iter__(self) -> Iterator[_KT_co]: ...
def __len__(self) -> int: ...
def discard(self, key: KT) -> "HashTrieMap[KT, VT]": ...
def items(self) -> ItemsView[KT, VT]: ...
def keys(self) -> KeysView[KT]: ...
def values(self) -> ValuesView[VT]: ...
def remove(self, key: KT) -> "HashTrieMap[KT, VT]": ...
def insert(self, key: KT, val: VT) -> "HashTrieMap[KT, VT]": ...
def discard(self, key: _KT_co) -> HashTrieMap[_KT_co, _VT_co]: ...
def items(self) -> ItemsView[_KT_co, _VT_co]: ...
def keys(self) -> KeysView[_KT_co]: ...
def values(self) -> ValuesView[_VT_co]: ...
def remove(self, key: _KT_co) -> HashTrieMap[_KT_co, _VT_co]: ...
def insert(
self, key: _KT_co, val: _VT_co
) -> HashTrieMap[_KT_co, _VT_co]: ...
def update(self, *args: Mapping): ...
@classmethod
def convert(
cls,
value: Mapping[KT, VT] | Iterable[tuple[KT, VT]],
) -> "HashTrieMap[KT, VT]": ...
value: Mapping[_KT_co, _VT_co] | Iterable[tuple[_KT_co, _VT_co]],
) -> HashTrieMap[_KT_co, _VT_co]: ...

class HashTrieSet(FrozenSet[T]):
def __init__(self, value: Iterable[T] = ()): ...
def __iter__(self) -> Iterator[T]: ...
class HashTrieSet(frozenset[_T]):
def __init__(self, value: Iterable[_T] = ()): ...
def __iter__(self) -> Iterator[_T]: ...
def __len__(self) -> int: ...
def discard(self, value: T) -> "HashTrieSet[T]": ...
def remove(self, value: T) -> "HashTrieSet[T]": ...
def insert(self, value: T) -> "HashTrieSet[T]": ...
def update(self, *args: Iterable[T]) -> "HashTrieSet[T]": ...
def discard(self, value: _T) -> HashTrieSet[_T]: ...
def remove(self, value: _T) -> HashTrieSet[_T]: ...
def insert(self, value: _T) -> HashTrieSet[_T]: ...
def update(self, *args: Iterable[_T]) -> HashTrieSet[_T]: ...

class List(Iterable[T]):
def __init__(self, value: Iterable[T] = (), *more: T): ...
def __iter__(self) -> Iterator[T]: ...
class List(Iterable[_T]):
def __init__(self, value: Iterable[_T] = (), *more: _T): ...
def __iter__(self) -> Iterator[_T]: ...
def __len__(self) -> int: ...
def push_front(self, value: T) -> "List[T]": ...
def drop_first(self) -> "List[T]": ...
def push_front(self, value: _T) -> List[_T]: ...
def drop_first(self) -> List[_T]: ...

class Queue(Iterable[T]):
def __init__(self, value: Iterable[T] = (), *more: T): ...
def __iter__(self) -> Iterator[T]: ...
class Queue(Iterable[_T]):
def __init__(self, value: Iterable[_T] = (), *more: _T): ...
def __iter__(self) -> Iterator[_T]: ...
def __len__(self) -> int: ...
def enqueue(self, T) -> "Queue[T]": ...
def dequeue(self, T) -> "Queue[T]": ...
def enqueue(self, _T) -> Queue[_T]: ...
def dequeue(self, _T) -> Queue[_T]: ...
@property
def is_empty(self) -> T: ...
def is_empty(self) -> _T: ...
@property
def peek(self) -> T: ...
def peek(self) -> _T: ...
Empty file added tests/__init__.py
Empty file.

0 comments on commit 98e1c7c

Please sign in to comment.