hassle.models API documentation

# hassle.models

View Source

      1import subprocess
      2from dataclasses import asdict, dataclass, field
      3from datetime import datetime
      4from functools import cached_property
      5
      6import black
      7import dacite
      8import isort
      9import requests
     10from bs4 import BeautifulSoup, Tag
     11from pathier import Pathier, Pathish
     12from typing_extensions import Self
     13
     14from hassle import utilities
     15
     16root = Pathier(__file__).parent
     17
     18
     19@dataclass
     20class Sdist:
     21    exclude: list[str]
     22
     23
     24@dataclass
     25class Targets:
     26    sdist: Sdist
     27
     28
     29@dataclass
     30class Build:
     31    targets: Targets
     32
     33
     34@dataclass
     35class BuildSystem:
     36    requires: list[str]
     37    build_backend: str
     38
     39
     40@dataclass
     41class Urls:
     42    Homepage: str = ""
     43    Documentation: str = ""
     44    Source_code: str = ""
     45
     46
     47@dataclass
     48class Author:
     49    name: str = ""
     50    email: str = ""
     51
     52
     53@dataclass
     54class Git:
     55    tag_prefix: str = ""
     56
     57
     58@dataclass
     59class IniOptions:
     60    addopts: list[str]
     61    pythonpath: str
     62
     63
     64@dataclass
     65class Pytest:
     66    ini_options: IniOptions
     67
     68
     69@dataclass
     70class Hatch:
     71    build: Build
     72
     73
     74@dataclass
     75class Tool:
     76    pytest: Pytest
     77    hatch: Hatch
     78
     79
     80@dataclass
     81class Project:
     82    name: str
     83    authors: list[Author] = field(default_factory=list)
     84    description: str = ""
     85    requires_python: str = ""
     86    version: str = ""
     87    dependencies: list[str] = field(default_factory=list)
     88    readme: str = ""
     89    keywords: list[str] = field(default_factory=list)
     90    classifiers: list[str] = field(default_factory=list)
     91    urls: Urls = field(default_factory=Urls)
     92    scripts: dict[str, str] = field(default_factory=dict)
     93
     94
     95@dataclass
     96class Pyproject:
     97    build_system: BuildSystem
     98    project: Project
     99    tool: Tool
    100
    101    @staticmethod
    102    def _swap_keys(data: dict) -> dict:
    103        """Swap between original toml key and valid Python variable."""
    104        if "build-system" in data:
    105            data = utilities.swap_keys(data, ("build-system", "build_system"))
    106            if "build-backend" in data["build_system"]:
    107                data["build_system"] = utilities.swap_keys(
    108                    data["build_system"], ("build-backend", "build_backend")
    109                )
    110        elif "build_system" in data:
    111            data = utilities.swap_keys(data, ("build-system", "build_system"))
    112            if "build_backend" in data["build-system"]:
    113                data["build-system"] = utilities.swap_keys(
    114                    data["build-system"], ("build-backend", "build_backend")
    115                )
    116
    117        if "project" in data and (
    118            "requires-python" in data["project"] or "requires_python"
    119        ):
    120            data["project"] = utilities.swap_keys(
    121                data["project"], ("requires-python", "requires_python")
    122            )
    123        if all(
    124            [
    125                "project" in data,
    126                "urls" in data["project"],
    127                (
    128                    "Source code" in data["project"]["urls"]
    129                    or "Source_code" in data["project"]["urls"]
    130                ),
    131            ]
    132        ):
    133            data["project"]["urls"] = utilities.swap_keys(
    134                data["project"]["urls"], ("Source code", "Source_code")
    135            )
    136
    137        return data
    138
    139    @classmethod
    140    def load(cls, path: Pathish = Pathier("pyproject.toml")) -> Self:
    141        """Return a `datamodel` object populated from `path`."""
    142        data = Pathier(path).loads()
    143        data = cls._swap_keys(data)
    144        return dacite.from_dict(cls, data)
    145
    146    def dump(self, path: Pathish = Pathier("pyproject.toml")):
    147        """Write the contents of this `datamodel` object to `path`."""
    148        data = asdict(self)
    149        data = self._swap_keys(data)
    150        Pathier(path).dumps(data)
    151
    152    @classmethod
    153    def from_template(cls) -> Self:
    154        """Return a `Pyproject` object using `templates/pyproject_template.toml`."""
    155        return cls.load(root / "templates" / "pyproject.toml")
    156
    157
    158@dataclass
    159class HassleConfig:
    160    authors: list[Author] = field(default_factory=list)
    161    project_urls: Urls = field(default_factory=Urls)
    162    git: Git = field(default_factory=Git)
    163
    164    @classmethod
    165    def load(
    166        cls, path: Pathish = Pathier(__file__).parent / "hassle_config.toml"
    167    ) -> Self:
    168        """Return a `datamodel` object populated from `path`."""
    169        path = Pathier(path)
    170        if not path.exists():
    171            raise FileNotFoundError(
    172                f"Could not find hassle config at {path}.\nRun hassle_config in a terminal to set it."
    173            )
    174        data = path.loads()
    175        data["project_urls"] = utilities.swap_keys(
    176            data["project_urls"], ("Source_code", "Source code")
    177        )
    178        return dacite.from_dict(cls, data)
    179
    180    def dump(self, path: Pathish = Pathier(__file__).parent / "hassle_config.toml"):
    181        """Write the contents of this `datamodel` object to `path`."""
    182        data = asdict(self)
    183        data["project_urls"] = utilities.swap_keys(
    184            data["project_urls"], ("Source_code", "Source code")
    185        )
    186        Pathier(path).dumps(data)
    187
    188    @staticmethod
    189    def warn():
    190        print("hassle_config.toml has not been set.")
    191        print("Run hassle_config to set it.")
    192        print("Run 'hassle config -h' for help.")
    193
    194    @staticmethod
    195    def exists(path: Pathish = Pathier(__file__).parent / "hassle_config.toml") -> bool:
    196        return Pathier(path).exists()
    197
    198    @classmethod
    199    def configure(
    200        cls,
    201        name: str | None = None,
    202        email: str | None = None,
    203        github_username: str | None = None,
    204        docs_url: str | None = None,
    205        tag_prefix: str | None = None,
    206        config_path: Pathish = Pathier(__file__).parent / "hassle_config.toml",
    207    ):
    208        """Create or edit `hassle_config.toml` from given params."""
    209        print(f"Manual edits can be made at {config_path}")
    210        if not cls.exists(config_path):
    211            config = cls()
    212        else:
    213            config = cls.load(config_path)
    214        # Add an author to config if a name or email is given.
    215        if name or email:
    216            config.authors.append(Author(name or "", email or ""))
    217        if github_username:
    218            homepage = f"https://github.com/{github_username}/$name"
    219            config.project_urls.Homepage = homepage
    220            config.project_urls.Source_code = f"{homepage}/tree/main/src/$name"
    221        if not config.project_urls.Documentation:
    222            if github_username and not docs_url:
    223                config.project_urls.Documentation = (
    224                    f"https://github.com/{github_username}/$name/tree/main/docs"
    225                )
    226            elif docs_url:
    227                config.project_urls.Documentation = docs_url
    228        if tag_prefix:
    229            config.git.tag_prefix = tag_prefix
    230        config.dump(config_path)
    231
    232
    233@dataclass
    234class HassleProject:
    235    pyproject: Pyproject
    236    projectdir: Pathier
    237    source_files: list[str]
    238    templatedir: Pathier = root / "templates"
    239
    240    @property
    241    def source_code(self) -> str:
    242        """Join and return all code from any `.py` files in `self.srcdir`.
    243
    244        Useful if a tool needs to scan all the source code for something."""
    245        return "\n".join(file.read_text() for file in self.srcdir.rglob("*.py"))
    246
    247    @cached_property
    248    def srcdir(self) -> Pathier:
    249        return self.projectdir / "src" / self.pyproject.project.name
    250
    251    @cached_property
    252    def changelog_path(self) -> Pathier:
    253        return self.projectdir / "CHANGELOG.md"
    254
    255    @cached_property
    256    def pyproject_path(self) -> Pathier:
    257        return self.projectdir / "pyproject.toml"
    258
    259    @cached_property
    260    def docsdir(self) -> Pathier:
    261        return self.projectdir / "docs"
    262
    263    @cached_property
    264    def testsdir(self) -> Pathier:
    265        return self.projectdir / "tests"
    266
    267    @cached_property
    268    def vsdir(self) -> Pathier:
    269        return self.projectdir / ".vscode"
    270
    271    @cached_property
    272    def distdir(self) -> Pathier:
    273        return self.projectdir / "dist"
    274
    275    @property
    276    def name(self) -> str:
    277        """This package's name."""
    278        return self.pyproject.project.name
    279
    280    @property
    281    def version(self) -> str:
    282        """This package's version."""
    283        return self.pyproject.project.version
    284
    285    @version.setter
    286    def version(self, new_version: str):
    287        self.pyproject.project.version = new_version
    288
    289    @classmethod
    290    def load(cls, projectdir: Pathish) -> Self:
    291        """Load a project given `projectdir`."""
    292        projectdir = Pathier(projectdir)
    293        pyproject = Pyproject.load(projectdir / "pyproject.toml")
    294        name = pyproject.project.name
    295        # Convert source files to path stems relative to projectdir/src/name
    296        # e.g `C:/python/projects/hassle/src/hassle/templates/pyproject.toml`
    297        # becomes `templates/pyproject.toml`
    298        source_files = [
    299            str(file.separate(name))
    300            for file in (projectdir / "src" / name).rglob("*")
    301            if file.is_file()
    302        ]
    303        return cls(pyproject, projectdir, source_files)
    304
    305    @classmethod
    306    def new(
    307        cls,
    308        targetdir: Pathier,
    309        name: str,
    310        description: str = "",
    311        dependencies: list[str] = [],
    312        keywords: list[str] = [],
    313        source_files: list[str] = [],
    314        add_script: bool = False,
    315        no_license: bool = False,
    316    ) -> Self:
    317        """Create and return a new hassle project."""
    318        pyproject = Pyproject.from_template()
    319        config = HassleConfig.load()
    320        pyproject.project.name = name
    321        pyproject.project.authors = config.authors
    322        pyproject.project.description = description
    323        pyproject.project.dependencies = dependencies
    324        pyproject.project.keywords = keywords
    325        pyproject.project.urls.Homepage = config.project_urls.Homepage.replace(
    326            "$name", name
    327        )
    328        pyproject.project.urls.Documentation = (
    329            config.project_urls.Documentation.replace("$name", name)
    330        )
    331        pyproject.project.urls.Source_code = config.project_urls.Source_code.replace(
    332            "$name", name
    333        )
    334        hassle = cls(pyproject, targetdir, source_files)
    335        if add_script:
    336            hassle.add_script(name, name)
    337        hassle.generate_files()
    338        if no_license:
    339            hassle.pyproject.project.classifiers.pop(1)
    340            (hassle.projectdir / "LICENSE.txt").delete()
    341        hassle.save()
    342        return hassle
    343
    344    def get_template(self, file_name: str) -> str:
    345        """Open are return the content of `{self.templatedir}/{file_name}`."""
    346        return (self.templatedir / file_name).read_text()
    347
    348    def save(self):
    349        """Dump `self.pyproject` to `{self.projectdir}/pyproject.toml`."""
    350        self.pyproject.dump(self.pyproject_path)
    351
    352    def format_source_files(self):
    353        """Use isort and black to format files"""
    354        for file in self.projectdir.rglob("*.py"):
    355            isort.file(file)
    356        try:
    357            black.main([str(self.projectdir)])
    358        except SystemExit as e:
    359            ...
    360        except Exception as e:
    361            raise e
    362
    363    def latest_version_is_published(self) -> bool:
    364        """Check if the current version of this project has been published to pypi.org."""
    365        pypi_url = f"https://pypi.org/project/{self.name}"
    366        response = requests.get(pypi_url)
    367        if response.status_code != 200:
    368            raise RuntimeError(
    369                f"{pypi_url} returned status code {response.status_code} :/"
    370            )
    371        soup = BeautifulSoup(response.text, "html.parser")
    372        header = soup.find("h1", class_="package-header__name")
    373        assert isinstance(header, Tag)
    374        text = header.text.strip()
    375        pypi_version = text[text.rfind(" ") + 1 :]
    376        return self.version == pypi_version
    377
    378    # ====================================================================================
    379    # Updaters ===========================================================================
    380    # ====================================================================================
    381    def add_script(self, name: str, file_stem: str, function: str = "main"):
    382        """Add a script to `pyproject.project.scripts` in the format `{name} = "{package_name}.{file_stem}:{function}"`"""
    383        self.pyproject.project.scripts[name] = f"{self.name}.{file_stem}:{function}"
    384
    385    def update_init_version(self):
    386        """Update the `__version__` in this projects `__init__.py` file
    387        to the current value of `self.pyproject.project.version`
    388        if it exists and has a `__version__` string.
    389
    390        If it doesn't have a `__version__` string, append one to it."""
    391        init_file = self.srcdir / "__init__.py"
    392        version = f'__version__ = "{self.version}"'
    393        if init_file.exists():
    394            content = init_file.read_text()
    395            if "__version__" in content:
    396                lines = content.splitlines()
    397                for i, line in enumerate(lines):
    398                    if line.startswith("__version__"):
    399                        lines[i] = version
    400                content = "\n".join(lines)
    401            else:
    402                content += f"\n{version}"
    403            init_file.write_text(content)
    404
    405    def bump_version(self, bump_type: str):
    406        """Bump the version of this project.
    407
    408        `bump_type` should be `major`, `minor`, or `patch`."""
    409        # bump pyproject version
    410        self.version = utilities.bump_version(self.version, bump_type)
    411        # bump `__version__` in __init__.py if the file exists and has a `__version__`.
    412        self.update_init_version()
    413
    414    def update_dependencies(
    415        self, overwrite_existing_packages: bool, include_versions: bool
    416    ):
    417        """Scan project for dependencies and update the corresponding field in the pyproject model."""
    418        dependencies = utilities.get_dependencies(self.projectdir)
    419        if overwrite_existing_packages:
    420            self.pyproject.project.dependencies = [
    421                utilities.format_dependency(dependency, include_versions)
    422                for dependency in dependencies
    423                if dependency[0] != self.name
    424            ]
    425        else:
    426            for dependency in dependencies:
    427                if (
    428                    all(
    429                        dependency[0] not in existing_dependency
    430                        for existing_dependency in self.pyproject.project.dependencies
    431                    )
    432                    and dependency[0] != self.name
    433                ):
    434                    self.pyproject.project.dependencies.append(
    435                        utilities.format_dependency(dependency, include_versions)
    436                    )
    437
    438    def _generate_changelog(self) -> list[str]:
    439        if HassleConfig.exists():
    440            tag_prefix = HassleConfig.load().git.tag_prefix
    441        else:
    442            HassleConfig.warn()
    443            print("Assuming no tag prefix.")
    444            tag_prefix = ""
    445        raw_changelog = [
    446            line
    447            for line in subprocess.run(
    448                [
    449                    "auto-changelog",
    450                    "-p",
    451                    self.projectdir,
    452                    "--tag-prefix",
    453                    tag_prefix,
    454                    "--stdout",
    455                ],
    456                stdout=subprocess.PIPE,
    457                text=True,
    458            ).stdout.splitlines(True)
    459            if not line.startswith(
    460                (
    461                    "Full set of changes:",
    462                    f"* build {tag_prefix}",
    463                    "* update changelog",
    464                )
    465            )
    466        ]
    467        return raw_changelog
    468
    469    def update_changelog(self):
    470        """Update `CHANGELOG.md` by invoking the `auto-changelog` module.
    471
    472        If `hassle_config.toml` doesn't exist, an empty tag prefix will be assumed."""
    473        raw_changelog = self._generate_changelog()
    474        # If there's no existing changelog, dump the generated one and get out of here.
    475        if not self.changelog_path.exists():
    476            self.changelog_path.write_text(raw_changelog)
    477            return
    478
    479        # Don't want to overwrite previously existing manual changes/edits
    480        existing_changelog = self.changelog_path.read_text().splitlines(True)[
    481            2:
    482        ]  # First two elements are "# Changelog\n" and "\n"
    483        new_changes = raw_changelog
    484        for line in existing_changelog:
    485            # Release headers are prefixed with "## "
    486            if line.startswith("## "):
    487                new_changes = raw_changelog[: raw_changelog.index(line)]
    488                break
    489        changes = "".join(new_changes)
    490        # If changes == "# Changelog\n\n" then there weren't actually any new changes
    491        if not changes == "# Changelog\n\n":
    492            self.changelog_path.write_text(changes + "".join(existing_changelog))
    493
    494    # ====================================================================================
    495    # File/Project creation ==============================================================
    496    # ====================================================================================
    497
    498    def create_source_files(self):
    499        """Generate source files in `self.srcdir`."""
    500        for file in self.source_files:
    501            (self.srcdir / file).touch()
    502        init = self.srcdir / "__init__.py"
    503        if init.exists():
    504            init.append(f'__version__ = "{self.version}"')
    505
    506    def create_readme(self):
    507        readme = self.get_template("README.md")
    508        readme = readme.replace("$name", self.name)
    509        readme = readme.replace("$description", self.pyproject.project.description)
    510        (self.projectdir / "README.md").write_text(readme)
    511
    512    def create_license(self):
    513        license_ = self.get_template("license.txt")
    514        license_ = license_.replace("$year", str(datetime.now().year))
    515        (self.projectdir / "LICENSE.txt").write_text(license_)
    516
    517    def create_gitignore(self):
    518        (self.templatedir / ".gitignore.txt").copy(self.projectdir / ".gitignore")
    519
    520    def create_vscode_settings(self):
    521        self.vsdir.mkdir()
    522        (self.templatedir / "vscode_settings.json").copy(self.vsdir / "settings.json")
    523
    524    def create_tests(self):
    525        (self.testsdir / f"test_{self.name}.py").touch()
    526
    527    def generate_files(self):
    528        """Create all the necessary files.
    529
    530        Note: This will overwrite any existing files."""
    531        self.projectdir.mkdir()
    532        for func in dir(self):
    533            if func.startswith("create_"):
    534                getattr(self, func)()
    535        self.pyproject.dump(self.pyproject_path)
    536
    537    def generate_docs(self):
    538        """Generate docs by invoking `pdoc`"""
    539        self.docsdir.delete()
    540        subprocess.run(["pdoc", "-o", self.docsdir, self.srcdir])

