diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 9e53293c8..99675f8fa 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -1,9 +1,11 @@ name: Poetry Downstream Tests on: - pull_request: {} - push: - branches: [main] + workflow_dispatch: +# TODO: enable again after updating Poetry to use poetry-core 2.0.0 +# pull_request: {} +# push: +# branches: [main] jobs: tests: diff --git a/pyproject.toml b/pyproject.toml index 1204d0418..405dec981 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry-core" -version = "1.9.0" +version = "2.0.0.dev0" description = "Poetry PEP 517 Build Backend" authors = ["Sébastien Eustace "] license = "MIT" diff --git a/src/poetry/core/__init__.py b/src/poetry/core/__init__.py index 8a2867f0f..1ae7a37cf 100644 --- a/src/poetry/core/__init__.py +++ b/src/poetry/core/__init__.py @@ -7,7 +7,7 @@ # this cannot presently be replaced with importlib.metadata.version as when building # itself, poetry-core is not available as an installed distribution. -__version__ = "1.9.0" +__version__ = "2.0.0.dev0" __vendor_site__ = (Path(__file__).parent / "_vendor").as_posix() diff --git a/src/poetry/core/factory.py b/src/poetry/core/factory.py index 4524b5769..02095772f 100644 --- a/src/poetry/core/factory.py +++ b/src/poetry/core/factory.py @@ -2,6 +2,7 @@ import logging +from collections import defaultdict from collections.abc import Mapping from pathlib import Path from typing import TYPE_CHECKING @@ -22,6 +23,7 @@ from poetry.core.packages.dependency_group import DependencyGroup from poetry.core.packages.project_package import ProjectPackage from poetry.core.poetry import Poetry + from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.spdx.license import License DependencyConstraint = Union[str, Mapping[str, Any]] @@ -45,10 +47,10 @@ def create_poetry( from poetry.core.pyproject.toml import PyProjectTOML poetry_file = self.locate(cwd) - local_config = PyProjectTOML(path=poetry_file).poetry_config + pyproject = PyProjectTOML(path=poetry_file) # Checking validity - check_result = self.validate(local_config) + check_result = self.validate(pyproject.data) if check_result["errors"]: message = "" for error in check_result["errors"]: @@ -63,16 +65,19 @@ def create_poetry( # If name or version were missing in package mode, we would have already # raised an error, so we can safely assume they might only be missing # in non-package mode and use some dummy values in this case. - name = local_config.get("name", "non-package-mode") + project = pyproject.data.get("project", {}) + name = project.get("name") or pyproject.poetry_config.get( + "name", "non-package-mode" + ) assert isinstance(name, str) - version = local_config.get("version", "0") + version = project.get("version") or pyproject.poetry_config.get("version", "0") assert isinstance(version, str) package = self.get_package(name, version) - package = self.configure_package( - package, local_config, poetry_file.parent, with_groups=with_groups + self.configure_package( + package, pyproject, poetry_file.parent, with_groups=with_groups ) - return Poetry(poetry_file, local_config, package) + return Poetry(poetry_file, pyproject.poetry_config, package) @classmethod def get_package(cls, name: str, version: str) -> ProjectPackage: @@ -107,7 +112,7 @@ def _add_package_group_dependencies( package.python_versions = _constraint continue - group.add_dependency( + group.add_poetry_dependency( cls.create_dependency( name, _constraint, @@ -122,51 +127,201 @@ def _add_package_group_dependencies( def configure_package( cls, package: ProjectPackage, - config: dict[str, Any], + pyproject: PyProjectTOML, root: Path, with_groups: bool = True, - ) -> ProjectPackage: - from poetry.core.packages.dependency import Dependency - from poetry.core.packages.dependency_group import MAIN_GROUP - from poetry.core.packages.dependency_group import DependencyGroup - from poetry.core.spdx.helpers import license_by_id + ) -> None: + project = pyproject.data.get("project", {}) + tool_poetry = pyproject.poetry_config package.root_dir = root - package.authors = [ - combine_unicode(author) for author in config.get("authors", []) - ] + cls._configure_package_metadata(package, project, tool_poetry, root) + cls._configure_entry_points(package, project, tool_poetry) + cls._configure_package_dependencies( + package, project, tool_poetry, with_groups=with_groups + ) + cls._configure_package_poetry_specifics(package, tool_poetry) - package.maintainers = [ - combine_unicode(maintainer) for maintainer in config.get("maintainers", []) - ] + @classmethod + def _configure_package_metadata( + cls, + package: ProjectPackage, + project: dict[str, Any], + tool_poetry: dict[str, Any], + root: Path, + ) -> None: + from poetry.core.spdx.helpers import license_by_id - package.description = config.get("description", "") - package.homepage = config.get("homepage") - package.repository_url = config.get("repository") - package.documentation_url = config.get("documentation") + for key in ("authors", "maintainers"): + if entries := project.get(key): + participants = [] + for entry in entries: + name, email = entry.get("name"), entry.get("email") + if name and email: + participants.append(combine_unicode(f"{name} <{email}>")) + elif name: + participants.append(combine_unicode(name)) + else: + participants.append(combine_unicode(email)) + else: + participants = [ + combine_unicode(author) for author in tool_poetry.get(key, []) + ] + if key == "authors": + package.authors = participants + else: + package.maintainers = participants + + package.description = project.get("description") or tool_poetry.get( + "description", "" + ) + if project_license := project.get("license"): + if isinstance(project_license, str): + raw_license = project_license + else: + raw_license = project_license.get("text", "") + if not raw_license and ( + license_file := project_license.get("file", "") + ): + raw_license = (root / license_file).read_text(encoding="utf-8") + else: + raw_license = tool_poetry.get("license", "") try: - license_: License | None = license_by_id(config.get("license", "")) + license_: License | None = license_by_id(raw_license) except ValueError: license_ = None - package.license = license_ - package.keywords = config.get("keywords", []) - package.classifiers = config.get("classifiers", []) - if "readme" in config: - if isinstance(config["readme"], str): - package.readmes = (root / config["readme"],) + package.requires_python = project.get("requires-python", "*") + package.keywords = project.get("keywords") or tool_poetry.get("keywords", []) + package.classifiers = ( + static_classifiers := project.get("classifiers") + ) or tool_poetry.get("classifiers", []) + package.dynamic_classifiers = not static_classifiers + + if urls := project.get("urls"): + package.homepage = urls.get("homepage") or urls.get("Homepage") + package.repository_url = urls.get("repository") or urls.get("Repository") + package.documentation_url = urls.get("documentation") or urls.get( + "Documentation" + ) + package.custom_urls = urls + else: + package.homepage = tool_poetry.get("homepage") + package.repository_url = tool_poetry.get("repository") + package.documentation_url = tool_poetry.get("documentation") + if "urls" in tool_poetry: + package.custom_urls = tool_poetry["urls"] + + if readme := project.get("readme"): + if isinstance(readme, str): + package.readmes = (root / readme,) + elif "file" in readme: + package.readmes = (root / readme["file"],) + package.readme_content_type = readme["content-type"] + elif "text" in readme: + package.readme_content = root / readme["text"] + package.readme_content_type = readme["content-type"] + elif custom_readme := tool_poetry.get("readme"): + if isinstance(custom_readme, str): + package.readmes = (root / custom_readme,) else: - package.readmes = tuple(root / readme for readme in config["readme"]) + package.readmes = tuple(root / readme for readme in custom_readme) - if "dependencies" in config: + @classmethod + def _configure_entry_points( + cls, + package: ProjectPackage, + project: dict[str, Any], + tool_poetry: dict[str, Any], + ) -> None: + entry_points: defaultdict[str, dict[str, str]] = defaultdict(dict) + + if scripts := project.get("scripts"): + entry_points["console-scripts"] = scripts + elif scripts := tool_poetry.get("scripts"): + for name, specification in scripts.items(): + if isinstance(specification, str): + specification = {"reference": specification, "type": "console"} + + if specification.get("type") != "console": + continue + + reference = specification.get("reference") + + if reference: + entry_points["console-scripts"][name] = reference + + if scripts := project.get("gui-scripts"): + entry_points["gui-scripts"] = scripts + + if other_scripts := project.get("entry-points"): + for group_name, scripts in sorted(other_scripts.items()): + if group_name in {"console-scripts", "gui-scripts"}: + raise ValueError( + f"Group '{group_name}' is reserved and cannot be used" + " as a custom entry-point group." + ) + entry_points[group_name] = scripts + elif other_scripts := tool_poetry.get("plugins"): + for group_name, scripts in sorted(other_scripts.items()): + entry_points[group_name] = scripts + + package.entry_points = dict(entry_points) + + @classmethod + def _configure_package_dependencies( + cls, + package: ProjectPackage, + project: dict[str, Any], + tool_poetry: dict[str, Any], + with_groups: bool = True, + ) -> None: + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.dependency_group import MAIN_GROUP + from poetry.core.packages.dependency_group import DependencyGroup + + dependencies = project.get("dependencies", {}) + optional_dependencies = project.get("optional-dependencies", {}) + + package_extras: dict[NormalizedName, list[Dependency]] + if dependencies or optional_dependencies: + group = DependencyGroup(MAIN_GROUP) + package.add_dependency_group(group) + + for constraint in dependencies: + group.add_dependency( + Dependency.create_from_pep_508( + constraint, relative_to=package.root_dir + ) + ) + package_extras = {} + for extra_name, dependencies in optional_dependencies.items(): + extra_name = canonicalize_name(extra_name) + package_extras[extra_name] = [] + + for dependency_constraint in dependencies: + dependency = Dependency.create_from_pep_508( + dependency_constraint, relative_to=package.root_dir + ) + dependency._optional = True + dependency._in_extras = [extra_name] + + package_extras[extra_name].append(dependency) + group.add_dependency(dependency) + + package.extras = package_extras + + if "dependencies" in tool_poetry: cls._add_package_group_dependencies( - package=package, group=MAIN_GROUP, dependencies=config["dependencies"] + package=package, + group=MAIN_GROUP, + dependencies=tool_poetry["dependencies"], ) - if with_groups and "group" in config: - for group_name, group_config in config["group"].items(): + if with_groups and "group" in tool_poetry: + for group_name, group_config in tool_poetry["group"].items(): group = DependencyGroup( group_name, optional=group_config.get("optional", False) ) @@ -176,38 +331,46 @@ def configure_package( dependencies=group_config["dependencies"], ) - if with_groups and "dev-dependencies" in config: + if with_groups and "dev-dependencies" in tool_poetry: cls._add_package_group_dependencies( - package=package, group="dev", dependencies=config["dev-dependencies"] + package=package, + group="dev", + dependencies=tool_poetry["dev-dependencies"], ) - package_extras: dict[NormalizedName, list[Dependency]] = {} - extras = config.get("extras", {}) - for extra_name, requirements in extras.items(): - extra_name = canonicalize_name(extra_name) - package_extras[extra_name] = [] + # ignore extras in [tool.poetry] if dependencies or optional-dependencies + # are declared in [project] + if not dependencies and not optional_dependencies: + package_extras = {} + extras = tool_poetry.get("extras", {}) + for extra_name, requirements in extras.items(): + extra_name = canonicalize_name(extra_name) + package_extras[extra_name] = [] - # Checking for dependency - for req in requirements: - req = Dependency(req, "*") + # Checking for dependency + for req in requirements: + req = Dependency(req, "*") - for dep in package.requires: - if dep.name == req.name: - dep._in_extras = [*dep._in_extras, extra_name] - package_extras[extra_name].append(dep) + for dep in package.requires: + if dep.name == req.name: + dep._in_extras = [*dep._in_extras, extra_name] + package_extras[extra_name].append(dep) - package.extras = package_extras + package.extras = package_extras - if "build" in config: - build = config["build"] + @classmethod + def _configure_package_poetry_specifics( + cls, package: ProjectPackage, tool_poetry: dict[str, Any] + ) -> None: + if build := tool_poetry.get("build"): if not isinstance(build, dict): build = {"script": build} package.build_config = build or {} - if "include" in config: + if includes := tool_poetry.get("include"): package.include = [] - for include in config["include"]: + for include in includes: if not isinstance(include, dict): include = {"path": include} @@ -218,17 +381,11 @@ def configure_package( package.include.append(include) - if "exclude" in config: - package.exclude = config["exclude"] - - if "packages" in config: - package.packages = config["packages"] + if exclude := tool_poetry.get("exclude"): + package.exclude = exclude - # Custom urls - if "urls" in config: - package.custom_urls = config["urls"] - - return package + if packages := tool_poetry.get("packages"): + package.packages = packages @classmethod def create_dependency( @@ -335,7 +492,7 @@ def create_dependency( extras=constraint.get("extras", []), ) else: - version = constraint["version"] + version = constraint.get("version", "*") dependency = Dependency( name, @@ -345,6 +502,8 @@ def create_dependency( allows_prereleases=allows_prereleases, extras=constraint.get("extras", []), ) + # Normally not valid, but required for enriching [project] dependencies + dependency._develop = constraint.get("develop", False) marker = parse_marker(markers) if markers else AnyMarker() @@ -377,7 +536,7 @@ def create_dependency( @classmethod def validate( - cls, config: dict[str, Any], strict: bool = False + cls, toml_data: dict[str, Any], strict: bool = False ) -> dict[str, list[str]]: """ Checks the validity of a configuration @@ -385,26 +544,37 @@ def validate( from poetry.core.json import validate_object result: dict[str, list[str]] = {"errors": [], "warnings": []} - # Schema validation errors - validation_errors = validate_object(config, "poetry-schema") - - # json validation may only say "data cannot be validated by any definition", - # which is quite vague, so we try to give a more precise error message - generic_error = "data cannot be validated by any definition" - if generic_error in validation_errors: - package_mode = config.get("package-mode", True) - if not isinstance(package_mode, bool): - validation_errors[validation_errors.index(generic_error)] = ( - f"Invalid value for package-mode: {package_mode}" - ) - elif package_mode: - required = {"name", "version", "description", "authors"} - if missing := required.difference(config): - validation_errors[validation_errors.index(generic_error)] = ( - f"The fields {sorted(missing)} are required in package mode." + + # Validate against schemas + project = toml_data.get("project") + if project is not None: + project_validation_errors = [ + e.replace("data", "project") + for e in validate_object(project, "project-schema") + ] + result["errors"] += project_validation_errors + # With PEP 621 [tool.poetry] is not mandatory anymore. We still create and + # validate it so that default values (e.g. for package-mode) are set. + tool_poetry = toml_data.setdefault("tool", {}).setdefault("poetry", {}) + tool_poetry_validation_errors = [ + e.replace("data.", "tool.poetry.") + for e in validate_object(tool_poetry, "poetry-schema") + ] + result["errors"] += tool_poetry_validation_errors + + # Check for required fields if package mode. + # In non-package mode, there are no required fields. + package_mode = tool_poetry.get("package-mode", True) + if package_mode: + for key in ("name", "version"): + value = (project or {}).get(key) or tool_poetry.get(key) + if not value: + result["errors"].append( + f"Either [project.{key}] or [tool.poetry.{key}]" + " is required in package mode." ) - result["errors"] += validation_errors + config = tool_poetry if "dev-dependencies" in config: result["warnings"].append( @@ -414,78 +584,216 @@ def validate( ) if strict: - # If strict, check the file more thoroughly - if "dependencies" in config: - python_versions = config["dependencies"]["python"] - if python_versions == "*": - result["warnings"].append( - "A wildcard Python dependency is ambiguous. " - "Consider specifying a more explicit one." - ) + # Validate relation between [project] and [tool.poetry] + cls._validate_legacy_vs_project(toml_data, result) + + cls._validate_strict(config, result) - for name, constraint in config["dependencies"].items(): - if not isinstance(constraint, dict): - continue + return result - if "allows-prereleases" in constraint: - result["warnings"].append( - f'The "{name}" dependency specifies ' - 'the "allows-prereleases" property, which is deprecated. ' - 'Use "allow-prereleases" instead.' + @classmethod + def _validate_legacy_vs_project( + cls, toml_data: dict[str, Any], result: dict[str, list[str]] + ) -> None: + project = toml_data.get("project", {}) + dynamic = project.get("dynamic", []) + tool_poetry = toml_data["tool"]["poetry"] + + redundant_fields = [ + # name, deprecated (if not dynamic), new name (or None if same as old) + ("name", True, None), + # version can be dynamically set via `build --local-version` or plugins + ("version", False, None), + ("description", True, None), + # multiple readmes are not supported in [project.readme] + ("readme", False, None), + ("license", True, None), + ("authors", True, None), + ("maintainers", True, None), + ("keywords", True, None), + # classifiers are enriched dynamically per default + ("classifiers", False, None), + ("homepage", True, "urls"), + ("repository", True, "urls"), + ("documentation", True, "urls"), + ("urls", True, "urls"), + ("plugins", True, "entry-points"), + ("extras", True, "optional-dependencies"), + ] + dynamic_information = { + "version": ( + "If you want to set the version dynamically via" + " `poetry build --local-version` or you are using a plugin, which" + " sets the version dynamically, you should define the version in" + " [tool.poetry] and add 'version' to [project.dynamic]." + ), + "readme": ( + "If you want to define multiple readmes, you should define them in" + " [tool.poetry] and add 'readme' to [project.dynamic]." + ), + "classifiers": ( + "ATTENTION: Per default Poetry determines classifiers for supported" + " Python versions and license automatically. If you define classifiers" + " in [project], you disable the automatic enrichment. In other words," + " you have to define all classifiers manually." + " If you want to use Poetry's automatic enrichment of classifiers," + " you should define them in [tool.poetry] and add 'classifiers'" + " to [project.dynamic]." + ), + } + assert {f[0] for f in redundant_fields if not f[1]} == set(dynamic_information) + + for name, deprecated, new_name in redundant_fields: + new_name = new_name or name + if name in tool_poetry: + warning = "" + if new_name in project: + warning = ( + f"[project.{new_name}] and [tool.poetry.{name}] are both set." + " The latter will be ignored." + ) + elif deprecated: + warning = ( + f"[tool.poetry.{name}] is deprecated." + f" Use [project.{new_name}] instead." + ) + elif new_name not in dynamic: + warning = ( + f"[tool.poetry.{name}] is set but '{new_name}' is not in" + f" [project.dynamic]. If it is static use [project.{new_name}]." + f" If it is dynamic, add '{new_name}' to [project.dynamic]." + ) + if warning: + if additional_info := dynamic_information.get(name): + warning += f"\n{additional_info}" + result["warnings"].append(warning) + + # scripts are special because entry-points are deprecated + # but files are not because there is no equivalent in [project] + if scripts := tool_poetry.get("scripts"): + for __, script in scripts.items(): + if not isinstance(script, dict) or script.get("type") != "file": + if "scripts" in project: + warning = ( + "[project.scripts] is set and there are console scripts in" + " [tool.poetry.scripts]. The latter will be ignored." ) + else: + warning = ( + "Defining console scripts in [tool.poetry.scripts] is" + " deprecated. Use [project.scripts] instead." + " ([tool.poetry.scripts] should only be used for scripts" + " of type 'file')." + ) + result["warnings"].append(warning) + break + + # dependencies are special because we consider + # [project.dependencies] as abstract dependencies for building + # and [tool.poetry.dependencies] as the concrete dependencies for locking + if ( + "dependencies" in tool_poetry + and "project" in toml_data + and "dependencies" not in project + and "dependencies" not in project.get("dynamic", []) + ): + result["warnings"].append( + "[tool.poetry.dependencies] is set but [project.dependencies] is not" + " and 'dependencies' is not in [project.dynamic]." + " You should either migrate [tool.poetry.depencencies] to" + " [project.dependencies] (if you do not need Poetry-specific features)" + " or add [project.dependencies] in addition to" + " [tool.poetry.dependencies] or add 'dependencies' to" + " [project.dynamic]." + ) + + # requires-python in [project] and python in [tool.poetry.dependencies] are + # special because we consider requires-python as abstract python version + # for building and python as concrete python version for locking + if ( + "python" in tool_poetry.get("dependencies", {}) + and "project" in toml_data + and "requires-python" not in project + and "requires-python" not in project.get("dynamic", []) + ): + result["warnings"].append( + "[tool.poetry.dependencies.python] is set but [project.requires-python]" + " is not set and 'requires-python' is not in [project.dynamic]." + ) + + @classmethod + def _validate_strict( + cls, config: dict[str, Any], result: dict[str, list[str]] + ) -> None: + if "dependencies" in config: + python_versions = config["dependencies"].get("python") + if python_versions == "*": + result["warnings"].append( + "A wildcard Python dependency is ambiguous. " + "Consider specifying a more explicit one." + ) + + for name, constraint in config["dependencies"].items(): + if not isinstance(constraint, dict): + continue - if "extras" in config: - for extra_name, requirements in config["extras"].items(): - extra_name = canonicalize_name(extra_name) - - for req in requirements: - req_name = canonicalize_name(req) - for dependency in config.get("dependencies", {}): - dep_name = canonicalize_name(dependency) - if req_name == dep_name: - break - else: - result["errors"].append( - f'Cannot find dependency "{req}" for extra ' - f'"{extra_name}" in main dependencies.' - ) - - # Checking for scripts with extras - if "scripts" in config: - scripts = config["scripts"] - config_extras = config.get("extras", {}) - - for name, script in scripts.items(): - if not isinstance(script, dict): - continue - - extras = script.get("extras", []) - if extras: - result["warnings"].append( - f'The script "{name}" depends on an extra. Scripts' - " depending on extras are deprecated and support for them" - " will be removed in a future version of" - " poetry/poetry-core. See" - " https://packaging.python.org/en/latest/specifications/entry-points/#data-model" - " for details." + if "allows-prereleases" in constraint: + result["warnings"].append( + f'The "{name}" dependency specifies ' + 'the "allows-prereleases" property, which is deprecated. ' + 'Use "allow-prereleases" instead.' + ) + + if "extras" in config: + for extra_name, requirements in config["extras"].items(): + extra_name = canonicalize_name(extra_name) + + for req in requirements: + req_name = canonicalize_name(req) + for dependency in config.get("dependencies", {}): + dep_name = canonicalize_name(dependency) + if req_name == dep_name: + break + else: + result["errors"].append( + f'Cannot find dependency "{req}" for extra ' + f'"{extra_name}" in main dependencies.' ) - for extra in extras: - if extra not in config_extras: - result["errors"].append( - f'The script "{name}" requires extra "{extra}"' - " which is not defined." - ) - - # Checking types of all readme files (must match) - if "readme" in config and not isinstance(config["readme"], str): - readme_types = {readme_content_type(r) for r in config["readme"]} - if len(readme_types) > 1: - result["errors"].append( - "Declared README files must be of same type: found" - f" {', '.join(sorted(readme_types))}" + + # Checking for scripts with extras + if "scripts" in config: + scripts = config["scripts"] + config_extras = config.get("extras", {}) + + for name, script in scripts.items(): + if not isinstance(script, dict): + continue + + extras = script.get("extras", []) + if extras: + result["warnings"].append( + f'The script "{name}" depends on an extra. Scripts' + " depending on extras are deprecated and support for them" + " will be removed in a future version of" + " poetry/poetry-core. See" + " https://packaging.python.org/en/latest/specifications/entry-points/#data-model" + " for details." ) + for extra in extras: + if extra not in config_extras: + result["errors"].append( + f'The script "{name}" requires extra "{extra}"' + " which is not defined." + ) - return result + # Checking types of all readme files (must match) + if "readme" in config and not isinstance(config["readme"], str): + readme_types = {readme_content_type(r) for r in config["readme"]} + if len(readme_types) > 1: + result["errors"].append( + "Declared README files must be of same type: found" + f" {', '.join(sorted(readme_types))}" + ) @classmethod def locate(cls, cwd: Path | None = None) -> Path: diff --git a/src/poetry/core/json/schemas/poetry-schema.json b/src/poetry/core/json/schemas/poetry-schema.json index b534e3b68..a746328a1 100644 --- a/src/poetry/core/json/schemas/poetry-schema.json +++ b/src/poetry/core/json/schemas/poetry-schema.json @@ -3,28 +3,6 @@ "name": "Package", "type": "object", "additionalProperties": true, - "anyOf": [ - { - "required": [ - "package-mode" - ], - "properties": { - "package-mode": { - "enum": [ - false - ] - } - } - }, - { - "required": [ - "name", - "version", - "description", - "authors" - ] - } - ], "properties": { "package-mode": { "type": "boolean", @@ -33,54 +11,56 @@ }, "name": { "type": "string", - "description": "Package name." + "description": "Package name (legacy)." }, "version": { "type": "string", - "description": "Package version." + "description": "Package version (legacy)." }, "description": { "type": "string", - "description": "Short package description.", + "description": "Short package description (legacy).", "pattern": "\\A[^\\n]*\\Z" }, "keywords": { "type": "array", "items": { "type": "string", - "description": "A tag/keyword that this package relates to." + "description": "A tag/keyword that this package relates to (legacy)." } }, "homepage": { "type": "string", - "description": "Homepage URL for the project.", + "description": "Homepage URL for the project (legacy).", "format": "uri" }, "repository": { "type": "string", - "description": "Repository URL for the project.", + "description": "Repository URL for the project (legacy).", "format": "uri" }, "documentation": { "type": "string", - "description": "Documentation URL for the project.", + "description": "Documentation URL for the project (legacy).", "format": "uri" }, "license": { "type": "string", - "description": "License name." + "description": "License name (legacy)." }, "authors": { - "$ref": "#/definitions/authors" + "$ref": "#/definitions/authors", + "description": "Authors (legacy)." }, "maintainers": { - "$ref": "#/definitions/maintainers" + "$ref": "#/definitions/maintainers", + "description": "Maintainers (legacy)." }, "readme": { "anyOf": [ { "type": "string", - "description": "The path to the README file." + "description": "The path to the README file (legacy)." }, { "type": "array", @@ -93,7 +73,7 @@ }, "classifiers": { "type": "array", - "description": "A list of trove classifiers." + "description": "A list of trove classifiers (legacy)." }, "packages": { "type": "array", @@ -156,9 +136,6 @@ "dependencies": { "type": "object", "description": "This is a hash of package name (keys) and version constraints (values) that are required to run this package.", - "required": [ - "python" - ], "properties": { "python": { "type": "string", @@ -176,6 +153,7 @@ }, "extras": { "type": "object", + "description": "Extras (legacy).", "patternProperties": { "^[a-zA-Z-_.0-9]+$": { "type": "array", @@ -250,7 +228,7 @@ "patternProperties": { "^.+$": { "type": "string", - "description": "The full url of the custom url." + "description": "The full url of the custom url (Legacy)." } } } @@ -258,14 +236,14 @@ "definitions": { "authors": { "type": "array", - "description": "List of authors that contributed to the package. This is typically the main maintainers, not the full list.", + "description": "List of authors that contributed to the package. This is typically the main maintainers, not the full list (legacy).", "items": { "type": "string" } }, "maintainers": { "type": "array", - "description": "List of maintainers, other than the original author(s), that upkeep the package.", + "description": "List of maintainers, other than the original author(s), that upkeep the package (legacy).", "items": { "type": "string" } @@ -321,6 +299,9 @@ }, { "$ref": "#/definitions/multiple-constraints-dependency" + }, + { + "$ref": "#/definitions/dependency-options" } ] } @@ -566,6 +547,36 @@ } } }, + "dependency-options": { + "type": "object", + "additionalProperties": false, + "properties": { + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "allow-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "source": { + "type": "string", + "description": "The exclusive source used to search for this dependency." + }, + "develop": { + "type": "boolean", + "description": "Whether to install the dependency in development mode." + } + } + }, "multiple-constraints-dependency": { "type": "array", "minItems": 1, @@ -588,6 +599,9 @@ }, { "$ref": "#/definitions/url-dependency" + }, + { + "$ref": "#/definitions/dependency-options" } ] } diff --git a/src/poetry/core/json/schemas/project-schema.json b/src/poetry/core/json/schemas/project-schema.json new file mode 100644 index 000000000..eb9ead856 --- /dev/null +++ b/src/poetry/core/json/schemas/project-schema.json @@ -0,0 +1,338 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "name": "project", + "type": "object", + "additionalProperties": true, + "required": [ + "name" + ], + "properties": { + "name": { + "title": "Project name", + "type": "string", + "pattern": "^([a-zA-Z\\d]|[a-zA-Z\\d][\\w.-]*[a-zA-Z\\d])$" + }, + "version": { + "title": "Project version", + "type": "string", + "pattern": "^v?((([0-9]+)!)?([0-9]+(\\.[0-9]+)*)([-_\\.]?(alpha|a|beta|b|preview|pre|c|rc)[-_\\.]?([0-9]+)?)?((-([0-9]+))|([-_\\.]?(post|rev|r)[-_\\.]?([0-9]+)?))?([-_\\.]?(dev)[-_\\.]?([0-9]+)?)?)(\\+([a-z0-9]+([-_\\.][a-z0-9]+)*))?$", + "examples": [ + "42.0.1", + "0.3.9rc7.post0.dev5" + ] + }, + "description": { + "title": "Project summary description", + "type": "string" + }, + "readme": { + "title": "Project full description", + "description": "AKA the README", + "oneOf": [ + { + "title": "README file path", + "type": "string" + }, + { + "type": "object", + "required": [ + "content-type" + ], + "properties": { + "content-type": { + "title": "README text content-type", + "description": "RFC 1341 compliant content-type (with optional charset, defaulting to UTF-8)", + "type": "string" + } + }, + "oneOf": [ + { + "additionalProperties": false, + "required": [ + "file" + ], + "properties": { + "content-type": true, + "file": { + "title": "README file path", + "type": "string" + } + } + }, + { + "additionalProperties": false, + "required": [ + "text" + ], + "properties": { + "content-type": true, + "text": { + "title": "README text", + "type": "string" + } + } + } + ] + } + ], + "examples": [ + "README.md", + { + "file": "README.txt", + "content-type": "text/plain" + }, + { + "text": "# Example project\n\nAn example project", + "content-type": "text/markdown" + } + ] + }, + "requires-python": { + "title": "Python version compatibility", + "type": "string", + "examples": [ + ">= 3.7" + ] + }, + "license": { + "title": "Project license", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "file" + ], + "properties": { + "file": { + "title": "License file path", + "type": "string" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "text" + ], + "properties": { + "text": { + "title": "License text", + "type": "string" + } + } + }, + { + "type": "string", + "description": "A SPDX license identifier" + } + ], + "examples": [ + { + "text": "MIT" + }, + { + "file": "LICENSE" + }, + "MIT", + "LicenseRef-Proprietary" + ] + }, + "authors": { + "title": "Project authors", + "type": "array", + "items": { + "$ref": "#/definitions/projectAuthor" + } + }, + "maintainers": { + "title": "Project maintainers", + "type": "array", + "items": { + "$ref": "#/definitions/projectAuthor" + } + }, + "keywords": { + "title": "Project keywords", + "type": "array", + "items": { + "type": "string" + } + }, + "classifiers": { + "title": "Applicable Trove classifiers", + "type": "array", + "items": { + "type": "string" + } + }, + "urls": { + "title": "Project URLs", + "type": "object", + "additionalProperties": { + "type": "string", + "format": "uri" + }, + "examples": [ + { + "homepage": "https://example.com/example-project" + } + ] + }, + "scripts": { + "title": "Console scripts", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "mycmd": "package.module:object.function" + } + ] + }, + "gui-scripts": { + "title": "GUI scripts", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "mycmd": "package.module:object.function" + } + ] + }, + "entry-points": { + "title": "Other entry-point groups", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^\\w+(\\.\\w+)*$": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "propertyNames": { + "not": { + "anyOf": [ + { + "const": "console_scripts" + }, + { + "const": "gui_scripts" + } + ] + } + }, + "examples": [ + { + "pygments.styles": { + "monokai": "package.module:object.attribute" + } + } + ] + }, + "dependencies": { + "title": "Project dependency requirements", + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "attrs", + "requests ~= 2.28" + ] + ] + }, + "optional-dependencies": { + "title": "Project extra dependency requirements", + "description": "keys are extra names", + "type": "object", + "patternProperties": { + "^([a-z\\d]|[a-z\\d]([a-z\\d-](?!--))*[a-z\\d])$": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "examples": [ + { + "typing": [ + "boto3-stubs", + "typing-extensions ~= 4.1" + ] + } + ] + }, + "dynamic": { + "title": "Dynamic metadata values", + "type": "array", + "items": { + "type": "string", + "enum": [ + "version", + "description", + "readme", + "requires-python", + "license", + "authors", + "maintainers", + "keywords", + "classifiers", + "urls", + "scripts", + "gui-scripts", + "entry-points", + "dependencies", + "optional-dependencies" + ] + }, + "examples": [ + [ + "version" + ] + ] + } + }, + "definitions": { + "projectAuthor": { + "type": "object", + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ], + "properties": { + "name": true + } + }, + { + "required": [ + "email" + ], + "properties": { + "email": true + } + } + ], + "properties": { + "name": { + "title": "Author name", + "type": "string" + }, + "email": { + "title": "Author email", + "type": "string", + "format": "email" + } + } + } + } +} diff --git a/src/poetry/core/masonry/builders/builder.py b/src/poetry/core/masonry/builders/builder.py index a6a6fd60e..de1038522 100644 --- a/src/poetry/core/masonry/builders/builder.py +++ b/src/poetry/core/masonry/builders/builder.py @@ -2,9 +2,8 @@ import logging import sys -import warnings +import textwrap -from collections import defaultdict from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING @@ -250,7 +249,15 @@ def get_metadata_content(self) -> str: content += f"Home-page: {self._meta.home_page}\n" if self._meta.license: - content += f"License: {self._meta.license}\n" + license_field = "License: " + # Indentation is not only for readability, but required + # so that the line break is not treated as end of field. + # The exact indentation does not matter, + # but it is essential to also indent empty lines. + escaped_license = textwrap.indent( + self._meta.license, " " * len(license_field), lambda line: True + ).strip() + content += f"{license_field}{escaped_license}\n" if self._meta.keywords: content += f"Keywords: {self._meta.keywords}\n" @@ -293,44 +300,18 @@ def get_metadata_content(self) -> str: return content def convert_entry_points(self) -> dict[str, list[str]]: - result = defaultdict(list) - - # Scripts -> Entry points - for name, specification in self._poetry.local_config.get("scripts", {}).items(): - if isinstance(specification, str): - # TODO: deprecate this in favour or reference - specification = {"reference": specification, "type": "console"} - - if specification.get("type") != "console": - continue - - extras = specification.get("extras", []) - if extras: - warnings.warn( - f'The script "{name}" depends on an extra. Scripts depending on' - " extras are deprecated and support for them will be removed in a" - " future version of poetry/poetry-core. See" - " https://packaging.python.org/en/latest/specifications/entry-points/#data-model" - " for details.", - DeprecationWarning, - stacklevel=1, - ) - extras = f"[{', '.join(extras)}]" if extras else "" - reference = specification.get("reference") - - if reference: - result["console_scripts"].append(f"{name} = {reference}{extras}") - - # Plugins -> entry points - plugins = self._poetry.local_config.get("plugins", {}) - for groupname, group in plugins.items(): - for name, specification in sorted(group.items()): - result[groupname].append(f"{name} = {specification}") - - for groupname in result: - result[groupname] = sorted(result[groupname]) + result: dict[str, list[str]] = {} + + for group_name, group in self._poetry.package.entry_points.items(): + if group_name == "console-scripts": + group_name = "console_scripts" + elif group_name == "gui-scripts": + group_name = "gui_scripts" + result[group_name] = sorted( + f"{name} = {specification}" for name, specification in group.items() + ) - return dict(result) + return result def convert_script_files(self) -> list[Path]: script_files: list[Path] = [] diff --git a/src/poetry/core/masonry/builders/sdist.py b/src/poetry/core/masonry/builders/sdist.py index 1062df7fb..02bfcc9d1 100644 --- a/src/poetry/core/masonry/builders/sdist.py +++ b/src/poetry/core/masonry/builders/sdist.py @@ -21,7 +21,6 @@ if TYPE_CHECKING: - from collections.abc import Iterable from collections.abc import Iterator from tarfile import TarInfo @@ -199,10 +198,8 @@ def build_setup(self) -> bytes: before.append(f"scripts = \\\n{pformat(rel_paths)}\n") extra.append("'scripts': scripts,") - if self._package.python_versions != "*": - python_requires = self._meta.requires_python - - extra.append(f"'python_requires': {python_requires!r},") + if self._meta.requires_python: + extra.append(f"'python_requires': {self._meta.requires_python!r},") return SETUP.format( before="\n".join(before), @@ -333,12 +330,7 @@ def find_files_to_add(self, exclude_build: bool = False) -> set[BuildIncludeFile additional_files.add(Path("pyproject.toml")) # add readme files if specified - if "readme" in self._poetry.local_config: - readme: str | Iterable[str] = self._poetry.local_config["readme"] - if isinstance(readme, str): - additional_files.add(Path(readme)) - else: - additional_files.update(Path(r) for r in readme) + additional_files.update(Path(r) for r in self._poetry.package.readmes) for additional_file in additional_files: file = BuildIncludeFile( diff --git a/src/poetry/core/masonry/builders/wheel.py b/src/poetry/core/masonry/builders/wheel.py index 21cd20387..3f19b697c 100644 --- a/src/poetry/core/masonry/builders/wheel.py +++ b/src/poetry/core/masonry/builders/wheel.py @@ -304,10 +304,7 @@ def prepare_metadata(self, metadata_directory: Path) -> Path: dist_info = metadata_directory / self.dist_info dist_info.mkdir(parents=True, exist_ok=True) - if ( - "scripts" in self._poetry.local_config - or "plugins" in self._poetry.local_config - ): + if self._poetry.package.entry_points: with (dist_info / "entry_points.txt").open( "w", encoding="utf-8", newline="\n" ) as f: diff --git a/src/poetry/core/masonry/metadata.py b/src/poetry/core/masonry/metadata.py index 0c18c1051..af723c2aa 100644 --- a/src/poetry/core/masonry/metadata.py +++ b/src/poetry/core/masonry/metadata.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from packaging.utils import NormalizedName - from poetry.core.packages.package import Package + from poetry.core.packages.project_package import ProjectPackage class Metadata: @@ -46,7 +46,7 @@ class Metadata: provides_extra: list[NormalizedName] = [] # noqa: RUF012 @classmethod - def from_package(cls, package: Package) -> Metadata: + def from_package(cls, package: ProjectPackage) -> Metadata: from poetry.core.version.helpers import format_python_constraint meta = cls() @@ -54,7 +54,9 @@ def from_package(cls, package: Package) -> Metadata: meta.name = package.pretty_name meta.version = package.version.to_string() meta.summary = package.description - if package.readmes: + if package.readme_content: + meta.description = package.readme_content + elif package.readmes: descriptions = [] for readme in package.readmes: with readme.open(encoding="utf-8") as f: @@ -76,20 +78,28 @@ def from_package(cls, package: Package) -> Metadata: meta.maintainer_email = package.maintainer_email # Requires python - if package.python_versions != "*": + if package.requires_python != "*": + meta.requires_python = package.requires_python + elif package.python_versions != "*": meta.requires_python = format_python_constraint(package.python_constraint) meta.requires_dist = [d.to_pep_508() for d in package.requires] # Version 2.1 - if package.readmes: + if package.readme_content_type: + meta.description_content_type = package.readme_content_type + elif package.readmes: meta.description_content_type = readme_content_type(package.readmes[0]) meta.provides_extra = list(package.extras) if package.urls: for name, url in package.urls.items(): - if name == "Homepage" and meta.home_page == url: + if name.lower() == "homepage" and meta.home_page == url: + continue + if name == "repository" and url == package.urls["Repository"]: + continue + if name == "documentation" and url == package.urls["Documentation"]: continue meta.project_urls += (f"{name}, {url}",) diff --git a/src/poetry/core/packages/dependency.py b/src/poetry/core/packages/dependency.py index ef6185e65..5f3f569ff 100644 --- a/src/poetry/core/packages/dependency.py +++ b/src/poetry/core/packages/dependency.py @@ -76,6 +76,8 @@ def __init__( self._groups = frozenset(groups) self._allows_prereleases = allows_prereleases + # "_develop" is only required for enriching [project] dependencies + self._develop = False self._python_versions = "*" self._python_constraint = parse_constraint("*") diff --git a/src/poetry/core/packages/dependency_group.py b/src/poetry/core/packages/dependency_group.py index 9afa692e4..e5e95b0b9 100644 --- a/src/poetry/core/packages/dependency_group.py +++ b/src/poetry/core/packages/dependency_group.py @@ -1,10 +1,12 @@ from __future__ import annotations +from collections import defaultdict from typing import TYPE_CHECKING if TYPE_CHECKING: from poetry.core.packages.dependency import Dependency + from poetry.core.version.markers import BaseMarker MAIN_GROUP = "main" @@ -15,6 +17,7 @@ def __init__(self, name: str, optional: bool = False) -> None: self._name: str = name self._optional: bool = optional self._dependencies: list[Dependency] = [] + self._poetry_dependencies: list[Dependency] = [] @property def name(self) -> str: @@ -22,13 +25,46 @@ def name(self) -> str: @property def dependencies(self) -> list[Dependency]: - return self._dependencies + return self._dependencies or self._poetry_dependencies + + @property + def dependencies_for_locking(self) -> list[Dependency]: + if not self._poetry_dependencies: + return self._dependencies + if not self._dependencies: + return self._poetry_dependencies + + poetry_dependencies_by_name = defaultdict(list) + for dep in self._poetry_dependencies: + poetry_dependencies_by_name[dep.name].append(dep) + + dependencies = [] + for dep in self._dependencies: + if dep.name in poetry_dependencies_by_name: + enriched = False + for poetry_dep in poetry_dependencies_by_name[dep.name]: + marker = dep.marker.intersect(poetry_dep.marker) + if not marker.is_empty(): + enriched = True + dependencies.append(_enrich_dependency(dep, poetry_dep, marker)) + if not enriched: + dependencies.append(dep) + else: + dependencies.append(dep) + + return dependencies def is_optional(self) -> bool: return self._optional def add_dependency(self, dependency: Dependency) -> None: - self._dependencies.append(dependency) + if not self._dependencies and self._poetry_dependencies: + self._poetry_dependencies.append(dependency) + else: + self._dependencies.append(dependency) + + def add_poetry_dependency(self, dependency: Dependency) -> None: + self._poetry_dependencies.append(dependency) def remove_dependency(self, name: str) -> None: from packaging.utils import canonicalize_name @@ -39,19 +75,62 @@ def remove_dependency(self, name: str) -> None: for dependency in self.dependencies: if dependency.name == name: continue - dependencies.append(dependency) - self._dependencies = dependencies + dependencies = [] + for dependency in self._poetry_dependencies: + if dependency.name == name: + continue + dependencies.append(dependency) + self._poetry_dependencies = dependencies + def __eq__(self, other: object) -> bool: if not isinstance(other, DependencyGroup): return NotImplemented - return self._name == other.name and set(self._dependencies) == set( - other.dependencies + return ( + self._name == other.name + and set(self._dependencies) == set(other.dependencies) + and set(self._poetry_dependencies) == set(other._poetry_dependencies) ) def __repr__(self) -> str: cls = self.__class__.__name__ return f"{cls}({self._name}, optional={self._optional})" + + +def _enrich_dependency( + project_dependency: Dependency, poetry_dependency: Dependency, marker: BaseMarker +) -> Dependency: + if ( + project_dependency.source_type is not None + and poetry_dependency.source_type is not None + and not poetry_dependency.is_same_source_as(project_dependency) + ): + raise ValueError( + "Cannot enrich dependency with different sources: " + f"{project_dependency} and {poetry_dependency}" + ) + + constraint = project_dependency.constraint.intersect(poetry_dependency.constraint) + if constraint.is_empty(): + raise ValueError( + "Cannot enrich dependency with incompatible constraints: " + f"{project_dependency} and {poetry_dependency}" + ) + + if project_dependency.source_type is not None: + from poetry.core.packages.directory_dependency import DirectoryDependency + from poetry.core.packages.vcs_dependency import VCSDependency + + dependency = project_dependency.clone() + if isinstance(project_dependency, (DirectoryDependency, VCSDependency)): + dependency._develop = poetry_dependency._develop # type: ignore[has-type] + else: + dependency = poetry_dependency.with_features(project_dependency.features) + + dependency.constraint = constraint + dependency.marker = marker + + return dependency diff --git a/src/poetry/core/packages/package.py b/src/poetry/core/packages/package.py index f75dad7b3..9bf89d585 100644 --- a/src/poetry/core/packages/package.py +++ b/src/poetry/core/packages/package.py @@ -3,7 +3,6 @@ import re import warnings -from contextlib import contextmanager from typing import TYPE_CHECKING from typing import ClassVar from typing import Mapping @@ -22,7 +21,6 @@ if TYPE_CHECKING: from collections.abc import Collection from collections.abc import Iterable - from collections.abc import Iterator from pathlib import Path from packaging.utils import NormalizedName @@ -111,6 +109,8 @@ def __init__( self.keywords: Sequence[str] = [] self._license: License | None = None self.readmes: tuple[Path, ...] = () + self.readme_content_type: str | None = None + self.readme_content: str | None = None self.extras: Mapping[NormalizedName, Sequence[Dependency]] = {} @@ -201,7 +201,7 @@ def maintainer_email(self) -> str | None: @property def requires(self) -> list[Dependency]: """ - Returns the main dependencies + Returns the main dependencies. """ if not self._dependency_groups or MAIN_GROUP not in self._dependency_groups: return [] @@ -209,16 +209,15 @@ def requires(self) -> list[Dependency]: return self._dependency_groups[MAIN_GROUP].dependencies @property - def all_requires( - self, - ) -> list[Dependency]: + def all_requires(self) -> list[Dependency]: """ - Returns the main dependencies and group dependencies. + Returns the main dependencies and group dependencies + enriched with Poetry-specific information for locking. """ return [ dependency for group in self._dependency_groups.values() - for dependency in group.dependencies + for dependency in group.dependencies_for_locking ] def _set_version(self, version: str | Version) -> None: @@ -564,16 +563,6 @@ def to_dependency(self) -> Dependency: return dep.with_constraint(self._version) - @contextmanager - def with_python_versions(self, python_versions: str) -> Iterator[None]: - original_python_versions = self.python_versions - - self.python_versions = python_versions - - yield - - self.python_versions = original_python_versions - def satisfies( self, dependency: Dependency, ignore_source_type: bool = False ) -> bool: diff --git a/src/poetry/core/packages/project_package.py b/src/poetry/core/packages/project_package.py index fad048258..ff3465ff4 100644 --- a/src/poetry/core/packages/project_package.py +++ b/src/poetry/core/packages/project_package.py @@ -44,6 +44,10 @@ def __init__( self.include: Sequence[Mapping[str, Any]] = [] self.exclude: Sequence[Mapping[str, Any]] = [] self.custom_urls: Mapping[str, str] = {} + self._requires_python: str = "*" + self.dynamic_classifiers = True + + self.entry_points: Mapping[str, dict[str, str]] = {} if self._python_versions == "*": self._python_constraint = parse_constraint("~2.7 || >=3.4") @@ -62,6 +66,15 @@ def to_dependency(self) -> Dependency: return dependency + @property + def requires_python(self) -> str: + return self._requires_python + + @requires_python.setter + def requires_python(self, value: str) -> None: + self._requires_python = value + self.python_versions = value + @property def python_versions(self) -> str: return self._python_versions @@ -71,9 +84,23 @@ def python_versions(self, value: str) -> None: self._python_versions = value if value == "*": + if self._requires_python != "*": + raise ValueError( + f'The Python constraint in [tool.poetry.dependencies] "{value}"' + ' is not a subset of "requires-python" in [project]' + f' "{self._requires_python}"' + ) value = "~2.7 || >=3.4" self._python_constraint = parse_constraint(value) + if not parse_constraint(self._requires_python).allows_all( + self._python_constraint + ): + raise ValueError( + f'The Python constraint in [tool.poetry.dependencies] "{value}"' + ' is not a subset of "requires-python" in [project]' + f' "{self._requires_python}"' + ) self._python_marker = parse_marker( create_nested_marker("python_version", self._python_constraint) ) @@ -87,6 +114,13 @@ def version(self) -> Version: def version(self, value: str | Version) -> None: self._set_version(value) + @property + def all_classifiers(self) -> list[str]: + if self.dynamic_classifiers: + return super().all_classifiers + + return list(self.classifiers) + @property def urls(self) -> dict[str, str]: urls = super().urls diff --git a/src/poetry/core/packages/vcs_dependency.py b/src/poetry/core/packages/vcs_dependency.py index 38a4e9680..7bedf82eb 100644 --- a/src/poetry/core/packages/vcs_dependency.py +++ b/src/poetry/core/packages/vcs_dependency.py @@ -37,7 +37,6 @@ def __init__( self._tag = tag self._rev = rev self._directory = directory - self._develop = develop super().__init__( name, @@ -54,6 +53,7 @@ def __init__( ) self._source = self.source_url or source + self._develop = develop @property def vcs(self) -> str: diff --git a/src/poetry/core/pyproject/toml.py b/src/poetry/core/pyproject/toml.py index 145f78a9f..8e9839c53 100644 --- a/src/poetry/core/pyproject/toml.py +++ b/src/poetry/core/pyproject/toml.py @@ -87,4 +87,16 @@ def is_poetry_project(self) -> bool: _ = self.poetry_config return True + # Even if there is no [tool.poetry] section, a project can still be a + # valid Poetry project if there is a name and a version in [project] + # and there are no dynamic fields. + with suppress(KeyError): + project = self.data["project"] + if ( + project["name"] + and project["version"] + and not project.get("dynamic") + ): + return True + return False diff --git a/tests/fixtures/complete.toml b/tests/fixtures/complete.toml index 99422c9a9..33e2ab81d 100644 --- a/tests/fixtures/complete.toml +++ b/tests/fixtures/complete.toml @@ -5,6 +5,9 @@ description = "Python dependency management and packaging made easy." authors = [ "Sébastien Eustace " ] +maintainers = [ + "Sébastien Eustace " +] license = "MIT" readme = "README.rst" @@ -14,17 +17,21 @@ repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.2" # Compatible python versions must be declared here +python = "^3.8" # Compatible python versions must be declared here toml = "^0.9" # Dependencies with extras requests = { version = "^2.13", extras = [ "security" ] } # Python specific dependencies with prereleases allowed -pathlib2 = { version = "^2.2", python = "~2.7", allows-prereleases = true } +pathlib2 = { version = "^2.2", python = "~3.8", allow-prereleases = true } # Git dependencies -cleo = { git = "https://github.com/sdispater/cleo.git", branch = "master" } +cleo = { git = "https://github.com/sdispater/cleo.git", branch = "main" } # Optional dependencies (extras) pendulum = { version = "^1.4", optional = true } @@ -41,6 +48,9 @@ my-script = 'my_package:main' sample_pyscript = { reference = "script-files/sample_script.py", type= "file" } sample_shscript = { reference = "script-files/sample_script.sh", type= "file" } +[tool.poetry.plugins."poetry.application.plugin"] +my-command = "my_package.plugins:MyApplicationPlugin" + [[tool.poetry.source]] name = "foo" diff --git a/tests/fixtures/complete_duplicates.toml b/tests/fixtures/complete_duplicates.toml new file mode 100644 index 000000000..b7a92549e --- /dev/null +++ b/tests/fixtures/complete_duplicates.toml @@ -0,0 +1,96 @@ +[project] +name = "poetry" +version = "0.5.0" +description = "Python dependency management and packaging made easy." +readme = "README.rst" +requires-python = ">=3.8" +license = { "text" = "MIT" } +authors = [ + { "name" = "Sébastien Eustace", "email" = "sebastien@eustace.io" } +] +maintainers = [ + { name = "Sébastien Eustace", email = "sebastien@eustace.io" } +] +keywords = ["packaging", "dependency", "poetry"] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] +dependencies = [ + "toml>=0.9", + "requests[security]>=2.13,<3.0", + "pathlib2 ~=2.2 ; python_version == '3.8'", + "cleo @ git+https://github.com/sdispater/cleo.git@main", +] + +[project.urls] +homepage = "https://python-poetry.org/" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +[project.optional-dependencies] +time = [ "pendulum>1.4,<2.0" ] + +[project.scripts] +my-script = "my_package:main" + +[project.entry-points."poetry.application.plugin"] +my-command = "my_package.plugins:MyApplicationPlugin" + +[tool.poetry] +name = "poetry" +version = "0.5.0" +description = "Python dependency management and packaging made easy." +authors = [ + "Sébastien Eustace " +] +maintainers = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = "README.rst" + +homepage = "https://python-poetry.org/" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "^3.8" # Compatible python versions must be declared here +toml = "^0.9" +# Dependencies with extras +requests = { version = "^2.13", extras = [ "security" ] } +# Python specific dependencies with prereleases allowed +pathlib2 = { version = "^2.2", python = "~3.8", allow-prereleases = true } +# Git dependencies +cleo = { git = "https://github.com/sdispater/cleo.git", branch = "master" } + +# Optional dependencies (extras) +pendulum = { version = "^1.4", optional = true } + +[tool.poetry.extras] +time = [ "pendulum" ] + +[tool.poetry.group.dev.dependencies] +pytest = "^3.0" +pytest-cov = "^2.4" + +[tool.poetry.scripts] +my-script = 'my_package:main' +sample_pyscript = { reference = "script-files/sample_script.py", type= "file" } +sample_shscript = { reference = "script-files/sample_script.sh", type= "file" } + +[tool.poetry.plugins."poetry.application.plugin"] +my-command = "my_package.plugins:MyApplicationPlugin" + + +[[tool.poetry.source]] +name = "foo" +url = "https://bar.com" diff --git a/tests/fixtures/complete_new.toml b/tests/fixtures/complete_new.toml new file mode 100644 index 000000000..e4b40f621 --- /dev/null +++ b/tests/fixtures/complete_new.toml @@ -0,0 +1,62 @@ +[project] +name = "poetry" +version = "0.5.0" +description = "Python dependency management and packaging made easy." +readme = "README.rst" +requires-python = ">=3.8" +license = { "text" = "MIT" } +authors = [ + { "name" = "Sébastien Eustace", "email" = "sebastien@eustace.io" } +] +maintainers = [ + { name = "Sébastien Eustace", email = "sebastien@eustace.io" } +] +keywords = ["packaging", "dependency", "poetry"] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] +dependencies = [ + "toml>=0.9", + "requests[security]>=2.13,<3.0", + "pathlib2 ~=2.2 ; python_version == '3.8'", + "cleo @ git+https://github.com/sdispater/cleo.git@main", +] + +[project.urls] +homepage = "https://python-poetry.org/" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +[project.optional-dependencies] +time = [ "pendulum>1.4,<2.0" ] + +[project.scripts] +my-script = "my_package:main" + +[project.entry-points."poetry.application.plugin"] +my-command = "my_package.plugins:MyApplicationPlugin" + +# Requirements +[tool.poetry.dependencies] +python = "^3.8" # Compatible python versions must be declared here +toml = "^0.9" +# Dependencies with extras +requests = { version = "^2.13", extras = [ "security" ] } +# Python specific dependencies with prereleases allowed +pathlib2 = { version = "^2.2", python = "~3.8", allow-prereleases = true } +# Git dependencies +cleo = { git = "https://github.com/sdispater/cleo.git", branch = "master" } + +[tool.poetry.group.dev.dependencies] +pytest = "^3.0" +pytest-cov = "^2.4" + +[tool.poetry.scripts] +sample_pyscript = { reference = "script-files/sample_script.py", type= "file" } +sample_shscript = { reference = "script-files/sample_script.sh", type= "file" } + + +[[tool.poetry.source]] +name = "foo" +url = "https://bar.com" diff --git a/tests/fixtures/complete_new_dynamic_invalid.toml b/tests/fixtures/complete_new_dynamic_invalid.toml new file mode 100644 index 000000000..edc4b0ab9 --- /dev/null +++ b/tests/fixtures/complete_new_dynamic_invalid.toml @@ -0,0 +1,75 @@ +[project] +dynamic = [ + "name", # This is not valid and will trigger an error in Factory.validate() + "version", + "description", + "readme", + "requires-python", + "license", + "authors", + "maintainers", + "keywords", + "classifiers", + "urls", + "dependencies", + "optional-dependencies", + "scripts", +] + +[tool.poetry] +name = "poetry" +version = "0.5.0" +description = "Python dependency management and packaging made easy." +authors = [ + "Sébastien Eustace " +] +maintainers = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = "README.rst" + +homepage = "https://python-poetry.org/" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "^3.8" # Compatible python versions must be declared here +toml = "^0.9" +# Dependencies with extras +requests = { version = "^2.13", extras = [ "security" ] } +# Python specific dependencies with prereleases allowed +pathlib2 = { version = "^2.2", python = "~3.8", allow-prereleases = true } +# Git dependencies +cleo = { git = "https://github.com/sdispater/cleo.git", branch = "master" } + +# Optional dependencies (extras) +pendulum = { version = "^1.4", optional = true } + +[tool.poetry.extras] +time = [ "pendulum" ] + +[tool.poetry.group.dev.dependencies] +pytest = "^3.0" +pytest-cov = "^2.4" + +[tool.poetry.scripts] +my-script = 'my_package:main' +sample_pyscript = { reference = "script-files/sample_script.py", type= "file" } +sample_shscript = { reference = "script-files/sample_script.sh", type= "file" } + +[project.entry-points."poetry.application.plugin"] +my-command = "my_package.plugins:MyApplicationPlugin" + + +[[tool.poetry.source]] +name = "foo" +url = "https://bar.com" diff --git a/tests/fixtures/invalid_pyproject/pyproject.toml b/tests/fixtures/invalid_pyproject/pyproject.toml index 06a8e27da..77dfdec14 100644 --- a/tests/fixtures/invalid_pyproject/pyproject.toml +++ b/tests/fixtures/invalid_pyproject/pyproject.toml @@ -1,10 +1,6 @@ [tool.poetry] -name = "invalid" -version = "1.0.0" -authors = [ - "Foo " -] -license = "INVALID" +# name missing +# version missing [tool.poetry.dependencies] python = "*" diff --git a/tests/fixtures/sample_project/pyproject.toml b/tests/fixtures/sample_project/pyproject.toml index 76712c62c..87195ed23 100644 --- a/tests/fixtures/sample_project/pyproject.toml +++ b/tests/fixtures/sample_project/pyproject.toml @@ -5,6 +5,9 @@ description = "Some description." authors = [ "Sébastien Eustace " ] +maintainers = [ + "Sébastien Eustace " +] license = "MIT" readme = "README.rst" @@ -22,11 +25,11 @@ classifiers = [ # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = ">=3.6" cleo = "^0.6" pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } -tomlkit = { git = "https://github.com/sdispater/tomlkit.git", rev = "3bff550", develop = false } -requests = { version = "^2.18", optional = true, extras=[ "security" ] } +tomlkit = { git = "https://github.com/sdispater/tomlkit.git", rev = "3bff550", develop = true } +requests = { version = "^2.18", optional = true, extras = [ "security" ] } pathlib2 = { version = "^2.2", python = "~2.7" } orator = { version = "^0.9", optional = true } @@ -44,11 +47,12 @@ simple-project = { path = "../simple_project/" } functools32 = { version = "^3.2.3", markers = "python_version ~= '2.7' and sys_platform == 'win32' or python_version in '3.4 3.5'" } # Dependency with python constraint -dataclasses = {version = "^0.7", python = ">=3.6.1,<3.7"} +dataclasses = { version = "^0.7", python = ">=3.6.1,<3.7" } [tool.poetry.extras] db = [ "orator" ] +network = [ "requests" ] # Non-regression test for https://github.com/python-poetry/poetry-core/pull/492. # The underlying issue occurred because `tomlkit` can either return a TOML table as `Table` instance or an diff --git a/tests/fixtures/sample_project_new/README.rst b/tests/fixtures/sample_project_new/README.rst new file mode 100644 index 000000000..f7fe15470 --- /dev/null +++ b/tests/fixtures/sample_project_new/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/fixtures/sample_project_new/pyproject.toml b/tests/fixtures/sample_project_new/pyproject.toml new file mode 100644 index 000000000..2354188d1 --- /dev/null +++ b/tests/fixtures/sample_project_new/pyproject.toml @@ -0,0 +1,62 @@ +[project] +name = "my-package" +version = "1.2.3" +description = "Some description." +readme = "README.rst" +requires-python = ">=3.6" +license = { text = "MIT" } +keywords = ["packaging", "dependency", "poetry"] +authors = [ + { name = "Sébastien Eustace", email = "sebastien@eustace.io" } +] +maintainers = [ + { name = "Sébastien Eustace", email = "sebastien@eustace.io" } +] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +dependencies = [ + "cleo ~=0.6", + "pendulum @ git+https://github.com/sdispater/pendulum.git@2.0", + "tomlkit @ git+https://github.com/sdispater/tomlkit.git@3bff550", + "pathlib2 ~=2.2 ; python_version == '2.7'", + # File dependency + "demo @ ../distributions/demo-0.1.0-py2.py3-none-any.whl", + # Dir dependency with setup.py + "my-package @ ../project_with_setup/", + # Dir dependency with pyproject.toml + "simple-project @ ../simple_project/", + # Dependency with markers + "functools32 ~=3.2.3 ; python_version ~= '2.7' and sys_platform == 'win32' or python_version in '3.4 3.5'", + # Dependency with python constraint + "dataclasses ~=0.7 ; python_full_version >= '3.6.1' and python_version < '3.7'" +] + +[project.optional-dependencies] +db = [ + "orator ~=0.9" +] +network = [ + "requests[security] ~=2.18" +] + +[project.urls] +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +[project.scripts] +my-script = "my_package:main" + +[project.entry-points."blogtool.parsers"] +".rst" = "some_module::SomeClass" + +[tool.poetry.dependencies] +tomlkit = { develop = true } + +[tool.poetry.group.dev.dependencies] +pytest = "~3.4" diff --git a/tests/fixtures/with_license_type_file/LICENSE b/tests/fixtures/with_license_type_file/LICENSE new file mode 100644 index 000000000..44f728ea7 --- /dev/null +++ b/tests/fixtures/with_license_type_file/LICENSE @@ -0,0 +1,4 @@ +Some license text +with multiple lines, + +empty lines and non-ASCII characters: éöß diff --git a/tests/fixtures/with_license_type_file/pyproject.toml b/tests/fixtures/with_license_type_file/pyproject.toml new file mode 100644 index 000000000..3d706f61c --- /dev/null +++ b/tests/fixtures/with_license_type_file/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "my-package" +version = "0.1" +license = { file = "LICENSE" } +keywords = ["special"] # field that comes after license in core metadata diff --git a/tests/fixtures/with_license_type_str/pyproject.toml b/tests/fixtures/with_license_type_str/pyproject.toml new file mode 100644 index 000000000..2d66d5ef0 --- /dev/null +++ b/tests/fixtures/with_license_type_str/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "my-package" +version = "0.1" +license = "MIT" +keywords = ["special"] # field that comes after license in core metadata diff --git a/tests/fixtures/with_license_type_text/pyproject.toml b/tests/fixtures/with_license_type_text/pyproject.toml new file mode 100644 index 000000000..57ba2213b --- /dev/null +++ b/tests/fixtures/with_license_type_text/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "my-package" +version = "0.1" +license = { text = """Some license text +with multiple lines, + +empty lines and non-ASCII characters: éöß" """} +keywords = ["special"] # field that comes after license in core metadata diff --git a/tests/fixtures/with_readme_files/pyproject.toml b/tests/fixtures/with_readme_files/pyproject.toml index d7995a4fa..bf80aff1e 100644 --- a/tests/fixtures/with_readme_files/pyproject.toml +++ b/tests/fixtures/with_readme_files/pyproject.toml @@ -1,19 +1,10 @@ -[tool.poetry] +[project] name = "my-package" version = "0.1" -description = "Some description." -authors = [ - "Wagner Macedo " -] -license = "MIT" +dynamic = ["readme"] +[tool.poetry] readme = [ "README-1.rst", "README-2.rst" ] - -homepage = "https://python-poetry.org/" - - -[tool.poetry.dependencies] -python = "^2.7" diff --git a/tests/json/test_poetry_schema.py b/tests/json/test_poetry_schema.py index 4f598a05c..210413ed0 100644 --- a/tests/json/test_poetry_schema.py +++ b/tests/json/test_poetry_schema.py @@ -38,22 +38,6 @@ def multi_url_object() -> dict[str, Any]: } -@pytest.mark.parametrize("explicit", [True, False]) -@pytest.mark.parametrize( - "missing_required", ["", "name", "version", "description", "authors"] -) -def test_package_mode( - base_object: dict[str, Any], explicit: bool, missing_required: str -) -> None: - if explicit: - base_object["package-mode"] = True - if missing_required: - del base_object[missing_required] - assert len(validate_object(base_object, "poetry-schema")) == 1 - else: - assert len(validate_object(base_object, "poetry-schema")) == 0 - - def test_non_package_mode_no_metadata() -> None: assert len(validate_object({"package-mode": False}, "poetry-schema")) == 0 diff --git a/tests/masonry/builders/fixtures/complete/pyproject.toml b/tests/masonry/builders/fixtures/complete/pyproject.toml index 938cbb810..63302b7ef 100644 --- a/tests/masonry/builders/fixtures/complete/pyproject.toml +++ b/tests/masonry/builders/fixtures/complete/pyproject.toml @@ -51,6 +51,8 @@ my-2nd-script = "my_package:main2" file-script = { reference = "bin/script.sh", type = "file" } extra-script = { reference = "my_package.extra:main", extras = ["time"], type = "console" } +[tool.poetry.plugins."poetry.application.plugin"] +my-command = "my_package.plugins:MyApplicationPlugin" [tool.poetry.urls] "Issue Tracker" = "https://github.com/python-poetry/poetry/issues" diff --git a/tests/masonry/builders/fixtures/complete_new/AUTHORS b/tests/masonry/builders/fixtures/complete_new/AUTHORS new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/complete_new/COPYING b/tests/masonry/builders/fixtures/complete_new/COPYING new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/complete_new/LICENCE b/tests/masonry/builders/fixtures/complete_new/LICENCE new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/complete_new/LICENSE b/tests/masonry/builders/fixtures/complete_new/LICENSE new file mode 100644 index 000000000..44cf2b30e --- /dev/null +++ b/tests/masonry/builders/fixtures/complete_new/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2018 Sébastien Eustace + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/masonry/builders/fixtures/complete_new/README.rst b/tests/masonry/builders/fixtures/complete_new/README.rst new file mode 100644 index 000000000..f7fe15470 --- /dev/null +++ b/tests/masonry/builders/fixtures/complete_new/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/masonry/builders/fixtures/complete_new/bin/script.sh b/tests/masonry/builders/fixtures/complete_new/bin/script.sh new file mode 100644 index 000000000..2a9686ac6 --- /dev/null +++ b/tests/masonry/builders/fixtures/complete_new/bin/script.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "Hello World!" \ No newline at end of file diff --git a/tests/masonry/builders/fixtures/complete_new/my_package/__init__.py b/tests/masonry/builders/fixtures/complete_new/my_package/__init__.py new file mode 100644 index 000000000..10aa336ce --- /dev/null +++ b/tests/masonry/builders/fixtures/complete_new/my_package/__init__.py @@ -0,0 +1 @@ +__version__ = "1.2.3" diff --git a/tests/masonry/builders/fixtures/complete_new/my_package/data1/test.json b/tests/masonry/builders/fixtures/complete_new/my_package/data1/test.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/masonry/builders/fixtures/complete_new/my_package/data1/test.json @@ -0,0 +1 @@ +{} diff --git a/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg1/__init__.py b/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg1/extra_file.xml b/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg1/extra_file.xml new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg2/__init__.py b/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg2/data2/data.json b/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg2/data2/data.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg2/data2/data.json @@ -0,0 +1 @@ +{} diff --git a/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg3/foo.py b/tests/masonry/builders/fixtures/complete_new/my_package/sub_pkg3/foo.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/complete_new/pyproject.toml b/tests/masonry/builders/fixtures/complete_new/pyproject.toml new file mode 100644 index 000000000..082501bae --- /dev/null +++ b/tests/masonry/builders/fixtures/complete_new/pyproject.toml @@ -0,0 +1,53 @@ +[project] +name = "my-package" +version = "1.2.3" +description = "Some description." +readme = "README.rst" +requires-python = ">=3.6,<4.0" +license = { "text" = "MIT" } +authors = [ + { "name" = "Sébastien Eustace", "email" = "sebastien@eustace.io" } +] +maintainers = [ + { name = "People Everywhere", email = "people@everywhere.com" } +] +keywords = ["packaging", "dependency", "poetry"] +dependencies = [ + "cleo>=0.6,<0.7", + "cachy[msgpack]>=0.2.0,<0.3.0", +] +dynamic = [ "classifiers" ] + +[project.optional-dependencies] +time = [ "pendulum>=1.4,<2.0 ; python_version ~= '2.7' and sys_platform == 'win32' or python_version in '3.4 3.5'" ] + +[project.urls] +homepage = "https://python-poetry.org/" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" +"Issue Tracker" = "https://github.com/python-poetry/poetry/issues" + +[project.scripts] +my-script = "my_package:main" +my-2nd-script = "my_package:main2" +extra-script = "my_package.extra:main" + +[project.entry-points."poetry.application.plugin"] +my-command = "my_package.plugins:MyApplicationPlugin" + +[tool.poetry] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +exclude = [ + "does-not-exist", + "**/*.xml" +] + +[tool.poetry.dev-dependencies] +pytest = "~3.4" + +[tool.poetry.scripts] +file-script = { reference = "bin/script.sh", type = "file" } diff --git a/tests/masonry/builders/test_builder.py b/tests/masonry/builders/test_builder.py index eb2b6dd4a..50e6fbf5e 100644 --- a/tests/masonry/builders/test_builder.py +++ b/tests/masonry/builders/test_builder.py @@ -160,6 +160,40 @@ def test_metadata_homepage_default() -> None: assert metadata["Home-page"] is None +@pytest.mark.parametrize("license_type", ["file", "text", "str"]) +def test_metadata_license_type_file(license_type: str) -> None: + project_path = ( + Path(__file__).parent.parent.parent + / "fixtures" + / f"with_license_type_{license_type}" + ) + builder = Builder(Factory().create_poetry(project_path)) + + if license_type == "file": + license_text = (project_path / "LICENSE").read_text(encoding="utf-8") + elif license_type == "text": + license_text = ( + (project_path / "pyproject.toml") + .read_text(encoding="utf-8") + .split('"""')[1] + ) + elif license_type == "str": + license_text = "MIT" + else: + raise RuntimeError("unexpected license type") + + raw_content = builder.get_metadata_content() + metadata = Parser().parsestr(raw_content) + + license_lines = metadata["License"].splitlines() + unindented_license = "\n".join([line.strip() for line in license_lines]) + assert unindented_license == license_text.rstrip() + + # Check that field after "license" is read correctly + assert raw_content.index("License:") < raw_content.index("Keywords:") + assert metadata["Keywords"] == "special" + + def test_metadata_with_vcs_dependencies() -> None: builder = Builder( Factory().create_poetry( @@ -229,7 +263,7 @@ def test_invalid_script_files_definition() -> None: "script_reference_console", { "console_scripts": [ - "extra-script = my_package.extra:main[time]", + "extra-script = my_package.extra:main", "script = my_package.extra:main", ] }, @@ -240,7 +274,6 @@ def test_invalid_script_files_definition() -> None: ), ], ) -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") def test_builder_convert_entry_points( fixture: str, result: dict[str, list[str]] ) -> None: diff --git a/tests/masonry/builders/test_complete.py b/tests/masonry/builders/test_complete.py index f1fab93bc..80f86459a 100644 --- a/tests/masonry/builders/test_complete.py +++ b/tests/masonry/builders/test_complete.py @@ -102,20 +102,20 @@ def test_wheel_c_extension(project: str, exptected_c_dir: str) -> None: assert len(set(record_files)) == len(record_files) +@pytest.mark.parametrize("project", ["complete", "complete_new"]) @pytest.mark.parametrize("no_vcs", [False, True]) -def test_complete(no_vcs: bool) -> None: - module_path = fixtures_dir / "complete" +def test_complete(project: str, no_vcs: bool) -> None: + module_path = fixtures_dir / project if no_vcs: # Copy the complete fixtures dir to a temporary directory - temporary_dir = Path(tempfile.mkdtemp()) / "complete" + temporary_dir = Path(tempfile.mkdtemp()) / project shutil.copytree(module_path.as_posix(), temporary_dir.as_posix()) module_path = temporary_dir poetry = Factory().create_poetry(module_path) - with pytest.warns(DeprecationWarning, match=".* script .* extra"): - SdistBuilder(poetry).build() - WheelBuilder(poetry).build() + SdistBuilder(poetry).build() + WheelBuilder(poetry).build() whl = module_path / "dist" / "my_package-1.2.3-py3-none-any.whl" @@ -159,10 +159,13 @@ def test_complete(no_vcs: bool) -> None: entry_points.decode() == """\ [console_scripts] -extra-script=my_package.extra:main[time] +extra-script=my_package.extra:main my-2nd-script=my_package:main2 my-script=my_package:main +[poetry.application.plugin] +my-command=my_package.plugins:MyApplicationPlugin + """ ) wheel_data = zipf.read("my_package-1.2.3.dist-info/WHEEL").decode() diff --git a/tests/masonry/builders/test_metadata.py b/tests/masonry/builders/test_metadata.py new file mode 100644 index 000000000..2613ab0ad --- /dev/null +++ b/tests/masonry/builders/test_metadata.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import pytest + +from poetry.core.masonry.metadata import Metadata +from poetry.core.packages.project_package import ProjectPackage + + +@pytest.mark.parametrize( + ("requires_python", "python", "expected"), + [ + (">=3.8", None, ">=3.8"), + (None, "^3.8", ">=3.8,<4.0"), + (">=3.8", "^3.8", ">=3.8"), + ], +) +def test_requires_python( + requires_python: str | None, python: str | None, expected: str +) -> None: + package = ProjectPackage("foo", "1") + if requires_python: + package.requires_python = requires_python + if python: + package.python_versions = python + + meta = Metadata.from_package(package) + + assert meta.requires_python == expected diff --git a/tests/masonry/builders/test_sdist.py b/tests/masonry/builders/test_sdist.py index bd1908450..fc4ba4a7a 100644 --- a/tests/masonry/builders/test_sdist.py +++ b/tests/masonry/builders/test_sdist.py @@ -116,9 +116,9 @@ def test_convert_dependencies() -> None: assert result == (main, extras) -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_make_setup() -> None: - poetry = Factory().create_poetry(project("complete")) +@pytest.mark.parametrize("project_name", ["complete", "complete_new"]) +def test_make_setup(project_name: str) -> None: + poetry = Factory().create_poetry(project(project_name)) builder = SdistBuilder(poetry) setup = builder.build_setup() @@ -136,10 +136,13 @@ def test_make_setup() -> None: assert ns["install_requires"] == ["cachy[msgpack]>=0.2.0,<0.3.0", "cleo>=0.6,<0.7"] assert ns["entry_points"] == { "console_scripts": [ - "extra-script = my_package.extra:main[time]", + "extra-script = my_package.extra:main", "my-2nd-script = my_package:main2", "my-script = my_package:main", - ] + ], + "poetry.application.plugin": [ + "my-command = my_package.plugins:MyApplicationPlugin" + ], } assert ns["scripts"] == [str(Path("bin") / "script.sh")] assert ns["extras_require"] == { @@ -149,11 +152,12 @@ def test_make_setup() -> None: } -def test_make_pkg_info(mocker: MockerFixture) -> None: +@pytest.mark.parametrize("project_name", ["complete", "complete_new"]) +def test_make_pkg_info(project_name: str, mocker: MockerFixture) -> None: get_metadata_content = mocker.patch( "poetry.core.masonry.builders.builder.Builder.get_metadata_content" ) - poetry = Factory().create_poetry(project("complete")) + poetry = Factory().create_poetry(project(project_name)) builder = SdistBuilder(poetry) builder.build_pkg_info() @@ -172,8 +176,9 @@ def test_make_pkg_info_any_python() -> None: assert "Requires-Python" not in parsed -def test_find_files_to_add() -> None: - poetry = Factory().create_poetry(project("complete")) +@pytest.mark.parametrize("project_name", ["complete", "complete_new"]) +def test_find_files_to_add(project_name: str) -> None: + poetry = Factory().create_poetry(project(project_name)) builder = SdistBuilder(poetry) result = {f.relative_to_source_root() for f in builder.find_files_to_add()} @@ -230,12 +235,13 @@ def test_make_pkg_info_multi_constraints_dependency() -> None: ] -def test_find_packages() -> None: - poetry = Factory().create_poetry(project("complete")) +@pytest.mark.parametrize("project_name", ["complete", "complete_new"]) +def test_find_packages(project_name: str) -> None: + poetry = Factory().create_poetry(project(project_name)) builder = SdistBuilder(poetry) - base = project("complete") + base = project(project_name) include = PackageInclude(base, "my_package") pkg_dir, packages, pkg_data = builder.find_packages(include) @@ -267,13 +273,14 @@ def test_find_packages() -> None: assert pkg_data == {"": ["*"]} -def test_package() -> None: - poetry = Factory().create_poetry(project("complete")) +@pytest.mark.parametrize("project_name", ["complete", "complete_new"]) +def test_package(project_name: str) -> None: + poetry = Factory().create_poetry(project(project_name)) builder = SdistBuilder(poetry) builder.build() - sdist = fixtures_dir / "complete" / "dist" / "my_package-1.2.3.tar.gz" + sdist = fixtures_dir / project_name / "dist" / "my_package-1.2.3.tar.gz" assert sdist.exists() @@ -281,8 +288,9 @@ def test_package() -> None: assert "my_package-1.2.3/LICENSE" in tar.getnames() -def test_sdist_reproducibility() -> None: - poetry = Factory().create_poetry(project("complete")) +@pytest.mark.parametrize("project_name", ["complete", "complete_new"]) +def test_sdist_reproducibility(project_name: str) -> None: + poetry = Factory().create_poetry(project(project_name)) hashes = set() @@ -290,7 +298,7 @@ def test_sdist_reproducibility() -> None: builder = SdistBuilder(poetry) builder.build() - sdist = fixtures_dir / "complete" / "dist" / "my_package-1.2.3.tar.gz" + sdist = fixtures_dir / project_name / "dist" / "my_package-1.2.3.tar.gz" assert sdist.exists() @@ -299,9 +307,9 @@ def test_sdist_reproducibility() -> None: assert len(hashes) == 1 -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_setup_py_context() -> None: - poetry = Factory().create_poetry(project("complete")) +@pytest.mark.parametrize("project_name", ["complete", "complete_new"]) +def test_setup_py_context(project_name: str) -> None: + poetry = Factory().create_poetry(project(project_name)) builder = SdistBuilder(poetry) diff --git a/tests/masonry/builders/test_wheel.py b/tests/masonry/builders/test_wheel.py index a7f6c890e..f6bb27eca 100644 --- a/tests/masonry/builders/test_wheel.py +++ b/tests/masonry/builders/test_wheel.py @@ -70,9 +70,9 @@ def test_wheel_module() -> None: assert "module1.py" in z.namelist() -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_wheel_package() -> None: - module_path = fixtures_dir / "complete" +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_wheel_package(project: str) -> None: + module_path = fixtures_dir / project WheelBuilder.make(Factory().create_poetry(module_path)) whl = module_path / "dist" / "my_package-1.2.3-py3-none-any.whl" @@ -209,9 +209,9 @@ def test_wheel_build_script_creates_package() -> None: shutil.rmtree(module_path / "my_package") -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_dist_info_file_permissions() -> None: - module_path = fixtures_dir / "complete" +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_dist_info_file_permissions(project: str) -> None: + module_path = fixtures_dir / project WheelBuilder.make(Factory().create_poetry(module_path)) whl = module_path / "dist" / "my_package-1.2.3-py3-none-any.whl" diff --git a/tests/masonry/test_api.py b/tests/masonry/test_api.py index f01181527..95c73a660 100644 --- a/tests/masonry/test_api.py +++ b/tests/masonry/test_api.py @@ -34,21 +34,23 @@ def cwd(directory: str | Path) -> Iterator[None]: fixtures = os.path.join(os.path.dirname(__file__), "builders", "fixtures") -def test_get_requires_for_build_wheel() -> None: +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_get_requires_for_build_wheel(project: str) -> None: expected: list[str] = [] - with cwd(os.path.join(fixtures, "complete")): + with cwd(os.path.join(fixtures, project)): assert api.get_requires_for_build_wheel() == expected -def test_get_requires_for_build_sdist() -> None: +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_get_requires_for_build_sdist(project: str) -> None: expected: list[str] = [] - with cwd(os.path.join(fixtures, "complete")): + with cwd(os.path.join(fixtures, project)): assert api.get_requires_for_build_sdist() == expected -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_build_wheel() -> None: - with temporary_directory() as tmp_dir, cwd(os.path.join(fixtures, "complete")): +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_build_wheel(project: str) -> None: + with temporary_directory() as tmp_dir, cwd(os.path.join(fixtures, project)): filename = api.build_wheel(tmp_dir) validate_wheel_contents( name="my_package", @@ -95,8 +97,9 @@ def test_build_wheel_extended() -> None: validate_wheel_contents(name="extended", version="0.1", path=whl.as_posix()) -def test_build_sdist() -> None: - with temporary_directory() as tmp_dir, cwd(os.path.join(fixtures, "complete")): +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_build_sdist(project: str) -> None: + with temporary_directory() as tmp_dir, cwd(os.path.join(fixtures, project)): filename = api.build_sdist(tmp_dir) validate_sdist_contents( name="my-package", @@ -135,14 +138,17 @@ def test_build_sdist_with_bad_path_dep_succeeds(caplog: LogCaptureFixture) -> No assert "does not exist" in record.message -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_prepare_metadata_for_build_wheel() -> None: +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_prepare_metadata_for_build_wheel(project: str) -> None: entry_points = """\ [console_scripts] -extra-script=my_package.extra:main[time] +extra-script=my_package.extra:main my-2nd-script=my_package:main2 my-script=my_package:main +[poetry.application.plugin] +my-command=my_package.plugins:MyApplicationPlugin + """ wheel_data = f"""\ Wheel-Version: 1.0 @@ -188,7 +194,7 @@ def test_prepare_metadata_for_build_wheel() -> None: ========== """ - with temporary_directory() as tmp_dir, cwd(os.path.join(fixtures, "complete")): + with temporary_directory() as tmp_dir, cwd(os.path.join(fixtures, project)): dirname = api.prepare_metadata_for_build_wheel(tmp_dir) assert dirname == "my_package-1.2.3.dist-info" @@ -229,9 +235,9 @@ def test_prepare_metadata_for_build_wheel_with_bad_path_dep_succeeds( assert "does not exist" in record.message -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_build_editable_wheel() -> None: - pkg_dir = Path(fixtures) / "complete" +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_build_editable_wheel(project: str) -> None: + pkg_dir = Path(fixtures) / project with temporary_directory() as tmp_dir, cwd(pkg_dir): filename = api.build_editable(tmp_dir) @@ -250,9 +256,9 @@ def test_build_editable_wheel() -> None: assert pkg_dir.as_posix() == z.read("my_package.pth").decode().strip() -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_build_wheel_with_metadata_directory() -> None: - pkg_dir = Path(fixtures) / "complete" +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_build_wheel_with_metadata_directory(project: str) -> None: + pkg_dir = Path(fixtures) / project with temporary_directory() as metadata_tmp_dir, cwd(pkg_dir): metadata_directory = api.prepare_metadata_for_build_wheel(metadata_tmp_dir) @@ -278,9 +284,9 @@ def test_build_wheel_with_metadata_directory() -> None: assert f"{metadata_directory}/CUSTOM" in namelist -@pytest.mark.filterwarnings("ignore:.* script .* extra:DeprecationWarning") -def test_build_editable_wheel_with_metadata_directory() -> None: - pkg_dir = Path(fixtures) / "complete" +@pytest.mark.parametrize("project", ["complete", "complete_new"]) +def test_build_editable_wheel_with_metadata_directory(project: str) -> None: + pkg_dir = Path(fixtures) / project with temporary_directory() as metadata_tmp_dir, cwd(pkg_dir): metadata_directory = api.prepare_metadata_for_build_editable(metadata_tmp_dir) diff --git a/tests/packages/test_dependency_group.py b/tests/packages/test_dependency_group.py index 9f65acfa8..fac4eeb3a 100644 --- a/tests/packages/test_dependency_group.py +++ b/tests/packages/test_dependency_group.py @@ -1,26 +1,310 @@ from __future__ import annotations +from pathlib import Path + +import pytest + from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import DependencyGroup +from poetry.core.packages.directory_dependency import DirectoryDependency +from poetry.core.packages.vcs_dependency import VCSDependency + + +def create_dependency( + name: str, + constraint: str = "*", + *, + extras: tuple[str, ...] = (), + allows_prereleases: bool = False, + develop: bool = False, + source_name: str | None = None, + marker: str | None = None, +) -> Dependency: + dep = Dependency( + name=name, + constraint=constraint, + extras=extras, + allows_prereleases=allows_prereleases, + ) + if develop: + dep._develop = develop + if source_name: + dep.source_name = source_name + if marker: + dep.marker = marker # type: ignore[assignment] + return dep + + +@pytest.mark.parametrize( + ( + "dependencies", + "poetry_dependencies", + "expected_dependencies", + ), + [ + ({"foo"}, set(), {"foo"}), + (set(), {"bar"}, {"bar"}), + ({"foo"}, {"bar"}, {"foo"}), + ], +) +def test_dependencies( + dependencies: set[str], + poetry_dependencies: set[str], + expected_dependencies: set[str], +) -> None: + group = DependencyGroup(name="group") + group._dependencies = [ + Dependency(name=name, constraint="*") for name in dependencies + ] + group._poetry_dependencies = [ + Dependency(name=name, constraint="*") for name in poetry_dependencies + ] + + assert {d.name for d in group.dependencies} == set(expected_dependencies) + + +@pytest.mark.parametrize( + ( + "initial_dependencies", + "initial_poetry_dependencies", + "expected_dependencies", + "expected_poetry_dependencies", + ), + [ + (set(), set(), {"new"}, set()), + ({"foo"}, set(), {"foo", "new"}, set()), + (set(), {"bar"}, set(), {"bar", "new"}), + ({"foo"}, {"bar"}, {"foo", "new"}, {"bar"}), + ], +) +def test_add_dependency_adds_to_correct_list( + initial_dependencies: set[str], + initial_poetry_dependencies: set[str], + expected_dependencies: set[str], + expected_poetry_dependencies: set[str], +) -> None: + group = DependencyGroup(name="group") + group._dependencies = [ + Dependency(name=name, constraint="*") for name in initial_dependencies + ] + group._poetry_dependencies = [ + Dependency(name=name, constraint="*") for name in initial_poetry_dependencies + ] + + group.add_dependency(Dependency(name="new", constraint="*")) + + assert {d.name for d in group._dependencies} == expected_dependencies + assert {d.name for d in group._poetry_dependencies} == expected_poetry_dependencies + + +def test_remove_dependency_removes_from_both_lists() -> None: + group = DependencyGroup(name="group") + group.add_dependency(Dependency(name="foo", constraint="*")) + group.add_dependency(Dependency(name="bar", constraint="*")) + group.add_dependency(Dependency(name="foo", constraint="*")) + group.add_poetry_dependency(Dependency(name="baz", constraint="*")) + group.add_poetry_dependency(Dependency(name="foo", constraint="*")) + + group.remove_dependency("foo") + + assert {d.name for d in group._dependencies} == {"bar"} + assert {d.name for d in group._poetry_dependencies} == {"baz"} -def test_dependency_group_remove_dependency() -> None: - group = DependencyGroup(name="linter") - group.add_dependency(Dependency(name="black", constraint="*")) - group.add_dependency(Dependency(name="isort", constraint="*")) - group.add_dependency(Dependency(name="flake8", constraint="*")) +@pytest.mark.parametrize( + ( + "dependencies", + "poetry_dependencies", + "expected_dependencies", + ), + [ + ([Dependency.create_from_pep_508("foo")], [], [create_dependency("foo")]), + ([], [Dependency.create_from_pep_508("bar")], [create_dependency("bar")]), + # refine constraint + ( + [Dependency.create_from_pep_508("foo>=1")], + [create_dependency("foo", "<2")], + [create_dependency("foo", ">=1,<2")], + ), + # refine constraint + other dependency + ( + [ + Dependency.create_from_pep_508("foo>=1"), + Dependency.create_from_pep_508("bar>=2"), + ], + [create_dependency("foo", "<2")], + [create_dependency("foo", ">=1,<2"), create_dependency("bar", ">=2")], + ), + # refine constraint depending on marker + ( + [Dependency.create_from_pep_508("foo>=1")], + [create_dependency("foo", "<2", marker="sys_platform == 'win32'")], + [create_dependency("foo", ">=1,<2", marker="sys_platform == 'win32'")], + ), + # allow pre-releases + ( + [Dependency.create_from_pep_508("foo>=1")], + [create_dependency("foo", allows_prereleases=True)], + [create_dependency("foo", ">=1", allows_prereleases=True)], + ), + # directory dependency - develop + ( + [DirectoryDependency("foo", Path("path/to/foo"))], + [create_dependency("foo", develop=True)], + [DirectoryDependency("foo", Path("path/to/foo"), develop=True)], + ), + # directory dependency - develop (full spec) + ( + [DirectoryDependency("foo", Path("path/to/foo"))], + [DirectoryDependency("foo", Path("path/to/foo"), develop=True)], + [DirectoryDependency("foo", Path("path/to/foo"), develop=True)], + ), + # vcs dependency - develop + ( + [VCSDependency("foo", "git", "https://example.org/foo")], + [create_dependency("foo", develop=True)], + [VCSDependency("foo", "git", "https://example.org/foo", develop=True)], + ), + # vcs dependency - develop (full spec) + ( + [VCSDependency("foo", "git", "https://example.org/foo")], + [VCSDependency("foo", "git", "https://example.org/foo", develop=True)], + [VCSDependency("foo", "git", "https://example.org/foo", develop=True)], + ), + # replace with directory dependency + ( + [Dependency.create_from_pep_508("foo>=1")], + [DirectoryDependency("foo", Path("path/to/foo"), develop=True)], + [DirectoryDependency("foo", Path("path/to/foo"), develop=True)], + ), + # source + ( + [Dependency.create_from_pep_508("foo>=1")], + [create_dependency("foo", source_name="src")], + [create_dependency("foo", ">=1", source_name="src")], + ), + # different sources depending on marker + ( + [Dependency.create_from_pep_508("foo>=1")], + [ + create_dependency( + "foo", source_name="src1", marker="sys_platform == 'win32'" + ), + create_dependency( + "foo", source_name="src2", marker="sys_platform == 'linux'" + ), + ], + [ + create_dependency( + "foo", ">=1", source_name="src1", marker="sys_platform == 'win32'" + ), + create_dependency( + "foo", ">=1", source_name="src2", marker="sys_platform == 'linux'" + ), + ], + ), + # pairwise different sources depending on marker + ( + [ + Dependency.create_from_pep_508("foo>=1; sys_platform == 'win32'"), + Dependency.create_from_pep_508("foo>=1.1; sys_platform == 'linux'"), + ], + [ + create_dependency( + "foo", source_name="src1", marker="sys_platform == 'win32'" + ), + create_dependency( + "foo", source_name="src2", marker="sys_platform == 'linux'" + ), + ], + [ + create_dependency( + "foo", ">=1", source_name="src1", marker="sys_platform == 'win32'" + ), + create_dependency( + "foo", ">=1.1", source_name="src2", marker="sys_platform == 'linux'" + ), + ], + ), + # enrich only one with source + ( + [ + Dependency.create_from_pep_508("foo>=1; sys_platform == 'win32'"), + Dependency.create_from_pep_508("foo>=1.1; sys_platform == 'linux'"), + ], + [ + create_dependency( + "foo", source_name="src1", marker="sys_platform == 'win32'" + ), + ], + [ + create_dependency( + "foo", ">=1", source_name="src1", marker="sys_platform == 'win32'" + ), + create_dependency("foo", ">=1.1", marker="sys_platform == 'linux'"), + ], + ), + # extras + ( + [Dependency.create_from_pep_508("foo[extra1,extra2]")], + [create_dependency("foo", source_name="src")], + [create_dependency("foo", source_name="src", extras=["extra1", "extra2"])], + ), + ( + [Dependency.create_from_pep_508("foo;extra=='extra1'")], + [create_dependency("foo", source_name="src")], + [create_dependency("foo", source_name="src", marker="extra == 'extra1'")], + ), + ], +) +def test_dependencies_for_locking( + dependencies: list[Dependency], + poetry_dependencies: list[Dependency], + expected_dependencies: list[Dependency], +) -> None: + group = DependencyGroup(name="group") + group._dependencies = dependencies + group._poetry_dependencies = poetry_dependencies - assert {dependency.name for dependency in group.dependencies} == { - "black", - "isort", - "flake8", - } + assert group.dependencies_for_locking == expected_dependencies + # explicitly check attributes that are not considered in __eq__ + assert [d.allows_prereleases() for d in group.dependencies_for_locking] == [ + d.allows_prereleases() for d in expected_dependencies + ] + assert [d.source_name for d in group.dependencies_for_locking] == [ + d.source_name for d in expected_dependencies + ] + assert [d.marker for d in group.dependencies_for_locking] == [ + d.marker for d in expected_dependencies + ] + assert [d._develop for d in group.dependencies_for_locking] == [ + d._develop for d in expected_dependencies + ] - group.remove_dependency("isort") - assert {dependency.name for dependency in group.dependencies} == {"black", "flake8"} - group.remove_dependency("black") - assert {dependency.name for dependency in group.dependencies} == {"flake8"} +@pytest.mark.parametrize( + ( + "dependencies", + "poetry_dependencies", + ), + [ + ( + [Dependency.create_from_pep_508("foo>=1")], + [create_dependency("foo", "<1")], + ), + ( + [DirectoryDependency("foo", Path("path/to/foo"))], + [VCSDependency("foo", "git", "https://example.org/foo")], + ), + ], +) +def test_dependencies_for_locking_failure( + dependencies: list[Dependency], + poetry_dependencies: list[Dependency], +) -> None: + group = DependencyGroup(name="group") + group._dependencies = dependencies + group._poetry_dependencies = poetry_dependencies - group.remove_dependency("flake8") - assert {dependency.name for dependency in group.dependencies} == set() + with pytest.raises(ValueError): + _ = group.dependencies_for_locking diff --git a/tests/pyproject/conftest.py b/tests/pyproject/conftest.py index 8aceff30d..3eddee0a9 100644 --- a/tests/pyproject/conftest.py +++ b/tests/pyproject/conftest.py @@ -41,3 +41,28 @@ def poetry_section(pyproject_toml: Path) -> str: with pyproject_toml.open(mode="a", encoding="utf-8") as f: f.write(content) return content + + +@pytest.fixture +def project_section(pyproject_toml: Path) -> str: + content = """ +[project] +name = "poetry" +version = "1.0.0" +""" + with pyproject_toml.open(mode="a", encoding="utf-8") as f: + f.write(content) + return content + + +@pytest.fixture +def project_section_dynamic(pyproject_toml: Path) -> str: + content = """ +[project] +name = "not-poetry" +version = "1.0.0" +dynamic = ["description"] +""" + with pyproject_toml.open(mode="a", encoding="utf-8") as f: + f.write(content) + return content diff --git a/tests/pyproject/test_pyproject_toml.py b/tests/pyproject/test_pyproject_toml.py index db0b5561d..e077683b0 100644 --- a/tests/pyproject/test_pyproject_toml.py +++ b/tests/pyproject/test_pyproject_toml.py @@ -30,6 +30,36 @@ def test_pyproject_toml_no_poetry_config(pyproject_toml: Path) -> None: ) +def test_pyproject_toml_no_poetry_config_but_project_section( + pyproject_toml: Path, project_section: str +) -> None: + pyproject = PyProjectTOML(pyproject_toml) + + assert pyproject.is_poetry_project() + + with pytest.raises(PyProjectException) as excval: + _ = pyproject.poetry_config + + assert f"[tool.poetry] section not found in {pyproject_toml.as_posix()}" in str( + excval.value + ) + + +def test_pyproject_toml_no_poetry_config_but_project_section_but_dynamic( + pyproject_toml: Path, project_section_dynamic: str +) -> None: + pyproject = PyProjectTOML(pyproject_toml) + + assert not pyproject.is_poetry_project() + + with pytest.raises(PyProjectException) as excval: + _ = pyproject.poetry_config + + assert f"[tool.poetry] section not found in {pyproject_toml.as_posix()}" in str( + excval.value + ) + + def test_pyproject_toml_poetry_config( pyproject_toml: Path, poetry_section: str ) -> None: diff --git a/tests/test_factory.py b/tests/test_factory.py index c0496a8a9..e8bb2e30d 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -12,6 +12,7 @@ from poetry.core.constraints.version import parse_constraint from poetry.core.factory import Factory from poetry.core.packages.url_dependency import URLDependency +from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.utils._compat import tomllib from poetry.core.version.markers import SingleMarker @@ -22,14 +23,147 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.directory_dependency import DirectoryDependency from poetry.core.packages.file_dependency import FileDependency - from poetry.core.packages.vcs_dependency import VCSDependency fixtures_dir = Path(__file__).parent / "fixtures" -def test_create_poetry() -> None: - poetry = Factory().create_poetry(fixtures_dir / "sample_project") +@pytest.fixture +def complete_legacy_warnings() -> list[str]: + return [ + "[tool.poetry.name] is deprecated. Use [project.name] instead.", + ( + "[tool.poetry.version] is set but 'version' is not in " + "[project.dynamic]. If it is static use [project.version]. If it " + "is dynamic, add 'version' to [project.dynamic].\n" + "If you want to set the version dynamically via `poetry build " + "--local-version` or you are using a plugin, which sets the " + "version dynamically, you should define the version in " + "[tool.poetry] and add 'version' to [project.dynamic]." + ), + "[tool.poetry.description] is deprecated. Use [project.description] instead.", + ( + "[tool.poetry.readme] is set but 'readme' is not in " + "[project.dynamic]. If it is static use [project.readme]. If it " + "is dynamic, add 'readme' to [project.dynamic].\n" + "If you want to define multiple readmes, you should define them " + "in [tool.poetry] and add 'readme' to [project.dynamic]." + ), + "[tool.poetry.license] is deprecated. Use [project.license] instead.", + "[tool.poetry.authors] is deprecated. Use [project.authors] instead.", + "[tool.poetry.maintainers] is deprecated. Use [project.maintainers] instead.", + "[tool.poetry.keywords] is deprecated. Use [project.keywords] instead.", + ( + "[tool.poetry.classifiers] is set but 'classifiers' is not in " + "[project.dynamic]. If it is static use [project.classifiers]. If it " + "is dynamic, add 'classifiers' to [project.dynamic].\n" + "ATTENTION: Per default Poetry determines classifiers for " + "supported Python versions and license automatically. If you " + "define classifiers in [project], you disable the automatic " + "enrichment. In other words, you have to define all classifiers " + "manually. If you want to use Poetry's automatic enrichment of " + "classifiers, you should define them in [tool.poetry] and add " + "'classifiers' to [project.dynamic]." + ), + "[tool.poetry.homepage] is deprecated. Use [project.urls] instead.", + "[tool.poetry.repository] is deprecated. Use [project.urls] instead.", + "[tool.poetry.documentation] is deprecated. Use [project.urls] instead.", + "[tool.poetry.plugins] is deprecated. Use [project.entry-points] instead.", + ( + "[tool.poetry.extras] is deprecated. Use " + "[project.optional-dependencies] instead." + ), + ( + "Defining console scripts in [tool.poetry.scripts] is deprecated. " + "Use [project.scripts] instead. " + "([tool.poetry.scripts] should only be used for scripts of type 'file')." + ), + ] + + +@pytest.fixture +def complete_legacy_duplicate_warnings() -> list[str]: + return [ + ( + "[project.name] and [tool.poetry.name] are both set. The latter " + "will be ignored." + ), + ( + "[project.version] and [tool.poetry.version] are both set. The " + "latter will be ignored.\n" + "If you want to set the version dynamically via `poetry build " + "--local-version` or you are using a plugin, which sets the " + "version dynamically, you should define the version in " + "[tool.poetry] and add 'version' to [project.dynamic]." + ), + ( + "[project.description] and [tool.poetry.description] are both " + "set. The latter will be ignored." + ), + ( + "[project.readme] and [tool.poetry.readme] are both set. The " + "latter will be ignored.\n" + "If you want to define multiple readmes, you should define them " + "in [tool.poetry] and add 'readme' to [project.dynamic]." + ), + ( + "[project.license] and [tool.poetry.license] are both set. The " + "latter will be ignored." + ), + ( + "[project.authors] and [tool.poetry.authors] are both set. The " + "latter will be ignored." + ), + ( + "[project.maintainers] and [tool.poetry.maintainers] are both " + "set. The latter will be ignored." + ), + ( + "[project.keywords] and [tool.poetry.keywords] are both set. The " + "latter will be ignored." + ), + ( + "[project.classifiers] and [tool.poetry.classifiers] are both " + "set. The latter will be ignored.\n" + "ATTENTION: Per default Poetry determines classifiers for " + "supported Python versions and license automatically. If you " + "define classifiers in [project], you disable the automatic " + "enrichment. In other words, you have to define all classifiers " + "manually. If you want to use Poetry's automatic enrichment of " + "classifiers, you should define them in [tool.poetry] and add " + "'classifiers' to [project.dynamic]." + ), + ( + "[project.urls] and [tool.poetry.homepage] are both set. The " + "latter will be ignored." + ), + ( + "[project.urls] and [tool.poetry.repository] are both set. The " + "latter will be ignored." + ), + ( + "[project.urls] and [tool.poetry.documentation] are both set. " + "The latter will be ignored." + ), + ( + "[project.entry-points] and [tool.poetry.plugins] are both set. The " + "latter will be ignored." + ), + ( + "[project.optional-dependencies] and [tool.poetry.extras] are " + "both set. The latter will be ignored." + ), + ( + "[project.scripts] is set and there are console scripts " + "in [tool.poetry.scripts]. The latter will be ignored." + ), + ] + + +@pytest.mark.parametrize("new_format", [False, True]) +def test_create_poetry(new_format: str) -> None: + project = "sample_project_new" if new_format else "sample_project" + poetry = Factory().create_poetry(fixtures_dir / project) assert poetry.is_package_mode @@ -39,33 +173,34 @@ def test_create_poetry() -> None: assert package.version.text == "1.2.3" assert package.description == "Some description." assert package.authors == ["Sébastien Eustace "] + assert package.maintainers == ["Sébastien Eustace "] assert package.license assert package.license.id == "MIT" assert ( package.readmes[0].relative_to(fixtures_dir).as_posix() - == "sample_project/README.rst" + == f"{project}/README.rst" ) assert package.homepage == "https://python-poetry.org" assert package.repository_url == "https://github.com/python-poetry/poetry" assert package.keywords == ["packaging", "dependency", "poetry"] - assert package.python_versions == "~2.7 || ^3.6" - assert str(package.python_constraint) == ">=2.7,<2.8 || >=3.6,<4.0" + assert package.python_versions == ">=3.6" + assert str(package.python_constraint) == ">=3.6" dependencies: dict[str, Dependency] = {} for dep in package.requires: dependencies[dep.name] = dep cleo = dependencies["cleo"] - assert cleo.pretty_constraint == "^0.6" + assert cleo.pretty_constraint == (">=0.6,<1.0" if new_format else "^0.6") assert not cleo.is_optional() pendulum = dependencies["pendulum"] - assert pendulum.pretty_constraint == "branch 2.0" + assert pendulum.pretty_constraint == ("rev 2.0" if new_format else "branch 2.0") assert pendulum.is_vcs() pendulum = cast("VCSDependency", pendulum) assert pendulum.vcs == "git" - assert pendulum.branch == "2.0" + assert pendulum.rev == "2.0" if new_format else pendulum.branch == "2.0" assert pendulum.source == "https://github.com/sdispater/pendulum.git" assert pendulum.allows_prereleases() assert not pendulum.develop @@ -78,18 +213,21 @@ def test_create_poetry() -> None: assert tomlkit.rev == "3bff550" assert tomlkit.source == "https://github.com/sdispater/tomlkit.git" assert tomlkit.allows_prereleases() - assert not tomlkit.develop + assert not tomlkit.develop if new_format else tomlkit.develop + tomlkit_for_locking = next(d for d in package.all_requires if d.name == "tomlkit") + assert isinstance(tomlkit_for_locking, VCSDependency) + assert tomlkit_for_locking.develop requests = dependencies["requests"] - assert requests.pretty_constraint == "^2.18" + assert requests.pretty_constraint == (">=2.18,<3.0" if new_format else "^2.18") assert not requests.is_vcs() assert not requests.allows_prereleases() assert requests.is_optional() assert requests.extras == frozenset({"security"}) pathlib2 = dependencies["pathlib2"] - assert pathlib2.pretty_constraint == "^2.2" - assert pathlib2.python_versions == ">=2.7 <2.8" + assert pathlib2.pretty_constraint == (">=2.2,<3.0" if new_format else "^2.2") + assert pathlib2.python_versions in {"~2.7", ">=2.7 <2.8"} assert not pathlib2.is_optional() demo = dependencies["demo"] @@ -114,7 +252,9 @@ def test_create_poetry() -> None: functools32 = dependencies["functools32"] assert functools32.name == "functools32" - assert functools32.pretty_constraint == "^3.2.3" + assert functools32.pretty_constraint == ( + ">=3.2.3,<3.3.0" if new_format else "^3.2.3" + ) assert ( str(functools32.marker) == 'python_version ~= "2.7" and sys_platform == "win32" or python_version in' @@ -123,7 +263,7 @@ def test_create_poetry() -> None: dataclasses = dependencies["dataclasses"] assert dataclasses.name == "dataclasses" - assert dataclasses.pretty_constraint == "^0.7" + assert dataclasses.pretty_constraint == (">=0.7,<1.0" if new_format else "^0.7") assert dataclasses.python_versions == ">=3.6.1 <3.7" assert ( str(dataclasses.marker) @@ -139,22 +279,23 @@ def test_create_poetry() -> None: "Topic :: Software Development :: Libraries :: Python Modules", ] - assert package.all_classifiers == [ - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries :: Python Modules", - ] + if new_format: + assert package.all_classifiers == package.classifiers + else: + assert package.all_classifiers == [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules", + ] def test_create_poetry_with_dependencies_with_subdirectory() -> None: @@ -246,23 +387,214 @@ def test_create_poetry_non_package_mode() -> None: assert not poetry.is_package_mode +@pytest.mark.parametrize("license_type", ["file", "text", "str"]) +def test_create_poetry_with_license_type_file(license_type: str) -> None: + project_dir = fixtures_dir / f"with_license_type_{license_type}" + poetry = Factory().create_poetry(project_dir) + + if license_type == "file": + license_content = (project_dir / "LICENSE").read_text(encoding="utf-8") + elif license_type == "text": + license_content = ( + (project_dir / "pyproject.toml").read_text(encoding="utf-8").split('"""')[1] + ) + elif license_type == "str": + license_content = "MIT" + else: + raise RuntimeError("unexpected license type") + + assert poetry.package.license + assert poetry.package.license.id == license_content + + +@pytest.mark.parametrize( + ("requires_python", "python", "expected_versions", "expected_constraint"), + [ + (">=3.8", None, ">=3.8", ">=3.8"), + (None, "^3.8", "^3.8", ">=3.8,<4.0"), + (">=3.8", "^3.8", "^3.8", ">=3.8,<4.0"), + ], +) +def test_create_poetry_python_version( + requires_python: str, + python: str, + expected_versions: str, + expected_constraint: str, + tmp_path: Path, +) -> None: + content = '[project]\nname = "foo"\nversion = "1"\n' + if requires_python: + content += f'requires-python = "{requires_python}"\n' + if python: + content += f'[tool.poetry.dependencies]\npython = "{python}"\n' + (tmp_path / "pyproject.toml").write_text(content) + poetry = Factory().create_poetry(tmp_path) + + package = poetry.package + assert package.requires_python == requires_python or python + assert package.python_versions == expected_versions + assert str(package.python_constraint) == expected_constraint + + +def test_create_poetry_python_version_not_compatible(tmp_path: Path) -> None: + content = """ +[project] +name = "foo" +version = "1" +requires-python = ">=3.8" + +[tool.poetry.dependencies] +python = ">=3.7" +""" + (tmp_path / "pyproject.toml").write_text(content) + with pytest.raises(ValueError) as e: + Factory().create_poetry(tmp_path) + + assert "not a subset" in str(e.value) + + +@pytest.mark.parametrize( + ("content", "expected"), + [ + ( # static + """\ +[project] +name = "foo" +version = "1" +requires-python = "3.10" +classifiers = ["License :: OSI Approved :: MIT License"] +""", + ["License :: OSI Approved :: MIT License"], + ), + ( # dynamic + """\ +[project] +name = "foo" +version = "1" +requires-python = "3.10" +dynamic = [ "classifiers" ] + +[tool.poetry] +classifiers = ["License :: OSI Approved :: MIT License"] +""", + [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + ], + ), + ( # legacy + """\ +[tool.poetry] +name = "foo" +version = "1" +classifiers = ["License :: OSI Approved :: MIT License"] + +[tool.poetry.dependencies] +python = "~3.10" +""", + [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + ], + ), + ], +) +def test_create_poetry_classifiers( + content: str, expected: list[str], tmp_path: Path +) -> None: + (tmp_path / "pyproject.toml").write_text(content) + poetry = Factory().create_poetry(tmp_path) + + assert poetry.package.all_classifiers == expected + + def test_validate() -> None: complete = fixtures_dir / "complete.toml" with complete.open("rb") as f: - doc = tomllib.load(f) - content = doc["tool"]["poetry"] + content = tomllib.load(f) assert Factory.validate(content) == {"errors": [], "warnings": []} +def test_validate_strict_legacy_warnings(complete_legacy_warnings: list[str]) -> None: + complete = fixtures_dir / "complete.toml" + with complete.open("rb") as f: + content = tomllib.load(f) + + assert Factory.validate(content, strict=True) == { + "errors": [], + "warnings": complete_legacy_warnings, + } + + +def test_validate_strict_legacy_duplicate_warnings( + complete_legacy_duplicate_warnings: list[str], +) -> None: + complete = fixtures_dir / "complete_duplicates.toml" + with complete.open("rb") as f: + content = tomllib.load(f) + + assert Factory.validate(content, strict=True) == { + "errors": [], + "warnings": complete_legacy_duplicate_warnings, + } + + +def test_validate_strict_new_no_warnings() -> None: + complete = fixtures_dir / "complete_new.toml" + with complete.open("rb") as f: + content = tomllib.load(f) + + assert Factory.validate(content, strict=True) == {"errors": [], "warnings": []} + + +def test_validate_strict_dynamic_warnings() -> None: + # some fields are allowed to be dynamic, but some are not + complete = fixtures_dir / "complete_new_dynamic_invalid.toml" + with complete.open("rb") as f: + content = tomllib.load(f) + + assert Factory.validate(content, strict=True) == { + "errors": ["project must contain ['name'] properties"], + "warnings": [ + # version, readme and classifiers are allowed to be dynamic! + "[tool.poetry.name] is deprecated. Use [project.name] instead.", + ( + "[tool.poetry.description] is deprecated. Use " + "[project.description] instead." + ), + "[tool.poetry.license] is deprecated. Use [project.license] instead.", + "[tool.poetry.authors] is deprecated. Use [project.authors] instead.", + ( + "[tool.poetry.maintainers] is deprecated. Use " + "[project.maintainers] instead." + ), + "[tool.poetry.keywords] is deprecated. Use [project.keywords] instead.", + "[tool.poetry.homepage] is deprecated. Use [project.urls] instead.", + "[tool.poetry.repository] is deprecated. Use [project.urls] instead.", + "[tool.poetry.documentation] is deprecated. Use [project.urls] instead.", + ( + "[tool.poetry.extras] is deprecated. Use " + "[project.optional-dependencies] instead." + ), + ( + "Defining console scripts in [tool.poetry.scripts] is deprecated. " + "Use [project.scripts] instead. " + "([tool.poetry.scripts] should only be used for scripts of type 'file')." + ), + ], + } + + def test_validate_fails() -> None: complete = fixtures_dir / "complete.toml" with complete.open("rb") as f: - doc = tomllib.load(f) - content = doc["tool"]["poetry"] - content["authors"] = "this is not a valid array" + content = tomllib.load(f) + content["tool"]["poetry"]["authors"] = "this is not a valid array" - expected = "data.authors must be array" + expected = "tool.poetry.authors must be array" assert Factory.validate(content) == {"errors": [expected], "warnings": []} @@ -272,14 +604,14 @@ def test_validate_without_strict_fails_only_non_strict() -> None: fixtures_dir / "project_failing_strict_validation" / "pyproject.toml" ) with project_failing_strict_validation.open("rb") as f: - doc = tomllib.load(f) - content = doc["tool"]["poetry"] + content = tomllib.load(f) assert Factory.validate(content) == { "errors": [ + "Either [project.name] or [tool.poetry.name] is required in package mode.", ( - "The fields ['authors', 'description', 'name', 'version']" - " are required in package mode." + "Either [project.version] or [tool.poetry.version] is required in " + "package mode." ), ], "warnings": [], @@ -291,14 +623,14 @@ def test_validate_strict_fails_strict_and_non_strict() -> None: fixtures_dir / "project_failing_strict_validation" / "pyproject.toml" ) with project_failing_strict_validation.open("rb") as f: - doc = tomllib.load(f) - content = doc["tool"]["poetry"] + content = tomllib.load(f) assert Factory.validate(content, strict=True) == { "errors": [ + "Either [project.name] or [tool.poetry.name] is required in package mode.", ( - "The fields ['authors', 'description', 'name', 'version']" - " are required in package mode." + "Either [project.version] or [tool.poetry.version] is required in " + "package mode." ), ( 'Cannot find dependency "missing_extra" for extra "some-extras" in ' @@ -318,6 +650,22 @@ def test_validate_strict_fails_strict_and_non_strict() -> None: ), ], "warnings": [ + ( + "[tool.poetry.readme] is set but 'readme' is not in " + "[project.dynamic]. If it is static use [project.readme]. If it " + "is dynamic, add 'readme' to [project.dynamic].\n" + "If you want to define multiple readmes, you should define them " + "in [tool.poetry] and add 'readme' to [project.dynamic]." + ), + ( + "[tool.poetry.extras] is deprecated. Use " + "[project.optional-dependencies] instead." + ), + ( + "Defining console scripts in [tool.poetry.scripts] is deprecated. " + "Use [project.scripts] instead. " + "([tool.poetry.scripts] should only be used for scripts of type 'file')." + ), ( "A wildcard Python dependency is ambiguous. Consider specifying a more" " explicit one." @@ -337,11 +685,49 @@ def test_validate_strict_fails_strict_and_non_strict() -> None: } +@pytest.mark.parametrize("with_project_section", [True, False]) +def test_validate_dependencies_non_package_mode(with_project_section: bool) -> None: + content: dict[str, Any] = { + "tool": {"poetry": {"package-mode": False, "dependencies": {"foo": "*"}}} + } + expected: dict[str, list[str]] = {"errors": [], "warnings": []} + if with_project_section: + content["project"] = {"name": "my-project"} + expected["warnings"] = [ + ( + "[tool.poetry.dependencies] is set but [project.dependencies] is " + "not and 'dependencies' is not in [project.dynamic]. You should " + "either migrate [tool.poetry.depencencies] to " + "[project.dependencies] (if you do not need Poetry-specific " + "features) or add [project.dependencies] in addition to " + "[tool.poetry.dependencies] or add 'dependencies' to " + "[project.dynamic]." + ) + ] + assert Factory.validate(content, strict=True) == expected + + +@pytest.mark.parametrize("with_project_section", [True, False]) +def test_validate_python_non_package_mode(with_project_section: bool) -> None: + content: dict[str, Any] = { + "tool": {"poetry": {"package-mode": False, "dependencies": {"python": ">=3.9"}}} + } + expected: dict[str, list[str]] = {"errors": [], "warnings": []} + if with_project_section: + content["project"] = {"name": "my-project", "dynamic": ["dependencies"]} + expected["warnings"] = [ + ( + "[tool.poetry.dependencies.python] is set but [project.requires-python]" + " is not set and 'requires-python' is not in [project.dynamic]." + ) + ] + assert Factory.validate(content, strict=True) == expected + + def test_strict_validation_success_on_multiple_readme_files() -> None: with_readme_files = fixtures_dir / "with_readme_files" / "pyproject.toml" with with_readme_files.open("rb") as f: - doc = tomllib.load(f) - content = doc["tool"]["poetry"] + content = tomllib.load(f) assert Factory.validate(content, strict=True) == {"errors": [], "warnings": []} @@ -349,9 +735,8 @@ def test_strict_validation_success_on_multiple_readme_files() -> None: def test_strict_validation_fails_on_readme_files_with_unmatching_types() -> None: with_readme_files = fixtures_dir / "with_readme_files" / "pyproject.toml" with with_readme_files.open("rb") as f: - doc = tomllib.load(f) - content = doc["tool"]["poetry"] - content["readme"][0] = "README.md" + content = tomllib.load(f) + content["tool"]["poetry"]["readme"][0] = "README.md" assert Factory.validate(content, strict=True) == { "errors": [ @@ -370,7 +755,8 @@ def test_create_poetry_fails_on_invalid_configuration() -> None: expected = """\ The Poetry configuration is invalid: - - The fields ['description'] are required in package mode. + - Either [project.name] or [tool.poetry.name] is required in package mode. + - Either [project.version] or [tool.poetry.version] is required in package mode. """ assert str(e.value) == expected @@ -383,7 +769,9 @@ def test_create_poetry_fails_on_invalid_mode() -> None: expected = """\ The Poetry configuration is invalid: - - Invalid value for package-mode: invalid + - tool.poetry.package-mode must be boolean + - Either [project.name] or [tool.poetry.name] is required in package mode. + - Either [project.version] or [tool.poetry.version] is required in package mode. """ assert str(e.value) == expected