|
| 1 | +"""Pyproject writers for setuptools and rust.""" |
| 2 | +import logging |
| 3 | +from pathlib import Path |
| 4 | +from typing import Any, List, Optional, Union |
| 5 | + |
| 6 | +from rich.pretty import pretty_repr |
| 7 | +from tomlkit import dump, load, table |
| 8 | + |
| 9 | +from somesy.core.models import Person, ProjectMetadata |
| 10 | +from somesy.core.writer import IgnoreKey, ProjectMetadataWriter |
| 11 | + |
| 12 | +from .models import RustConfig, check_keyword |
| 13 | + |
| 14 | +logger = logging.getLogger("somesy") |
| 15 | + |
| 16 | + |
| 17 | +class Rust(ProjectMetadataWriter): |
| 18 | + """Rust config file handler parsed from Cargo.toml.""" |
| 19 | + |
| 20 | + def __init__(self, path: Path, *, direct_mappings=None): |
| 21 | + """Rust config file handler parsed from Cargo.toml. |
| 22 | +
|
| 23 | + See [somesy.core.writer.ProjectMetadataWriter.__init__][]. |
| 24 | + """ |
| 25 | + self._section = ["package"] |
| 26 | + super().__init__( |
| 27 | + path, create_if_not_exists=False, direct_mappings=direct_mappings or {} |
| 28 | + ) |
| 29 | + |
| 30 | + def _load(self) -> None: |
| 31 | + """Load Cargo.toml file.""" |
| 32 | + with open(self.path) as f: |
| 33 | + self._data = load(f) |
| 34 | + |
| 35 | + def _validate(self) -> None: |
| 36 | + """Validate rust config using pydantic class. |
| 37 | +
|
| 38 | + In order to preserve toml comments and structure, tomlkit library is used. |
| 39 | + Pydantic class only used for validation. |
| 40 | + """ |
| 41 | + config = dict(self._get_property([])) |
| 42 | + logger.debug( |
| 43 | + f"Validating config using {RustConfig.__name__}: {pretty_repr(config)}" |
| 44 | + ) |
| 45 | + RustConfig(**config) |
| 46 | + |
| 47 | + def save(self, path: Optional[Path] = None) -> None: |
| 48 | + """Save the Cargo.toml file.""" |
| 49 | + path = path or self.path |
| 50 | + with open(path, "w") as f: |
| 51 | + dump(self._data, f) |
| 52 | + |
| 53 | + def _get_property( |
| 54 | + self, key: Union[str, List[str]], *, remove: bool = False, **kwargs |
| 55 | + ) -> Optional[Any]: |
| 56 | + """Get a property from the Cargo.toml file.""" |
| 57 | + key_path = [key] if isinstance(key, str) else key |
| 58 | + full_path = self._section + key_path |
| 59 | + return super()._get_property(full_path, remove=remove, **kwargs) |
| 60 | + |
| 61 | + def _set_property(self, key: Union[str, List[str], IgnoreKey], value: Any) -> None: |
| 62 | + """Set a property in the Cargo.toml file.""" |
| 63 | + if isinstance(key, IgnoreKey): |
| 64 | + return |
| 65 | + key_path = [key] if isinstance(key, str) else key |
| 66 | + |
| 67 | + if not value: # remove value and clean up the sub-dict |
| 68 | + self._get_property(key_path, remove=True) |
| 69 | + return |
| 70 | + |
| 71 | + # get the tomlkit object of the section |
| 72 | + dat = self._get_property([]) |
| 73 | + |
| 74 | + # dig down, create missing nested objects on the fly |
| 75 | + curr = dat |
| 76 | + for key in key_path[:-1]: |
| 77 | + if key not in curr: |
| 78 | + curr.add(key, table()) |
| 79 | + curr = curr[key] |
| 80 | + curr[key_path[-1]] = value |
| 81 | + |
| 82 | + @staticmethod |
| 83 | + def _from_person(person: Person): |
| 84 | + """Convert project metadata person object to rust string for person format "full name <email>.""" |
| 85 | + return person.to_name_email_string() |
| 86 | + |
| 87 | + @staticmethod |
| 88 | + def _to_person(person_obj: str) -> Person: |
| 89 | + """Parse rust person string to a Person. It has format "full name <email>." but email is optional.""" |
| 90 | + try: |
| 91 | + return Person.from_name_email_string(person_obj) |
| 92 | + except ValueError: |
| 93 | + return None |
| 94 | + |
| 95 | + @classmethod |
| 96 | + def _parse_people(cls, people: Optional[List[Any]]) -> List[Person]: |
| 97 | + """Return a list of Persons parsed from list of format-specific people representations. to_person can return None, so filter out None values.""" |
| 98 | + return list(filter(None, map(cls._to_person, people or []))) |
| 99 | + |
| 100 | + @property |
| 101 | + def keywords(self) -> Optional[List[str]]: |
| 102 | + """Return the keywords of the project.""" |
| 103 | + return self._get_property(self._get_key("keywords")) |
| 104 | + |
| 105 | + @keywords.setter |
| 106 | + def keywords(self, keywords: List[str]) -> None: |
| 107 | + """Set the keywords of the project.""" |
| 108 | + validated_keywords = [] |
| 109 | + for keyword in keywords: |
| 110 | + try: |
| 111 | + check_keyword(keyword) |
| 112 | + validated_keywords.append(keyword) |
| 113 | + except ValueError as e: |
| 114 | + logger.debug(f"Invalid keyword {keyword}: {e}") |
| 115 | + |
| 116 | + # keyword count should max 5, so delete the rest |
| 117 | + if len(validated_keywords) > 5: |
| 118 | + validated_keywords = validated_keywords[:5] |
| 119 | + self._set_property(self._get_key("keywords"), validated_keywords) |
| 120 | + |
| 121 | + def sync(self, metadata: ProjectMetadata) -> None: |
| 122 | + """Sync the rust config with the project metadata.""" |
| 123 | + super().sync(metadata) |
| 124 | + |
| 125 | + # if there is a license file, remove the license field |
| 126 | + if self._get_key("license_file"): |
| 127 | + self.license = None |
0 commit comments