@dataclass
class Sdist: View Source

    20@dataclass
    21class Sdist:
    22    exclude: list[str]

Sdist(exclude: list[str])
@dataclass
class Targets: View Source

    25@dataclass
    26class Targets:
    27    sdist: Sdist

Targets(sdist: hassle.models.Sdist)
@dataclass
class Build: View Source

    30@dataclass
    31class Build:
    32    targets: Targets

Build(targets: hassle.models.Targets)
@dataclass
class BuildSystem: View Source

    35@dataclass
    36class BuildSystem:
    37    requires: list[str]
    38    build_backend: str

BuildSystem(requires: list[str], build_backend: str)
@dataclass
class Urls: View Source

    41@dataclass
    42class Urls:
    43    Homepage: str = ""
    44    Documentation: str = ""
    45    Source_code: str = ""

Urls(Homepage: str = '', Documentation: str = '', Source_code: str = '')
@dataclass
class Author: View Source

    48@dataclass
    49class Author:
    50    name: str = ""
    51    email: str = ""

Author(name: str = '', email: str = '')
@dataclass
class Git: View Source

    54@dataclass
    55class Git:
    56    tag_prefix: str = ""

Git(tag_prefix: str = '')
@dataclass
class IniOptions: View Source

    59@dataclass
    60class IniOptions:
    61    addopts: list[str]
    62    pythonpath: str

