Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pdm for openSUSE:Factory checked in at 2026-03-31 15:22:49 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pdm (Old) and /work/SRC/openSUSE:Factory/.python-pdm.new.1999 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pdm" Tue Mar 31 15:22:49 2026 rev:20 rq:1343756 version:2.26.7 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pdm/python-pdm.changes 2026-02-12 17:29:54.773769249 +0100 +++ /work/SRC/openSUSE:Factory/.python-pdm.new.1999/python-pdm.changes 2026-03-31 15:23:55.864424180 +0200 @@ -1,0 +2,10 @@ +Mon Mar 30 16:33:27 UTC 2026 - Dirk Müller <[email protected]> + +- update to 2.26.7: + * Speed up dependency resolution when there are complex + conflicts. + * Switch to Zensical as docs generator. + * Add comprehensive tests for completion, show, search, and + info commands to improve test coverage + +------------------------------------------------------------------- Old: ---- pdm-2.26.6.tar.gz New: ---- pdm-2.26.7.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pdm.spec ++++++ --- /var/tmp/diff_new_pack.wbWvMf/_old 2026-03-31 15:23:56.536452176 +0200 +++ /var/tmp/diff_new_pack.wbWvMf/_new 2026-03-31 15:23:56.536452176 +0200 @@ -27,7 +27,7 @@ %endif %{?sle15_python_module_pythons} Name: python-pdm%{psuffix} -Version: 2.26.6 +Version: 2.26.7 Release: 0 Summary: Python Development Master License: MIT ++++++ pdm-2.26.6.tar.gz -> pdm-2.26.7.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/CHANGELOG.md new/pdm-2.26.7/CHANGELOG.md --- old/pdm-2.26.6/CHANGELOG.md 2026-01-22 10:47:17.162061200 +0100 +++ new/pdm-2.26.7/CHANGELOG.md 2026-03-24 08:21:54.119341600 +0100 @@ -1,3 +1,18 @@ +## Release v2.26.7 (2026-03-24) + +### Features & Improvements + +- Speed up dependency resolution when there are complex conflicts. ([#3751](https://github.com/pdm-project/pdm/issues/3751)) + +### Documentation + +- Switch to Zensical as docs generator. ([#3752](https://github.com/pdm-project/pdm/issues/3752)) + +### Miscellany + +- Add comprehensive tests for completion, show, search, and info commands to improve test coverage ([#3541](https://github.com/pdm-project/pdm/issues/3541)) + + ## Release v2.26.6 (2026-01-22) ### Bug Fixes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/PKG-INFO new/pdm-2.26.7/PKG-INFO --- old/pdm-2.26.6/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/pdm-2.26.7/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: pdm -Version: 2.26.6 +Version: 2.26.7 Summary: A modern Python package and dependency manager supporting the latest PEP standards Keywords: packaging,dependency,workflow Author-Email: Frost Ming <[email protected]> @@ -124,7 +124,7 @@  -[](https://pdm-project.org) +[](https://pdm-project.org) [](https://twitter.com/pdm_project) [](https://discord.gg/Phn8smztpv) @@ -248,6 +248,22 @@ [Awesome PDM](https://github.com/pdm-project/awesome-pdm) is a curated list of awesome PDM plugins and resources. +## Experimental + +Enable [PEP 582](https://peps.python.org/pep-0582/) for a project: + + pdm config python.use_venv False + +This makes PDM install packages into a local project folder instead of a venv (similar to how npm installs into node_modules). + +Enable [uv](https://github.com/astral-sh/uv) integration: + + pdm config use_uv true + +uv is a very fast Python package installer written in Rust. + +Note: `uv` does not work with `PEP 582`. + ## Sponsors <p align="center"> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/README.md new/pdm-2.26.7/README.md --- old/pdm-2.26.6/README.md 2026-01-22 10:47:17.162061200 +0100 +++ new/pdm-2.26.7/README.md 2026-03-24 08:21:54.119341600 +0100 @@ -7,7 +7,7 @@  -[](https://pdm-project.org) +[](https://pdm-project.org) [](https://twitter.com/pdm_project) [](https://discord.gg/Phn8smztpv) @@ -131,6 +131,22 @@ [Awesome PDM](https://github.com/pdm-project/awesome-pdm) is a curated list of awesome PDM plugins and resources. +## Experimental + +Enable [PEP 582](https://peps.python.org/pep-0582/) for a project: + + pdm config python.use_venv False + +This makes PDM install packages into a local project folder instead of a venv (similar to how npm installs into node_modules). + +Enable [uv](https://github.com/astral-sh/uv) integration: + + pdm config use_uv true + +uv is a very fast Python package installer written in Rust. + +Note: `uv` does not work with `PEP 582`. + ## Sponsors <p align="center"> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/pyproject.toml new/pdm-2.26.7/pyproject.toml --- old/pdm-2.26.6/pyproject.toml 2026-01-22 10:47:23.148037000 +0100 +++ new/pdm-2.26.7/pyproject.toml 2026-03-24 08:21:59.608373600 +0100 @@ -58,7 +58,7 @@ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -version = "2.26.6" +version = "2.26.7" [project.urls] Homepage = "https://pdm-project.org" @@ -164,14 +164,9 @@ "tox-pdm>=0.5", ] doc = [ - "mkdocs>=1.1", - "mkdocs-material>=7.3", + "zensical>=0.0.28; python_version >= '3.10'", "mkdocstrings[python]>=0.18", "setuptools>=62.3.3", - "markdown-exec>=0.7.0", - "mkdocs-redirects>=1.2.0", - "mkdocs-version-annotations>=1.0.0", - "mkdocs-llmstxt>=0.2.0", ] workflow = [ "parver>=0.3.1", @@ -323,15 +318,23 @@ release = "python tasks/release.py" test = "pytest" tox = "tox" +pre_doc = "python tasks/render_reference_docs.py" lint = "prek run --all-files" [tool.pdm.scripts.coverage] shell = "python -m pytest --verbosity=3 --cov=src/pdm --cov-branch --cov-report term-missing tests/\n " [tool.pdm.scripts.doc] -cmd = "mkdocs serve" +cmd = "zensical serve" help = "Start the dev server for docs preview" +[tool.pdm.scripts.doc-build] +composite = [ + "pre_doc", + "zensical build", +] +help = "Build the documentation site" + [tool.pdm.scripts.complete] call = "tasks.complete:main" help = "Create autocomplete files for bash and fish" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/src/pdm/VERSION new/pdm-2.26.7/src/pdm/VERSION --- old/pdm-2.26.6/src/pdm/VERSION 2026-01-22 10:47:23.074037300 +0100 +++ new/pdm-2.26.7/src/pdm/VERSION 2026-03-24 08:21:59.535711300 +0100 @@ -1 +1 @@ -2.26.6 +2.26.7 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/src/pdm/cli/utils.py new/pdm-2.26.7/src/pdm/cli/utils.py --- old/pdm-2.26.6/src/pdm/cli/utils.py 2026-01-22 10:47:17.169061200 +0100 +++ new/pdm-2.26.7/src/pdm/cli/utils.py 2026-03-24 08:21:54.127488000 +0100 @@ -125,9 +125,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: if sys.version_info >= (3, 14): - kwargs["formatter_class"] = argparse.RawDescriptionHelpFormatter - else: - kwargs["formatter_class"] = PdmFormatter + kwargs["color"] = False + kwargs["formatter_class"] = PdmFormatter kwargs["add_help"] = False super().__init__(*args, **kwargs) self.add_argument( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/src/pdm/models/python_max_versions.json new/pdm-2.26.7/src/pdm/models/python_max_versions.json --- old/pdm-2.26.6/src/pdm/models/python_max_versions.json 2026-01-22 10:47:17.172061000 +0100 +++ new/pdm-2.26.7/src/pdm/models/python_max_versions.json 2026-03-24 08:21:54.129763600 +0100 @@ -11,11 +11,11 @@ "3": 14, "3.0": 1, "3.1": 5, - "3.10": 19, - "3.11": 14, - "3.12": 12, - "3.13": 11, - "3.14": 2, + "3.10": 20, + "3.11": 15, + "3.12": 13, + "3.13": 12, + "3.14": 3, "3.2": 6, "3.3": 7, "3.4": 10, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/src/pdm/project/core.py new/pdm-2.26.7/src/pdm/project/core.py --- old/pdm-2.26.6/src/pdm/project/core.py 2026-01-22 10:47:17.173061100 +0100 +++ new/pdm-2.26.7/src/pdm/project/core.py 2026-03-24 08:21:54.131075000 +0100 @@ -714,9 +714,11 @@ deps_setter = [ ( metadata.get("optional-dependencies", {}), - lambda x: metadata.setdefault("optional-dependencies", {}).__setitem__(group, x) - if x - else metadata.setdefault("optional-dependencies", {}).pop(group, None), + lambda x: ( + metadata.setdefault("optional-dependencies", {}).__setitem__(group, x) + if x + else metadata.setdefault("optional-dependencies", {}).pop(group, None) + ), ), (dev_dependencies, update_dev_dependencies), ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/src/pdm/pytest.py new/pdm-2.26.7/src/pdm/pytest.py --- old/pdm-2.26.6/src/pdm/pytest.py 2026-01-22 10:47:17.173061100 +0100 +++ new/pdm-2.26.7/src/pdm/pytest.py 2026-03-24 08:21:54.131433200 +0100 @@ -653,6 +653,10 @@ os.environ.update(old_env) if cleanup: core.exit_stack.close() + # Clear the build directory cache to avoid stale references + from pdm.models.candidates import PreparedCandidate + + PreparedCandidate._build_dir_cache.clear() result = RunResult(exit_code, stdout.getvalue(), stderr.getvalue(), exception) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/src/pdm/resolver/providers.py new/pdm-2.26.7/src/pdm/resolver/providers.py --- old/pdm-2.26.6/src/pdm/resolver/providers.py 2026-01-22 10:47:17.174061000 +0100 +++ new/pdm-2.26.7/src/pdm/resolver/providers.py 2026-03-24 08:21:54.131875300 +0100 @@ -2,6 +2,7 @@ import dataclasses import os +from collections import defaultdict from functools import cached_property from typing import TYPE_CHECKING, Callable @@ -39,6 +40,7 @@ _PROVIDER_REGISTRY: dict[str, type[BaseProvider]] = {} +_CONFLICT_PRIORITY_THRESHOLD = 5 def get_provider(strategy: str) -> type[BaseProvider]: @@ -79,6 +81,8 @@ self.excludes = {normalize_name(k) for k in project.pyproject.resolution.get("excludes", [])} self.direct_minimal_versions = direct_minimal_versions self.locked_repository = locked_repository + self._conflict_counts: defaultdict[str, int] = defaultdict(int) + self._conflict_promoted: set[str] = set() def requirement_preference(self, requirement: Requirement) -> Comparable: """Return the preference of a requirement to find candidates. @@ -97,12 +101,49 @@ def identify(self, requirement_or_candidate: Requirement | Candidate) -> str: return requirement_or_candidate.identify() + def narrow_requirement_selection( + self, + identifiers: Iterable[str], + resolutions: Mapping[str, Candidate], + candidates: Mapping[str, Iterator[Candidate]], + information: Mapping[str, Iterator[RequirementInformation]], + backtrack_causes: Sequence[RequirementInformation], + ) -> Iterable[str]: + backtrack_identifiers: set[str] = set() + for requirement, parent in backtrack_causes: + names = [requirement.identify()] + if parent is not None: + names.append(parent.identify()) + for name in names: + backtrack_identifiers.add(name) + if name not in resolutions: + self._conflict_counts[name] += 1 + if self._conflict_counts[name] >= _CONFLICT_PRIORITY_THRESHOLD: + self._conflict_promoted.add(name) + + current_backtrack_causes: list[str] = [] + promoted: list[str] = [] + for identifier in identifiers: + if identifier == "python": + return [identifier] + if identifier in backtrack_identifiers: + current_backtrack_causes.append(identifier) + continue + if identifier in self._conflict_promoted: + promoted.append(identifier) + + if current_backtrack_causes: + return current_backtrack_causes + if promoted: + return promoted + return identifiers + def get_preference( self, identifier: str, - resolutions: dict[str, Candidate], - candidates: dict[str, Iterator[Candidate]], - information: dict[str, Iterator[RequirementInformation]], + resolutions: Mapping[str, Candidate], + candidates: Mapping[str, Iterator[Candidate]], + information: Mapping[str, Iterator[RequirementInformation]], backtrack_causes: Sequence[RequirementInformation], ) -> tuple[Comparable, ...]: is_top = any(parent is None for _, parent in information[identifier]) @@ -123,9 +164,11 @@ is_python = identifier == "python" is_pinned = any(op[:2] == "==" for op in operators) constraints = len(operators) + is_conflict_promoted = identifier in self._conflict_promoted return ( not is_python, not is_top, + not is_conflict_promoted, not is_file_or_url, not is_pinned, not is_backtrack_cause, @@ -458,9 +501,9 @@ def get_preference( self, identifier: str, - resolutions: dict[str, Candidate], - candidates: dict[str, Iterator[Candidate]], - information: dict[str, Iterator[RequirementInformation]], + resolutions: Mapping[str, Candidate], + candidates: Mapping[str, Iterator[Candidate]], + information: Mapping[str, Iterator[RequirementInformation]], backtrack_causes: Sequence[RequirementInformation], ) -> tuple[Comparable, ...]: # Resolve tracking packages so we have a chance to unpin them first. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/src/pdm/utils.py new/pdm-2.26.7/src/pdm/utils.py --- old/pdm-2.26.6/src/pdm/utils.py 2026-01-22 10:47:17.174061000 +0100 +++ new/pdm-2.26.7/src/pdm/utils.py 2026-03-24 08:21:54.131875300 +0100 @@ -352,17 +352,27 @@ if hasattr(parsed, "__replace__"): # packaging >= 26 parsed = parsed.__replace__(local=None) else: + # packaging < 26 does not have __replace__ method + # In this version, we need to manually update _version and recompute _key + # Note: In packaging >= 26, _key is a read-only property, but this else branch + # only executes on packaging < 26 where _key is a regular attribute that can be + # assigned. We use object.__setattr__() instead of direct assignment to satisfy + # type checkers that analyze based on the current packaging version. from packaging.version import _cmpkey parsed._version = parsed._version._replace(local=None) - parsed._key = _cmpkey( - parsed._version.epoch, - parsed._version.release, - parsed._version.pre, - parsed._version.post, - parsed._version.dev, - parsed._version.local, + object.__setattr__( + parsed, + "_key", + _cmpkey( + parsed._version.epoch, + parsed._version.release, + parsed._version.pre, + parsed._version.post, + parsed._version.dev, + parsed._version.local, + ), ) return parsed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/tests/cli/test_completion.py new/pdm-2.26.7/tests/cli/test_completion.py --- old/pdm-2.26.6/tests/cli/test_completion.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pdm-2.26.7/tests/cli/test_completion.py 2026-03-24 08:21:54.132939800 +0100 @@ -0,0 +1,63 @@ +"""Tests for the completion command""" + + +def test_completion_bash(pdm): + """Test completion for bash shell""" + result = pdm(["completion", "bash"]) + assert result.exit_code == 0 + assert "BASH completion script for pdm" in result.output + + +def test_completion_zsh(pdm): + """Test completion for zsh shell""" + result = pdm(["completion", "zsh"]) + assert result.exit_code == 0 + assert "#compdef pdm" in result.output + + +def test_completion_fish(pdm): + """Test completion for fish shell""" + result = pdm(["completion", "fish"]) + assert result.exit_code == 0 + assert "FISH completion script for pdm" in result.output + + +def test_completion_powershell(pdm): + """Test completion for powershell""" + result = pdm(["completion", "powershell"]) + assert result.exit_code == 0 + assert "Powershell completion script for pdm" in result.output + + +def test_completion_pwsh(pdm): + """Test completion for pwsh (PowerShell Core)""" + result = pdm(["completion", "pwsh"]) + assert result.exit_code == 0 + assert "Powershell completion script for pdm" in result.output + + +def test_completion_unsupported_shell(pdm): + """Test completion with unsupported shell raises error""" + result = pdm(["completion", "unsupported_shell"]) + assert result.exit_code != 0 + assert "Unsupported shell" in result.stderr + + +def test_completion_auto_detect(pdm, monkeypatch): + """Test completion with auto-detected shell""" + import shellingham + + monkeypatch.setattr(shellingham, "detect_shell", lambda: ("bash", "/bin/bash")) + result = pdm(["completion"]) + assert result.exit_code == 0 + assert "BASH completion script for pdm" in result.output + + +def test_completion_auto_detect_unsupported(pdm, monkeypatch): + """Test completion with auto-detected unsupported shell""" + import shellingham + + monkeypatch.setattr(shellingham, "detect_shell", lambda: ("csh", "/bin/csh")) + result = pdm(["completion"]) + assert result.exit_code != 0 + assert "Unsupported shell" in result.stderr diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/tests/cli/test_info.py new/pdm-2.26.7/tests/cli/test_info.py --- old/pdm-2.26.6/tests/cli/test_info.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pdm-2.26.7/tests/cli/test_info.py 2026-03-24 08:21:54.132939800 +0100 @@ -0,0 +1,112 @@ +"""Additional tests for the info command to improve coverage""" + +import json + + +def test_info_command_packages_option(project, pdm): + """Test info command with --packages option""" + result = pdm(["info", "--packages"], obj=project) + assert result.exit_code == 0 + # Should show packages path + assert result.output.strip() != "" + + +def test_info_command_all_options_mutually_exclusive(project, pdm): + """Test that info command options are mutually exclusive""" + # Only one field option should work at a time + result = pdm(["info", "--python"], obj=project) + assert result.exit_code == 0 + + result = pdm(["info", "--where"], obj=project) + assert result.exit_code == 0 + + result = pdm(["info", "--packages"], obj=project) + assert result.exit_code == 0 + + result = pdm(["info", "--env"], obj=project) + assert result.exit_code == 0 + + +def test_info_command_env_output_format(project, pdm): + """Test that --env outputs valid JSON""" + result = pdm(["info", "--env"], obj=project, strict=True) + # Try to parse as JSON + try: + data = json.loads(result.output) + assert isinstance(data, dict) + except json.JSONDecodeError: + # If it's not JSON, it should at least contain marker info + assert "python_version" in result.output or "implementation" in result.output + + +def test_info_command_json_contains_all_fields(project, pdm): + """Test that --json output contains all expected fields""" + result = pdm(["info", "--json"], obj=project, strict=True) + + data = json.loads(result.output) + + # Check pdm section + assert "pdm" in data + assert "version" in data["pdm"] + + # Check python section + assert "python" in data + assert "interpreter" in data["python"] + assert "version" in data["python"] + assert "markers" in data["python"] + assert isinstance(data["python"]["markers"], dict) + + # Check project section + assert "project" in data + assert "root" in data["project"] + assert "pypackages" in data["project"] + + +def test_info_command_default_output(project, pdm): + """Test info command with no options shows formatted output""" + result = pdm(["info"], obj=project, strict=True) + + # Should contain all major sections + assert "PDM version" in result.output or "PDM" in result.output + assert "Python Interpreter" in result.output or "Interpreter" in result.output + assert "Project Root" in result.output or "Root" in result.output + assert "Local Packages" in result.output or "Packages" in result.output + + +def test_info_command_with_global_project(pdm, tmp_path, monkeypatch): + """Test info command with global project""" + monkeypatch.chdir(tmp_path) + + result = pdm(["info", "-g", "--python"]) + assert result.exit_code == 0 + + +def test_info_command_shows_venv_info(project, pdm): + """Test info command shows virtual environment information when applicable""" + # Create a venv + project.global_config["python.use_venv"] = True + + result = pdm(["info"], obj=project) + assert result.exit_code == 0 + + +def test_info_command_non_local_environment(project, pdm, mocker): + """Test info command with non-local environment shows site-packages""" + # Mock the environment to be non-local + project.environment.is_local = False + + # Mock get_paths to return purelib + project.environment.get_paths = mocker.Mock(return_value={"purelib": "/fake/purelib"}) + + result = pdm(["info", "--packages"], obj=project) + assert result.exit_code == 0 + assert "/fake/purelib" in result.output or "purelib" in result.output + + +def test_info_command_global_project_prefix(project, pdm): + """Test that global project shows 'Global' prefix in output""" + result = pdm(["info", "-g"], obj=project) + + # If it's a global project, should show "Global" prefix + # This test checks if the code path exists + assert result.exit_code == 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/tests/cli/test_lock.py new/pdm-2.26.7/tests/cli/test_lock.py --- old/pdm-2.26.6/tests/cli/test_lock.py 2026-01-22 10:47:17.175061000 +0100 +++ new/pdm-2.26.7/tests/cli/test_lock.py 2026-03-24 08:21:54.132939800 +0100 @@ -60,9 +60,11 @@ core.repository_class, "get_hashes", side_effect=( - lambda c: [{"url": url, "file": Link(url).filename, "hash": hash} for url, hash in url_hashes.items()] - if c.identify() == "requests" - else [] + lambda c: ( + [{"url": url, "file": Link(url).filename, "hash": hash} for url, hash in url_hashes.items()] + if c.identify() == "requests" + else [] + ) ), ) assert not project.is_lockfile_hash_match() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/tests/cli/test_search.py new/pdm-2.26.7/tests/cli/test_search.py --- old/pdm-2.26.6/tests/cli/test_search.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pdm-2.26.7/tests/cli/test_search.py 2026-03-24 08:21:54.133341600 +0100 @@ -0,0 +1,103 @@ +"""Tests for the search command utilities""" + +from pdm.cli.commands.search import print_results + + +def test_print_results_empty_hits(mocker): + """Test print_results with empty hits returns early""" + ui = mocker.Mock() + working_set = mocker.Mock() + + # Should return early without calling echo + print_results(ui, [], working_set) + + ui.echo.assert_not_called() + + +def test_print_results_with_hits(mocker): + """Test print_results with search hits""" + ui = mocker.Mock() + working_set = {} + + # Create mock search hits + hit1 = mocker.Mock() + hit1.name = "test-package" + hit1.version = "1.0.0" + hit1.summary = "A test package" + + hit2 = mocker.Mock() + hit2.name = "another-package" + hit2.version = "2.0.0" + hit2.summary = "Another test package" + + hits = [hit1, hit2] + + print_results(ui, hits, working_set) + + # Should call echo for each hit + assert ui.echo.call_count >= 2 + + +def test_print_results_with_installed_package(mocker): + """Test print_results shows INSTALLED for packages in working set""" + ui = mocker.Mock() + working_set = mocker.Mock() + + # Mock a package that's installed + hit = mocker.Mock() + hit.name = "installed-package" + hit.version = "1.0.0" + hit.summary = "An installed package" + + # Mock working set to return a distribution + dist = mocker.Mock() + dist.version = "1.0.0" + working_set.__contains__ = mocker.Mock(return_value=True) + working_set.__getitem__ = mocker.Mock(return_value=dist) + + print_results(ui, [hit], working_set) + + # Should show INSTALLED label + calls = [str(call) for call in ui.echo.call_args_list] + assert any("INSTALLED" in str(call) for call in calls) + + +def test_print_results_with_terminal_width(mocker): + """Test print_results respects terminal width for wrapping""" + ui = mocker.Mock() + working_set = {} + + hit = mocker.Mock() + hit.name = "test-package" + hit.version = "1.0.0" + hit.summary = "This is a very long summary that should be wrapped when terminal width is specified" + + print_results(ui, [hit], working_set, terminal_width=40) + + # Should call echo + ui.echo.assert_called() + + +def test_print_results_unicode_error(mocker): + """Test print_results handles UnicodeEncodeError gracefully""" + ui = mocker.Mock() + working_set = {} + + hit = mocker.Mock() + hit.name = "test-package" + hit.version = "1.0.0" + hit.summary = "Test summary" + + # Make echo raise UnicodeEncodeError + ui.echo.side_effect = UnicodeEncodeError("utf-8", "", 0, 1, "test") + + # Should not raise exception + print_results(ui, [hit], working_set) + + +def test_search_command_deprecation_warning(pdm): + """Test that search command shows deprecation warning""" + result = pdm(["search", "test"]) + # Command should succeed but show warning + assert result.exit_code == 0 + assert "deprecated" in result.stderr.lower() or "deprecated" in result.output.lower() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/tests/cli/test_show.py new/pdm-2.26.7/tests/cli/test_show.py --- old/pdm-2.26.6/tests/cli/test_show.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pdm-2.26.7/tests/cli/test_show.py 2026-03-24 08:21:54.133341600 +0100 @@ -0,0 +1,71 @@ +"""Additional tests for the show command""" + +import pytest + +from pdm.cli.commands.show import filter_stable + + +def test_filter_stable_with_stable_version(mocker): + """Test filter_stable returns True for stable versions""" + # Mock package with stable version + package = mocker.Mock() + package.version = "1.0.0" + assert filter_stable(package) is True + + +def test_filter_stable_with_prerelease_alpha(mocker): + """Test filter_stable returns False for alpha prereleases""" + package = mocker.Mock() + package.version = "1.0.0a1" + assert filter_stable(package) is False + + +def test_filter_stable_with_prerelease_beta(mocker): + """Test filter_stable returns False for beta prereleases""" + package = mocker.Mock() + package.version = "2.0.0b2" + assert filter_stable(package) is False + + +def test_filter_stable_with_prerelease_rc(mocker): + """Test filter_stable returns False for release candidates""" + package = mocker.Mock() + package.version = "3.0.0rc1" + assert filter_stable(package) is False + + +def test_filter_stable_with_dev_version(mocker): + """Test filter_stable returns False for dev versions""" + package = mocker.Mock() + package.version = "1.0.0.dev1" + assert filter_stable(package) is False + + [email protected] +def test_show_command_with_specific_metadata_keys(pdm): + """Test show command with specific metadata keys""" + result = pdm(["show", "requests", "--name"]) + assert result.exit_code == 0 + assert "requests" in result.output.lower() + + result = pdm(["show", "requests", "--version"]) + assert result.exit_code == 0 + # Should contain a version number + + [email protected] +def test_show_command_with_multiple_metadata_keys(pdm): + """Test show command with multiple metadata keys only shows selected ones""" + result = pdm(["show", "requests", "--name", "--version"]) + assert result.exit_code == 0 + # Should only show name and version, not full metadata + + +def test_show_command_non_distribution_project(project, pdm): + """Test show command on a non-distribution project raises error""" + # Mark the project as non-distribution + project.pyproject.settings["distribution"] = False + + result = pdm(["show"], obj=project) + assert result.exit_code != 0 + assert "not a library" in result.stderr diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pdm-2.26.6/tests/resolver/test_providers.py new/pdm-2.26.7/tests/resolver/test_providers.py --- old/pdm-2.26.6/tests/resolver/test_providers.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pdm-2.26.7/tests/resolver/test_providers.py 2026-03-24 08:21:54.150679000 +0100 @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Iterator + +from resolvelib.resolvers import RequirementInformation + +from pdm.models.candidates import Candidate +from pdm.models.requirements import parse_requirement +from pdm.resolver.providers import _CONFLICT_PRIORITY_THRESHOLD + + +def _build_candidates(identifier: str) -> dict[str, Iterator[Candidate]]: + requirement = parse_requirement(identifier) + candidate = Candidate(requirement, name=requirement.project_name, version="1.0") + return {identifier: iter([candidate])} + + +def _build_information(identifier: str) -> dict[str, Iterator[RequirementInformation]]: + requirement = parse_requirement(identifier) + return {identifier: iter([RequirementInformation(requirement, None)])} + + +def test_narrow_requirement_selection_promotes_repeated_conflicts(project, repository): + repository.add_candidate("conflict-pkg", "1.0") + repository.add_candidate("other-pkg", "1.0") + + provider = project.get_provider() + narrow = provider.narrow_requirement_selection + causes = [RequirementInformation(parse_requirement("conflict-pkg"), None)] + + for _ in range(1, _CONFLICT_PRIORITY_THRESHOLD): + result = list(narrow(["other-pkg"], {}, {}, {}, causes)) + assert result == ["other-pkg"] + + result = list(narrow(["other-pkg", "conflict-pkg"], {}, {}, {}, causes)) + assert result == ["conflict-pkg"] + + result = list(narrow(["other-pkg", "conflict-pkg"], {}, {}, {}, [])) + assert result == ["conflict-pkg"] + + other_causes = [RequirementInformation(parse_requirement("other-pkg"), None)] + result = list(narrow(["other-pkg", "conflict-pkg"], {}, {}, {}, other_causes)) + assert result == ["other-pkg"] + + +def test_get_preference_prioritizes_promoted_conflicts(project, repository): + repository.add_candidate("promoted-pkg", "1.0") + repository.add_candidate("normal-pkg", "1.0") + + provider = project.get_provider() + provider._conflict_promoted.add("promoted-pkg") + + promoted_preference = provider.get_preference( + "promoted-pkg", + {}, + _build_candidates("promoted-pkg"), + _build_information("promoted-pkg"), + [], + ) + normal_preference = provider.get_preference( + "normal-pkg", + {}, + _build_candidates("normal-pkg"), + _build_information("normal-pkg"), + [], + ) + + assert promoted_preference < normal_preference
