def parse( # type: ignore[override] self, config: Dict[str, TOML_TYPES], set_defaults: bool = False, ) -> ProjectDict: """ Parse the TOML configuration. :param config: :param set_defaults: If :py:obj:`True`, the values in :attr:`self.defaults <dom_toml.parser.AbstractConfigParser.defaults>` and :attr:`self.factories <dom_toml.parser.AbstractConfigParser.factories>` will be set as defaults for the returned mapping. """ dynamic_fields = config.get("dynamic", []) if "name" in dynamic_fields: raise BadConfigError("The 'project.name' field may not be dynamic.") elif "name" not in config: raise BadConfigError("The 'project.name' field must be provided.") if "dependencies" not in config and "dependencies" not in dynamic_fields: raise BadConfigError("The 'project.dependencies' field must be provided or marked as 'dynamic'.") if "optional-dependencies" not in config and "optional-dependencies" in dynamic_fields: raise BadConfigError("The '[project.optional-dependencies]' table may not be dynamic.") parsed_config = {"dynamic": dynamic_fields} parsed_config.update(super().parse(config, set_defaults)) return cast(ProjectDict, parsed_config)
def parse_license(config: Dict[str, TOML_TYPES]) -> License: """ Parse the :pep621:`license` key. * **Format**: :toml:`Table` * **Core Metadata**: :core-meta:`License` The table may have one of two keys: * ``file`` -- a string value that is a relative file path to the file which contains the license for the project. The file's encoding MUST be UTF-8. * ``text`` -- string value which is the license of the project. These keys are mutually exclusive, so a tool MUST raise an error if the metadata specifies both keys. :bold-title:`Example:` .. code-block:: TOML [project.license] file = "LICENSE.rst" [project.license] file = "COPYING" [project.license] text = \"\"\" This software may only be obtained by sending the author a postcard, and then the user promises not to redistribute it. \"\"\" :param config: The unparsed TOML config for the :pep621:`project table <table-name>`. """ # noqa: D300,D301 license = config["license"] # noqa: A001 # pylint: disable=redefined-builtin if "text" in license and "file" in license: raise BadConfigError( "The 'project.license.file' and 'project.license.text' keys " "are mutually exclusive.") elif "text" in license: return License(text=str(license["text"])) elif "file" in license: os.stat(license["file"]) return License(license["file"]) else: raise BadConfigError( "The 'project.license' table should contain one of 'text' or 'file'." )
def parse_extras( self, config: Dict[str, TOML_TYPES] ) -> Union[Literal["all"], Literal["none"], List[str]]: """ Parse the ``extras`` key, giving a list of extras to include as requirements in the conda package. * The special keyword ``'all'`` indicates all extras should be included. * The special keyword ``'none'`` indicates no extras should be included. :param config: The unparsed TOML config for the ``[tool.mkrecipe]`` table. """ extras = config["extras"] path_elements = [*self.table_name, "extras"] if isinstance(extras, str): extras_lower = extras.lower() if extras_lower == "all": return "all" elif extras_lower == "none": return "none" else: raise BadConfigError( f"Invalid value for [{construct_path(path_elements)}]: " "Expected 'all', 'none' or a list of strings.") for idx, impl in enumerate(extras): self.assert_indexed_type(impl, str, path_elements, idx=idx) return extras
def test_badconfigerror_documentation(): with pytest.raises(BadConfigError, match="Hello World") as e: raise BadConfigError("Hello World", documentation="This is the documentation") assert e.value.documentation == "This is the documentation"
def parse( # type: ignore[override] self, config: Dict[str, TOML_TYPES], set_defaults: bool = False, ) -> ProjectDict: """ Parse the TOML configuration. :param config: :param set_defaults: If :py:obj:`True`, the values in :attr:`self.defaults <dom_toml.parser.AbstractConfigParser.defaults>` and :attr:`self.factories <dom_toml.parser.AbstractConfigParser.factories>` will be set as defaults for the returned mapping. """ dynamic_fields = config.get("dynamic", []) if "name" in dynamic_fields: raise BadConfigError( "The 'project.name' field may not be dynamic.") super_parsed_config = super().parse(config, set_defaults=set_defaults) return { **super_parsed_config, # type: ignore[misc] "dynamic": dynamic_fields, }
def parse( self, config: Dict[str, TOML_TYPES], set_defaults: bool = False, ) -> Dict[str, TOML_TYPES]: """ Parse the TOML configuration. :param config: :param set_defaults: If :py:obj:`True`, the values in :attr:`self.defaults <dom_toml.parser.AbstractConfigParser.defaults>` and :attr:`self.factories <dom_toml.parser.AbstractConfigParser.factories>` will be set as defaults for the returned mapping. """ for key in self.required_keys: if key in config: continue elif set_defaults and (key in self.defaults or key in self.factories): continue # pragma: no cover https://github.com/nedbat/coveragepy/issues/198 else: raise BadConfigError( f"The {construct_path([self.table_name, key])!r} field must be provided." ) return super().parse(config, set_defaults)
def parse_name(config: Dict[str, TOML_TYPES]) -> str: """ Parse the :pep621:`name` key, giving the name of the project. * **Format**: :toml:`String` * **Core Metadata**: :core-meta:`Name` This key is required, and must be defined statically. Tools SHOULD normalize this name, as specified by :pep:`503`, as soon as it is read for internal consistency. :bold-title:`Example:` .. code-block:: TOML [project] name = "spam" :param config: The unparsed TOML config for the :pep621:`project table <table-name>`. """ normalized_name = _NormalisedName(normalize(config["name"])) normalized_name.unnormalized = config["name"] # https://packaging.python.org/specifications/core-metadata/#name if not name_re.match(normalized_name): raise BadConfigError("The value for 'project.name' is invalid.") return normalized_name
def parse_requires_python(config: Dict[str, TOML_TYPES]) -> SpecifierSet: """ Parse the :pep621:`requires-python` key, giving the Python version requirements of the project. The requirement should be in the form of a :pep:`508` marker. * **Format**: :toml:`String` * **Core Metadata**: :core-meta:`Requires-Python` :bold-title:`Example:` .. code-block:: TOML [project] requires-python = ">=3.6" :param config: The unparsed TOML config for the :pep621:`project table <table-name>`. :rtype: .. latex:clearpage:: """ version = str(config["requires-python"]) try: return SpecifierSet(str(version)) except InvalidSpecifier as e: raise BadConfigError(str(e))
def parse_version(config: Dict[str, TOML_TYPES]) -> Version: """ Parse the :pep621:`version` key, giving the version of the project as supported by :pep:`440`. * **Format**: :toml:`String` * **Core Metadata**: :core-meta:`Version` Users SHOULD prefer to specify normalized versions. :bold-title:`Example:` .. code-block:: TOML [project] version = "2020.0.0" :param config: The unparsed TOML config for the :pep621:`project table <table-name>`. """ version = str(config["version"]) try: return Version(str(version)) except InvalidVersion as e: raise BadConfigError(str(e))
def parse( # type: ignore[override] self, config: Dict[str, TOML_TYPES], set_defaults: bool = False, ) -> BuildSystemDict: """ Parse the TOML configuration. :param config: :param set_defaults: If :py:obj:`True`, the values in :attr:`self.defaults <dom_toml.parser.AbstractConfigParser.defaults>` and :attr:`self.factories <dom_toml.parser.AbstractConfigParser.factories>` will be set as defaults for the returned mapping. :rtype: .. latex:clearpage:: """ parsed_config = super().parse(config, set_defaults) if (parsed_config.get("backend-path", None) is not None and parsed_config.get("build-backend", None) is None): raise BadConfigError( f"{construct_path([self.table_name, 'backend-path'])!r} " f"cannot be specified without also specifying " f"{construct_path([self.table_name, 'build-backend'])!r}") return cast(BuildSystemDict, parsed_config)
def error_on_unknown(keys: Iterable[str], expected_keys: Iterable[str], table_name: str): unknown_keys = set(keys) - set(expected_keys) if unknown_keys: raise BadConfigError( f"Unknown {_keys(len(unknown_keys))} in '[{table_name}]': " f"{word_join(sorted(unknown_keys), use_repr=True)}", )
def load_toml(filename: PathLike) -> Dict[str, Any]: # TODO: TypedDict """ Load the ``mkrecipe`` configuration mapping from the given TOML file. :param filename: """ filename = PathPlus(filename) project_dir = filename.parent config = dom_toml.load(filename) parsed_config: Dict[str, Any] = {} tool_table = config.get("tool", {}) with in_directory(filename.parent): parsed_config.update(BuildSystemParser().parse(config.get( "build-system", {}), set_defaults=True)) parsed_config.update(whey.config.WheyParser().parse( tool_table.get("whey", {}))) parsed_config.update(MkrecipeParser().parse(tool_table.get( "mkrecipe", {}), set_defaults=True)) if "project" in config: parsed_config.update(PEP621Parser().parse(config["project"], set_defaults=True)) else: raise KeyError(f"'project' table not found in '{filename!s}'") # set defaults parsed_config.setdefault("package", config["project"]["name"].split('.', 1)[0]) parsed_config.setdefault("license-key", None) if "dependencies" in parsed_config.get("dynamic", []): if (project_dir / "requirements.txt").is_file(): dependencies = read_requirements(project_dir / "requirements.txt", include_invalid=True)[0] parsed_config["dependencies"] = sorted( combine_requirements(dependencies)) else: raise BadConfigError( "'project.dependencies' was listed as a dynamic field " "but no 'requirements.txt' file was found.") parsed_config["version"] = str(parsed_config["version"]) parsed_config["requires"] = sorted( set( combine_requirements( parsed_config["requires"], ComparableRequirement("setuptools"), ComparableRequirement("wheel"), ))) return parsed_config
def _parse_authors(config: Dict[str, TOML_TYPES], key_name: str = "authors") -> List[Author]: all_authors: List[Author] = [] for idx, author in enumerate(config[key_name]): name = author.get("name", None) email = author.get("email", None) if name is not None and ',' in name: raise BadConfigError( f"The 'project.{key_name}[{idx}].name' key cannot contain commas." ) if email is not None: try: email = validate_email(email).email except EmailSyntaxError as e: raise BadConfigError(f"Invalid email {email!r}: {e} ") all_authors.append({"name": name, "email": email}) return all_authors
def load_toml(filename: PathLike) -> ConfigDict: """ Load the ``pyproject-devenv`` configuration mapping from the given TOML file. :param filename: """ filename = PathPlus(filename) devenv_config = _DevenvConfig.load(filename, set_defaults=True) if devenv_config.project is None: raise BadConfigError(f"The '[project]' table was not found in {filename.as_posix()!r}") dynamic = set(devenv_config.project["dynamic"]) project_dir = filename.parent if "dependencies" in dynamic: if (project_dir / "requirements.txt").is_file(): dependencies = read_requirements(project_dir / "requirements.txt", include_invalid=True)[0] devenv_config.project["dependencies"] = sorted(combine_requirements(dependencies)) else: raise BadConfigError( "'project.dependencies' was listed as a dynamic field " "but no 'requirements.txt' file was found." ) if devenv_config.build_system is None: build_dependencies = None else: build_dependencies = devenv_config.build_system["requires"] return { "name": devenv_config.project["name"], "dependencies": devenv_config.project["dependencies"], "optional_dependencies": devenv_config.project["optional-dependencies"], "build_dependencies": build_dependencies, }
def parse_name(config: Dict[str, TOML_TYPES]) -> str: """ Parse the `name <https://www.python.org/dev/peps/pep-0621/#name>`_ key. :param config: The unparsed TOML config for the ``[project]`` table. """ # preserve underscores, dots and hyphens, as conda doesn't treat them as equivalent. normalized_name = config["name"].lower() # https://packaging.python.org/specifications/core-metadata/#name if not name_re.match(normalized_name): raise BadConfigError("The value for 'project.name' is invalid.") return normalized_name
def parse( self, config: Dict[str, TOML_TYPES], set_defaults: bool = False, ) -> Dict[str, TOML_TYPES]: """ Parse the TOML configuration. :param config: :param set_defaults: If :py:obj:`True`, the values in :attr:`self.defaults <dom_toml.parser.AbstractConfigParser.defaults>` and :attr:`self.factories <dom_toml.parser.AbstractConfigParser.factories>` will be set as defaults for the returned mapping. """ if "name" not in config: raise BadConfigError("The [tool.whey-pth.name] key is required.") if "pth-content" not in config: raise BadConfigError( "The [tool.whey-pth.pth-content] key is required.") parsed_config = super().parse(config, set_defaults=set_defaults) return parsed_config
def parse( # type: ignore[override] self, config: Dict[str, TOML_TYPES], set_defaults: bool = False, ) -> ProjectDict: """ Parse the TOML configuration. :param config: :param set_defaults: If :py:obj:`True`, the values in :attr:`dom_toml.parser.AbstractConfigParser.defaults` and :attr:`dom_toml.parser.AbstractConfigParser.factories` will be set as defaults for the returned mapping. """ dynamic_fields = config.get("dynamic", []) if "name" in dynamic_fields: raise BadConfigError( "The 'project.name' field may not be dynamic.") elif "name" not in config: raise BadConfigError("The 'project.name' field must be provided.") if "version" in dynamic_fields: raise BadConfigError( "The 'project.version' field may not be dynamic.") elif "version" not in config: raise BadConfigError( "The 'project.version' field must be provided.") if "dependencies" not in config and "dependencies" not in dynamic_fields: raise BadConfigError( "The 'project.dependencies' field must be provided or marked as 'dynamic'" ) return self._parse(config, set_defaults)
def parse_entry_points( self, config: Dict[str, TOML_TYPES]) -> Dict[str, Dict[str, str]]: """ Parse the :pep621:`entry-points` table. **Format**: :toml:`Table` of :toml:`tables <table>`, with keys and values of :toml:`strings <string>` Each sub-table's name is an entry point group. * Users MUST NOT create nested sub-tables but instead keep the entry point groups to only one level deep. * Users MUST NOT created sub-tables for ``console_scripts`` or ``gui_scripts``. Use ``[project.scripts]`` and ``[project.gui-scripts]`` instead. See the `entry point specification`_ for more details. .. _entry point specification: https://packaging.python.org/specifications/entry-points/ :bold-title:`Example:` .. code-block:: TOML [project.entry-points."spam.magical"] tomatoes = "spam:main_tomatoes" # pytest plugins refer to a module, so there is no ':obj' [project.entry-points.pytest11] nbval = "nbval.plugin" :param config: The unparsed TOML config for the :pep621:`project table <table-name>`. :rtype: .. latex:clearpage:: """ entry_points = config["entry-points"] self.assert_type(entry_points, dict, ["project", "entry-points"]) for group, sub_table in entry_points.items(): self.assert_value_type(sub_table, dict, ["project", "entry-points", group]) if normalize(group) in "console-scripts": name = construct_path(["project", "entry-points"]) suggested_name = construct_path(["project", "scripts"]) raise BadConfigError( f"{name!r} may not contain a {group!r} sub-table. Use {suggested_name!r} instead." ) elif normalize(group) in "gui-scripts": name = construct_path(["project", "entry-points"]) suggested_name = construct_path(["project", "gui-scripts"]) raise BadConfigError( f"{name!r} may not contain a {group!r} sub-table. Use {suggested_name!r} instead." ) for name, func in sub_table.items(): self.assert_value_type( func, str, ["project", "entry-points", group, name]) return entry_points
def parse_readme(config: Dict[str, TOML_TYPES]) -> Readme: """ Parse the :pep621:`readme` key, giving the full description of the project (i.e. the README). * **Format**: :toml:`String` or :toml:`table` * **Core Metadata**: :core-meta:`Description` This field accepts either a string or a table. If it is a string then it is the relative path to a text file containing the full description. The file's encoding MUST be UTF-8, and have one of the following content types: * ``text/markdown``, with a case-insensitive ``.md`` suffix. * ``text/x-rst``, with a case-insensitive ``.rst`` suffix. * ``text/plain``, with a case-insensitive ``.txt`` suffix. If a tool recognizes more extensions than this PEP, they MAY infer the content-type for the user without specifying this field as dynamic. For all unrecognized suffixes when a content-type is not provided, tools MUST raise an error. .. space:: .. latex:clearpage:: The readme field may instead be a table with the following keys: * ``file`` -- a string value representing a relative path to a file containing the full description. * ``text`` -- a string value which is the full description. * ``content-type`` -- (required) a string specifying the content-type of the full description. * ``charset`` -- (optional, default UTF-8) the encoding of the ``file``. Tools MAY support other encodings if they choose to. The ``file`` and ``text`` keys are mutually exclusive, but one must be provided in the table. :bold-title:`Examples:` .. code-block:: TOML [project] readme = "README.rst" [project.readme] file = "README.rst" content-type = "text/x-rst" encoding = "UTF-8" [project.readme] text = "Spam is a brand of canned cooked pork made by Hormel Foods Corporation." content-type = "text/x-rst" :param config: The unparsed TOML config for the :pep621:`project table <table-name>`. """ readme: Union[Dict, str] = config["readme"] if isinstance(readme, str): # path to readme_file readme_content_type = content_type_from_filename(readme) render_readme(readme, readme_content_type) return Readme(file=readme, content_type=readme_content_type) elif isinstance(readme, dict): if not readme: raise BadConfigError( "The 'project.readme' table cannot be empty.") if "file" in readme and "text" in readme: raise BadConfigError( "The 'project.readme.file' and 'project.readme.text' keys " "are mutually exclusive.") elif set(readme.keys()) in ({"file"}, {"file", "charset"}): readme_encoding = readme.get("charset", "UTF-8") render_readme(readme["file"], encoding=readme_encoding) readme_content_type = content_type_from_filename( readme["file"]) return Readme(file=readme["file"], content_type=readme_content_type, charset=readme_encoding) elif set(readme.keys()) in ({"file", "content-type"}, {"file", "charset", "content-type"}): readme_encoding = readme.get("charset", "UTF-8") render_readme(readme["file"], encoding=readme_encoding) return Readme(file=readme["file"], content_type=readme["content-type"], charset=readme_encoding) elif "content-type" in readme and "text" not in readme: raise BadConfigError( "The 'project.readme.content-type' key cannot be provided on its own; " "Please provide the 'project.readme.text' key too.") elif "charset" in readme and "text" not in readme: raise BadConfigError( "The 'project.readme.charset' key cannot be provided on its own; " "Please provide the 'project.readme.text' key too.") elif "text" in readme: if "content-type" not in readme: raise BadConfigError( "The 'project.readme.content-type' key must be provided " "when 'project.readme.text' is given.") elif readme["content-type"] not in { "text/markdown", "text/x-rst", "text/plain" }: raise BadConfigError( f"Unrecognised value for 'project.readme.content-type': {readme['content-type']!r}" ) if "charset" in readme: raise BadConfigError( "The 'project.readme.charset' key cannot be provided " "when 'project.readme.text' is given.") return Readme(text=readme["text"], content_type=readme["content-type"]) else: raise BadConfigError( f"Unknown format for 'project.readme': {readme!r}") raise TypeError( f"Unsupported type for 'project.readme': {type(readme)!r}")
from dom_toml.parser import BadConfigError # this package from pyproject_parser.cli import ConfigTracebackHandler, resolve_class exceptions = pytest.mark.parametrize( "exception", [ pytest.param(FileNotFoundError("foo.txt"), id="FileNotFoundError"), pytest.param(FileExistsError("foo.txt"), id="FileExistsError"), pytest.param(Exception("Something's awry!"), id="Exception"), pytest.param(ValueError("'age' must be >= 0"), id="ValueError"), pytest.param(TypeError("Expected type int, got type str"), id="TypeError"), pytest.param(NameError("name 'hello' is not defined"), id="NameError"), pytest.param(SyntaxError("invalid syntax"), id="SyntaxError"), pytest.param(BadConfigError("Expected a string value for 'name'"), id="BadConfigError"), pytest.param(KeyError("name"), id="KeyError"), ] ) @exceptions def test_traceback_handler( exception, file_regression, cli_runner: CliRunner, ): @click.command() def demo():
def load( cls: Type[_PP], filename: PathLike, set_defaults: bool = False, ) -> _PP: """ Load the ``pyproject.toml`` configuration mapping from the given file. :param filename: :param set_defaults: If :py:obj:`True`, passes ``set_defaults=True`` the :meth:`parse() <dom_toml.parser.AbstractConfigParser.parse>` method on :attr:`~.build_system_table_parser` and :attr:`~.project_table_parser`. """ filename = PathPlus(filename) project_dir = filename.parent config = dom_toml.load(filename) keys = set(config.keys()) build_system_table: Optional[BuildSystemDict] = None project_table: Optional[ProjectDict] = None tool_table: Dict[str, Dict[str, Any]] = {} with in_directory(project_dir): if "build-system" in config: build_system_table = cls.build_system_table_parser.parse( config["build-system"], set_defaults=set_defaults) keys.remove("build-system") if "project" in config: project_table = cls.project_table_parser.parse( config["project"], set_defaults=set_defaults) keys.remove("project") if "tool" in config: tool_table = config["tool"] keys.remove("tool") for tool_name, tool_subtable in tool_table.items(): if tool_name in cls.tool_parsers: tool_table[tool_name] = cls.tool_parsers[ tool_name].parse(tool_subtable) if keys: allowed_top_level = ("build-system", "project", "tool") for top_level_key in sorted(keys): if top_level_key in allowed_top_level: continue if normalize(top_level_key) in allowed_top_level: raise BadConfigError( f"Unexpected top-level key {top_level_key!r}. " f"Did you mean {normalize(top_level_key)!r}?", ) raise BadConfigError( f"Unexpected top-level key {top_level_key!r}. " f"Only {word_join(allowed_top_level, use_repr=True)} are allowed.", ) return cls( build_system=build_system_table, project=project_table, tool=tool_table, )