IniOptions(addopts: list[str], pythonpath: str)
@dataclass
class Pytest: View Source

    65@dataclass
    66class Pytest:
    67    ini_options: IniOptions

Pytest(ini_options: hassle.models.IniOptions)
@dataclass
class Hatch: View Source

    70@dataclass
    71class Hatch:
    72    build: Build

Hatch(build: hassle.models.Build)
@dataclass
class Tool: View Source

    75@dataclass
    76class Tool:
    77    pytest: Pytest
    78    hatch: Hatch

Tool(pytest: hassle.models.Pytest, hatch: hassle.models.Hatch)
@dataclass
class Project: View Source

    81@dataclass
    82class Project:
    83    name: str
    84    authors: list[Author] = field(default_factory=list)
    85    description: str = ""
    86    requires_python: str = ""
    87    version: str = ""
    88    dependencies: list[str] = field(default_factory=list)
    89    readme: str = ""
    90    keywords: list[str] = field(default_factory=list)
    91    classifiers: list[str] = field(default_factory=list)
    92    urls: Urls = field(default_factory=Urls)
    93    scripts: dict[str, str] = field(default_factory=dict)

Project( name: str, authors: list[hassle.models.Author] = <factory>, description: str = '', requires_python: str = '', version: str = '', dependencies: list[str] = <factory>, readme: str = '', keywords: list[str] = <factory>, classifiers: list[str] = <factory>, urls: hassle.models.Urls = <factory>, scripts: dict[str, str] = <factory>)
@dataclass
class Pyproject: View Source

     96@dataclass
     97class Pyproject:
     98    build_system: BuildSystem
     99    project: Project
    100    tool: Tool
    101
    102    @staticmethod
    103    def _swap_keys(data: dict) -> dict:
    104        """Swap between original toml key and valid Python variable."""
    105        if "build-system" in data:
    106            data = utilities.swap_keys(data, ("build-system", "build_system"))
    107            if "build-backend" in data["build_system"]:
    108                data["build_system"] = utilities.swap_keys(
    109                    data["build_system"], ("build-backend", "build_backend")
    110                )
    111        elif "build_system" in data:
    112            data = utilities.swap_keys(data, ("build-system", "build_system"))
    113            if "build_backend" in data["build-system"]:
    114                data["build-system"] = utilities.swap_keys(
    115                    data["build-system"], ("build-backend", "build_backend")
    116                )
    117
    118        if "project" in data and (
    119            "requires-python" in data["project"] or "requires_python"
    120        ):
    121            data["project"] = utilities.swap_keys(
    122                data["project"], ("requires-python", "requires_python")
    123            )
    124        if all(
    125            [
    126                "project" in data,
    127                "urls" in data["project"],
    128                (
    129                    "Source code" in data["project"]["urls"]
    130                    or "Source_code" in data["project"]["urls"]
    131                ),
    132            ]
    133        ):
    134            data["project"]["urls"] = utilities.swap_keys(
    135                data["project"]["urls"], ("Source code", "Source_code")
    136            )
    137
    138        return data
    139
    140    @classmethod
    141    def load(cls, path: Pathish = Pathier("pyproject.toml")) -> Self:
    142        """Return a `datamodel` object populated from `path`."""
    143        data = Pathier(path).loads()
    144        data = cls._swap_keys(data)
    145        return dacite.from_dict(cls, data)
    146
    147    def dump(self, path: Pathish = Pathier("pyproject.toml")):
    148        """Write the contents of this `datamodel` object to `path`."""
    149        data = asdict(self)
    150        data = self._swap_keys(data)
    151        Pathier(path).dumps(data)
    152
    153    @classmethod
    154    def from_template(cls) -> Self:
    155        """Return a `Pyproject` object using `templates/pyproject_template.toml`."""
    156        return cls.load(root / "templates" / "pyproject.toml")

