Commit ddbcfcda by Arun Babu Neelicattu Committed by Bjorn Neergaard

schema: validate source objects in poetry

This change is intended to help migrate validation of sections in
pyproject.toml that are not relevant to poetry-core into poetry. This
allows for poetry frontend specific configuration to be introduced
without dependent changes in poetry-core.
parent 711cca93
...@@ -17,6 +17,7 @@ from poetry.core.toml.file import TOMLFile ...@@ -17,6 +17,7 @@ from poetry.core.toml.file import TOMLFile
from tomlkit.toml_document import TOMLDocument from tomlkit.toml_document import TOMLDocument
from poetry.config.config import Config from poetry.config.config import Config
from poetry.json import validate_object
from poetry.packages.locker import Locker from poetry.packages.locker import Locker
from poetry.plugins.plugin import Plugin from poetry.plugins.plugin import Plugin
from poetry.plugins.plugin_manager import PluginManager from poetry.plugins.plugin_manager import PluginManager
...@@ -300,3 +301,13 @@ class Factory(BaseFactory): ...@@ -300,3 +301,13 @@ class Factory(BaseFactory):
) )
return cast(TOMLDocument, pyproject) return cast(TOMLDocument, pyproject)
@classmethod
def validate(
cls, config: dict[str, Any], strict: bool = False
) -> dict[str, list[str]]:
results = super().validate(config, strict)
results["errors"].extend(validate_object(config))
return results
...@@ -3,10 +3,13 @@ from __future__ import annotations ...@@ -3,10 +3,13 @@ from __future__ import annotations
import json import json
import os import os
from pathlib import Path
from typing import Any from typing import Any
import jsonschema import jsonschema
from poetry.core.json import SCHEMA_DIR as CORE_SCHEMA_DIR
SCHEMA_DIR = os.path.join(os.path.dirname(__file__), "schemas") SCHEMA_DIR = os.path.join(os.path.dirname(__file__), "schemas")
...@@ -16,14 +19,9 @@ class ValidationError(ValueError): ...@@ -16,14 +19,9 @@ class ValidationError(ValueError):
pass pass
def validate_object(obj: dict[str, Any], schema_name: str) -> list[str]: def validate_object(obj: dict[str, Any]) -> list[str]:
schema_file = os.path.join(SCHEMA_DIR, f"{schema_name}.json") schema_file = Path(SCHEMA_DIR, "poetry.json")
schema = json.loads(schema_file.read_text(encoding="utf-8"))
if not os.path.exists(schema_file):
raise ValueError(f"Schema {schema_name} does not exist.")
with open(schema_file, encoding="utf-8") as f:
schema = json.loads(f.read())
validator = jsonschema.Draft7Validator(schema) validator = jsonschema.Draft7Validator(schema)
validation_errors = sorted( validation_errors = sorted(
...@@ -41,4 +39,17 @@ def validate_object(obj: dict[str, Any], schema_name: str) -> list[str]: ...@@ -41,4 +39,17 @@ def validate_object(obj: dict[str, Any], schema_name: str) -> list[str]:
errors.append(message) errors.append(message)
core_schema = json.loads(
Path(CORE_SCHEMA_DIR, "poetry-schema.json").read_text(encoding="utf-8")
)
if core_schema["additionalProperties"]:
# TODO: make this un-conditional once core update to >1.1.0b2
properties = {*schema["properties"].keys(), *core_schema["properties"].keys()}
additional_properties = set(obj.keys()) - properties
for key in additional_properties:
errors.append(
f"Additional properties are not allowed ('{key}' was unexpected)"
)
return errors return errors
{
"$schema": "http://json-schema.org/draft-04/schema#",
"name": "Package",
"type": "object",
"additionalProperties": false,
"required": [
"name",
"version",
"description"
],
"properties": {
"name": {
"type": "string",
"description": "Package name."
},
"version": {
"type": "string",
"description": "Package version."
},
"description": {
"type": "string",
"description": "Short package description."
},
"keywords": {
"type": "array",
"items": {
"type": "string",
"description": "A tag/keyword that this package relates to."
}
},
"homepage": {
"type": "string",
"description": "Homepage URL for the project.",
"format": "uri"
},
"repository": {
"type": "string",
"description": "Repository URL for the project.",
"format": "uri"
},
"documentation": {
"type": "string",
"description": "Documentation URL for the project.",
"format": "uri"
},
"license": {
"type": "string",
"description": "License name."
},
"authors": {
"$ref": "#/definitions/authors"
},
"maintainers": {
"$ref": "#/definitions/maintainers"
},
"readme": {
"type": "string",
"description": "The path to the README file"
},
"classifiers": {
"type": "array",
"description": "A list of trove classifiers."
},
"packages": {
"type": "array",
"description": "A list of packages to include in the final distribution.",
"items": {
"type": "object",
"description": "Information about where the package resides.",
"additionalProperties": false,
"required": [
"include"
],
"properties": {
"include": {
"type": "string",
"description": "What to include in the package."
},
"from": {
"type": "string",
"description": "Where the source directory of the package resides."
},
"format": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "The format(s) for which the package must be included."
}
}
}
},
"include": {
"type": "array",
"description": "A list of files and folders to include."
},
"exclude": {
"type": "array",
"description": "A list of files and folders to exclude."
},
"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",
"description": "The Python versions the package is compatible with."
}
},
"$ref": "#/definitions/dependencies",
"additionalProperties": false
},
"dev-dependencies": {
"type": "object",
"description": "This is a hash of package name (keys) and version constraints (values) that this package requires for developing it (testing tools and such).",
"$ref": "#/definitions/dependencies",
"additionalProperties": false
},
"extras": {
"type": "object",
"patternProperties": {
"^[a-zA-Z-_.0-9]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"build": {
"type": "string",
"description": "The file used to build extensions."
},
"source": {
"type": "array",
"description": "A set of additional repositories where packages can be found.",
"additionalProperties": {
"$ref": "#/definitions/repository"
},
"items": {
"$ref": "#/definitions/repository"
}
},
"scripts": {
"type": "object",
"description": "A hash of scripts to be installed.",
"items": {
"type": "string"
}
},
"plugins": {
"type": "object",
"description": "A hash of hashes representing plugins",
"patternProperties": {
"^[a-zA-Z-_.0-9]+$": {
"type": "object",
"patternProperties": {
"^[a-zA-Z-_.0-9]+$": {
"type": "string"
}
}
}
}
},
"urls": {
"type": "object",
"patternProperties": {
"^.+$": {
"type": "string",
"description": "The full url of the custom url."
}
}
}
},
"definitions": {
"authors": {
"type": "array",
"description": "List of authors that contributed to the package. This is typically the main maintainers, not the full list.",
"items": {
"type": "string"
}
},
"maintainers": {
"type": "array",
"description": "List of maintainers, other than the original author(s), that upkeep the package.",
"items": {
"type": "string"
}
},
"dependencies": {
"type": "object",
"patternProperties": {
"^[a-zA-Z-_.0-9]+$": {
"oneOf": [
{
"$ref": "#/definitions/dependency"
},
{
"$ref": "#/definitions/long-dependency"
},
{
"$ref": "#/definitions/git-dependency"
},
{
"$ref": "#/definitions/file-dependency"
},
{
"$ref": "#/definitions/path-dependency"
},
{
"$ref": "#/definitions/url-dependency"
},
{
"$ref": "#/definitions/multiple-constraints-dependency"
}
]
}
}
},
"dependency": {
"type": "string",
"description": "The constraint of the dependency."
},
"long-dependency": {
"type": "object",
"required": [
"version"
],
"additionalProperties": false,
"properties": {
"version": {
"type": "string",
"description": "The constraint of the dependency."
},
"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."
},
"allows-prereleases": {
"type": "boolean",
"description": "Whether the dependency allows prereleases or not."
},
"optional": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
},
"extras": {
"type": "array",
"description": "The required extras for this dependency.",
"items": {
"type": "string"
}
},
"source": {
"type": "string",
"description": "The exclusive source used to search for this dependency."
}
}
},
"git-dependency": {
"type": "object",
"required": [
"git"
],
"additionalProperties": false,
"properties": {
"git": {
"type": "string",
"description": "The url of the git repository.",
"format": "uri"
},
"branch": {
"type": "string",
"description": "The branch to checkout."
},
"tag": {
"type": "string",
"description": "The tag to checkout."
},
"rev": {
"type": "string",
"description": "The revision to checkout."
},
"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."
},
"allows-prereleases": {
"type": "boolean",
"description": "Whether the dependency allows prereleases or not."
},
"optional": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
},
"extras": {
"type": "array",
"description": "The required extras for this dependency.",
"items": {
"type": "string"
}
}
}
},
"file-dependency": {
"type": "object",
"required": [
"file"
],
"additionalProperties": false,
"properties": {
"file": {
"type": "string",
"description": "The path to the file."
},
"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."
},
"optional": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
},
"extras": {
"type": "array",
"description": "The required extras for this dependency.",
"items": {
"type": "string"
}
}
}
},
"path-dependency": {
"type": "object",
"required": [
"path"
],
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"description": "The path to the dependency."
},
"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."
},
"optional": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
},
"extras": {
"type": "array",
"description": "The required extras for this dependency.",
"items": {
"type": "string"
}
},
"develop": {
"type": "boolean",
"description": "Whether to install the dependency in development mode."
}
}
},
"url-dependency": {
"type": "object",
"required": [
"url"
],
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"description": "The url to the file."
},
"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."
},
"optional": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
},
"extras": {
"type": "array",
"description": "The required extras for this dependency.",
"items": {
"type": "string"
}
}
}
},
"multiple-constraints-dependency": {
"type": "array",
"minItems": 1,
"items": {
"oneOf": [
{
"$ref": "#/definitions/dependency"
},
{
"$ref": "#/definitions/long-dependency"
},
{
"$ref": "#/definitions/git-dependency"
},
{
"$ref": "#/definitions/file-dependency"
},
{
"$ref": "#/definitions/path-dependency"
},
{
"$ref": "#/definitions/url-dependency"
}
]
}
},
"scripts": {
"type": "object",
"patternProperties": {
"^[a-zA-Z-_.0-9]+$": {
"oneOf": [
{
"$ref": "#/definitions/script"
},
{
"$ref": "#/definitions/extra-script"
}
]
}
}
},
"script": {
"type": "string",
"description": "A simple script pointing to a callable object."
},
"extra-script": {
"type": "object",
"description": "A script that should be installed only if extras are activated.",
"additionalProperties": false,
"properties": {
"callable": {
"$ref": "#/definitions/script"
},
"extras": {
"type": "array",
"description": "The required extras for this script.",
"items": {
"type": "string"
}
}
}
},
"repository": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "The name of the repository"
},
"url": {
"type": "string",
"description": "The url of the repository",
"format": "uri"
},
"default": {
"type": "boolean",
"description": "Make this repository the default (disable PyPI)"
},
"secondary": {
"type": "boolean",
"description": "Declare this repository as secondary, i.e. it will only be looked up last for packages."
}
}
}
}
}
{
"$schema": "http://json-schema.org/draft-04/schema#",
"additionalProperties": true,
"type": "object",
"required": [],
"properties": {
"source": {
"type": "array",
"description": "A set of additional repositories where packages can be found.",
"additionalProperties": {
"$ref": "#/definitions/repository"
},
"items": {
"$ref": "#/definitions/repository"
}
}
},
"definitions": {
"repository": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"url"
],
"properties": {
"name": {
"type": "string",
"description": "The name of the repository"
},
"url": {
"type": "string",
"description": "The url of the repository",
"format": "uri"
},
"default": {
"type": "boolean",
"description": "Make this repository the default (disable PyPI)"
},
"secondary": {
"type": "boolean",
"description": "Declare this repository as secondary, i.e. it will only be looked up last for packages."
},
"links": {
"type": "boolean",
"description": "Declare this as a link source. Links at uri/path can point to sdist or bdist archives."
},
"indexed": {
"type": "boolean",
"description": "For PEP 503 simple API repositories, pre-fetch and index the available packages. (experimental)"
}
}
}
}
}
[tool.poetry]
name = "foobar"
version = "0.1.0"
description = ""
[tool.poetry.dependencies]
python = "^3.10"
[[tool.poetry.source]]
name = "pypi-simple"
default = false
secondary = false
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "foobar"
version = "0.1.0"
description = ""
[tool.poetry.dependencies]
python = "^3.10"
[[tool.poetry.source]]
name = "pypi-simple"
url = "https://pypi.org/simple/"
default = false
secondary = false
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
from __future__ import annotations
from pathlib import Path
from poetry.core.toml import TOMLFile
from poetry.factory import Factory
FIXTURE_DIR = Path(__file__).parent / "fixtures" / "source"
def test_pyproject_toml_valid() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_valid.toml").read()
content = toml["tool"]["poetry"]
assert Factory.validate(content) == {"errors": [], "warnings": []}
def test_pyproject_toml_invalid() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_invalid.toml").read()
content = toml["tool"]["poetry"]
assert Factory.validate(content) == {
"errors": ["[source.0] 'url' is a required property"],
"warnings": [],
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment