From 332ff64ad74147d83a160a0478a28e701fdd7736 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Tue, 8 Nov 2022 15:38:00 +0800 Subject: [PATCH] refactor: project files (#1499) * refactor: project files * ensure the file exists * add news * fix tests * update test cases * reload the lockfile --- news/1499.refactor.md | 1 + src/pdm/cli/actions.py | 25 ++- src/pdm/cli/commands/init.py | 2 +- src/pdm/cli/commands/install.py | 2 +- src/pdm/cli/commands/show.py | 6 +- src/pdm/cli/utils.py | 14 +- src/pdm/core.py | 2 +- src/pdm/formats/requirements.py | 2 +- src/pdm/installers/synchronizers.py | 8 +- src/pdm/models/candidates.py | 28 ++-- src/pdm/models/environment.py | 6 +- src/pdm/models/project_info.py | 29 +--- src/pdm/models/repositories.py | 31 ++-- src/pdm/models/setup.py | 115 ++++++++++---- src/pdm/project/core.py | 233 +++++++++++----------------- src/pdm/project/lockfile.py | 21 +++ src/pdm/project/metadata.py | 100 ------------ src/pdm/project/project_file.py | 67 ++++++++ src/pdm/project/toml_file.py | 42 +++++ src/pdm/resolver/core.py | 2 +- tests/cli/test_add.py | 26 ++-- tests/cli/test_build.py | 19 ++- tests/cli/test_hooks.py | 24 +-- tests/cli/test_install.py | 8 +- tests/cli/test_lock.py | 11 +- tests/cli/test_remove.py | 12 +- tests/cli/test_run.py | 108 ++++++------- tests/cli/test_update.py | 16 +- tests/cli/test_use.py | 4 +- tests/conftest.py | 6 +- tests/resolver/test_resolve.py | 58 +++---- tests/test_formats.py | 3 +- tests/test_integration.py | 18 +-- tests/test_project.py | 19 ++- tests/test_utils.py | 6 +- 35 files changed, 534 insertions(+), 540 deletions(-) create mode 100644 news/1499.refactor.md create mode 100644 src/pdm/project/lockfile.py delete mode 100644 src/pdm/project/metadata.py create mode 100644 src/pdm/project/project_file.py create mode 100644 src/pdm/project/toml_file.py diff --git a/news/1499.refactor.md b/news/1499.refactor.md new file mode 100644 index 0000000000..8083d1ef9b --- /dev/null +++ b/news/1499.refactor.md @@ -0,0 +1 @@ +Refactor the project module, extract the TOML reading and writing related logic into classes. diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py index 36caf33a70..6a805d24b4 100644 --- a/src/pdm/cli/actions.py +++ b/src/pdm/cli/actions.py @@ -153,7 +153,7 @@ def resolve_candidates_from_lockfile( def check_lockfile(project: Project, raise_not_exist: bool = True) -> str | None: """Check if the lock file exists and is up to date. Return the update strategy.""" - if not project.lockfile_file.exists(): + if not project.lockfile.exists: if raise_not_exist: raise ProjectError("Lock file does not exist, nothing to install") project.core.ui.echo("Lock file does not exist", style="warning", err=True) @@ -249,7 +249,7 @@ def do_add( if ( group == "default" or not dev - and group not in project.tool_settings.get("dev-dependencies", {}) + and group not in project.pyproject.settings.get("dev-dependencies", {}) ): if editables: raise PdmUsageError( @@ -295,7 +295,7 @@ def do_add( save_version_specifiers({group: deps_to_update}, resolved, save) if not dry_run: project.add_dependencies(deps_to_update, group, dev) - project.write_lockfile(project.lockfile, False) + project.write_lockfile(project.lockfile._data, False) hooks.try_emit("post_lock", resolution=resolved, dry_run=dry_run) _populate_requirement_names(group_deps) if sync: @@ -400,8 +400,7 @@ def do_update( save_version_specifiers(updated_deps, resolved, save) for group, deps in updated_deps.items(): project.add_dependencies(deps, group, dev or False) - lockfile = project.lockfile - project.write_lockfile(lockfile, False) + project.write_lockfile(project.lockfile._data, False) if sync or dry_run: do_sync( project, @@ -463,7 +462,7 @@ def do_remove( cast(Array, deps).multiline(True) if not dry_run: - project.write_pyproject() + project.pyproject.write() do_lock(project, "reuse", dry_run=dry_run, hooks=hooks) if sync: do_sync( @@ -557,12 +556,8 @@ def do_init( readme.write_text(f"# {name}\n\n{description}\n") data["project"]["readme"] = readme.name # type: ignore get_specifier(python_requires) - if not project.pyproject: - project._pyproject = data - else: - project._pyproject["project"] = data["project"] # type: ignore - project._pyproject["build-system"] = data["build-system"] # type: ignore - project.write_pyproject() + project.pyproject._data.update(data) + project.pyproject.write() hooks.try_emit("post_init") @@ -701,7 +696,7 @@ def do_import( if options is None: options = Namespace(dev=False, group=None) project_data, settings = FORMATS[key].convert(project, filename, options) - pyproject = project.pyproject or tomlkit.document() + pyproject = project.pyproject._data if "tool" not in pyproject or "pdm" not in pyproject["tool"]: # type: ignore pyproject.setdefault("tool", {})["pdm"] = tomlkit.table() @@ -727,7 +722,7 @@ def do_import( "requires": ["pdm-pep517>=1.0.0"], "build-backend": "pdm.pep517.api", } - project.pyproject = cast(dict, pyproject) + if "requires-python" not in pyproject["project"]: python_version = f"{project.python.major}.{project.python.minor}" pyproject["project"]["requires-python"] = f">={python_version}" @@ -735,7 +730,7 @@ def do_import( "The project's [primary]requires-python[/] has been set to [primary]>=" f"{python_version}[/]. You can change it later if necessary." ) - project.write_pyproject() + project.pyproject.write() def ask_for_import(project: Project) -> None: diff --git a/src/pdm/cli/commands/init.py b/src/pdm/cli/commands/init.py index b5b3ffc1ca..97f76e0c15 100644 --- a/src/pdm/cli/commands/init.py +++ b/src/pdm/cli/commands/init.py @@ -40,7 +40,7 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: from pdm.cli.commands.venv.utils import get_venv_python hooks = HookManager(project, options.skip) - if project.pyproject_file.exists(): + if project.pyproject.exists: project.core.ui.echo( "pyproject.toml already exists, update it now.", style="primary" ) diff --git a/src/pdm/cli/commands/install.py b/src/pdm/cli/commands/install.py index 09c93befa5..cec13039ea 100644 --- a/src/pdm/cli/commands/install.py +++ b/src/pdm/cli/commands/install.py @@ -38,7 +38,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: ) def handle(self, project: Project, options: argparse.Namespace) -> None: - if not project.meta and termui.is_interactive(): + if not project.pyproject.is_valid and termui.is_interactive(): actions.ask_for_import(project) hooks = HookManager(project, options.skip) diff --git a/src/pdm/cli/commands/show.py b/src/pdm/cli/commands/show.py index 3b503be267..52e959669d 100644 --- a/src/pdm/cli/commands/show.py +++ b/src/pdm/cli/commands/show.py @@ -53,8 +53,10 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: else: if not project.name: raise PdmUsageError("This project is not a package") - metadata = project.meta - package = normalize_name(metadata.name) + package = normalize_name(project.name) + metadata = ( + project.make_self_candidate().prepare(project.environment).metadata + ) latest_stable = None assert metadata project_info = ProjectInfo(metadata) diff --git a/src/pdm/cli/utils.py b/src/pdm/cli/utils.py index 3f8ac2ddc7..9caacd87c8 100644 --- a/src/pdm/cli/utils.py +++ b/src/pdm/cli/utils.py @@ -40,7 +40,7 @@ ) from pdm.models.specifiers import get_specifier from pdm.project import Project -from pdm.utils import is_path_relative_to, url_to_path +from pdm.utils import is_path_relative_to, normalize_name, url_to_path if TYPE_CHECKING: from resolvelib.resolvers import RequirementInformation, ResolutionImpossible @@ -310,8 +310,8 @@ def add_package_to_reverse_tree( def package_is_project(package: Package, project: Project) -> bool: return ( not project.environment.is_global - and bool(project.name) - and package.name == project.meta.project_name.lower() + and project.name is not None + and package.name == normalize_name(project.name) ) @@ -526,11 +526,11 @@ def save_version_specifiers( def check_project_file(project: Project) -> None: """Check the existence of the project file and throws an error on failure.""" - if not project.meta: + if not project.pyproject.is_valid: raise ProjectError( "The pyproject.toml has not been initialized yet. You can do this " "by running [success]`pdm init`[/]." - ) + ) from None def find_importable_files(project: Project) -> Iterable[tuple[str, Path]]: @@ -630,8 +630,8 @@ def translate_groups( project: Project, default: bool, dev: bool, groups: Iterable[str] ) -> list[str]: """Translate default, dev and groups containing ":all" into a list of groups""" - optional_groups = set(project.meta.optional_dependencies or []) - dev_groups = set(project.tool_settings.get("dev-dependencies", [])) + optional_groups = set(project.pyproject.metadata.get("optional-dependencies", {})) + dev_groups = set(project.pyproject.settings.get("dev-dependencies", {})) groups_set = set(groups) if dev is None: dev = True diff --git a/src/pdm/core.py b/src/pdm/core.py index acfecb7261..2df3263f4e 100644 --- a/src/pdm/core.py +++ b/src/pdm/core.py @@ -115,7 +115,7 @@ def ensure_project( ) if getattr(options, "lockfile", None): - project.lockfile_file = options.lockfile + project.set_lockfile(options.lockfile) return project def create_project( diff --git a/src/pdm/formats/requirements.py b/src/pdm/formats/requirements.py index 24d46b69a6..e9bc6b9e61 100644 --- a/src/pdm/formats/requirements.py +++ b/src/pdm/formats/requirements.py @@ -190,7 +190,7 @@ def export( for item in sorted(set(candidate.hashes.values())): # type: ignore lines.append(f" \\\n --hash={item}") lines.append("\n") - sources = project.tool_settings.get("source", []) + sources = project.pyproject.settings.get("source", []) for source in sources: url = expand_env_vars_in_auth(source["url"]) prefix = "--index-url" if source["name"] == "pypi" else "--extra-index-url" diff --git a/src/pdm/installers/synchronizers.py b/src/pdm/installers/synchronizers.py index 8322bfbb47..6eaa45ceeb 100644 --- a/src/pdm/installers/synchronizers.py +++ b/src/pdm/installers/synchronizers.py @@ -15,7 +15,7 @@ from pdm.models.candidates import Candidate, make_candidate from pdm.models.environment import Environment from pdm.models.requirements import Requirement, parse_requirement, strip_extras -from pdm.utils import is_editable +from pdm.utils import is_editable, normalize_name if TYPE_CHECKING: from rich.progress import Progress @@ -130,8 +130,8 @@ def __init__( keys = [] if ( self.install_self - and getattr( - self.environment.project.meta.config, "editable_backend", "path" + and self.environment.project.pyproject.settings.get("build", {}).get( + "editable_backend", "path" ) == "editables" and "editables" not in candidates @@ -174,7 +174,7 @@ def get_manager(self) -> InstallManager: def self_key(self) -> str | None: name = self.environment.project.name if name: - return self.environment.project.meta.project_name.lower() + return normalize_name(name) return name def _should_update(self, dist: Distribution, can: Candidate) -> bool: diff --git a/src/pdm/models/candidates.py b/src/pdm/models/candidates.py index 13209a1fc1..5239e9f36f 100644 --- a/src/pdm/models/candidates.py +++ b/src/pdm/models/candidates.py @@ -15,7 +15,7 @@ from pdm import termui from pdm.builders import EditableBuilder, WheelBuilder from pdm.compat import importlib_metadata as im -from pdm.exceptions import BuildError, CandidateNotFound, InvalidPyVersion +from pdm.exceptions import BuildError, CandidateNotFound, InvalidPyVersion, ProjectError from pdm.models.requirements import ( FileRequirement, Requirement, @@ -25,7 +25,7 @@ ) from pdm.models.setup import Setup from pdm.models.specifiers import PySpecSet -from pdm.project.metadata import MutableMetadata, SetupDistribution +from pdm.project.project_file import PyProject from pdm.utils import ( cached_property, cd, @@ -451,11 +451,13 @@ def prepare_metadata(self) -> im.Distribution: pyproject_toml = self._unpacked_dir / "pyproject.toml" if pyproject_toml.exists(): try: - metadata = MutableMetadata.from_file(pyproject_toml) - except ValueError: + metadata = PyProject( + pyproject_toml, ui=self.environment.project.core.ui + ).metadata.unwrap() + except ProjectError: termui.logger.warn("Failed to parse pyproject.toml") else: - dynamic_fields = metadata.dynamic or [] + dynamic_fields = metadata.get("dynamic", []) # Use the parse result only when all are static if set(dynamic_fields).isdisjoint( { @@ -467,14 +469,14 @@ def prepare_metadata(self) -> im.Distribution: } ): setup = Setup( - name=metadata.name, - summary=metadata.description, - version=metadata.version, - install_requires=metadata.dependencies or [], - extras_require=metadata.optional_dependencies or {}, - python_requires=metadata.requires_python or None, + name=metadata.get("name"), + summary=metadata.get("description"), + version=metadata.get("version"), + install_requires=metadata.get("dependencies", []), + extras_require=metadata.get("optional-dependencies", {}), + python_requires=metadata.get("requires-python"), ) - return SetupDistribution(setup) + return setup.as_dist() # If all fail, try building the source to get the metadata builder = EditableBuilder if self.req.editable else WheelBuilder try: @@ -495,7 +497,7 @@ def prepare_metadata(self) -> im.Distribution: termui.logger.warn(message) warnings.warn(message, RuntimeWarning) setup = Setup() - return SetupDistribution(setup) + return setup.as_dist() else: return im.PathDistribution(Path(cast(str, self._metadata_dir))) diff --git a/src/pdm/models/environment.py b/src/pdm/models/environment.py index 1083091a85..a964f4e8a4 100644 --- a/src/pdm/models/environment.py +++ b/src/pdm/models/environment.py @@ -161,9 +161,9 @@ def get_finder( ignore_compatibility=ignore_compatibility, no_binary=os.getenv("PDM_NO_BINARY", "").split(","), only_binary=os.getenv("PDM_ONLY_BINARY", "").split(","), - respect_source_order=self.project.tool_settings.get("resolution", {}).get( - "respect-source-order", False - ), + respect_source_order=self.project.pyproject.settings.get( + "resolution", {} + ).get("respect-source-order", False), verbosity=self.project.core.ui.verbosity, ) try: diff --git a/src/pdm/models/project_info.py b/src/pdm/models/project_info.py index d3f7f439a6..d84d4c3827 100644 --- a/src/pdm/models/project_info.py +++ b/src/pdm/models/project_info.py @@ -4,20 +4,15 @@ from email.message import Message from typing import TYPE_CHECKING, Any, Iterator, cast -from pdm.pep517.metadata import Metadata - if TYPE_CHECKING: from pdm.compat import Distribution class ProjectInfo: - def __init__(self, metadata: Distribution | Metadata) -> None: + def __init__(self, metadata: Distribution) -> None: self.latest_stable_version = "" self.installed_version = "" - if isinstance(metadata, Metadata): - self._parsed = self._parse_self(metadata) - else: - self._parsed = self._parse(metadata) + self._parsed = self._parse(metadata) def _parse(self, data: Distribution) -> dict[str, Any]: metadata = cast(Message, data.metadata) @@ -48,26 +43,6 @@ def _parse(self, data: Distribution) -> dict[str, Any]: "project-urls": [": ".join(parts) for parts in project_urls.items()], } - def _parse_self(self, metadata: Metadata) -> dict[str, Any]: - license_expression = getattr(metadata, "license_expression", None) - if license_expression is None: - license_expression = getattr(metadata, "license", "") - return { - "name": str(metadata.name), - "version": str(metadata.version), - "summary": str(metadata.description), - "author": str(metadata.author), - "email": str(metadata.author_email), - "license": str(license_expression), - "requires-python": str(metadata.requires_python), - "platform": "", - "keywords": ", ".join(metadata.keywords or []), - "homepage": "", - "project-urls": [ - ": ".join(parts) for parts in (metadata.project_urls or {}).items() - ], - } - def __getitem__(self, key: str) -> Any: return self._parsed[key] diff --git a/src/pdm/models/repositories.py b/src/pdm/models/repositories.py index 0ec80f8076..2e46cc3070 100644 --- a/src/pdm/models/repositories.py +++ b/src/pdm/models/repositories.py @@ -121,21 +121,16 @@ def is_this_package(self, requirement: Requirement) -> bool: and requirement.key == normalize_name(project.name) ) - def make_this_candidate(self, requirement: Requirement) -> Candidate | None: - """Make a candidate for this package, or None if the requirement doesn't match. + def make_this_candidate(self, requirement: Requirement) -> Candidate: + """Make a candidate for this package. In this case the finder will look for a candidate from the package sources """ project = self.environment.project assert project.name link = Link.from_path(project.root) # type: ignore - version: str = project.meta.version - if ( - not version - or requirement.specifier - and not requirement.specifier.contains(version, True) - ): - return None - return make_candidate(requirement, project.name, version, link) + candidate = make_candidate(requirement, project.name, link=link) + candidate.prepare(self.environment).metadata + return candidate def find_candidates( self, @@ -150,9 +145,7 @@ def find_candidates( # include prereleases if self.is_this_package(requirement): - candidate = self.make_this_candidate(requirement) - if candidate is not None: - return [candidate] + return [self.make_this_candidate(requirement)] requires_python = requirement.requires_python & self.environment.python_requires cans = list(self._find_candidates(requirement)) applicable_cans = [ @@ -442,20 +435,20 @@ def _get_dependency_from_local_package(self, candidate: Candidate) -> CandidateI if candidate.name != self.environment.project.name: raise CandidateInfoNotFound(candidate) from None - reqs = self.environment.project.meta.dependencies + reqs = self.environment.project.pyproject.metadata.get("dependencies", []) + optional_dependencies = self.environment.project.pyproject.metadata.get( + "optional-dependencies", {} + ) if candidate.req.extras is not None: reqs = sum( - ( - self.environment.project.meta.optional_dependencies[g] - for g in candidate.req.extras - ), + (optional_dependencies.get(g, []) for g in candidate.req.extras), [], ) return ( reqs, str(self.environment.python_requires), - self.environment.project.meta.description, + self.environment.project.pyproject.metadata.get("description", "UNKNOWN"), ) def dependency_generators(self) -> Iterable[Callable[[Candidate], CandidateInfo]]: diff --git a/src/pdm/models/setup.py b/src/pdm/models/setup.py index 00da7cb59f..a4231b854d 100644 --- a/src/pdm/models/setup.py +++ b/src/pdm/models/setup.py @@ -1,8 +1,13 @@ +from __future__ import annotations + import ast from configparser import ConfigParser from dataclasses import asdict, dataclass, field, fields +import os from pathlib import Path -from typing import Any, Dict, Iterable, List, Optional, Tuple, no_type_check +from typing import Any, Iterable, no_type_check + +from pdm.compat import Distribution @dataclass @@ -11,12 +16,12 @@ class Setup: Abstraction of a Python project setup file. """ - name: Optional[str] = None - version: Optional[str] = None - install_requires: List[str] = field(default_factory=list) - extras_require: Dict[str, List[str]] = field(default_factory=dict) - python_requires: Optional[str] = None - summary: Optional[str] = None + name: str | None = None + version: str | None = None + install_requires: list[str] = field(default_factory=list) + extras_require: dict[str, list[str]] = field(default_factory=dict) + python_requires: str | None = None + summary: str | None = None def update(self, other: "Setup") -> None: for f in fields(self): @@ -24,13 +29,16 @@ def update(self, other: "Setup") -> None: if other_field: setattr(self, f.name, other_field) - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: return asdict(self) @classmethod def from_directory(cls, dir: Path) -> "Setup": return _SetupReader.read_from_directory(dir) + def as_dist(self) -> Distribution: + return SetupDistribution(self) + class _SetupReader: """ @@ -58,18 +66,21 @@ def read_from_directory(cls, directory: Path) -> Setup: @staticmethod def read_pyproject_toml(file: Path) -> Setup: - from pdm.project.metadata import MutableMetadata + from pdm import termui + from pdm.exceptions import ProjectError + from pdm.project.project_file import PyProject try: - metadata = MutableMetadata.from_file(file) - except ValueError: + metadata = PyProject(file, ui=termui.UI()).metadata.unwrap() + except ProjectError: return Setup() return Setup( - name=metadata.name, - version=metadata.version, - install_requires=metadata.dependencies or [], - extras_require=metadata.optional_dependencies or {}, - python_requires=metadata.requires_python, + name=metadata.get("name"), + summary=metadata.get("description"), + version=metadata.get("version"), + install_requires=metadata.get("dependencies", []), + extras_require=metadata.get("optional-dependencies", {}), + python_requires=metadata.get("requires-python"), ) @no_type_check @@ -112,7 +123,7 @@ def read_setup_cfg(file: Path) -> Setup: version = meta_version install_requires = [] - extras_require: Dict[str, List[str]] = {} + extras_require: dict[str, list[str]] = {} python_requires = None if parser.has_section("options"): if parser.has_option("options", "install_requires"): @@ -147,8 +158,8 @@ def read_setup_cfg(file: Path) -> Setup: @classmethod def _find_setup_call( - cls, elements: List[Any] - ) -> Tuple[Optional[ast.Call], Optional[List[Any]]]: + cls, elements: list[Any] + ) -> tuple[ast.Call | None, list[Any | None]]: funcdefs = [] for i, element in enumerate(elements): if isinstance(element, ast.If) and i == len(elements) - 1: @@ -198,8 +209,8 @@ def _find_setup_call( @no_type_check @classmethod def _find_sub_setup_call( - cls, elements: List[Any] - ) -> Tuple[Optional[ast.Call], Optional[List[Any]]]: + cls, elements: list[Any] + ) -> tuple[ast.Call | None, list[Any | None]]: for element in elements: if not isinstance(element, (ast.FunctionDef, ast.If)): continue @@ -216,8 +227,8 @@ def _find_sub_setup_call( @no_type_check @classmethod - def _find_install_requires(cls, call: ast.Call, body: Iterable[Any]) -> List[str]: - install_requires: List[str] = [] + def _find_install_requires(cls, call: ast.Call, body: Iterable[Any]) -> list[str]: + install_requires: list[str] = [] value = cls._find_in_call(call, "install_requires") if value is None: # Trying to find in kwargs @@ -260,8 +271,8 @@ def _find_install_requires(cls, call: ast.Call, body: Iterable[Any]) -> List[str @classmethod def _find_extras_require( cls, call: ast.Call, body: Iterable[Any] - ) -> Dict[str, List[str]]: - extras_require: Dict[str, List[str]] = {} + ) -> dict[str, list[str]]: + extras_require: dict[str, list[str]] = {} value = cls._find_in_call(call, "extras_require") if value is None: # Trying to find in kwargs @@ -312,8 +323,8 @@ def _find_extras_require( @classmethod def _find_single_string( - cls, call: ast.Call, body: List[Any], name: str - ) -> Optional[str]: + cls, call: ast.Call, body: list[Any], name: str + ) -> str | None: value = cls._find_in_call(call, name) if value is None: # Trying to find in kwargs @@ -351,14 +362,14 @@ def _find_single_string( return None @staticmethod - def _find_in_call(call: ast.Call, name: str) -> Optional[Any]: + def _find_in_call(call: ast.Call, name: str) -> Any | None: for keyword in call.keywords: if keyword.arg == name: return keyword.value return None @staticmethod - def _find_call_kwargs(call: ast.Call) -> Optional[Any]: + def _find_call_kwargs(call: ast.Call) -> Any | None: kwargs = None for keyword in call.keywords: if keyword.arg is None: @@ -367,7 +378,7 @@ def _find_call_kwargs(call: ast.Call) -> Optional[Any]: return kwargs @staticmethod - def _find_variable_in_body(body: Iterable[Any], name: str) -> Optional[Any]: + def _find_variable_in_body(body: Iterable[Any], name: str) -> Any | None: for elem in body: if not isinstance(elem, ast.Assign): @@ -382,8 +393,50 @@ def _find_variable_in_body(body: Iterable[Any], name: str) -> Optional[Any]: return None @staticmethod - def _find_in_dict(dict_: ast.Dict, name: str) -> Optional[Any]: + def _find_in_dict(dict_: ast.Dict, name: str) -> Any | None: for key, val in zip(dict_.keys, dict_.values): if isinstance(key, ast.Str) and key.s == name: return val return None + + +class SetupDistribution(Distribution): + def __init__(self, data: Setup) -> None: + self._data = data + + def read_text(self, filename: str) -> str | None: + return None + + def locate_file(self, path: str | os.PathLike[str]) -> os.PathLike[str]: + return Path() + + @property + def metadata(self) -> dict[str, Any]: # type: ignore + return { + "Name": self._data.name, + "Version": self._data.version, + "Summary": self._data.summary, + "Requires-Python": self._data.python_requires, + } + + @property + def requires(self) -> list[str] | None: + from pdm.models.requirements import parse_requirement + from pdm.models.markers import Marker + + result = self._data.install_requires + for extra, reqs in self._data.extras_require.items(): + extra_marker = f"extra == '{extra}'" + for req in reqs: + parsed = parse_requirement(req) + old_marker = str(parsed.marker) if parsed.marker else None + if old_marker: + if " or " in old_marker: + new_marker = f"({old_marker}) and {extra_marker}" + else: + new_marker = f"{old_marker} and {extra_marker}" + else: + new_marker = extra_marker + parsed.marker = Marker(new_marker) + result.append(parsed.as_line()) + return result diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py index 289a6c86f2..41e408f4af 100644 --- a/src/pdm/project/core.py +++ b/src/pdm/project/core.py @@ -1,7 +1,6 @@ from __future__ import annotations import hashlib -import json import os import re import shutil @@ -14,6 +13,7 @@ import tomlkit from findpython import Finder from tomlkit.items import Array +from unearth import Link from pdm import termui from pdm._types import Source @@ -26,9 +26,9 @@ from pdm.models.requirements import Requirement, parse_requirement, strip_extras from pdm.models.specifiers import PySpecSet, get_specifier from pdm.project.config import Config -from pdm.project.metadata import MutableMetadata as Metadata +from pdm.project.lockfile import Lockfile +from pdm.project.project_file import PyProject from pdm.utils import ( - atomic_open_for_write, cached_property, cd, deprecation_warning, @@ -62,8 +62,8 @@ class Project: """ PYPROJECT_FILENAME = "pyproject.toml" + LOCKFILE_FILENAME = "pdm.lock" DEPENDENCIES_RE = re.compile(r"(?:(.+?)-)?dependencies") - LOCKFILE_VERSION = "4.1" def __init__( self, @@ -72,8 +72,7 @@ def __init__( is_global: bool = False, global_config: str | Path | None = None, ) -> None: - self._pyproject: dict | None = None - self._lockfile: dict | None = None + self._lockfile: Lockfile | None = None self._environment: Environment | None = None self._python: PythonInfo | None = None self.core = core @@ -106,61 +105,24 @@ def __init__( self.root: Path = Path(root_path or "").absolute() self.is_global = is_global self.init_global_project() - self._lockfile_file = self.root / "pdm.lock" def __repr__(self) -> str: return f"" - @property - def pyproject_file(self) -> Path: - return self.root / self.PYPROJECT_FILENAME - - @property - def lockfile_file(self) -> Path: - return self._lockfile_file - - @lockfile_file.setter - def lockfile_file(self, path: str) -> None: - self._lockfile_file = Path(path).absolute() - self._lockfile = None - - @property - def pyproject(self) -> dict | None: - if not self._pyproject: - if self.pyproject_file.exists(): - data = tomlkit.parse(self.pyproject_file.read_text("utf-8")) - self._pyproject = cast(dict, data) - else: - self._pyproject = cast(dict, tomlkit.document()) - return self._pyproject - - @pyproject.setter - def pyproject(self, data: dict[str, Any]) -> None: - self._pyproject = data - - @property - def tool_settings(self) -> dict: - data = self.pyproject - if not data: - return {} - return data.setdefault("tool", {}).setdefault("pdm", {}) - - @property - def name(self) -> str | None: - return self.meta.get("name") + @cached_property + def pyproject(self) -> PyProject: + return PyProject(self.root / self.PYPROJECT_FILENAME, ui=self.core.ui) @property - def lockfile(self) -> dict: - if not self._lockfile: - if not self.lockfile_file.is_file(): - raise ProjectError("Lock file does not exist.") - data = tomlkit.parse(self.lockfile_file.read_text("utf-8")) - self._lockfile = cast(dict, data) + def lockfile(self) -> Lockfile: + if self._lockfile is None: + self._lockfile = Lockfile( + self.root / self.LOCKFILE_FILENAME, ui=self.core.ui + ) return self._lockfile - @lockfile.setter - def lockfile(self, data: dict[str, Any]) -> None: - self._lockfile = data + def set_lockfile(self, path: str | Path) -> None: + self._lockfile = Lockfile(path, ui=self.core.ui) @property def config(self) -> dict[str, Any]: @@ -171,13 +133,17 @@ def config(self) -> dict[str, Any]: @property def scripts(self) -> dict[str, str | dict[str, str]]: - return self.tool_settings.get("scripts", {}) # type: ignore + return self.pyproject.settings.get("scripts", {}) # type: ignore @cached_property def project_config(self) -> Config: """Read-and-writable configuration dict for project settings""" return Config(self.root / ".pdm.toml") + @property + def name(self) -> str | None: + return self.pyproject.metadata.get("name") + @property def python(self) -> PythonInfo: if not self._python: @@ -195,11 +161,6 @@ def python(self, value: PythonInfo) -> None: self._python = value self.project_config["python.path"] = value.path - @property - def python_executable(self) -> str: - """For backward compatibility""" - return str(self.python.executable) - def resolve_interpreter(self) -> PythonInfo: """Get the Python interpreter path.""" from pdm.cli.commands.venv.utils import get_venv_python, iter_venvs @@ -303,13 +264,16 @@ def environment(self, value: Environment) -> None: @property def python_requires(self) -> PySpecSet: - return PySpecSet(self.meta.requires_python) + try: + return PySpecSet(self.pyproject.metadata.get("requires-python", "")) + except ProjectError: + return PySpecSet() def get_dependencies(self, group: str | None = None) -> dict[str, Requirement]: - metadata = self.meta + metadata = self.pyproject.metadata group = group or "default" optional_dependencies = metadata.get("optional-dependencies", {}) - dev_dependencies = self.tool_settings.get("dev-dependencies", {}) + dev_dependencies = self.pyproject.settings.get("dev-dependencies", {}) in_metadata = group == "default" or group in optional_dependencies if group == "default": deps = metadata.get("dependencies", []) @@ -354,7 +318,7 @@ def dependencies(self) -> dict[str, Requirement]: @property def dev_dependencies(self) -> dict[str, Requirement]: """All development dependencies""" - dev_group = self.tool_settings.get("dev-dependencies", {}) + dev_group = self.pyproject.settings.get("dev-dependencies", {}) if not dev_group: return {} result = {} @@ -370,10 +334,10 @@ def dev_dependencies(self) -> dict[str, Requirement]: def iter_groups(self) -> Iterable[str]: groups = {"default"} - if self.meta.optional_dependencies: - groups.update(self.meta.optional_dependencies.keys()) - if self.tool_settings.get("dev-dependencies"): - groups.update(self.tool_settings["dev-dependencies"].keys()) + if self.pyproject.metadata.get("optional-dependencies"): + groups.update(self.pyproject.metadata["optional-dependencies"].keys()) + if self.pyproject.settings.get("dev-dependencies"): + groups.update(self.pyproject.settings["dev-dependencies"].keys()) return groups @property @@ -382,7 +346,7 @@ def all_dependencies(self) -> dict[str, dict[str, Requirement]]: @property def allow_prereleases(self) -> bool | None: - return self.tool_settings.get("allow_prereleases") + return self.pyproject.settings.get("allow_prereleases") @property def default_source(self) -> Source: @@ -398,7 +362,7 @@ def default_source(self) -> Source: @property def sources(self) -> list[Source]: - sources = list(self.tool_settings.get("source", [])) + sources = list(self.pyproject.settings.get("source", [])) if all(source.get("name") != "pypi" for source in sources): sources.insert(0, self.default_source) expanded_sources: list[Source] = [ @@ -423,10 +387,8 @@ def get_repository( @property def locked_repository(self) -> LockedRepository: - import copy - try: - lockfile = copy.deepcopy(self.lockfile) + lockfile = self.lockfile._data.unwrap() except ProjectError: lockfile = {} @@ -457,7 +419,7 @@ def get_provider( allow_prereleases = self.allow_prereleases overrides = { normalize_name(k): v - for k, v in self.tool_settings.get("overrides", {}).items() + for k, v in self.pyproject.settings.get("overrides", {}).items() } locked_repository: LockedRepository | None = None if strategy != "all" or for_install: @@ -506,72 +468,51 @@ def get_reporter( return SpinnerReporter(spinner or termui.DummySpinner(), requirements) def get_lock_metadata(self) -> dict[str, Any]: - content_hash = tomlkit.string("sha256:" + self.get_content_hash("sha256")) + content_hash = tomlkit.string("sha256:" + self.pyproject.content_hash("sha256")) content_hash.trivia.trail = "\n\n" - return {"lock_version": self.LOCKFILE_VERSION, "content_hash": content_hash} + return { + "lock_version": self.lockfile.spec_version, + "content_hash": content_hash, + } def write_lockfile( self, toml_data: dict, show_message: bool = True, write: bool = True ) -> None: toml_data["metadata"].update(self.get_lock_metadata()) + self.lockfile.set_data(toml_data) if write: - with atomic_open_for_write(self.lockfile_file) as fp: - tomlkit.dump(toml_data, fp) # type: ignore - if show_message: - self.core.ui.echo("Changes are written to [success]pdm.lock[/].") - self._lockfile = None - else: - self._lockfile = toml_data + self.lockfile.write(show_message) def make_self_candidate(self, editable: bool = True) -> Candidate: req = parse_requirement(path_to_url(self.root.as_posix()), editable) - req.name = self.meta.name - return make_candidate(req, name=self.meta.name, version=self.meta.version) - - def get_content_hash(self, algo: str = "md5") -> str: - # Only calculate sources and dependencies groups. Otherwise lock file is - # considered as unchanged. - dump_data = { - "sources": self.tool_settings.get("source", []), - "dependencies": self.meta.get("dependencies", []), - "dev-dependencies": self.tool_settings.get("dev-dependencies", {}), - "optional-dependencies": self.meta.get("optional-dependencies", {}), - "requires-python": self.meta.get("requires-python", ""), - "overrides": self.tool_settings.get("overrides", {}), - } - pyproject_content = json.dumps(dump_data, sort_keys=True) - hasher = hashlib.new(algo) - hasher.update(pyproject_content.encode("utf-8")) - return hasher.hexdigest() + assert self.name + req.name = self.name + can = make_candidate(req, name=self.name, link=Link.from_path(self.root)) + can.prepare(self.environment).metadata + return can def is_lockfile_hash_match(self) -> bool: - if not self.lockfile_file.exists(): - return False - hash_in_lockfile = str( - self.lockfile.get("metadata", {}).get("content_hash", "") - ) + hash_in_lockfile = str(self.lockfile.hash) if not hash_in_lockfile: return False algo, hash_value = hash_in_lockfile.split(":") - content_hash = self.get_content_hash(algo) + content_hash = self.pyproject.content_hash(algo) return content_hash == hash_value def is_lockfile_compatible(self) -> bool: """Within the same major version, the higher lockfile generator can work with lower lockfile but not vice versa. """ - if not self.lockfile_file.exists(): + if not self.lockfile.exists: return True - lockfile_version = str( - self.lockfile.get("metadata", {}).get("lock_version", "") - ) + lockfile_version = str(self.lockfile.file_version) if not lockfile_version: return False if "." not in lockfile_version: lockfile_version += ".0" accepted = get_specifier(f"~={lockfile_version},>={lockfile_version}") - return accepted.contains(self.LOCKFILE_VERSION) + return accepted.contains(self.lockfile.spec_version) def get_pyproject_dependencies( self, group: str, dev: bool = False @@ -580,16 +521,26 @@ def get_pyproject_dependencies( Return a tuple of two elements, the first is the dependencies array, and the second tells whether it is a dev-dependencies group. """ + metadata, settings = self.pyproject.metadata, self.pyproject.settings if group == "default": - return self.meta.setdefault("dependencies", []), False + return metadata.setdefault("dependencies", []), False deps_dict = { - False: self.meta.setdefault("optional-dependencies", {}), - True: self.tool_settings.setdefault("dev-dependencies", {}), + False: metadata.get("optional-dependencies", {}), + True: settings.get("dev-dependencies", {}), } for is_dev, deps in deps_dict.items(): if group in deps: return deps[group], is_dev - return deps_dict[dev].setdefault(group, []), dev + if dev: + return ( + settings.setdefault("dev-dependencies", {}).setdefault(group, []), + dev, + ) + else: + return ( + metadata.setdefault("optional-dependencies", {}).setdefault(group, []), + dev, + ) def add_dependencies( self, @@ -615,36 +566,18 @@ def add_dependencies( deps[matched_index] = req if not is_dev and has_variable: field = "dependencies" if to_group == "default" else "optional-dependencies" - self.meta["dynamic"] = sorted(set(self.meta.get("dynamic", []) + [field])) - self.write_pyproject(show_message) - - def write_pyproject(self, show_message: bool = True) -> None: - with atomic_open_for_write( - self.pyproject_file.as_posix(), encoding="utf-8" - ) as f: - tomlkit.dump(self.pyproject, f) # type: ignore - if show_message: - self.core.ui.echo("Changes are written to [success]pyproject.toml[/].") - self._pyproject = None - - @property - def meta(self) -> Metadata: - if not self.pyproject: - self.pyproject = {"project": tomlkit.table()} - return Metadata(self.root, self.pyproject) + metadata = self.pyproject.metadata + metadata["dynamic"] = sorted(set(metadata.get("dynamic", []) + [field])) + self.pyproject.write(show_message) def init_global_project(self) -> None: - if not self.is_global: + if not self.is_global or not self.pyproject.empty: return - if not self.pyproject_file.exists(): - self.root.mkdir(parents=True, exist_ok=True) - self.pyproject_file.write_text( - """\ -[project] -dependencies = ["pip", "setuptools", "wheel"] -""" - ) - self._pyproject = None + self.root.mkdir(parents=True, exist_ok=True) + self.pyproject.set_data( + {"project": {"dependencies": ["pip", "setuptools", "wheel"]}} + ) + self.pyproject.write() @property def cache_dir(self) -> Path: @@ -723,3 +656,19 @@ def _get_python_finder(self) -> Finder: if self.config["python.use_venv"]: finder._providers.insert(0, VenvProvider(self)) return finder + + # compatibility, shouldn't be used directly + @property + def meta(self) -> dict[str, Any]: + deprecation_warning( + "project.meta is deprecated, use project.pyproject.metadata instead" + ) + return self.pyproject.metadata + + @property + def tool_settings(self) -> dict[str, Any]: + deprecation_warning( + "project.tool_settings is deprecated, " + "use project.pyproject.settings instead" + ) + return self.pyproject.settings diff --git a/src/pdm/project/lockfile.py b/src/pdm/project/lockfile.py new file mode 100644 index 0000000000..20806f992b --- /dev/null +++ b/src/pdm/project/lockfile.py @@ -0,0 +1,21 @@ +from pdm.project.toml_file import TOMLBase + + +class Lockfile(TOMLBase): + spec_version = "4.1" + + @property + def hash(self) -> str: + return self._data.get("metadata", {}).get("content_hash", "") + + @property + def file_version(self) -> str: + return self._data.get("metadata", {}).get("lock_version", "") + + def write(self, show_message: bool = True) -> None: + super().write() + if show_message: + self.ui.echo(f"Changes are written to [success]{self._path.name}[/].") + + def __getitem__(self, key: str) -> dict: + return self._data[key] diff --git a/src/pdm/project/metadata.py b/src/pdm/project/metadata.py deleted file mode 100644 index 65cb87107a..0000000000 --- a/src/pdm/project/metadata.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -import os -from collections.abc import MutableMapping -from pathlib import Path -from typing import Any, Iterator, TypeVar - -from pdm.compat import Distribution, tomllib -from pdm.models.markers import Marker -from pdm.models.requirements import parse_requirement -from pdm.models.setup import Setup -from pdm.pep517.metadata import Metadata - -T = TypeVar("T") - - -class MutableMetadata(Metadata, MutableMapping): - """ - A subclass of Metadata that delegates some modifying methods - to the underlying toml parsed dict. - """ - - def __init__(self, root: str | Path, pyproject: dict[str, Any]) -> None: - from pdm.formats import flit, poetry - - try: - super().__init__(root, pyproject) - except ValueError as e: - for converter in (flit, poetry): - filename = os.path.join(root, "pyproject.toml") - if converter.check_fingerprint(None, filename): - data, settings = converter.convert(None, filename, None) - pyproject.setdefault("project", {}).update(data) - pyproject.setdefault("tool", {}).setdefault("pdm", {}).update( - settings - ) - return super().__init__(root, pyproject) - raise e from None - - @classmethod - def from_file(cls, filename: str | Path) -> MutableMetadata: - """Get the metadata from a pyproject.toml file""" - return cls(os.path.dirname(filename), tomllib.load(open(filename, "rb"))) - - def __getitem__(self, k: str) -> dict | list[str] | str: - return self.data[k] - - def __setitem__(self, k: str, v: dict | list[str] | str) -> None: - self.data[k] = v - - def __delitem__(self, k: str) -> None: - del self.data[k] - - def __iter__(self) -> Iterator: - return iter(self.data) - - def __len__(self) -> int: - return len(self.data) - - def setdefault(self, key: str, default: T) -> T: # type: ignore - return self.data.setdefault(key, default) - - -class SetupDistribution(Distribution): - def __init__(self, data: Setup) -> None: - self._data = data - - def read_text(self, filename: str) -> str | None: - return None - - def locate_file(self, path: os.PathLike[str] | str) -> os.PathLike[str]: - return Path("") - - @property - def metadata(self) -> dict[str, Any]: # type: ignore - return { - "Name": self._data.name, - "Version": self._data.version, - "Summary": self._data.summary, - "Requires-Python": self._data.python_requires, - } - - @property - def requires(self) -> list[str] | None: - result = self._data.install_requires - for extra, reqs in self._data.extras_require.items(): - extra_marker = f"extra == '{extra}'" - for req in reqs: - parsed = parse_requirement(req) - old_marker = str(parsed.marker) if parsed.marker else None - if old_marker: - if " or " in old_marker: - new_marker = f"({old_marker}) and {extra_marker}" - else: - new_marker = f"{old_marker} and {extra_marker}" - else: - new_marker = extra_marker - parsed.marker = Marker(new_marker) - result.append(parsed.as_line()) - return result diff --git a/src/pdm/project/project_file.py b/src/pdm/project/project_file.py new file mode 100644 index 0000000000..880667374a --- /dev/null +++ b/src/pdm/project/project_file.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import hashlib +import json + +from tomlkit import TOMLDocument, items + +from pdm.exceptions import ProjectError +from pdm.project.toml_file import TOMLBase + + +class PyProject(TOMLBase): + """The data object representing th pyproject.toml file""" + + def read(self) -> TOMLDocument: + from pdm.formats import flit, poetry + + data = super().read() + if "project" not in data and self._path.exists(): + # Try converting from flit and poetry + for converter in (flit, poetry): + if converter.check_fingerprint(None, self._path): + metadata, settings = converter.convert(None, self._path, None) + data["project"] = metadata + if settings: + data.setdefault("tool", {}).setdefault("pdm", {}).update( + settings + ) + break + return data + + def write(self, show_message: bool = True) -> None: + """Write the TOMLDocument to the file.""" + super().write() + if show_message: + self.ui.echo("Changes are written to [success]pyproject.toml[/].") + + @property + def is_valid(self) -> bool: + return "project" in self._data + + @property + def metadata(self) -> items.Table: + if not self.is_valid: + raise ProjectError("No [project] table found in pyproject.toml") + return self._data["project"] + + @property + def settings(self) -> items.Table: + return self._data.setdefault("tool", {}).setdefault("pdm", {}) + + def content_hash(self, algo: str = "sha256") -> str: + """Generate a hash of the sensible content of the pyproject.toml file. + When the hash changes, it means the project needs to be relocked. + """ + dump_data = { + "sources": self.settings.get("source", []), + "dependencies": self.metadata.get("dependencies", []), + "dev-dependencies": self.settings.get("dev-dependencies", {}), + "optional-dependencies": self.metadata.get("optional-dependencies", {}), + "requires-python": self.metadata.get("requires-python", ""), + "overrides": self.settings.get("overrides", {}), + } + pyproject_content = json.dumps(dump_data, sort_keys=True) + hasher = hashlib.new(algo) + hasher.update(pyproject_content.encode("utf-8")) + return hasher.hexdigest() diff --git a/src/pdm/project/toml_file.py b/src/pdm/project/toml_file.py new file mode 100644 index 0000000000..d157b07d60 --- /dev/null +++ b/src/pdm/project/toml_file.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, Mapping + +import tomlkit +from tomlkit.toml_document import TOMLDocument +from tomlkit.toml_file import TOMLFile + +from pdm import termui + + +class TOMLBase(TOMLFile): + def __init__(self, path: str | Path, *, ui: termui.UI) -> None: + super().__init__(path) + self._path = Path(path) + self.ui = ui + self._data = self.read() + + def read(self) -> TOMLDocument: + if not self._path.exists(): + return tomlkit.document() + return super().read() + + def set_data(self, data: Mapping[str, Any]) -> None: + """Set the data of the TOML file.""" + self._data = tomlkit.document() + self._data.update(data) + + def reload(self) -> None: + self._data = self.read() + + def write(self) -> None: + return super().write(self._data) + + @property + def exists(self) -> bool: + return self._path.exists() + + @property + def empty(self) -> bool: + return not self._data diff --git a/src/pdm/resolver/core.py b/src/pdm/resolver/core.py index d6a85741a2..4c03468410 100644 --- a/src/pdm/resolver/core.py +++ b/src/pdm/resolver/core.py @@ -38,7 +38,7 @@ def resolve( mapping.pop("python", None) local_name = ( - normalize_name(repository.environment.project.meta.name) + normalize_name(repository.environment.project.name) if repository.environment.project.name else None ) diff --git a/tests/cli/test_add.py b/tests/cli/test_add.py index 05613fb108..0a6753ea3b 100644 --- a/tests/cli/test_add.py +++ b/tests/cli/test_add.py @@ -14,9 +14,9 @@ def test_add_package(project, working_set, is_dev): actions.do_add(project, is_dev, packages=["requests"]) group = ( - project.tool_settings["dev-dependencies"]["dev"] + project.pyproject.settings["dev-dependencies"]["dev"] if is_dev - else project.meta["dependencies"] + else project.pyproject.metadata["dependencies"] ) assert group[0] == "requests~=2.19" @@ -36,7 +36,7 @@ def test_add_command(project, invoke, mocker): def test_add_package_to_custom_group(project, working_set): actions.do_add(project, group="test", packages=["requests"]) - assert "requests" in project.meta.optional_dependencies["test"][0] + assert "requests" in project.pyproject.metadata["optional-dependencies"]["test"][0] locked_candidates = project.locked_repository.all_candidates assert locked_candidates["idna"].version == "2.7" for package in ("requests", "idna", "chardet", "urllib3", "certifi"): @@ -47,7 +47,7 @@ def test_add_package_to_custom_group(project, working_set): def test_add_package_to_custom_dev_group(project, working_set): actions.do_add(project, dev=True, group="test", packages=["requests"]) - dependencies = project.tool_settings["dev-dependencies"]["test"] + dependencies = project.pyproject.settings["dev-dependencies"]["test"] assert "requests" in dependencies[0] locked_candidates = project.locked_repository.all_candidates assert locked_candidates["idna"].version == "2.7" @@ -65,7 +65,7 @@ def test_add_editable_package(project, working_set): True, editables=["git+https://github.com/test-root/demo.git#egg=demo"], ) - group = project.tool_settings["dev-dependencies"]["dev"] + group = project.pyproject.settings["dev-dependencies"]["dev"] assert group == ["-e git+https://github.com/test-root/demo.git#egg=demo"] locked_candidates = project.locked_repository.all_candidates assert ( @@ -122,9 +122,9 @@ def test_add_remote_package_url(project, is_dev): packages=["http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl"], ) group = ( - project.tool_settings["dev-dependencies"]["dev"] + project.pyproject.settings["dev-dependencies"]["dev"] if is_dev - else project.meta["dependencies"] + else project.pyproject.metadata["dependencies"] ) assert ( group[0] @@ -142,19 +142,19 @@ def test_add_no_install(project, working_set): @pytest.mark.usefixtures("repository") def test_add_package_save_exact(project): actions.do_add(project, sync=False, save="exact", packages=["requests"]) - assert project.meta.dependencies[0] == "requests==2.19.1" + assert project.pyproject.metadata["dependencies"][0] == "requests==2.19.1" @pytest.mark.usefixtures("repository") def test_add_package_save_wildcard(project): actions.do_add(project, sync=False, save="wildcard", packages=["requests"]) - assert project.meta.dependencies[0] == "requests" + assert project.pyproject.metadata["dependencies"][0] == "requests" @pytest.mark.usefixtures("repository") def test_add_package_save_minimum(project): actions.do_add(project, sync=False, save="minimum", packages=["requests"]) - assert project.meta.dependencies[0] == "requests>=2.19.1" + assert project.pyproject.metadata["dependencies"][0] == "requests>=2.19.1" def test_add_package_update_reuse(project, repository): @@ -250,14 +250,14 @@ def test_add_package_unconstrained_rewrite_specifier(project): actions.do_add(project, packages=["django"], no_self=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["django"].version == "2.2.9" - assert project.meta.dependencies[0] == "django~=2.2" + assert project.pyproject.metadata["dependencies"][0] == "django~=2.2" actions.do_add( project, packages=["django-toolbar"], no_self=True, unconstrained=True ) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["django"].version == "1.11.8" - assert project.meta.dependencies[0] == "django~=1.11" + assert project.pyproject.metadata["dependencies"][0] == "django~=1.11" @pytest.mark.usefixtures("repository", "working_set", "vcs") @@ -294,7 +294,7 @@ def test_add_with_dry_run(project, capsys): def test_add_with_prerelease(project, working_set): actions.do_add(project, packages=["urllib3"], prerelease=True) assert working_set["urllib3"].version == "1.23b0" - assert project.meta.dependencies[0] == "urllib3<2,>=1.23b0" + assert project.pyproject.metadata["dependencies"][0] == "urllib3<2,>=1.23b0" @pytest.mark.usefixtures("repository") diff --git a/tests/cli/test_build.py b/tests/cli/test_build.py index 5f816d2cbc..5c9b8eae32 100644 --- a/tests/cli/test_build.py +++ b/tests/cli/test_build.py @@ -31,7 +31,6 @@ def test_build_global_project_forbidden(invoke): def test_build_single_module(fixture_project): project = fixture_project("demo-module") - assert project.meta.version == "0.1.0" actions.do_build(project) tar_names = get_tarball_names(project.root / "dist/demo-module-0.1.0.tar.gz") @@ -56,8 +55,8 @@ def test_build_single_module(fixture_project): def test_build_single_module_with_readme(fixture_project): project = fixture_project("demo-module") - project.meta["readme"] = "README.md" - project.write_pyproject() + project.pyproject.metadata["readme"] = "README.md" + project.pyproject.write() actions.do_build(project) assert "demo-module-0.1.0/README.md" in get_tarball_names( project.root / "dist/demo-module-0.1.0.tar.gz" @@ -100,13 +99,13 @@ def test_build_src_package(fixture_project): def test_build_package_include(fixture_project): project = fixture_project("demo-package") - project.tool_settings["includes"] = [ + project.pyproject.settings["includes"] = [ "my_package/", "single_module.py", "data_out.json", ] - project.tool_settings["excludes"] = ["my_package/*.json"] - project.write_pyproject() + project.pyproject.settings["excludes"] = ["my_package/*.json"] + project.pyproject.write() actions.do_build(project) tar_names = get_tarball_names(project.root / "dist/demo-package-0.1.0.tar.gz") @@ -126,8 +125,8 @@ def test_build_package_include(fixture_project): def test_build_src_package_by_include(fixture_project): project = fixture_project("demo-src-package") - project.includes = ["src/my_package"] - project.write_pyproject() + project.pyproject.settings["includes"] = ["src/my_package"] + project.pyproject.write() actions.do_build(project) tar_names = get_tarball_names(project.root / "dist/demo-package-0.1.0.tar.gz") @@ -159,8 +158,8 @@ def test_cli_build_with_config_settings(fixture_project, invoke): @pytest.mark.parametrize("isolated", (True, False)) def test_build_with_no_isolation(fixture_project, invoke, isolated): project = fixture_project("demo-failure") - project.pyproject = {"project": {"name": "demo", "version": "0.1.0"}} - project.write_pyproject() + project.pyproject.set_data({"project": {"name": "demo", "version": "0.1.0"}}) + project.pyproject.write() invoke(["add", "first"], obj=project) args = ["build"] if not isolated: diff --git a/tests/cli/test_hooks.py b/tests/cli/test_hooks.py index a73bda0e1b..d53558c954 100644 --- a/tests/cli/test_hooks.py +++ b/tests/cli/test_hooks.py @@ -14,11 +14,11 @@ def test_pre_script_fail_fast(project, invoke, capfd, mocker): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "pre_install": "python -c \"print('PRE INSTALL CALLED'); exit(1)\"", "post_install": "python -c \"print('POST INSTALL CALLED')\"", } - project.write_pyproject() + project.pyproject.write() synchronize = mocker.patch("pdm.installers.synchronizers.Synchronizer.synchronize") result = invoke(["install"], obj=project) assert result.exit_code == 1 @@ -29,7 +29,7 @@ def test_pre_script_fail_fast(project, invoke, capfd, mocker): def test_pre_and_post_scripts(project, invoke, capfd, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "pre_script": "python echo.py pre_script", "post_script": "python echo.py post_script", "pre_test": "python echo.py pre_test", @@ -38,7 +38,7 @@ def test_pre_and_post_scripts(project, invoke, capfd, _echo): "pre_run": "python echo.py pre_run", "post_run": "python echo.py post_run", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -57,7 +57,7 @@ def test_pre_and_post_scripts(project, invoke, capfd, _echo): def test_composite_runs_all_hooks(project, invoke, capfd, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test": {"composite": ["first", "second"]}, "pre_test": "python echo.py Pre-Test", "post_test": "python echo.py Post-Test", @@ -70,7 +70,7 @@ def test_composite_runs_all_hooks(project, invoke, capfd, _echo): "pre_run": "python echo.py Pre-Run", "post_run": "python echo.py Post-Run", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -97,7 +97,7 @@ def test_composite_runs_all_hooks(project, invoke, capfd, _echo): @pytest.mark.parametrize("option", [":all", ":pre,:post"]) def test_skip_all_hooks_option(project, invoke, capfd, option: str, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test": {"composite": ["first", "second"]}, "pre_test": "python echo.py Pre-Test", "post_test": "python echo.py Post-Test", @@ -112,7 +112,7 @@ def test_skip_all_hooks_option(project, invoke, capfd, option: str, _echo): "pre_run": "python echo.py Pre-Run", "post_run": "python echo.py Post-Run", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", f"--skip={option}", "first"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -150,7 +150,7 @@ def test_skip_all_hooks_option(project, invoke, capfd, option: str, _echo): ], ) def test_skip_option(project, invoke, capfd, args, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test": {"composite": ["first", "second"]}, "pre_test": "python echo.py Pre-Test", "post_test": "python echo.py Post-Test", @@ -161,7 +161,7 @@ def test_skip_option(project, invoke, capfd, args, _echo): "pre_second": "python echo.py Pre-Second", "post_second": "python echo.py Post-Second", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", *shlex.split(args), "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -241,10 +241,10 @@ def test_skip_option_default_from_env(env, expected, monkeypatch): @pytest.fixture def hooked_project(project, capfd, specs, request): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { hook: f"python -c \"print('{hook} CALLED')\"" for hook in KNOWN_HOOKS } - project.write_pyproject() + project.pyproject.write() for fixture in specs.fixtures: request.getfixturevalue(fixture) capfd.readouterr() diff --git a/tests/cli/test_install.py b/tests/cli/test_install.py index 1ed2871c94..3e7d8d2379 100644 --- a/tests/cli/test_install.py +++ b/tests/cli/test_install.py @@ -120,9 +120,9 @@ def test_sync_without_self(project, working_set): def test_sync_with_index_change(project, index): project.project_config["pypi.url"] = "https://my.pypi.org/simple" - project.meta["requires-python"] = ">=3.6" - project.meta["dependencies"] = ["future-fstrings"] - project.write_pyproject() + project.pyproject.metadata["requires-python"] = ">=3.6" + project.pyproject.metadata["dependencies"] = ["future-fstrings"] + project.pyproject.write() index[ "/simple/future-fstrings/" ] = b""" @@ -177,7 +177,7 @@ def test_install_with_lockfile(project, invoke, working_set, repository): def test_install_with_dry_run(project, invoke, repository): project.add_dependencies({"pytz": parse_requirement("pytz")}, "default") result = invoke(["install", "--dry-run"], obj=project) - project._lockfile = None + project.lockfile.reload() assert "pytz" not in project.locked_repository.all_candidates assert "pytz 2019.3" in result.output diff --git a/tests/cli/test_lock.py b/tests/cli/test_lock.py index da14dcd2e0..35cc7f8141 100644 --- a/tests/cli/test_lock.py +++ b/tests/cli/test_lock.py @@ -17,7 +17,7 @@ def test_lock_command(project, invoke, mocker): def test_lock_dependencies(project): project.add_dependencies({"requests": parse_requirement("requests")}) actions.do_lock(project) - assert project.lockfile_file.exists() + assert project.lockfile.exists locked = project.locked_repository.all_candidates for package in ("requests", "idna", "chardet", "certifi"): assert package in locked @@ -54,10 +54,10 @@ def test_lock_refresh_keep_consistent(invoke, project, repository): result = invoke(["lock"], obj=project) assert result.exit_code == 0 assert project.is_lockfile_hash_match() - previous = project.lockfile_file.read_text() + previous = project.lockfile._path.read_text() result = invoke(["lock", "--refresh"], obj=project) assert result.exit_code == 0 - assert project.lockfile_file.read_text() == previous + assert project.lockfile._path.read_text() == previous def test_lock_check_no_change_success(invoke, project, repository): @@ -86,7 +86,7 @@ def test_innovations_with_specified_lockfile(invoke, project, working_set): project.add_dependencies({"requests": parse_requirement("requests")}) lockfile = str(project.root / "mylock.lock") invoke(["lock", "--lockfile", lockfile], strict=True, obj=project) - assert project.lockfile_file == project.root / "mylock.lock" + assert project.lockfile._path == project.root / "mylock.lock" assert project.is_lockfile_hash_match() locked = project.locked_repository.all_candidates assert "requests" in locked @@ -96,10 +96,9 @@ def test_innovations_with_specified_lockfile(invoke, project, working_set): @pytest.mark.usefixtures("repository", "vcs") def test_skip_editable_dependencies_in_metadata(project, capsys): - project.meta["dependencies"] = [ + project.pyproject.metadata["dependencies"] = [ "-e git+https://github.com/test-root/demo.git@1234567890abcdef#egg=demo" ] - project.write_pyproject() actions.do_lock(project) _, err = capsys.readouterr() assert "WARNING: Skipping editable dependency" in err diff --git a/tests/cli/test_remove.py b/tests/cli/test_remove.py index 3d1b5cd9fe..c518e8e54b 100644 --- a/tests/cli/test_remove.py +++ b/tests/cli/test_remove.py @@ -20,8 +20,8 @@ def test_remove_editable_packages_while_keeping_normal(project): True, editables=["git+https://github.com/test-root/demo.git#egg=demo"], ) - dev_group = project.tool_settings["dev-dependencies"]["dev"] - default_group = project.meta["dependencies"] + dev_group = project.pyproject.settings["dev-dependencies"]["dev"] + default_group = project.pyproject.metadata["dependencies"] actions.do_remove(project, True, packages=["demo"]) assert not dev_group assert len(default_group) == 1 @@ -72,7 +72,7 @@ def test_remove_package_exist_in_multi_groups(project, working_set): actions.do_remove(project, dev=True, packages=["urllib3"]) assert all( "urllib3" not in line - for line in project.tool_settings["dev-dependencies"]["dev"] + for line in project.pyproject.settings["dev-dependencies"]["dev"] ) assert "urllib3" in working_set assert "requests" in working_set @@ -89,7 +89,7 @@ def test_add_remove_no_package(project): @pytest.mark.usefixtures("repository", "working_set") def test_remove_package_wont_break_toml(project_no_init): - project_no_init.pyproject_file.write_text( + project_no_init.pyproject._path.write_text( """ [project] dependencies = [ @@ -98,6 +98,6 @@ def test_remove_package_wont_break_toml(project_no_init): ] """ ) - project_no_init.pyproject = None + project_no_init.pyproject.reload() actions.do_remove(project_no_init, packages=["requests"]) - assert project_no_init.pyproject["project"]["dependencies"] == [] + assert project_no_init.pyproject.metadata["dependencies"] == [] diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index cd2405bcfe..bf86c19231 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -65,13 +65,13 @@ def test_auto_isolate_site_packages(project, invoke): def test_run_with_site_packages(project, invoke): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "foo": { "cmd": ["python", "-c", "import sys;print(sys.path, sep='\\n')"], "site_packages": True, } } - project.write_pyproject() + project.pyproject.write() result = invoke( [ "run", @@ -99,30 +99,30 @@ def test_run_pass_exit_code(invoke): def test_run_cmd_script(project, invoke): - project.tool_settings["scripts"] = {"test_script": "python -V"} - project.write_pyproject() + project.pyproject.settings["scripts"] = {"test_script": "python -V"} + project.pyproject.write() result = invoke(["run", "test_script"], obj=project) assert result.exit_code == 0 def test_run_cmd_script_with_array(project, invoke): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test_script": ["python", "-c", "import sys; sys.exit(22)"] } - project.write_pyproject() + project.pyproject.write() result = invoke(["run", "test_script"], obj=project) assert result.exit_code == 22 def test_run_script_pass_project_root(project, invoke, capfd): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test_script": [ "python", "-c", "import os;print(os.getenv('PDM_PROJECT_ROOT'))", ] } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() result = invoke(["run", "test_script"], obj=project) assert result.exit_code == 0 @@ -131,13 +131,13 @@ def test_run_script_pass_project_root(project, invoke, capfd): def test_run_shell_script(project, invoke): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test_script": { "shell": "echo hello > output.txt", "help": "test it won't fail", } } - project.write_pyproject() + project.pyproject.write() with cd(project.root): result = invoke(["run", "test_script"], obj=project) assert result.exit_code == 0 @@ -159,11 +159,11 @@ def main(argv=None): """ ) ) - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test_script": {"call": "test_script:main"}, "test_script_with_args": {"call": "test_script:main(['-c', '9'])"}, } - project.write_pyproject() + project.pyproject.write() with cd(project.root): result = invoke(["run", "test_script", "-c", "8"], obj=project) assert result.exit_code == 8 @@ -181,8 +181,8 @@ def test_run_script_with_extra_args(project, invoke, capfd): """ ) ) - project.tool_settings["scripts"] = {"test_script": "python test_script.py"} - project.write_pyproject() + project.pyproject.settings["scripts"] = {"test_script": "python test_script.py"} + project.pyproject.write() with cd(project.root): invoke(["run", "test_script", "-a", "-b", "-c"], obj=project) out, _ = capfd.readouterr() @@ -191,14 +191,14 @@ def test_run_script_with_extra_args(project, invoke, capfd): def test_run_expand_env_vars(project, invoke, capfd, monkeypatch): (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))") - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test_cmd": 'python -c "foo, bar = 0, 1;print($FOO)"', "test_cmd_no_expand": "python -c 'print($FOO)'", "test_script": "python test_script.py", "test_cmd_array": ["python", "test_script.py"], "test_shell": {"shell": "echo $FOO"}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() with cd(project.root): monkeypatch.setenv("FOO", "bar") @@ -220,10 +220,10 @@ def test_run_expand_env_vars(project, invoke, capfd, monkeypatch): def test_run_script_with_env_defined(project, invoke, capfd): (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))") - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test_script": {"cmd": "python test_script.py", "env": {"FOO": "bar"}} } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() with cd(project.root): invoke(["run", "test_script"], obj=project) @@ -234,14 +234,14 @@ def test_run_script_with_dotenv_file(project, invoke, capfd, monkeypatch): (project.root / "test_script.py").write_text( "import os; print(os.getenv('FOO'), os.getenv('BAR'))" ) - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test_override": { "cmd": "python test_script.py", "env_file": {"override": ".env"}, }, "test_default": {"cmd": "python test_script.py", "env_file": ".env"}, } - project.write_pyproject() + project.pyproject.write() monkeypatch.setenv("BAR", "foo") (project.root / ".env").write_text("FOO=bar\nBAR=override") capfd.readouterr() @@ -254,12 +254,12 @@ def test_run_script_with_dotenv_file(project, invoke, capfd, monkeypatch): def test_run_script_override_global_env(project, invoke, capfd): (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))") - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "_": {"env": {"FOO": "bar"}}, "test_env": {"cmd": "python test_script.py"}, "test_env_override": {"cmd": "python test_script.py", "env": {"FOO": "foobar"}}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() with cd(project.root): invoke(["run", "test_env"], obj=project) @@ -269,7 +269,7 @@ def test_run_script_override_global_env(project, invoke, capfd): def test_run_show_list_of_scripts(project, invoke): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test_composite": {"composite": ["test_cmd", "test_script", "test_shell"]}, "test_cmd": "flask db upgrade", "test_multi": """\ @@ -279,7 +279,7 @@ def test_run_show_list_of_scripts(project, invoke): "test_script": {"call": "test_script:main", "help": "call a python function"}, "test_shell": {"shell": "echo $FOO", "help": "shell command"}, } - project.write_pyproject() + project.pyproject.write() result = invoke(["run", "--list"], obj=project) result_lines = result.output.splitlines()[3:] assert ( @@ -302,8 +302,8 @@ def test_run_show_list_of_scripts(project, invoke): def test_run_with_another_project_root(project, local_finder, invoke, capfd): - project.meta["requires-python"] = ">=3.6" - project.write_pyproject() + project.pyproject.metadata["requires-python"] = ">=3.6" + project.pyproject.write() invoke(["add", "first"], obj=project) with TemporaryDirectory(prefix="pytest-run-") as tmp_dir: Path(tmp_dir).joinpath("main.py").write_text( @@ -318,8 +318,8 @@ def test_run_with_another_project_root(project, local_finder, invoke, capfd): def test_import_another_sitecustomize(project, invoke, capfd): - project.meta["requires-python"] = ">=2.7" - project.write_pyproject() + project.pyproject.metadata["requires-python"] = ">=2.7" + project.pyproject.write() # a script for checking another sitecustomize is imported project.root.joinpath("foo.py").write_text("import os;print(os.getenv('FOO'))") # ensure there have at least one sitecustomize can be imported @@ -358,12 +358,12 @@ def test_run_with_patched_sysconfig(project, invoke, capfd): def test_run_composite(project, invoke, capfd, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "first": "python echo.py First", "second": "python echo.py Second", "test": {"composite": ["first", "second"]}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -372,13 +372,13 @@ def test_run_composite(project, invoke, capfd, _echo): def test_composite_stops_on_first_failure(project, invoke, capfd): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "first": {"cmd": ["python", "-c", "print('First CALLED')"]}, "fail": "python -c 'raise Exception'", "second": "echo 'Second CALLED'", "test": {"composite": ["first", "fail", "second"]}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() result = invoke(["run", "test"], obj=project) assert result.exit_code == 1 @@ -388,7 +388,7 @@ def test_composite_stops_on_first_failure(project, invoke, capfd): def test_composite_inherit_env(project, invoke, capfd, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "first": { "cmd": "python echo.py First VAR", "env": {"VAR": "42"}, @@ -399,7 +399,7 @@ def test_composite_inherit_env(project, invoke, capfd, _echo): }, "test": {"composite": ["first", "second"], "env": {"VAR": "overriden"}}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -408,12 +408,12 @@ def test_composite_inherit_env(project, invoke, capfd, _echo): def test_composite_fail_on_first_missing_task(project, invoke, capfd, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "first": "python echo.py First", "second": "python echo.py Second", "test": {"composite": ["first", "fail", "second"]}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() result = invoke(["run", "test"], obj=project) assert result.exit_code == 1 @@ -423,7 +423,7 @@ def test_composite_fail_on_first_missing_task(project, invoke, capfd, _echo): def test_composite_runs_all_hooks(project, invoke, capfd, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test": {"composite": ["first", "second"]}, "pre_test": "python echo.py Pre-Test", "post_test": "python echo.py Post-Test", @@ -432,7 +432,7 @@ def test_composite_runs_all_hooks(project, invoke, capfd, _echo): "second": "python echo.py Second", "post_second": "python echo.py Post-Second", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -445,7 +445,7 @@ def test_composite_runs_all_hooks(project, invoke, capfd, _echo): def test_composite_pass_parameters_to_subtasks(project, invoke, capfd, _args): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test": {"composite": ["first", "second"]}, "pre_test": "python args.py Pre-Test", "post_test": "python args.py Post-Test", @@ -454,7 +454,7 @@ def test_composite_pass_parameters_to_subtasks(project, invoke, capfd, _args): "second": "python args.py Second", "post_second": "python args.py Post-Second", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test", "param=value"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -467,7 +467,7 @@ def test_composite_pass_parameters_to_subtasks(project, invoke, capfd, _args): def test_composite_can_pass_parameters(project, invoke, capfd, _args): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test": {"composite": ["first param=first", "second param=second"]}, "pre_test": "python args.py Pre-Test", "post_test": "python args.py Post-Test", @@ -476,7 +476,7 @@ def test_composite_can_pass_parameters(project, invoke, capfd, _args): "second": "python args.py Second", "post_second": "python args.py Post-Second", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -489,13 +489,13 @@ def test_composite_can_pass_parameters(project, invoke, capfd, _args): def test_composite_hooks_inherit_env(project, invoke, capfd, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "pre_task": {"cmd": "python echo.py Pre-Task VAR", "env": {"VAR": "42"}}, "task": "python echo.py Task", "post_task": {"cmd": "python echo.py Post-Task VAR", "env": {"VAR": "42"}}, "test": {"composite": ["task"], "env": {"VAR": "overriden"}}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -505,7 +505,7 @@ def test_composite_hooks_inherit_env(project, invoke, capfd, _echo): def test_composite_inherit_env_in_cascade(project, invoke, capfd, _echo): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "_": {"env": {"FOO": "BAR", "TIK": "TOK"}}, "pre_task": { "cmd": "python echo.py Pre-Task VAR FOO TIK", @@ -521,7 +521,7 @@ def test_composite_inherit_env_in_cascade(project, invoke, capfd, _echo): }, "test": {"composite": ["task"], "env": {"VAR": "overriden"}}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -533,13 +533,13 @@ def test_composite_inherit_env_in_cascade(project, invoke, capfd, _echo): def test_composite_inherit_dotfile(project, invoke, capfd, _echo): (project.root / ".env").write_text("VAR=42") (project.root / "override.env").write_text("VAR=overriden") - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "pre_task": {"cmd": "python echo.py Pre-Task VAR", "env_file": ".env"}, "task": {"cmd": "python echo.py Task VAR", "env_file": ".env"}, "post_task": {"cmd": "python echo.py Post-Task VAR", "env_file": ".env"}, "test": {"composite": ["task"], "env_file": "override.env"}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -549,11 +549,11 @@ def test_composite_inherit_dotfile(project, invoke, capfd, _echo): def test_composite_can_have_commands(project, invoke, capfd): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "task": {"cmd": ["python", "-c", 'print("Task CALLED")']}, "test": {"composite": ["task", "python -c 'print(\"Command CALLED\")'"]}, } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() invoke(["run", "-v", "test"], strict=True, obj=project) out, _ = capfd.readouterr() @@ -562,10 +562,10 @@ def test_composite_can_have_commands(project, invoke, capfd): def test_run_shortcut(project, invoke, capfd): - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "test": "echo 'Everything is fine'", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() result = invoke(["test"], obj=project, strict=True) assert result.exit_code == 0 @@ -576,10 +576,10 @@ def test_run_shortcut(project, invoke, capfd): def test_run_shortcuts_dont_override_commands(project, invoke, capfd, mocker): do_lock = mocker.patch.object(actions, "do_lock") do_sync = mocker.patch.object(actions, "do_sync") - project.tool_settings["scripts"] = { + project.pyproject.settings["scripts"] = { "install": "echo 'Should not run'", } - project.write_pyproject() + project.pyproject.write() capfd.readouterr() result = invoke(["install"], obj=project, strict=True) assert result.exit_code == 0 diff --git a/tests/cli/test_update.py b/tests/cli/test_update.py index c9571c9880..584201017f 100644 --- a/tests/cli/test_update.py +++ b/tests/cli/test_update.py @@ -20,15 +20,15 @@ def test_update_command(project, invoke, mocker): @pytest.mark.usefixtures("working_set") def test_update_ignore_constraints(project, repository): actions.do_add(project, packages=("pytz",)) - assert project.meta.dependencies == ["pytz~=2019.3"] + assert project.pyproject.metadata["dependencies"] == ["pytz~=2019.3"] repository.add_candidate("pytz", "2020.2") actions.do_update(project, unconstrained=False, packages=("pytz",)) - assert project.meta.dependencies == ["pytz~=2019.3"] + assert project.pyproject.metadata["dependencies"] == ["pytz~=2019.3"] assert project.locked_repository.all_candidates["pytz"].version == "2019.3" actions.do_update(project, unconstrained=True, packages=("pytz",)) - assert project.meta.dependencies == ["pytz~=2020.2"] + assert project.pyproject.metadata["dependencies"] == ["pytz~=2020.2"] assert project.locked_repository.all_candidates["pytz"].version == "2020.2" @@ -82,12 +82,12 @@ def test_update_dry_run(project, repository, capsys): ], ) actions.do_update(project, dry_run=True) - project.lockfile = None + out, _ = capsys.readouterr() + project.lockfile.reload() locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.19.1" assert locked_candidates["chardet"].version == "3.0.4" assert locked_candidates["pytz"].version == "2019.3" - out, _ = capsys.readouterr() assert "requests 2.19.1 -> 2.20.0" in out @@ -180,17 +180,17 @@ def test_update_with_prerelease_without_package_argument(project): @pytest.mark.usefixtures("repository") def test_update_existing_package_with_prerelease(project, working_set): actions.do_add(project, packages=["urllib3"]) - assert project.meta.dependencies[0] == "urllib3~=1.22" + assert project.pyproject.metadata["dependencies"][0] == "urllib3~=1.22" assert working_set["urllib3"].version == "1.22" actions.do_update(project, packages=["urllib3"], prerelease=True) - assert project.meta.dependencies[0] == "urllib3~=1.22" + assert project.pyproject.metadata["dependencies"][0] == "urllib3~=1.22" assert working_set["urllib3"].version == "1.23b0" actions.do_update( project, packages=["urllib3"], prerelease=True, unconstrained=True ) - assert project.meta.dependencies[0] == "urllib3<2,>=1.23b0" + assert project.pyproject.metadata["dependencies"][0] == "urllib3<2,>=1.23b0" assert working_set["urllib3"].version == "1.23b0" diff --git a/tests/cli/test_use.py b/tests/cli/test_use.py index e7d401a4c1..ca1c573122 100644 --- a/tests/cli/test_use.py +++ b/tests/cli/test_use.py @@ -20,9 +20,7 @@ def test_use_command(project, invoke): result = invoke(["use", "-f", python_path], obj=project) assert result.exit_code == 0 - - project.meta["requires-python"] = ">=3.6" - project.write_pyproject() + project.pyproject.metadata["requires-python"] = ">=3.6" result = invoke(["use", "2.7"], obj=project) assert result.exit_code == 1 diff --git a/tests/conftest.py b/tests/conftest.py index 98e2af79cc..86070f6996 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -345,7 +345,7 @@ def project_no_init(tmp_path, mocker, core, index, monkeypatch, build_env): @pytest.fixture() def local_finder(project_no_init): artifacts_dir = str(FIXTURES / "artifacts") - project_no_init.tool_settings["source"] = [ + project_no_init.pyproject.settings["source"] = [ { "type": "find_links", "verify_ssl": False, @@ -353,7 +353,7 @@ def local_finder(project_no_init): "name": "pypi", } ] - project_no_init.write_pyproject() + project_no_init.pyproject.write() @pytest.fixture() @@ -382,7 +382,7 @@ def fixture_project(project_no_init): def func(project_name): source = FIXTURES / "projects" / project_name copytree(source, project_no_init.root) - project_no_init._pyproject = None + project_no_init.pyproject.reload() return project_no_init return func diff --git a/tests/resolver/test_resolve.py b/tests/resolver/test_resolve.py index ac2b3acbf6..402bdfa425 100644 --- a/tests/resolver/test_resolve.py +++ b/tests/resolver/test_resolve.py @@ -20,7 +20,7 @@ def resolve_func( ): repository.environment.python_requires = PySpecSet(requires_python) if allow_prereleases is not None: - project.tool_settings["allow_prereleases"] = allow_prereleases + project.pyproject.settings["allow_prereleases"] = allow_prereleases requirements = [] for line in lines: if line.startswith("-e "): @@ -170,7 +170,7 @@ def test_resolve_conflicting_dependencies_with_overrides( repository.add_dependencies("bar", "0.1.0", ["hoho~=1.1"]) repository.add_candidate("hoho", "2.1") repository.add_candidate("hoho", "1.5") - project.tool_settings["overrides"] = {"hoho": overrides} + project.pyproject.settings["overrides"] = {"hoho": overrides} result = resolve(["foo", "bar"]) assert result["hoho"].version == "2.1" @@ -239,32 +239,34 @@ def test_resolve_circular_dependencies(resolve, repository): def test_resolve_candidates_to_install(project): - project.lockfile = { - "package": [ - { - "name": "pytest", - "version": "4.6.0", - "summary": "pytest module", - "dependencies": ["py>=3.0", "configparser; sys_platform=='win32'"], - }, - { - "name": "configparser", - "version": "1.2.0", - "summary": "configparser module", - "dependencies": ["backports"], - }, - { - "name": "py", - "version": "3.6.0", - "summary": "py module", - }, - { - "name": "backports", - "version": "2.2.0", - "summary": "backports module", - }, - ] - } + project.lockfile.set_data( + { + "package": [ + { + "name": "pytest", + "version": "4.6.0", + "summary": "pytest module", + "dependencies": ["py>=3.0", "configparser; sys_platform=='win32'"], + }, + { + "name": "configparser", + "version": "1.2.0", + "summary": "configparser module", + "dependencies": ["backports"], + }, + { + "name": "py", + "version": "3.6.0", + "summary": "py module", + }, + { + "name": "backports", + "version": "2.2.0", + "summary": "backports module", + }, + ] + } + ) project.environment.marker_environment["sys_platform"] = "linux" reqs = [parse_requirement("pytest")] result = resolve_candidates_from_lockfile(project, reqs) diff --git a/tests/test_formats.py b/tests/test_formats.py index 33f6f5a80a..d31997ae67 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -150,10 +150,9 @@ def test_import_requirements_with_group(project): def test_export_expand_env_vars_in_source(project, monkeypatch): monkeypatch.setenv("USER", "foo") monkeypatch.setenv("PASSWORD", "bar") - project.tool_settings["source"] = [ + project.pyproject.settings["source"] = [ {"url": "https://${USER}:${PASSWORD}@test.pypi.org/simple", "name": "pypi"} ] - project.write_pyproject() result = requirements.export(project, [], Namespace()) assert ( result.strip().splitlines()[-1] diff --git a/tests/test_integration.py b/tests/test_integration.py index a2d49e32d1..ec4bd95160 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -4,16 +4,10 @@ from pdm.utils import cd PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] -PYPROJECT = """\ -[project] -name = "test-project" -version = "0.1.0" -requires-python = ">=3.6" - -[build-system] -requires = ["pdm-pep517"] -build-backend = "pdm.pep517.api" -""" +PYPROJECT = { + "project": {"name": "test-project", "version": "0.1.0", "requires-python": ">=3.6"}, + "build-system": {"requires": ["pdm-pep517"], "build-backend": "pdm.pep517.api"}, +} def get_python_versions(): @@ -33,9 +27,9 @@ def get_python_versions(): def test_basic_integration(python_version, core, tmp_path, invoke): """An e2e test case to ensure PDM works on all supported Python versions""" project = core.create_project(tmp_path) - project.pyproject_file.write_text(PYPROJECT) + project.pyproject.set_data(PYPROJECT) project.root.joinpath("foo.py").write_text("import django\n") - project._environment = project.pyproject = None + project._environment = None invoke(["use", "-f", python_version], obj=project, strict=True) invoke(["add", "django", "-v"], obj=project, strict=True) with cd(project.root): diff --git a/tests/test_project.py b/tests/test_project.py index 8aaa2faa59..5954aba2dc 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -56,7 +56,7 @@ def test_project_sources_overriding(project): project.project_config["pypi.url"] = "https://test.pypi.org/simple" assert project.sources[0]["url"] == "https://test.pypi.org/simple" - project.tool_settings["source"] = [ + project.pyproject.settings["source"] = [ {"url": "https://example.org/simple", "name": "pypi", "verify_ssl": True} ] assert project.sources[0]["url"] == "https://example.org/simple" @@ -76,7 +76,7 @@ def test_project_sources_env_var_expansion(project, monkeypatch): == "https://${PYPI_USER}:${PYPI_PASS}@test.pypi.org/simple" ) - project.tool_settings["source"] = [ + project.pyproject.settings["source"] = [ { "url": "https://${PYPI_USER}:${PYPI_PASS}@example.org/simple", "name": "pypi", @@ -87,11 +87,11 @@ def test_project_sources_env_var_expansion(project, monkeypatch): assert project.sources[0]["url"] == "https://user:password@example.org/simple" # not expanded in tool settings assert ( - project.tool_settings["source"][0]["url"] + project.pyproject.settings["source"][0]["url"] == "https://${PYPI_USER}:${PYPI_PASS}@example.org/simple" ) - project.tool_settings["source"] = [ + project.pyproject.settings["source"] = [ { "url": "https://${PYPI_USER}:${PYPI_PASS}@example2.org/simple", "name": "example2", @@ -102,7 +102,7 @@ def test_project_sources_env_var_expansion(project, monkeypatch): assert project.sources[1]["url"] == "https://user:password@example2.org/simple" # not expanded in tool settings assert ( - project.tool_settings["source"][0]["url"] + project.pyproject.settings["source"][0]["url"] == "https://${PYPI_USER}:${PYPI_PASS}@example2.org/simple" ) @@ -178,12 +178,15 @@ def test_ignore_saved_python(project, monkeypatch): def test_select_dependencies(project): - project.meta["dependencies"] = ["requests"] - project.meta["optional-dependencies"] = { + project.pyproject.metadata["dependencies"] = ["requests"] + project.pyproject.metadata["optional-dependencies"] = { "security": ["cryptography"], "venv": ["virtualenv"], } - project.tool_settings["dev-dependencies"] = {"test": ["pytest"], "doc": ["mkdocs"]} + project.pyproject.settings["dev-dependencies"] = { + "test": ["pytest"], + "doc": ["mkdocs"], + } assert sorted(project.get_dependencies()) == ["requests"] assert sorted(project.dependencies) == ["requests"] diff --git a/tests/test_utils.py b/tests/test_utils.py index 234baa84aa..51b91e26cb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -89,16 +89,16 @@ def test_merge_dictionary(): def setup_dependencies(project): - project.pyproject["project"].update( + project.pyproject.metadata.update( { "dependencies": ["requests"], "optional-dependencies": {"web": ["flask"], "auth": ["passlib"]}, } ) - project.tool_settings.update( + project.pyproject.settings.update( {"dev-dependencies": {"test": ["pytest"], "doc": ["mkdocs"]}} ) - project.write_pyproject() + project.pyproject.write() @pytest.mark.parametrize(