Pyproject( build_system: hassle.models.BuildSystem, project: hassle.models.Project, tool: hassle.models.Tool)
@classmethod
def load( cls, path: pathier.pathier.Pathier | pathlib.Path | str = WindowsPath('pyproject.toml')) -> Self: View Source

    140    @classmethod
    141    def load(cls, path: Pathish = Pathier("pyproject.toml")) -> Self:
    142        """Return a `datamodel` object populated from `path`."""
    143        data = Pathier(path).loads()
    144        data = cls._swap_keys(data)
    145        return dacite.from_dict(cls, data)

Return a `datamodel` object populated from `path`.

def dump( self, path: pathier.pathier.Pathier | pathlib.Path | str = WindowsPath('pyproject.toml')): View Source

    147    def dump(self, path: Pathish = Pathier("pyproject.toml")):
    148        """Write the contents of this `datamodel` object to `path`."""
    149        data = asdict(self)
    150        data = self._swap_keys(data)
    151        Pathier(path).dumps(data)

Write the contents of this `datamodel` object to `path`.

@classmethod
def from_template(cls) -> Self: View Source

    153    @classmethod
    154    def from_template(cls) -> Self:
    155        """Return a `Pyproject` object using `templates/pyproject_template.toml`."""
    156        return cls.load(root / "templates" / "pyproject.toml")

Return a `Pyproject` object using `templates/pyproject_template.toml`.

@dataclass
class HassleConfig: View Source

    159@dataclass
    160class HassleConfig:
    161    authors: list[Author] = field(default_factory=list)
    162    project_urls: Urls = field(default_factory=Urls)
    163    git: Git = field(default_factory=Git)
    164
    165    @classmethod
    166    def load(
    167        cls, path: Pathish = Pathier(__file__).parent / "hassle_config.toml"
    168    ) -> Self:
    169        """Return a `datamodel` object populated from `path`."""
    170        path = Pathier(path)
    171        if not path.exists():
    172            raise FileNotFoundError(
    173                f"Could not find hassle config at {path}.\nRun hassle_config in a terminal to set it."
    174            )
    175        data = path.loads()
    176        data["project_urls"] = utilities.swap_keys(
    177            data["project_urls"], ("Source_code", "Source code")
    178        )
    179        return dacite.from_dict(cls, data)
    180
    181    def dump(self, path: Pathish = Pathier(__file__).parent / "hassle_config.toml"):
    182        """Write the contents of this `datamodel` object to `path`."""
    183        data = asdict(self)
    184        data["project_urls"] = utilities.swap_keys(
    185            data["project_urls"], ("Source_code", "Source code")
    186        )
    187        Pathier(path).dumps(data)
    188
    189    @staticmethod
    190    def warn():
    191        print("hassle_config.toml has not been set.")
    192        print("Run hassle_config to set it.")
    193        print("Run 'hassle config -h' for help.")
    194
    195    @staticmethod
    196    def exists(path: Pathish = Pathier(__file__).parent / "hassle_config.toml") -> bool:
    197        return Pathier(path).exists()
    198
    199    @classmethod
    200    def configure(
    201        cls,
    202        name: str | None = None,
    203        email: str | None = None,
    204        github_username: str | None = None,
    205        docs_url: str | None = None,
    206        tag_prefix: str | None = None,
    207        config_path: Pathish = Pathier(__file__).parent / "hassle_config.toml",
    208    ):
    209        """Create or edit `hassle_config.toml` from given params."""
    210        print(f"Manual edits can be made at {config_path}")
    211        if not cls.exists(config_path):
    212            config = cls()
    213        else:
    214            config = cls.load(config_path)
    215        # Add an author to config if a name or email is given.
    216        if name or email:
    217            config.authors.append(Author(name or "", email or ""))
    218        if github_username:
    219            homepage = f"https://github.com/{github_username}/$name"
    220            config.project_urls.Homepage = homepage
    221            config.project_urls.Source_code = f"{homepage}/tree/main/src/$name"
    222        if not config.project_urls.Documentation:
    223            if github_username and not docs_url:
    224                config.project_urls.Documentation = (
    225                    f"https://github.com/{github_username}/$name/tree/main/docs"
    226                )
    227            elif docs_url:
    228                config.project_urls.Documentation = docs_url
    229        if tag_prefix:
    230            config.git.tag_prefix = tag_prefix
    231        config.dump(config_path)

