diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5cb6038..1976015 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,8 +2,8 @@ name: CI on: push: - branches: - - main + branches-ignore: + - "wip*" tags: - "v*" pull_request: @@ -56,6 +56,7 @@ jobs: pypy3.9 pypy3.10 allow-prereleases: true + - name: Set up uv uses: hynek/setup-cached-uv@v2 - name: Set up nox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37ca89f..0e4c602 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,20 +24,10 @@ repos: - id: fmt - id: clippy args: [--fix, --allow-staged] - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 - hooks: - - id: pyupgrade - repo: https://github.com/psf/black rev: 24.10.0 hooks: - - name: black - id: black - args: ["--line-length", "79"] + - id: black - repo: https://github.com/pre-commit/mirrors-prettier rev: "v4.0.0-alpha.8" hooks: diff --git a/noxfile.py b/noxfile.py index 1fff7b6..72551ea 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,8 +5,15 @@ import nox ROOT = Path(__file__).parent -TESTS = ROOT / "tests" PYPROJECT = ROOT / "pyproject.toml" +TESTS = ROOT / "tests" + +REQUIREMENTS = dict( + tests=TESTS / "requirements.txt", +) +REQUIREMENTS_IN = [ # this is actually ordered, as files depend on each other + (path.parent / f"{path.stem}.in", path) for path in REQUIREMENTS.values() +] SUPPORTED = ["3.9", "3.10", "pypy3.10", "3.11", "3.12", "3.13"] LATEST = SUPPORTED[-1] @@ -39,7 +46,7 @@ def tests(session): if session.posargs and session.posargs[0] == "coverage": if len(session.posargs) > 1 and session.posargs[1] == "github": - github = os.environ["GITHUB_STEP_SUMMARY"] + github = Path(os.environ["GITHUB_STEP_SUMMARY"]) else: github = None @@ -48,7 +55,7 @@ def tests(session): if github is None: session.run("coverage", "report") else: - with open(github, "a") as summary: + with github.open("a") as summary: summary.write("### Coverage\n\n") summary.flush() # without a flush, output seems out of order. session.run( @@ -61,6 +68,15 @@ def tests(session): 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): """ @@ -70,3 +86,40 @@ def build(session): 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", TESTS, __file__) + + +@session() +def typing(session): + """ + Check the codebase using pyright by type checking the test suite. + """ + session.install("pyright", ROOT, "-r", REQUIREMENTS["tests"]) + session.run("pyright", TESTS) + + +@session(default=False) +def requirements(session): + """ + Update the project's pinned requirements. + + You should commit the result afterwards. + """ + if session.venv_backend == "uv": + cmd = ["uv", "pip", "compile"] + else: + session.install("pip-tools") + cmd = ["pip-compile", "--resolver", "backtracking", "--strip-extras"] + + for each, out in REQUIREMENTS_IN: + # otherwise output files end up with silly absolute path comments... + relative = each.relative_to(ROOT) + session.run(*cmd, "--upgrade", "--output-file", out, relative) diff --git a/pyproject.toml b/pyproject.toml index 8f61a57..470668d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=1.0,<2.0"] +requires = ["maturin>=1.2,<2.0"] build-backend = "maturin" [project] @@ -7,8 +7,6 @@ name = "regress" description = "Python bindings to Rust's regress ECMA regular expressions library" requires-python = ">=3.9" readme = "README.rst" -license = {text = "MIT"} -requires-python = ">=3.8" keywords = ["regex", "rust", "javascript", "ecmascript", "regular expressions"] authors = [ { name = "Julian Berman", email = "Julian+regress@GrayVines.com" }, @@ -37,11 +35,78 @@ Funding = "https://github.com/sponsors/Julian" Source = "https://github.com/crate-py/regress" Upstream = "https://github.com/ridiculousfish/regress" -[tool.isort] -combine_as_imports = true -from_first = true -include_trailing_comma = true -multi_line_output = 3 - [tool.maturin] features = ["pyo3/extension-module"] + +[tool.pyright] +reportUnnecessaryTypeIgnoreComment = true +strict = ["**/*"] +exclude = [ + "**/tests/__init__.py", + "**/tests/test_*.py", +] + +[tool.ruff] +line-length = 79 + +[tool.ruff.lint] +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 + "B006", # Mutable arguments require care but are OK if you don't abuse them + "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 + "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.9 +] + +[tool.ruff.lint.flake8-pytest-style] +mark-parentheses = false + +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" + +[tool.ruff.lint.isort] +combine-as-imports = true +from-first = true +known-first-party = ["regress"] + +[tool.ruff.lint.per-file-ignores] +"noxfile.py" = ["ANN", "D100", "S101", "T201"] +"docs/*" = ["ANN", "D", "INP001"] +"tests/*" = ["ANN", "B018", "D", "PLR", "RUF012", "S", "SIM", "TRY"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/requirements.in b/tests/requirements.in index e079f8a..700f15c 100644 --- a/tests/requirements.in +++ b/tests/requirements.in @@ -1 +1,2 @@ +file:. pytest diff --git a/tests/requirements.txt b/tests/requirements.txt index 51980dd..a61e897 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,14 +1,12 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile tests/requirements.in -# +# This file was autogenerated by uv via the following command: +# uv pip compile --output-file /Users/julian/Development/regress/tests/requirements.txt tests/requirements.in iniconfig==2.0.0 # via pytest -packaging==23.2 +packaging==24.1 # via pytest -pluggy==1.3.0 +pluggy==1.5.0 # via pytest -pytest==7.4.3 +pytest==8.3.3 + # via -r tests/requirements.in +file:. # via -r tests/requirements.in diff --git a/tests/test_regress.py b/tests/test_regress.py index 399dcf7..f536e43 100644 --- a/tests/test_regress.py +++ b/tests/test_regress.py @@ -1,3 +1,5 @@ +import pytest + import regress @@ -57,4 +59,4 @@ def test_error_handling(): except regress.RegressError: pass else: - assert False, "error not reached" + pytest.fail("error not reached")