def test_pep621_class_bad_config( config: str, expects: Type[Exception], match: str, tmp_pathplus: PathPlus, ): (tmp_pathplus / "pyproject.toml").write_clean(config) with in_directory(tmp_pathplus), pytest.raises(expects, match=match): PEP621Parser().parse( dom_toml.load(tmp_pathplus / "pyproject.toml")["project"])
def test_parse_config_readme_errors(filename: str, tmp_pathplus: PathPlus): config = dedent(f""" [project] name = "spam" version = "2020.0.0" readme = "{filename}" """) (tmp_pathplus / "pyproject.toml").write_clean(config) (tmp_pathplus / filename).write_text("This is the readme.") with in_directory(tmp_pathplus), pytest.raises( ValueError, match=f"Unsupported extension for '{filename}'"): PEP621Parser().parse( dom_toml.load(tmp_pathplus / "pyproject.toml")["project"])
def test_pep621_class_valid_config( toml_config: str, tmp_pathplus: PathPlus, advanced_data_regression: AdvancedDataRegressionFixture, set_defaults: bool, ): (tmp_pathplus / "pyproject.toml").write_clean(toml_config) with in_directory(tmp_pathplus): config = PEP621Parser().parse( dom_toml.load(tmp_pathplus / "pyproject.toml")["project"], set_defaults=set_defaults, ) advanced_data_regression.check(config)
def test_pep621_class_bad_config_license( license_key: str, expected: str, tmp_pathplus: PathPlus, ): (tmp_pathplus / "pyproject.toml").write_lines([ f'[project]', f'name = "spam"', f'version = "2020.0.0"', license_key, ]) with in_directory(tmp_pathplus), pytest.raises(BadConfigError, match=expected): PEP621Parser().parse( dom_toml.load(tmp_pathplus / "pyproject.toml")["project"])
def test_pep621_class_valid_config_license_dict( tmp_pathplus: PathPlus, advanced_data_regression: AdvancedDataRegressionFixture, ): (tmp_pathplus / "pyproject.toml").write_lines([ f'[project]', f'name = "spam"', f'version = "2020.0.0"', f'license = {{text = "This is the MIT License"}}', ]) with in_directory(tmp_pathplus): config = PEP621Parser().parse( dom_toml.load(tmp_pathplus / "pyproject.toml")["project"]) advanced_data_regression.check(config)
def test_pep621_class_bad_config_readme( readme: str, expected: str, exception: Type[Exception], tmp_pathplus: PathPlus, ): (tmp_pathplus / "pyproject.toml").write_lines([ "[project]", 'name = "spam"', 'version = "2020.0.0"', readme, ]) with in_directory(tmp_pathplus), pytest.raises(exception, match=expected): PEP621Parser().parse( dom_toml.load(tmp_pathplus / "pyproject.toml")["project"])
def test_pep621_class_valid_config_readme( filename: str, tmp_pathplus: PathPlus, advanced_data_regression: AdvancedDataRegressionFixture, ): (tmp_pathplus / "pyproject.toml").write_lines([ "[project]", 'name = "spam"', 'version = "2020.0.0"', f'readme = {filename!r}', ]) (tmp_pathplus / filename).write_text("This is the readme.") with in_directory(tmp_pathplus): config = PEP621Parser().parse( dom_toml.load(tmp_pathplus / "pyproject.toml")["project"]) advanced_data_regression.check(config)
def test_pep621_class_valid_config_readme_dict( readme, tmp_pathplus: PathPlus, advanced_data_regression: AdvancedDataRegressionFixture, ): (tmp_pathplus / "pyproject.toml").write_lines([ "[project]", 'name = "spam"', 'version = "2020.0.0"', readme, ]) (tmp_pathplus / "README.rst").write_text("This is the reStructuredText README.") (tmp_pathplus / "README.md").write_text("This is the markdown README.") (tmp_pathplus / "README.txt").write_text("This is the plaintext README.") (tmp_pathplus / "README").write_text("This is the README.") with in_directory(tmp_pathplus): config = PEP621Parser().parse( dom_toml.load(tmp_pathplus / "pyproject.toml")["project"]) advanced_data_regression.check(config)
class PyProject: """ Represents a ``pyproject.toml`` file. :param build_system: .. autosummary-widths:: 23/64 :html: 6/16 .. autoclasssumm:: PyProject :autosummary-sections: Attributes .. clearpage:: .. autoclasssumm:: PyProject :autosummary-sections: Methods :autosummary-exclude-members: __ge__,__gt__,__le__,__lt__,__ne__,__init__ .. latex:vspace:: 10px """ #: Represents the :pep:`build-system table <518#build-system-table>` defined in :pep:`517` and :pep:`518`. build_system: Optional[BuildSystemDict] = attr.ib(default=None) #: Represents the :pep621:`project table <table-name>` defined in :pep:`621`. project: Optional[ProjectDict] = attr.ib(default=None) #: Represents the :pep:`tool table <518#tool-table>` defined in :pep:`518`. tool: Dict[str, Dict[str, Any]] = attr.ib(factory=dict) build_system_table_parser: ClassVar[BuildSystemParser] = BuildSystemParser( ) """ The :class:`dom_toml.parser.AbstractConfigParser` to parse the :pep:`build-system table <518#build-system-table>` with. """ project_table_parser: ClassVar[PEP621Parser] = PEP621Parser() """ The :class:`dom_toml.parser.AbstractConfigParser` to parse the :pep621:`project table <table-name>` with. """ tool_parsers: ClassVar[Mapping[str, AbstractConfigParser]] = {} """ A mapping of subtable names to :class:`dom_toml.parser.AbstractConfigParser` to parse the :pep:`tool table <518#tool-table>` with. For example, to parse ``[tool.whey]``: .. code-block:: python class WheyParser(AbstractConfigParser): pass class CustomPyProject(PyProject): tool_parsers = {"whey": WheyParser()} """ @classmethod 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, ) def dumps( self, encoder: Union[Type[toml.TomlEncoder], toml.TomlEncoder] = PyProjectTomlEncoder, ) -> str: """ Serialise to TOML. :param encoder: The :class:`toml.TomlEncoder` to use for constructing the output string. """ # TODO: filter out default values (lists and dicts) toml_dict: _PyProjectAsTomlDict = { "build-system": self.build_system, "project": self.project, "tool": self.tool } if toml_dict["project"] is not None: if "license" in toml_dict["project"] and toml_dict["project"][ "license"] is not None: toml_dict["project"] = { # type: ignore **toml_dict["project"], # type: ignore "license": toml_dict["project"]["license"].to_pep621_dict() } if toml_dict["project"] is not None: if "readme" in toml_dict["project"] and toml_dict["project"][ "readme"] is not None: readme_dict = toml_dict["project"]["readme"].to_pep621_dict() if set(readme_dict.keys()) == {"file"}: toml_dict["project"] = { **toml_dict["project"], "readme": readme_dict["file"] } # type: ignore else: toml_dict["project"] = { **toml_dict["project"], "readme": readme_dict } # type: ignore return dom_toml.dumps(toml_dict, encoder) def dump( self, filename: PathLike, encoder: Union[Type[toml.TomlEncoder], toml.TomlEncoder] = PyProjectTomlEncoder, ): """ Write as TOML to the given file. :param filename: The filename to write to. :param encoder: The :class:`toml.TomlEncoder` to use for constructing the output string. :returns: A string containing the TOML representation. """ filename = PathPlus(filename) as_toml = self.dumps(encoder=encoder) filename.write_clean(as_toml) return as_toml @classmethod def reformat( cls: Type[_PP], filename: PathLike, encoder: Union[Type[toml.TomlEncoder], toml.TomlEncoder] = PyProjectTomlEncoder, ) -> str: """ Reformat the given ``pyproject.toml`` file. :param filename: The file to reformat. :param encoder: The :class:`toml.TomlEncoder` to use for constructing the output string. :returns: A string containing the reformatted TOML. .. versionchanged:: 0.2.0 * Added the ``encoder`` argument. * The parser configured as :attr:`~.project_table_parser` is now used to parse the :pep621:`project table <table-name>`, rather than always using :class:`~.PEP621Parser`. """ config = cls.load(filename, set_defaults=False) if config.project is not None and isinstance(config.project["name"], _NormalisedName): config.project["name"] = config.project["name"].unnormalized return config.dump(filename, encoder=encoder) def resolve_files(self): """ Resolve the ``file`` key in :pep621:`readme` and :pep621:`license` (if present) to retrieve the content of the file. Calling this method may mean it is no longer possible to recreate the original ``TOML`` file from this object. """ # noqa: D400 if self.project is not None: readme = self.project.get("readme", None) if readme is not None and isinstance(readme, Readme): readme.resolve(inplace=True) lic = self.project.get("license", None) if lic is not None and isinstance(lic, License): lic.resolve(inplace=True) @classmethod def from_dict(cls, d: Mapping[str, Any]): """ Construct an instance of :class:`~.PyProject` from a dictionary. :param d: The dictionary. """ kwargs = {} for key, value in d.items(): if key == "build-system": key = "build_system" kwargs[key] = value return cls(**kwargs) def to_dict(self) -> MutableMapping[str, Any]: """ Returns a dictionary containing the contents of the class. .. seealso:: :func:`attr.asdict` """ return { "build_system": self.build_system, "project": self.project, "tool": self.tool, }