HassleConfig( authors: list[hassle.models.Author] = <factory>, project_urls: hassle.models.Urls = <factory>, git: hassle.models.Git = <factory>)
@classmethod
def load( cls, path: pathier.pathier.Pathier | pathlib.Path | str = WindowsPath('E:/1vsCode/python/hassle/src/hassle/hassle_config.toml')) -> Self: View Source

    165    @classmethod
    166    def load(
    167        cls, path: Pathish = Pathier(__file__).parent / "hassle_config.toml"
    168    ) -> Self:
    169        """Return a `datamodel` object populated from `path`."""
    170        path = Pathier(path)
    171        if not path.exists():
    172            raise FileNotFoundError(
    173                f"Could not find hassle config at {path}.\nRun hassle_config in a terminal to set it."
    174            )
    175        data = path.loads()
    176        data["project_urls"] = utilities.swap_keys(
    177            data["project_urls"], ("Source_code", "Source code")
    178        )
    179        return dacite.from_dict(cls, data)

Return a `datamodel` object populated from `path`.

def dump( self, path: pathier.pathier.Pathier | pathlib.Path | str = WindowsPath('E:/1vsCode/python/hassle/src/hassle/hassle_config.toml')): View Source

    181    def dump(self, path: Pathish = Pathier(__file__).parent / "hassle_config.toml"):
    182        """Write the contents of this `datamodel` object to `path`."""
    183        data = asdict(self)
    184        data["project_urls"] = utilities.swap_keys(
    185            data["project_urls"], ("Source_code", "Source code")
    186        )
    187        Pathier(path).dumps(data)

Write the contents of this `datamodel` object to `path`.

@staticmethod
def warn(): View Source

    189    @staticmethod
    190    def warn():
    191        print("hassle_config.toml has not been set.")
    192        print("Run hassle_config to set it.")
    193        print("Run 'hassle config -h' for help.")

@staticmethod
def exists( path: pathier.pathier.Pathier | pathlib.Path | str = WindowsPath('E:/1vsCode/python/hassle/src/hassle/hassle_config.toml')) -> bool: View Source

    195    @staticmethod
    196    def exists(path: Pathish = Pathier(__file__).parent / "hassle_config.toml") -> bool:
    197        return Pathier(path).exists()

@classmethod
def configure( cls, name: str | None = None, email: str | None = None, github_username: str | None = None, docs_url: str | None = None, tag_prefix: str | None = None, config_path: pathier.pathier.Pathier | pathlib.Path | str = WindowsPath('E:/1vsCode/python/hassle/src/hassle/hassle_config.toml')): View Source

    199    @classmethod
    200    def configure(
    201        cls,
    202        name: str | None = None,
    203        email: str | None = None,
    204        github_username: str | None = None,
    205        docs_url: str | None = None,
    206        tag_prefix: str | None = None,
    207        config_path: Pathish = Pathier(__file__).parent / "hassle_config.toml",
    208    ):
    209        """Create or edit `hassle_config.toml` from given params."""
    210        print(f"Manual edits can be made at {config_path}")
    211        if not cls.exists(config_path):
    212            config = cls()
    213        else:
    214            config = cls.load(config_path)
    215        # Add an author to config if a name or email is given.
    216        if name or email:
    217            config.authors.append(Author(name or "", email or ""))
    218        if github_username:
    219            homepage = f"https://github.com/{github_username}/$name"
    220            config.project_urls.Homepage = homepage
    221            config.project_urls.Source_code = f"{homepage}/tree/main/src/$name"
    222        if not config.project_urls.Documentation:
    223            if github_username and not docs_url:
    224                config.project_urls.Documentation = (
    225                    f"https://github.com/{github_username}/$name/tree/main/docs"
    226                )
    227            elif docs_url:
    228                config.project_urls.Documentation = docs_url
    229        if tag_prefix:
    230            config.git.tag_prefix = tag_prefix
    231        config.dump(config_path)

Create or edit `hassle_config.toml` from given params.

