diff --git a/CHANGES b/CHANGES index 896425e9..ba893f33 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,18 @@ $ uv add libvcs --prerelease allow _Notes on the upcoming release will go here._ +### Fixes + +#### Progress callback timestamps carry a time zone (#549) + +The `timestamp` passed to a +{class}`~libvcs._internal.run.ProgressCallbackProtocol` is now an aware +`datetime` in UTC. It was a naive local-time value, so timestamps taken on +hosts in different zones compared and serialized as though they named the +same instant, and a DST rollover made them ambiguous on a single host. +Callbacks that format the value for display are unaffected; callbacks that +compare, subtract, or serialize it will see the offset. + ### Documentation #### Class fields describe themselves in the API reference (#548) @@ -38,6 +50,22 @@ Workflow actions moved to their current major releases: `actions/checkout` v7, and `dorny/paths-filter` v4. Workflow behavior is unchanged, though setup-uv no longer prunes the uv cache, so the first run after this repopulates it. +#### Ruff moves to 0.16 (#549) + +Minimum `ruff>=0.16.0` (was unpinned). Contributors need the newer formatter: +`ruff format` descends into Python code blocks inside Markdown, so +`just ruff-format` now reaches `README.md` and the design notes alongside +`src/` and `tests/`. + +Linting also picks up ruff's own default rule set. The project used to name +its linters with `select`, which replaces the defaults rather than adding to +them; `extend-select` layers the same list on top instead, so `just ruff` now +enforces the rules ruff recommends out of the box as well as the ones libvcs +asks for. Rules that flag a deliberate idiom — the blind excepts that let +{class}`~libvcs._internal.query_list.QueryList` treat an unresolvable lookup +as a non-match, `subprocess.Popen`'s `preexec_fn` pass-through — are scoped +per file with the reason recorded in `pyproject.toml`. + ## libvcs 0.45.1 (2026-07-11) libvcs 0.45.1 unbreaks test suites that build their documentation before running pytest: the bundled pytest plugin no longer overrides pytest's own collection-ignore rules, so generated `_build/` output stops aborting collection. The documentation now runs every Python example as a doctest against real temporary repositories, which surfaced and corrected examples that had drifted from the API. diff --git a/README.md b/README.md index abddc30b..00cfd238 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,7 @@ from libvcs.sync.git import GitSync repo = GitSync( url="https://github.com/vcs-python/libvcs", path=pathlib.Path.cwd() / "libvcs", - remotes={ - 'gitlab': 'https://gitlab.com/vcs-python/libvcs' - } + remotes={"gitlab": "https://gitlab.com/vcs-python/libvcs"}, ) # Clone (if not exists) or fetch & update (if exists) @@ -89,19 +87,19 @@ import pathlib from libvcs.cmd.git import Git # Initialize the wrapper -git = Git(path=pathlib.Path.cwd() / 'libvcs') +git = Git(path=pathlib.Path.cwd() / "libvcs") # Run commands directly -git.clone(url='https://github.com/vcs-python/libvcs.git') -git.checkout(ref='master') +git.clone(url="https://github.com/vcs-python/libvcs.git") +git.checkout(ref="master") # Traverse branches with ORM-like filtering -git.branches.create('feature/new-gui') +git.branches.create("feature/new-gui") print(git.branches.ls()) # Returns QueryList for filtering # Target specific entities with contextual commands -git.remotes.set_url(name='origin', url='git@github.com:vcs-python/libvcs.git') -git.tags.create(name='v1.0.0', message='Release version 1.0.0') +git.remotes.set_url(name="origin", url="git@github.com:vcs-python/libvcs.git") +git.tags.create(name="v1.0.0", message="Release version 1.0.0") ``` ### 3. URL Parsing @@ -113,17 +111,17 @@ Stop writing regex for Git URLs. Let `libvcs` handle the edge cases. from libvcs.url.git import GitURL # Validate URLs -GitURL.is_valid(url='https://github.com/vcs-python/libvcs.git') # True +GitURL.is_valid(url="https://github.com/vcs-python/libvcs.git") # True # Parse complex URLs -url = GitURL(url='git@github.com:vcs-python/libvcs.git') +url = GitURL(url="git@github.com:vcs-python/libvcs.git") -print(url.user) # 'git' +print(url.user) # 'git' print(url.hostname) # 'github.com' -print(url.path) # 'vcs-python/libvcs' +print(url.path) # 'vcs-python/libvcs' # Transform URLs -url.hostname = 'gitlab.com' +url.hostname = "gitlab.com" print(url.to_url()) # 'git@gitlab.com:vcs-python/libvcs.git' ``` @@ -137,18 +135,16 @@ import pathlib from libvcs.pytest_plugin import CreateRepoFn from libvcs.sync.git import GitSync -def test_my_git_tool( - create_git_remote_repo: CreateRepoFn, - tmp_path: pathlib.Path -): + +def test_my_git_tool(create_git_remote_repo: CreateRepoFn, tmp_path: pathlib.Path): # Spin up a real, temporary Git server git_server = create_git_remote_repo() - + # Clone it to a temporary directory checkout_path = tmp_path / "checkout" repo = GitSync(path=checkout_path, url=f"file://{git_server}") repo.obtain() - + assert checkout_path.exists() assert (checkout_path / ".git").is_dir() ``` diff --git a/notes/2025-11-26-command-support.md b/notes/2025-11-26-command-support.md index 32bc4be6..de26d22a 100644 --- a/notes/2025-11-26-command-support.md +++ b/notes/2025-11-26-command-support.md @@ -558,23 +558,23 @@ reflog_pattern = r"(?P[a-f0-9]+) (?P[^@]+)@\{(?P\d+)\}: (?P=0.16.0", "mypy", ] @@ -106,7 +106,7 @@ coverage =[ "pytest-cov", ] lint = [ - "ruff", + "ruff>=0.16.0", "mypy", ] @@ -176,7 +176,10 @@ exclude_lines = [ target-version = "py310" [tool.ruff.lint] -select = [ +# `select` is deliberately unset: ruff 0.16 enables a curated default rule +# set, and an explicit `select` would replace it rather than extend it. +# `extend-select` layers this project's additional linters on top. +extend-select = [ "E", # pycodestyle "F", # pyflakes "I", # isort @@ -193,7 +196,7 @@ select = [ "PERF", # Perflint "RUF", # Ruff-specific rules "D", # pydocstyle - "FA100", # future annotations + "FA100", # future annotations ] ignore = [ "COM812", # missing trailing comma, ruff format conflict @@ -227,6 +230,24 @@ convention = "numpy" [tool.ruff.lint.per-file-ignores] "*/__init__.py" = ["F401"] "src/libvcs/_internal/subprocess.py" = ["A002"] +# Sphinx conf reads version metadata by exec'ing `__about__.py` rather than +# importing libvcs, so the docs build does not depend on the package being +# importable. The exec'd file is this repository's own source. +"docs/conf.py" = ["S102"] +# `run()` mirrors the full `subprocess.Popen` signature, `preexec_fn` +# included, and defaults it to None. Dropping the parameter would break +# callers who need it; whether it is safe to pass is the caller's call. +"src/libvcs/_internal/run.py" = ["PLW1509"] +# QueryList traverses and compares caller-supplied objects, whose +# `__getattr__`, properties, and `__eq__` may raise anything at all. A +# lookup that cannot be resolved is a non-match, not an error, so each +# handler degrades to None/False after logging rather than propagating. +"src/libvcs/_internal/query_list.py" = ["BLE001"] +# `SvnSync.url_rev` falls back to shelling out to `svn info --xml` and +# scraping it. Whether that path yields a URL depends on the installed +# svn, the working copy layout, and the output format; an unreadable +# working copy answers `(None, 0)` rather than raising at the caller. +"src/libvcs/sync/svn.py" = ["BLE001"] [tool.pytest.ini_options] addopts = [ diff --git a/src/libvcs/_internal/query_list.py b/src/libvcs/_internal/query_list.py index a76a22ea..d465d054 100644 --- a/src/libvcs/_internal/query_list.py +++ b/src/libvcs/_internal/query_list.py @@ -30,7 +30,7 @@ class ObjectDoesNotExist(Exception): def keygetter( obj: Mapping[str, t.Any], path: str, -) -> None | t.Any | str | list[str] | Mapping[str, str]: +) -> t.Any | str | list[str] | Mapping[str, str] | None: """Fetch values in objects and keys, supported nested data. **With dictionaries**: @@ -311,12 +311,12 @@ def lookup_iregex( class PKRequiredException(Exception): def __init__(self, *args: object) -> None: - return super().__init__("items() require a pk_key exists") + super().__init__("items() require a pk_key exists") class OpNotFound(ValueError): def __init__(self, op: str, *args: object) -> None: - return super().__init__(f"{op} not in LOOKUP_NAME_MAP") + super().__init__(f"{op} not in LOOKUP_NAME_MAP") class QueryList(list[T], t.Generic[T]): diff --git a/src/libvcs/_internal/run.py b/src/libvcs/_internal/run.py index 345ca7dd..c559116f 100644 --- a/src/libvcs/_internal/run.py +++ b/src/libvcs/_internal/run.py @@ -276,7 +276,10 @@ def progress_cb(output: t.AnyStr, timestamp: datetime.datetime) -> None: if callback and callable(callback) and proc.stderr is not None: line = console_to_str(proc.stderr.read(128)) if line: - callback(output=line, timestamp=datetime.datetime.now()) + callback( + output=line, + timestamp=datetime.datetime.now(tz=datetime.timezone.utc), + ) else: code, timeout_stdout, timeout_stderr = _wait_with_deadline( proc, @@ -286,7 +289,7 @@ def progress_cb(output: t.AnyStr, timestamp: datetime.datetime) -> None: cmd=_stringify_command(normalized_args), ) if callback and callable(callback): - callback(output="\r", timestamp=datetime.datetime.now()) + callback(output="\r", timestamp=datetime.datetime.now(tz=datetime.timezone.utc)) if proc.stdout is not None: raw_stdout: bytes = ( @@ -403,7 +406,9 @@ def _wait_with_deadline( ): callback( output=console_to_str(trailing), - timestamp=datetime.datetime.now(), + timestamp=datetime.datetime.now( + tz=datetime.timezone.utc + ), ) break @@ -464,7 +469,7 @@ def _wait_with_deadline( ): callback( output=console_to_str(chunk), - timestamp=datetime.datetime.now(), + timestamp=datetime.datetime.now(tz=datetime.timezone.utc), ) finally: # Restore blocking mode so any subsequent read by the caller behaves diff --git a/src/libvcs/_internal/shortcuts.py b/src/libvcs/_internal/shortcuts.py index 9cd8a94e..f4ae20bd 100644 --- a/src/libvcs/_internal/shortcuts.py +++ b/src/libvcs/_internal/shortcuts.py @@ -18,17 +18,17 @@ class VCSNoMatchFoundForUrl(exc.LibVCSException): def __init__(self, url: str, *args: object) -> None: - return super().__init__(f"No VCS found for url: {url}") + super().__init__(f"No VCS found for url: {url}") class VCSMultipleMatchFoundForUrl(exc.LibVCSException): def __init__(self, url: str, *args: object) -> None: - return super().__init__(f"Multiple VCS found for url: {url}") + super().__init__(f"Multiple VCS found for url: {url}") class VCSNotSupported(exc.LibVCSException): def __init__(self, url: str, vcs: str, *args: object) -> None: - return super().__init__(f"VCS '{vcs}' not supported, based on URL: {url}") + super().__init__(f"VCS '{vcs}' not supported, based on URL: {url}") @t.overload diff --git a/src/libvcs/_internal/subprocess.py b/src/libvcs/_internal/subprocess.py index 228598d7..c40715b3 100644 --- a/src/libvcs/_internal/subprocess.py +++ b/src/libvcs/_internal/subprocess.py @@ -55,15 +55,14 @@ class SubprocessCheckOutputError(Exception): def __init__(self, output: str, *args: object) -> None: - return super().__init__(f"output is not str or bytes: {output}") + super().__init__(f"output is not str or bytes: {output}") if sys.platform == "win32": _ENV: t.TypeAlias = Mapping[str, str] else: _ENV: t.TypeAlias = Mapping[bytes, StrOrBytesPath] | Mapping[str, StrOrBytesPath] -_FILE: t.TypeAlias = None | int | t.IO[t.Any] -_TXT: t.TypeAlias = bytes | str +_FILE: t.TypeAlias = int | t.IO[t.Any] | None #: Command _CMD: t.TypeAlias = StrOrBytesPath | Sequence[StrOrBytesPath] diff --git a/src/libvcs/cmd/git.py b/src/libvcs/cmd/git.py index fdebeaa2..e6f695f1 100644 --- a/src/libvcs/cmd/git.py +++ b/src/libvcs/cmd/git.py @@ -3482,35 +3482,26 @@ def _ls( branch = None # Try to get name and URL from git config - try: - name_result = self.cmd.run( - ["config", "-f", ".gitmodules", f"submodule.{path}.name"], - check_returncode=False, - ) - if name_result and "error" not in name_result.lower(): - submodule_name = name_result.strip() - except Exception: - pass - - try: - url_result = self.cmd.run( - ["config", "-f", ".gitmodules", f"submodule.{path}.url"], - check_returncode=False, - ) - if url_result and "error" not in url_result.lower(): - url = url_result.strip() - except Exception: - pass - - try: - branch_result = self.cmd.run( - ["config", "-f", ".gitmodules", f"submodule.{path}.branch"], - check_returncode=False, - ) - if branch_result and "error" not in branch_result.lower(): - branch = branch_result.strip() - except Exception: - pass + name_result = self.cmd.run( + ["config", "-f", ".gitmodules", f"submodule.{path}.name"], + check_returncode=False, + ) + if name_result and "error" not in name_result.lower(): + submodule_name = name_result.strip() + + url_result = self.cmd.run( + ["config", "-f", ".gitmodules", f"submodule.{path}.url"], + check_returncode=False, + ) + if url_result and "error" not in url_result.lower(): + url = url_result.strip() + + branch_result = self.cmd.run( + ["config", "-f", ".gitmodules", f"submodule.{path}.branch"], + check_returncode=False, + ) + if branch_result and "error" not in branch_result.lower(): + branch = branch_result.strip() submodules.append( { diff --git a/src/libvcs/cmd/svn.py b/src/libvcs/cmd/svn.py index 3bc05832..8c32a821 100644 --- a/src/libvcs/cmd/svn.py +++ b/src/libvcs/cmd/svn.py @@ -29,7 +29,7 @@ class SvnPropsetValueOrValuePathRequired(exc.LibVCSException, TypeError): """Raised when required parameters are not passed.""" def __init__(self, *args: object) -> None: - return super().__init__("Must enter a value or value_path") + super().__init__("Must enter a value or value_path") class Svn: diff --git a/src/libvcs/pytest_plugin.py b/src/libvcs/pytest_plugin.py index e4a6fe4d..474ff552 100644 --- a/src/libvcs/pytest_plugin.py +++ b/src/libvcs/pytest_plugin.py @@ -24,7 +24,7 @@ class MaxUniqueRepoAttemptsExceeded(exc.LibVCSException): def __init__(self, attempts: int, *args: object) -> None: """Raise LibVCSException exception with message including attempts tried.""" - return super().__init__( + super().__init__( f"Could not find unused repo destination (attempts: {attempts})", ) diff --git a/src/libvcs/sync/git.py b/src/libvcs/sync/git.py index 71527109..c56c1486 100644 --- a/src/libvcs/sync/git.py +++ b/src/libvcs/sync/git.py @@ -42,7 +42,7 @@ class GitStatusParsingException(exc.LibVCSException): """Raised when git status output is not in the expected format.""" def __init__(self, git_status_output: str, *args: object) -> None: - return super().__init__( + super().__init__( "Could not find match for git-status(1)" + f"Output: {git_status_output}", ) @@ -51,21 +51,21 @@ class GitRemoteOriginMissing(exc.LibVCSException): """Raised when git origin remote was not found.""" def __init__(self, remotes: list[str], *args: object) -> None: - return super().__init__(f"Missing origin. Remotes: {', '.join(remotes)}") + super().__init__(f"Missing origin. Remotes: {', '.join(remotes)}") class GitRemoteSetError(exc.LibVCSException): """Raised when a git remote could not be set.""" def __init__(self, remote_name: str) -> None: - return super().__init__(f"Remote {remote_name} not found after setting") + super().__init__(f"Remote {remote_name} not found after setting") class GitNoBranchFound(exc.LibVCSException): """Raised with git branch could not be found.""" def __init__(self, *args: object) -> None: - return super().__init__("No branch found for git repository") + super().__init__("No branch found for git repository") class GitRemoteRefNotFound(exc.CommandError): diff --git a/src/libvcs/sync/svn.py b/src/libvcs/sync/svn.py index 49fd7818..f4968ea0 100644 --- a/src/libvcs/sync/svn.py +++ b/src/libvcs/sync/svn.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Tool to manage a local SVN (Subversion) working copy from a repository. .. todo:: @@ -33,7 +32,7 @@ class SvnUrlRevFormattingError(ValueError): """Raised when SVN Revision output is not in the expected format.""" def __init__(self, data: str, *args: object) -> None: - return super().__init__(f"Badly formatted data: {data!r}") + super().__init__(f"Badly formatted data: {data!r}") class SvnSync(BaseSync): diff --git a/tests/cmd/test_git.py b/tests/cmd/test_git.py index c5288f92..09e9978f 100644 --- a/tests/cmd/test_git.py +++ b/tests/cmd/test_git.py @@ -2744,6 +2744,7 @@ def test_run_trim_false_preserves_diff(git_repo: GitSync) -> None: input=captured, capture_output=True, text=True, + check=False, ) assert check.returncode == 0, check.stderr diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py index 6c9373d2..df29d1b3 100644 --- a/tests/test_pytest_plugin.py +++ b/tests/test_pytest_plugin.py @@ -248,6 +248,7 @@ def test_gitconfig_submodule_file_protocol( cwd=main_repo, capture_output=True, text=True, + check=False, ) # Assert: submodule add should succeed (no "fatal" errors) diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py index db2547f8..d3b8d082 100644 --- a/tests/test_shortcuts.py +++ b/tests/test_shortcuts.py @@ -58,7 +58,7 @@ def test_create_project( tmp_path: pathlib.Path, repo_dict: CreateProjectKwargsDict, repo_class: type[SvnSync | GitSync | HgSync], - raises_exception: None | type[E] | tuple[type[E], ...], + raises_exception: type[E] | tuple[type[E], ...] | None, ) -> None: """Tests for create_project().""" # add parent_dir via fixture diff --git a/uv.lock b/uv.lock index ae280bee..2f98003e 100644 --- a/uv.lock +++ b/uv.lock @@ -660,7 +660,7 @@ dev = [ { name = "pytest-rerunfailures" }, { name = "pytest-watcher" }, { name = "pytest-xdist" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "sphinx-autobuild" }, { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a36" }, { name = "sphinx-autodoc-pytest-fixtures", specifier = "==0.0.1a36" }, @@ -674,7 +674,7 @@ docs = [ ] lint = [ { name = "mypy" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, ] testing = [ { name = "gp-libs", specifier = ">=0.0.19" }, @@ -1166,27 +1166,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, - { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, - { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, - { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, - { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, - { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, - { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]]