From b57e32c1bc558031dbae371ec85894e941bf039e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:52:33 +0200 Subject: [PATCH] add support for PEP 621: use project part of pyproject schema from https://json.schemastore.org/pyproject.json and support strings in the license field (#708) --- src/poetry/core/factory.py | 11 +- .../core/json/schemas/project-schema.json | 380 ++++++++++++------ .../with_license_type_str/pyproject.toml | 5 + .../with_license_type_text/pyproject.toml | 8 + tests/masonry/builders/test_builder.py | 21 +- tests/test_factory.py | 16 +- 6 files changed, 303 insertions(+), 138 deletions(-) create mode 100644 tests/fixtures/with_license_type_str/pyproject.toml create mode 100644 tests/fixtures/with_license_type_text/pyproject.toml diff --git a/src/poetry/core/factory.py b/src/poetry/core/factory.py index 301c93392..02095772f 100644 --- a/src/poetry/core/factory.py +++ b/src/poetry/core/factory.py @@ -177,9 +177,14 @@ def _configure_package_metadata( "description", "" ) if project_license := project.get("license"): - 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") + 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: diff --git a/src/poetry/core/json/schemas/project-schema.json b/src/poetry/core/json/schemas/project-schema.json index 20ff5c3e8..eb9ead856 100644 --- a/src/poetry/core/json/schemas/project-schema.json +++ b/src/poetry/core/json/schemas/project-schema.json @@ -1,6 +1,6 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", - "name": "Package", + "$schema": "http://json-schema.org/draft-07/schema#", + "name": "project", "type": "object", "additionalProperties": true, "required": [ @@ -8,209 +8,331 @@ ], "properties": { "name": { + "title": "Project name", "type": "string", - "description": "The name of the project." + "pattern": "^([a-zA-Z\\d]|[a-zA-Z\\d][\\w.-]*[a-zA-Z\\d])$" }, "version": { + "title": "Project version", "type": "string", - "description": "The version of the project." + "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": { - "type": "string", - "description": "The summary description of the project.", - "pattern": "^[^\n]*$" + "title": "Project summary description", + "type": "string" }, "readme": { - "$ref": "#/definitions/readme", - "description": "The full description of the project (i.e. the 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", - "description": "The Python version requirements of the project." + "examples": [ + ">= 3.7" + ] }, "license": { - "$ref": "#/definitions/license", - "description": "The license of the project (file or string)." + "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": { - "$ref": "#/definitions/authors", - "description": "The authors of the project." + "title": "Project authors", + "type": "array", + "items": { + "$ref": "#/definitions/projectAuthor" + } }, "maintainers": { - "$ref": "#/definitions/maintainers", - "description": "The maintainers of the project." + "title": "Project maintainers", + "type": "array", + "items": { + "$ref": "#/definitions/projectAuthor" + } }, "keywords": { + "title": "Project keywords", "type": "array", "items": { - "type": "string", - "description": "A tag/keyword that this project relates to." + "type": "string" } }, "classifiers": { + "title": "Applicable Trove classifiers", "type": "array", - "description": "Trove classifiers which apply to the project." + "items": { + "type": "string" + } }, "urls": { + "title": "Project URLs", "type": "object", - "patternProperties": { - "^.+$": { - "type": "string", - "description": "The full url of the custom url." + "additionalProperties": { + "type": "string", + "format": "uri" + }, + "examples": [ + { + "homepage": "https://example.com/example-project" } - } + ] }, "scripts": { + "title": "Console scripts", "type": "object", - "description": "Names and object references of console scripts.", - "patternProperties": { - "^[a-zA-Z-_.0-9]+$": { - "type": "string" + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "mycmd": "package.module:object.function" } - } + ] }, "gui-scripts": { + "title": "GUI scripts", "type": "object", - "description": "Names and object references of gui scripts.", - "patternProperties": { - "^[a-zA-Z-_.0-9]+$": { - "type": "string" + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "mycmd": "package.module:object.function" } - } + ] }, "entry-points": { + "title": "Other entry-point groups", "type": "object", - "description": "Entry point groups. Each group is like scripts or gui-scripts.", + "additionalProperties": false, "patternProperties": { - "^[a-zA-Z-_.0-9]+$": { + "^\\w+(\\.\\w+)*$": { "type": "object", - "patternProperties": { - "^[a-zA-Z-_.0-9]+$": { - "type": "string" + "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", - "description": "A list of runtime dependencies in the PEP 508 format.", "items": { "type": "string" - } + }, + "examples": [ + [ + "attrs", + "requests ~= 2.28" + ] + ] }, "optional-dependencies": { + "title": "Project extra dependency requirements", + "description": "keys are extra names", "type": "object", - "description": "Extras and their dependencies.", "patternProperties": { - "^[a-zA-Z-_.0-9]+$": { + "^([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", - "description": "Keys that are defined dynamically or in a tool specific section.", "items": { - "type": "string" - } + "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": { - "license": { - "oneOf": [ + "projectAuthor": { + "type": "object", + "additionalProperties": false, + "anyOf": [ { - "type": "object", - "additionalProperties": false, + "required": [ + "name" + ], "properties": { - "file": { - "type": "string" - } + "name": true } }, { - "type": "object", - "additionalProperties": false, + "required": [ + "email" + ], "properties": { - "text": { - "type": "string" - } - } - } - ] - }, - "authors": { - "type": "array", - "description": "List of authors that contributed to the package. This is typically the main maintainers, not the full list.", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string" + "email": true } } - } - }, - "maintainers": { - "type": "array", - "description": "List of maintainers, other than the original author(s), that upkeep the package.", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string" - } - } - } - }, - "readme": { - "oneOf": [ - { - "type": "string", - "description": "Relative path to the README file" + ], + "properties": { + "name": { + "title": "Author name", + "type": "string" }, - { - "oneOf": [ - { - "type": "object", - "description": "Readme with file and content type", - "properties": { - "file": { - "type": "string", - "description": "The relative path to the readme file" - }, - "content-type": { - "type": "string", - "description": "The content type of the README file content" - } - } - }, - { - "type": "object", - "description": "Readme with full description and content type", - "properties": { - "file": { - "type": "string", - "description": "The full content of the description" - }, - "content-type": { - "type": "string", - "description": "The content type of the description" - } - } - } - ] + "email": { + "title": "Author email", + "type": "string", + "format": "email" } - ] + } } } } 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/masonry/builders/test_builder.py b/tests/masonry/builders/test_builder.py index 2cce7d1b9..50e6fbf5e 100644 --- a/tests/masonry/builders/test_builder.py +++ b/tests/masonry/builders/test_builder.py @@ -160,12 +160,27 @@ def test_metadata_homepage_default() -> None: assert metadata["Home-page"] is None -def test_metadata_license_type_file() -> 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" / "with_license_type_file" + Path(__file__).parent.parent.parent + / "fixtures" + / f"with_license_type_{license_type}" ) builder = Builder(Factory().create_poetry(project_path)) - license_text = (project_path / "LICENSE").read_text(encoding="utf-8") + + 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) diff --git a/tests/test_factory.py b/tests/test_factory.py index e0be5c200..e8bb2e30d 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -387,11 +387,21 @@ def test_create_poetry_non_package_mode() -> None: assert not poetry.is_package_mode -def test_create_poetry_with_license_type_file() -> None: - project_dir = fixtures_dir / "with_license_type_file" +@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) - license_content = (project_dir / "LICENSE").read_text(encoding="utf-8") + 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