@dataclass
class HassleProject: View Source

    234@dataclass
    235class HassleProject:
    236    pyproject: Pyproject
    237    projectdir: Pathier
    238    source_files: list[str]
    239    templatedir: Pathier = root / "templates"
    240
    241    @property
    242    def source_code(self) -> str:
    243        """Join and return all code from any `.py` files in `self.srcdir`.
    244
    245        Useful if a tool needs to scan all the source code for something."""
    246        return "\n".join(file.read_text() for file in self.srcdir.rglob("*.py"))
    247
    248    @cached_property
    249    def srcdir(self) -> Pathier:
    250        return self.projectdir / "src" / self.pyproject.project.name
    251
    252    @cached_property
    253    def changelog_path(self) -> Pathier:
    254        return self.projectdir / "CHANGELOG.md"
    255
    256    @cached_property
    257    def pyproject_path(self) -> Pathier:
    258        return self.projectdir / "pyproject.toml"
    259
    260    @cached_property
    261    def docsdir(self) -> Pathier:
    262        return self.projectdir / "docs"
    263
    264    @cached_property
    265    def testsdir(self) -> Pathier:
    266        return self.projectdir / "tests"
    267
    268    @cached_property
    269    def vsdir(self) -> Pathier:
    270        return self.projectdir / ".vscode"
    271
    272    @cached_property
    273    def distdir(self) -> Pathier:
    274        return self.projectdir / "dist"
    275
    276    @property
    277    def name(self) -> str:
    278        """This package's name."""
    279        return self.pyproject.project.name
    280
    281    @property
    282    def version(self) -> str:
    283        """This package's version."""
    284        return self.pyproject.project.version
    285
    286    @version.setter
    287    def version(self, new_version: str):
    288        self.pyproject.project.version = new_version
    289
    290    @classmethod
    291    def load(cls, projectdir: Pathish) -> Self:
    292        """Load a project given `projectdir`."""
    293        projectdir = Pathier(projectdir)
    294        pyproject = Pyproject.load(projectdir / "pyproject.toml")
    295        name = pyproject.project.name
    296        # Convert source files to path stems relative to projectdir/src/name
    297        # e.g `C:/python/projects/hassle/src/hassle/templates/pyproject.toml`
    298        # becomes `templates/pyproject.toml`
    299        source_files = [
    300            str(file.separate(name))
    301            for file in (projectdir / "src" / name).rglob("*")
    302            if file.is_file()
    303        ]
    304        return cls(pyproject, projectdir, source_files)
    305
    306    @classmethod
    307    def new(
    308        cls,
    309        targetdir: Pathier,
    310        name: str,
    311        description: str = "",
    312        dependencies: list[str] = [],
    313        keywords: list[str] = [],
    314        source_files: list[str] = [],
    315        add_script: bool = False,
    316        no_license: bool = False,
    317    ) -> Self:
    318        """Create and return a new hassle project."""
    319        pyproject = Pyproject.from_template()
    320        config = HassleConfig.load()
    321        pyproject.project.name = name
    322        pyproject.project.authors = config.authors
    323        pyproject.project.description = description
    324        pyproject.project.dependencies = dependencies
    325        pyproject.project.keywords = keywords
    326        pyproject.project.urls.Homepage = config.project_urls.Homepage.replace(
    327            "$name", name
    328        )
    329        pyproject.project.urls.Documentation = (
    330            config.project_urls.Documentation.replace("$name", name)
    331        )
    332        pyproject.project.urls.Source_code = config.project_urls.Source_code.replace(
    333            "$name", name
    334        )
    335        hassle = cls(pyproject, targetdir, source_files)
    336        if add_script:
    337            hassle.add_script(name, name)
    338        hassle.generate_files()
    339        if no_license:
    340            hassle.pyproject.project.classifiers.pop(1)
    341            (hassle.projectdir / "LICENSE.txt").delete()
    342        hassle.save()
    343        return hassle
    344
    345    def get_template(self, file_name: str) -> str:
    346        """Open are return the content of `{self.templatedir}/{file_name}`."""
    347        return (self.templatedir / file_name).read_text()
    348
    349    def save(self):
    350        """Dump `self.pyproject` to `{self.projectdir}/pyproject.toml`."""
    351        self.pyproject.dump(self.pyproject_path)
    352
    353    def format_source_files(self):
    354        """Use isort and black to format files"""
    355        for file in self.projectdir.rglob("*.py"):
    356            isort.file(file)
    357        try:
    358            black.main([str(self.projectdir)])
    359        except SystemExit as e:
    360            ...
    361        except Exception as e:
    362            raise e
    363
    364    def latest_version_is_published(self) -> bool:
    365        """Check if the current version of this project has been published to pypi.org."""
    366        pypi_url = f"https://pypi.org/project/{self.name}"
    367        response = requests.get(pypi_url)
    368        if response.status_code != 200:
    369            raise RuntimeError(
    370                f"{pypi_url} returned status code {response.status_code} :/"
    371            )
    372        soup = BeautifulSoup(response.text, "html.parser")
    373        header = soup.find("h1", class_="package-header__name")
    374        assert isinstance(header, Tag)
    375        text = header.text.strip()
    376        pypi_version = text[text.rfind(" ") + 1 :]
    377        return self.version == pypi_version
    378
    379    # ====================================================================================
    380    # Updaters ===========================================================================
    381    # ====================================================================================
    382    def add_script(self, name: str, file_stem: str, function: str = "main"):
    383        """Add a script to `pyproject.project.scripts` in the format `{name} = "{package_name}.{file_stem}:{function}"`"""
    384        self.pyproject.project.scripts[name] = f"{self.name}.{file_stem}:{function}"
    385
    386    def update_init_version(self):
    387        """Update the `__version__` in this projects `__init__.py` file
    388        to the current value of `self.pyproject.project.version`
    389        if it exists and has a `__version__` string.
    390
    391        If it doesn't have a `__version__` string, append one to it."""
    392        init_file = self.srcdir / "__init__.py"
    393        version = f'__version__ = "{self.version}"'
    394        if init_file.exists():
    395            content = init_file.read_text()
    396            if "__version__" in content:
    397                lines = content.splitlines()
    398                for i, line in enumerate(lines):
    399                    if line.startswith("__version__"):
    400                        lines[i] = version
    401                content = "\n".join(lines)
    402            else:
    403                content += f"\n{version}"
    404            init_file.write_text(content)
    405
    406    def bump_version(self, bump_type: str):
    407        """Bump the version of this project.
    408
    409        `bump_type` should be `major`, `minor`, or `patch`."""
    410        # bump pyproject version
    411        self.version = utilities.bump_version(self.version, bump_type)
    412        # bump `__version__` in __init__.py if the file exists and has a `__version__`.
    413        self.update_init_version()
    414
    415    def update_dependencies(
    416        self, overwrite_existing_packages: bool, include_versions: bool
    417    ):
    418        """Scan project for dependencies and update the corresponding field in the pyproject model."""
    419        dependencies = utilities.get_dependencies(self.projectdir)
    420        if overwrite_existing_packages:
    421            self.pyproject.project.dependencies = [
    422                utilities.format_dependency(dependency, include_versions)
    423                for dependency in dependencies
    424                if dependency[0] != self.name
    425            ]
    426        else:
    427            for dependency in dependencies:
    428                if (
    429                    all(
    430                        dependency[0] not in existing_dependency
    431                        for existing_dependency in self.pyproject.project.dependencies
    432                    )
    433                    and dependency[0] != self.name
    434                ):
    435                    self.pyproject.project.dependencies.append(
    436                        utilities.format_dependency(dependency, include_versions)
    437                    )
    438
    439    def _generate_changelog(self) -> list[str]:
    440        if HassleConfig.exists():
    441            tag_prefix = HassleConfig.load().git.tag_prefix
    442        else:
    443            HassleConfig.warn()
    444            print("Assuming no tag prefix.")
    445            tag_prefix = ""
    446        raw_changelog = [
    447            line
    448            for line in subprocess.run(
    449                [
    450                    "auto-changelog",
    451                    "-p",
    452                    self.projectdir,
    453                    "--tag-prefix",
    454                    tag_prefix,
    455                    "--stdout",
    456                ],
    457                stdout=subprocess.PIPE,
    458                text=True,
    459            ).stdout.splitlines(True)
    460            if not line.startswith(
    461                (
    462                    "Full set of changes:",
    463                    f"* build {tag_prefix}",
    464                    "* update changelog",
    465                )
    466            )
    467        ]
    468        return raw_changelog
    469
    470    def update_changelog(self):
    471        """Update `CHANGELOG.md` by invoking the `auto-changelog` module.
    472
    473        If `hassle_config.toml` doesn't exist, an empty tag prefix will be assumed."""
    474        raw_changelog = self._generate_changelog()
    475        # If there's no existing changelog, dump the generated one and get out of here.
    476        if not self.changelog_path.exists():
    477            self.changelog_path.write_text(raw_changelog)
    478            return
    479
    480        # Don't want to overwrite previously existing manual changes/edits
    481        existing_changelog = self.changelog_path.read_text().splitlines(True)[
    482            2:
    483        ]  # First two elements are "# Changelog\n" and "\n"
    484        new_changes = raw_changelog
    485        for line in existing_changelog:
    486            # Release headers are prefixed with "## "
    487            if line.startswith("## "):
    488                new_changes = raw_changelog[: raw_changelog.index(line)]
    489                break
    490        changes = "".join(new_changes)
    491        # If changes == "# Changelog\n\n" then there weren't actually any new changes
    492        if not changes == "# Changelog\n\n":
    493            self.changelog_path.write_text(changes + "".join(existing_changelog))
    494
    495    # ====================================================================================
    496    # File/Project creation ==============================================================
    497    # ====================================================================================
    498
    499    def create_source_files(self):
    500        """Generate source files in `self.srcdir`."""
    501        for file in self.source_files:
    502            (self.srcdir / file).touch()
    503        init = self.srcdir / "__init__.py"
    504        if init.exists():
    505            init.append(f'__version__ = "{self.version}"')
    506
    507    def create_readme(self):
    508        readme = self.get_template("README.md")
    509        readme = readme.replace("$name", self.name)
    510        readme = readme.replace("$description", self.pyproject.project.description)
    511        (self.projectdir / "README.md").write_text(readme)
    512
    513    def create_license(self):
    514        license_ = self.get_template("license.txt")
    515        license_ = license_.replace("$year", str(datetime.now().year))
    516        (self.projectdir / "LICENSE.txt").write_text(license_)
    517
    518    def create_gitignore(self):
    519        (self.templatedir / ".gitignore.txt").copy(self.projectdir / ".gitignore")
    520
    521    def create_vscode_settings(self):
    522        self.vsdir.mkdir()
    523        (self.templatedir / "vscode_settings.json").copy(self.vsdir / "settings.json")
    524
    525    def create_tests(self):
    526        (self.testsdir / f"test_{self.name}.py").touch()
    527
    528    def generate_files(self):
    529        """Create all the necessary files.
    530
    531        Note: This will overwrite any existing files."""
    532        self.projectdir.mkdir()
    533        for func in dir(self):
    534            if func.startswith("create_"):
    535                getattr(self, func)()
    536        self.pyproject.dump(self.pyproject_path)
    537
    538    def generate_docs(self):
    539        """Generate docs by invoking `pdoc`"""
    540        self.docsdir.delete()
    541        subprocess.run(["pdoc", "-o", self.docsdir, self.srcdir])

