Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog/1168.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use ``pyrefly`` for project type checking and require 100% package type coverage - by :user:`gaborbernat`.
5 changes: 2 additions & 3 deletions docs/development/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ build follows modern Python code style conventions enforced by `ruff <https://do
a pull request, run the linter to ensure your code meets the project's style guidelines. The ruff configuration is
defined in the pyproject.toml file and includes both formatting and linting rules.

The project also uses type annotations throughout the codebase. All new code should include appropriate type hints, and
changes to existing code should preserve or improve type annotations. Use `pyright
<https://microsoft.github.io/pyright/>`_ for type checking to verify your type annotations are correct.
The project uses type annotations throughout the codebase. Add type hints to new code and preserve or improve existing
annotations. The ``type`` tox environment runs `Pyrefly <https://pyrefly.org/>`_ and requires full package coverage.

***************
Documentation
Expand Down
52 changes: 17 additions & 35 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ test = [
{ include-group = "extra" },
]
typing = [
"colorama",
"importlib-metadata >= 5.1",
"tomli",
"typing-extensions >= 4.0.0",
Expand All @@ -116,8 +117,8 @@ typing = [
{ include-group = "test-core" },
{ include-group = "extra" },
]
mypy = [
"mypy ~= 2.3.0",
pyrefly = [
"pyrefly >= 1.2",
{ include-group = "typing" },
]
release = [
Expand All @@ -130,7 +131,7 @@ release = [
dev = [
"flit-core",
{ include-group = "test" },
{ include-group = "mypy" },
{ include-group = "pyrefly" },
]

[tool.flit.sdist]
Expand Down Expand Up @@ -191,38 +192,19 @@ filterwarnings = [
"ignore:os.path.commonprefix:DeprecationWarning" # https://github.com/pypa/pyproject-hooks/pull/222
]

[tool.mypy]
files = ["src", "tests", "tasks", "docs"]
exclude = ["tests/packages"]
python_version = "3.10"
native_parser = true
strict = true
disallow_any_explicit = true
disallow_any_decorated = true
disallow_any_unimported = true
disallow_untyped_globals = true
disallow_redefinition = true
warn_unused_configs = true
warn_unreachable = true
enable_error_code = [
"deprecated",
"exhaustive-match",
"ignore-without-code",
"mutable-override",
"possibly-undefined",
"redundant-expr",
"redundant-self",
"truthy-bool",
"truthy-iterable",
"unimported-reveal",
"unused-awaitable",
]

[[tool.mypy.overrides]]
module = [
"virtualenv", # Optional dependency
]
ignore_missing_imports = true
[tool.pyrefly]
project-includes = ["src", "tests", "tasks", "docs"]
project-excludes = ["**/tests/packages*"]
search-path = ["src", "tests"]
python-version = "3.10"
preset = "all"

[tool.pyrefly.errors]
# `@override` needs typing_extensions before 3.12; build takes no runtime deps for typing
missing-override-decorator = false
# These style rules reject intentional truthiness checks and side-effect calls throughout the project.
implicit-bool = false
unused-call-result = false

[tool.ruff]
exclude = [
Expand Down
9 changes: 7 additions & 2 deletions src/build/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,15 @@ def get_requires_for_build(
"""
_ctx.log(f'Getting build dependencies for {distribution}...', kind=('step',))
hook_name = f'get_requires_for_build_{distribution}'
get_requires = getattr(self._hook, hook_name)

with self._handle_backend(hook_name):
return set(get_requires(config_settings))
if distribution == 'editable':
requires = self._hook.get_requires_for_build_editable(config_settings)
elif distribution == 'sdist':
requires = self._hook.get_requires_for_build_sdist(config_settings)
else:
requires = self._hook.get_requires_for_build_wheel(config_settings)
return set(requires)

def check_dependencies(
self,
Expand Down
7 changes: 3 additions & 4 deletions src/build/_ctx.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,16 @@ class Logger(typing.Protocol): # pragma: no cover
def __call__(self, message: str, *, kind: tuple[str, ...] | None = None) -> None: ...


_package_name = __spec__.parent
_default_logger = logging.getLogger(_package_name)
_default_logger = logging.getLogger(typing.cast('str', __spec__.parent))


def _log_default(message: str, *, kind: tuple[str, ...] | None = None) -> None: # noqa: ARG001
# the log function that works in tests, real log function is set in __main__
_default_logger.log(logging.INFO, message, stacklevel=2)


LOGGER = contextvars.ContextVar('LOGGER', default=_log_default)
VERBOSITY = contextvars.ContextVar('VERBOSITY', default=0)
LOGGER = contextvars.ContextVar[Logger]('LOGGER', default=_log_default)
VERBOSITY = contextvars.ContextVar[int]('VERBOSITY', default=0)


def log_subprocess_error(error: subprocess.CalledProcessError) -> None:
Expand Down
4 changes: 2 additions & 2 deletions src/build/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(
description: str | None = None,
) -> None:
super().__init__()
self.exception = exception
self.exception: Exception = exception
self._description = description

def __str__(self) -> str:
Expand All @@ -49,7 +49,7 @@ class FailedProcessError(Exception):

def __init__(self, exception: subprocess.CalledProcessError, description: str) -> None:
super().__init__()
self.exception = exception
self.exception: subprocess.CalledProcessError = exception
self._description = description

def __str__(self) -> str:
Expand Down
6 changes: 4 additions & 2 deletions src/build/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@

# A decoded JSON value, as produced by ``json.load``. Uses the covariant ``Sequence``/``Mapping``
# so concrete literals (e.g. ``str | list[str]``) are also assignable to it.
JSONValue = str | int | float | bool | None | collections.abc.Sequence['JSONValue'] | collections.abc.Mapping[str, 'JSONValue']
JSONValue: typing.TypeAlias = (
str | int | float | bool | collections.abc.Sequence['JSONValue'] | collections.abc.Mapping[str, 'JSONValue'] | None
)

# A value as produced by ``tomllib`` when parsing ``pyproject.toml``. Uses the covariant
# ``Sequence``/``Mapping`` so concrete literals (e.g. ``dict[str, list[str]]``) are assignable.
TOMLValue = (
TOMLValue: typing.TypeAlias = (
str
| int
| float
Expand Down
10 changes: 7 additions & 3 deletions src/build/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class _DistArgs(typing.TypedDict, total=False):

Installer = typing.Literal['pip', 'uv']

INSTALLERS = typing.get_args(Installer)
INSTALLERS: tuple[Installer, ...] = typing.get_args(Installer)


class IsolatedEnv(typing.Protocol):
Expand Down Expand Up @@ -107,6 +107,9 @@ class DefaultIsolatedEnv(IsolatedEnv):
Isolated environment which supports several different underlying implementations.
"""

_env_backend: _EnvBackend
_path: str

def __init__(
self,
*,
Expand Down Expand Up @@ -142,8 +145,6 @@ def __enter__(self) -> Self:
path = os.path.realpath(path)
self._path = path

self._env_backend: _EnvBackend

# uv is opt-in only.
if self.installer == 'uv':
self._env_backend = _UvBackend()
Expand Down Expand Up @@ -437,6 +438,9 @@ def display_name(self) -> str:


class _UvBackend(_EnvBackend):
_env_path: str
_uv_bin: str

def create(self, path: str) -> None:
import venv

Expand Down
3 changes: 2 additions & 1 deletion tasks/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pathlib import Path
from subprocess import call, check_call
from typing import cast

from git import Commit, Remote, Repo, TagReference
from packaging.version import Version
Expand Down Expand Up @@ -39,7 +40,7 @@ def main(version_str: str, *, push: bool) -> None:
def resolve_version(version_str: str, repo: Repo) -> Version:
if version_str not in {'auto', 'major', 'minor', 'patch'}:
return Version(version_str)
parts = [int(x) for x in repo.git.describe('--tags', '--abbrev=0').lstrip('v').split('.')[:3]]
parts = [int(part) for part in cast('str', repo.git.describe('--tags', '--abbrev=0')).lstrip('v').split('.')[:3]]
match detect_bump() if version_str == 'auto' else version_str:
case 'major':
parts = [parts[0] + 1, 0, 0]
Expand Down
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from collections.abc import Callable, Generator
from functools import partial, update_wrapper
from pathlib import Path
from typing import Protocol
from typing import Protocol, cast

import pytest

Expand Down Expand Up @@ -53,7 +53,7 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item
elif config.getoption('--only-integration'): # pragma: no cover
item.add_marker(skip_other)
# run integration tests after unit tests
items.sort(key=lambda i: 1 if is_integration(i) else 0)
items.sort(key=is_integration)


def _xfail_isolated_strict(item: pytest.Item, *, is_integration_file: bool) -> bool:
Expand Down Expand Up @@ -123,7 +123,7 @@ def is_setuptools(package_path: Path) -> bool:
pyproject = package_path / 'pyproject.toml'
try:
with pyproject.open('rb') as f:
pp = tomllib.load(f)
pp = cast(dict[str, dict[str, str]], tomllib.load(f))
except (FileNotFoundError, ValueError):
return True
return 'setuptools' in pp.get('build-system', {}).get('build-backend', 'setuptools')
Expand Down
3 changes: 2 additions & 1 deletion tests/integration_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import os
import shutil
import time
import typing
import urllib.request

from pathlib import Path
Expand Down Expand Up @@ -44,7 +45,7 @@ def download_archive(name: str) -> tuple[Path, str]:
for attempt in range(1, ATTEMPTS + 1):
try:
with urllib.request.urlopen(url) as request, partial.open('wb') as file_handler:
shutil.copyfileobj(request, file_handler)
shutil.copyfileobj(typing.cast(http.client.HTTPResponse, request), file_handler)
except NETWORK_ERRORS as exception: # noqa: PERF203
partial.unlink(missing_ok=True)
if attempt == ATTEMPTS:
Expand Down
6 changes: 3 additions & 3 deletions tests/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_make_extra_environ_overrides_pythonpath() -> None:

def test_installed_versions(mocker: pytest_mock.MockerFixture) -> None:
env = build.env.DefaultIsolatedEnv()
env._env_backend = SimpleNamespace(purelib='/purelib')
mocker.patch.object(env, '_env_backend', SimpleNamespace(purelib='/purelib'), create=True)
distributions = mocker.patch(
'build._compat.importlib.metadata.distributions',
return_value=[
Expand Down Expand Up @@ -567,7 +567,7 @@ def test_virtualenv_no_wheel_flag(
backend = build.env._PipBackend()
backend.create('/some/path')

call_args = cli_run.call_args[0][0]
call_args = typing.cast(list[str], cli_run.call_args.args[0])
assert ('--no-wheel' in call_args) is has_no_wheel


Expand All @@ -581,7 +581,7 @@ def test_install_dependencies_with_outer_pip(
with build.env.DefaultIsolatedEnv() as env:
env.install(['some-package'])

cmd = run_subprocess.call_args_list[-1][0][0]
cmd = typing.cast(list[str], run_subprocess.call_args_list[-1].args[0])
assert cmd[:4] == [sys.executable, '-m', 'pip', '--python']


Expand Down
24 changes: 19 additions & 5 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import zipfile

from collections.abc import Callable, Generator
from typing import TYPE_CHECKING, Protocol, TypedDict
from typing import TYPE_CHECKING, Protocol, TypedDict, cast

import pytest
import pytest_mock
Expand Down Expand Up @@ -59,6 +59,19 @@ class BuildKwargs(TypedDict):
env_dir: str | None


class ArtifactReport(TypedDict):
name: str
path: str
kind: str
size: int
hashes: dict[str, str]


class BuildReport(TypedDict):
version: str
artifacts: list[ArtifactReport]


def make_kwargs(
*,
distributions: list[str] | None = None,
Expand Down Expand Up @@ -300,7 +313,7 @@ def test_build_package_via_sdist_passes_config_settings_to_build(mocker: pytest_
)

assert built == ['demo-1.0.0.tar.gz', 'demo-1.0.0-py3-none-any.whl']
extractall = tar_open.return_value.__enter__.return_value.extractall
extractall = cast(unittest.mock.MagicMock, tar_open.return_value.__enter__.return_value.extractall)
extractall.assert_called_once()
assert extractall.call_args.args[0] == 'temp-sdist-dir'
build_cmd.assert_has_calls(
Expand Down Expand Up @@ -818,7 +831,7 @@ def test_metadata_json_output(
build.__main__.main([package_test_setuptools, '--metadata', '-n'])

stdout = capsys.readouterr().out
metadata = json.loads(stdout)
metadata = cast(dict[str, str], json.loads(stdout))
# Name normalised in old versions of setuptools.
assert metadata['name'] in {'test_setuptools', 'test-setuptools'}
assert metadata['version'] == '1.0.0'
Expand Down Expand Up @@ -916,7 +929,8 @@ def test_log_dependency_versions(mocker: pytest_mock.MockerFixture) -> None:

def test_log_dependency_versions_none(mocker: pytest_mock.MockerFixture) -> None:
env = mocker.create_autospec(build.env.DefaultIsolatedEnv, instance=True)
env.installed_versions.return_value = {}
installed: dict[str, str] = {}
env.installed_versions.return_value = installed
log = mocker.patch('build.__main__._ctx.log')

build.__main__._log_dependency_versions(env, set())
Expand Down Expand Up @@ -1370,7 +1384,7 @@ def test_report_written(

build.__main__.main([str(tmp_path), '-o', str(outdir), '--report', str(report)])

payload = json.loads(report.read_text(encoding='utf-8'))
payload = cast(BuildReport, json.loads(report.read_text(encoding='utf-8')))
assert payload['version'] == '1.0'
assert [artifact['name'] for artifact in payload['artifacts']] == names
for artifact, name in zip(payload['artifacts'], names, strict=True):
Expand Down
Loading
Loading