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'."
            )
Exemple #3
0
    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
Exemple #4
0
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)
Exemple #11
0
        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)}", )
Exemple #12
0
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,
			}
Exemple #15
0
    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
Exemple #16
0
    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
Exemple #17
0
    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,
        )