HassleProject( pyproject: hassle.models.Pyproject, projectdir: pathier.pathier.Pathier, source_files: list[str], templatedir: pathier.pathier.Pathier = WindowsPath('E:/1vsCode/python/hassle/src/hassle/templates'))
source_code: str

Join and return all code from any `.py` files in `self.srcdir`.

Useful if a tool needs to scan all the source code for something.

name: str

This package's name.

version: str

This package's version.

@classmethod
def load(cls, projectdir: pathier.pathier.Pathier | pathlib.Path | str) -> Self: View Source

    290    @classmethod
    291    def load(cls, projectdir: Pathish) -> Self:
    292        """Load a project given `projectdir`."""
    293        projectdir = Pathier(projectdir)
    294        pyproject = Pyproject.load(projectdir / "pyproject.toml")
    295        name = pyproject.project.name
    296        # Convert source files to path stems relative to projectdir/src/name
    297        # e.g `C:/python/projects/hassle/src/hassle/templates/pyproject.toml`
    298        # becomes `templates/pyproject.toml`
    299        source_files = [
    300            str(file.separate(name))
    301            for file in (projectdir / "src" / name).rglob("*")
    302            if file.is_file()
    303        ]
    304        return cls(pyproject, projectdir, source_files)

Load a project given `projectdir`.

@classmethod
def new( cls, targetdir: pathier.pathier.Pathier, name: str, description: str = '', dependencies: list[str] = [], keywords: list[str] = [], source_files: list[str] = [], add_script: bool = False, no_license: bool = False) -> Self: View Source

    306    @classmethod
    307    def new(
    308        cls,
    309        targetdir: Pathier,
    310        name: str,
    311        description: str = "",
    312        dependencies: list[str] = [],
    313        keywords: list[str] = [],
    314        source_files: list[str] = [],
    315        add_script: bool = False,
    316        no_license: bool = False,
    317    ) -> Self:
    318        """Create and return a new hassle project."""
    319        pyproject = Pyproject.from_template()
    320        config = HassleConfig.load()
    321        pyproject.project.name = name
    322        pyproject.project.authors = config.authors
    323        pyproject.project.description = description
    324        pyproject.project.dependencies = dependencies
    325        pyproject.project.keywords = keywords
    326        pyproject.project.urls.Homepage = config.project_urls.Homepage.replace(
    327            "$name", name
    328        )
    329        pyproject.project.urls.Documentation = (
    330            config.project_urls.Documentation.replace("$name", name)
    331        )
    332        pyproject.project.urls.Source_code = config.project_urls.Source_code.replace(
    333            "$name", name
    334        )
    335        hassle = cls(pyproject, targetdir, source_files)
    336        if add_script:
    337            hassle.add_script(name, name)
    338        hassle.generate_files()
    339        if no_license:
    340            hassle.pyproject.project.classifiers.pop(1)
    341            (hassle.projectdir / "LICENSE.txt").delete()
    342        hassle.save()
    343        return hassle

Create and return a new hassle project.

def get_template(self, file_name: str) -> str: View Source

    345    def get_template(self, file_name: str) -> str:
    346        """Open are return the content of `{self.templatedir}/{file_name}`."""
    347        return (self.templatedir / file_name).read_text()

Open are return the content of `{self.templatedir}/{file_name}`.

def save(self): View Source

    349    def save(self):
    350        """Dump `self.pyproject` to `{self.projectdir}/pyproject.toml`."""
    351        self.pyproject.dump(self.pyproject_path)

Dump `self.pyproject` to `{self.projectdir}/pyproject.toml`.

def format_source_files(self): View Source

    353    def format_source_files(self):
    354        """Use isort and black to format files"""
    355        for file in self.projectdir.rglob("*.py"):
    356            isort.file(file)
    357        try:
    358            black.main([str(self.projectdir)])
    359        except SystemExit as e:
    360            ...
    361        except Exception as e:
    362            raise e

Use isort and black to format files

def latest_version_is_published(self) -> bool: View Source

    364    def latest_version_is_published(self) -> bool:
    365        """Check if the current version of this project has been published to pypi.org."""
    366        pypi_url = f"https://pypi.org/project/{self.name}"
    367        response = requests.get(pypi_url)
    368        if response.status_code != 200:
    369            raise RuntimeError(
    370                f"{pypi_url} returned status code {response.status_code} :/"
    371            )
    372        soup = BeautifulSoup(response.text, "html.parser")
    373        header = soup.find("h1", class_="package-header__name")
    374        assert isinstance(header, Tag)
    375        text = header.text.strip()
    376        pypi_version = text[text.rfind(" ") + 1 :]
    377        return self.version == pypi_version

Check if the current version of this project has been published to pypi.org.

def add_script(self, name: str, file_stem: str, function: str = 'main'): View Source

    382    def add_script(self, name: str, file_stem: str, function: str = "main"):
    383        """Add a script to `pyproject.project.scripts` in the format `{name} = "{package_name}.{file_stem}:{function}"`"""
    384        self.pyproject.project.scripts[name] = f"{self.name}.{file_stem}:{function}"

Add a script to `pyproject.project.scripts` in the format `{name} = "{package_name}.{file_stem}:{function}"`

def update_init_version(self): View Source

    386    def update_init_version(self):
    387        """Update the `__version__` in this projects `__init__.py` file
    388        to the current value of `self.pyproject.project.version`
    389        if it exists and has a `__version__` string.
    390
    391        If it doesn't have a `__version__` string, append one to it."""
    392        init_file = self.srcdir / "__init__.py"
    393        version = f'__version__ = "{self.version}"'
    394        if init_file.exists():
    395            content = init_file.read_text()
    396            if "__version__" in content:
    397                lines = content.splitlines()
    398                for i, line in enumerate(lines):
    399                    if line.startswith("__version__"):
    400                        lines[i] = version
    401                content = "\n".join(lines)
    402            else:
    403                content += f"\n{version}"
    404            init_file.write_text(content)

Update the `__version__` in this projects `__init__.py` file to the current value of `self.pyproject.project.version` if it exists and has a `__version__` string.

If it doesn't have a `__version__` string, append one to it.

def bump_version(self, bump_type: str): View Source

    406    def bump_version(self, bump_type: str):
    407        """Bump the version of this project.
    408
    409        `bump_type` should be `major`, `minor`, or `patch`."""
    410        # bump pyproject version
    411        self.version = utilities.bump_version(self.version, bump_type)
    412        # bump `__version__` in __init__.py if the file exists and has a `__version__`.
    413        self.update_init_version()

Bump the version of this project.

`bump_type` should be `major`, `minor`, or `patch`.

def update_dependencies(self, overwrite_existing_packages: bool, include_versions: bool): View Source

    415    def update_dependencies(
    416        self, overwrite_existing_packages: bool, include_versions: bool
    417    ):
    418        """Scan project for dependencies and update the corresponding field in the pyproject model."""
    419        dependencies = utilities.get_dependencies(self.projectdir)
    420        if overwrite_existing_packages:
    421            self.pyproject.project.dependencies = [
    422                utilities.format_dependency(dependency, include_versions)
    423                for dependency in dependencies
    424                if dependency[0] != self.name
    425            ]
    426        else:
    427            for dependency in dependencies:
    428                if (
    429                    all(
    430                        dependency[0] not in existing_dependency
    431                        for existing_dependency in self.pyproject.project.dependencies
    432                    )
    433                    and dependency[0] != self.name
    434                ):
    435                    self.pyproject.project.dependencies.append(
    436                        utilities.format_dependency(dependency, include_versions)
    437                    )

Scan project for dependencies and update the corresponding field in the pyproject model.

def update_changelog(self): View Source

    470    def update_changelog(self):
    471        """Update `CHANGELOG.md` by invoking the `auto-changelog` module.
    472
    473        If `hassle_config.toml` doesn't exist, an empty tag prefix will be assumed."""
    474        raw_changelog = self._generate_changelog()
    475        # If there's no existing changelog, dump the generated one and get out of here.
    476        if not self.changelog_path.exists():
    477            self.changelog_path.write_text(raw_changelog)
    478            return
    479
    480        # Don't want to overwrite previously existing manual changes/edits
    481        existing_changelog = self.changelog_path.read_text().splitlines(True)[
    482            2:
    483        ]  # First two elements are "# Changelog\n" and "\n"
    484        new_changes = raw_changelog
    485        for line in existing_changelog:
    486            # Release headers are prefixed with "## "
    487            if line.startswith("## "):
    488                new_changes = raw_changelog[: raw_changelog.index(line)]
    489                break
    490        changes = "".join(new_changes)
    491        # If changes == "# Changelog\n\n" then there weren't actually any new changes
    492        if not changes == "# Changelog\n\n":
    493            self.changelog_path.write_text(changes + "".join(existing_changelog))

Update `CHANGELOG.md` by invoking the `auto-changelog` module.

If `hassle_config.toml` doesn't exist, an empty tag prefix will be assumed.

def create_source_files(self): View Source

    499    def create_source_files(self):
    500        """Generate source files in `self.srcdir`."""
    501        for file in self.source_files:
    502            (self.srcdir / file).touch()
    503        init = self.srcdir / "__init__.py"
    504        if init.exists():
    505            init.append(f'__version__ = "{self.version}"')

Generate source files in `self.srcdir`.

def create_readme(self): View Source

    507    def create_readme(self):
    508        readme = self.get_template("README.md")
    509        readme = readme.replace("$name", self.name)
    510        readme = readme.replace("$description", self.pyproject.project.description)
    511        (self.projectdir / "README.md").write_text(readme)

def create_license(self): View Source

    513    def create_license(self):
    514        license_ = self.get_template("license.txt")
    515        license_ = license_.replace("$year", str(datetime.now().year))
    516        (self.projectdir / "LICENSE.txt").write_text(license_)

def create_gitignore(self): View Source

    518    def create_gitignore(self):
    519        (self.templatedir / ".gitignore.txt").copy(self.projectdir / ".gitignore")

def create_vscode_settings(self): View Source

    521    def create_vscode_settings(self):
    522        self.vsdir.mkdir()
    523        (self.templatedir / "vscode_settings.json").copy(self.vsdir / "settings.json")

def create_tests(self): View Source

    525    def create_tests(self):
    526        (self.testsdir / f"test_{self.name}.py").touch()

def generate_files(self): View Source

    528    def generate_files(self):
    529        """Create all the necessary files.
    530
    531        Note: This will overwrite any existing files."""
    532        self.projectdir.mkdir()
    533        for func in dir(self):
    534            if func.startswith("create_"):
    535                getattr(self, func)()
    536        self.pyproject.dump(self.pyproject_path)

Create all the necessary files.

Note: This will overwrite any existing files.

def generate_docs(self): View Source

    538    def generate_docs(self):
    539        """Generate docs by invoking `pdoc`"""
    540        self.docsdir.delete()
    541        subprocess.run(["pdoc", "-o", self.docsdir, self.srcdir])

Generate docs by invoking `pdoc`